Skip to content

Commit

Permalink
test: add unit tests for Withdrawal Vault excess fee refund behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
mkurayan committed Jan 28, 2025
1 parent ade67a7 commit 89d583a
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 7 deletions.
31 changes: 31 additions & 0 deletions test/0.8.9/contracts/RefundFailureTester.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: UNLICENSED
// for testing purposes only

pragma solidity 0.8.9;

interface IWithdrawalVault {
function addFullWithdrawalRequests(bytes calldata pubkeys) external payable;
function getWithdrawalRequestFee() external view returns (uint256);
}

/**
* @notice This is a contract for testing refund failure in WithdrawalVault contract
*/
contract RefundFailureTester {
IWithdrawalVault private immutable withdrawalVault;

constructor(address _withdrawalVault) {
withdrawalVault = IWithdrawalVault(_withdrawalVault);
}

receive() external payable {
revert("Refund failed intentionally");
}

function addFullWithdrawalRequests(bytes calldata pubkeys) external payable {
require(msg.value > withdrawalVault.getWithdrawalRequestFee(), "Not enough eth for Refund");

// withdrawal vault should fail to refund
withdrawalVault.addFullWithdrawalRequests{value: msg.value}(pubkeys);
}
}
68 changes: 64 additions & 4 deletions test/0.8.9/withdrawalVault.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ERC20__Harness,
ERC721__Harness,
Lido__MockForWithdrawalVault,
RefundFailureTester,
WithdrawalVault__Harness,
} from "typechain-types";

Expand Down Expand Up @@ -389,6 +390,34 @@ describe("WithdrawalVault.sol", () => {
).to.be.revertedWithCustomError(vault, "WithdrawalRequestFeeReadFailed");
});

it("should revert if refund failed", async function () {
const refundFailureTester: RefundFailureTester = await ethers.deployContract("RefundFailureTester", [
vaultAddress,
]);
const refundFailureTesterAddress = await refundFailureTester.getAddress();

await vault.connect(owner).grantRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE, refundFailureTesterAddress);

const requestCount = 3;
const { pubkeysHexString } = generateWithdrawalRequestPayload(requestCount);

const fee = 3n;
await withdrawalsPredeployed.setFee(fee);
const expectedTotalWithdrawalFee = 9n; // 3 requests * 3 gwei (fee) = 9 gwei

await expect(
refundFailureTester
.connect(stranger)
.addFullWithdrawalRequests(pubkeysHexString, { value: expectedTotalWithdrawalFee + 1n }),
).to.be.revertedWithCustomError(vault, "TriggerableWithdrawalRefundFailed");

await expect(
refundFailureTester
.connect(stranger)
.addFullWithdrawalRequests(pubkeysHexString, { value: expectedTotalWithdrawalFee + ethers.parseEther("1") }),
).to.be.revertedWithCustomError(vault, "TriggerableWithdrawalRefundFailed");
});

it("Should accept withdrawal requests when the provided fee matches the exact required amount", async function () {
const requestCount = 3;
const { pubkeysHexString, pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount);
Expand Down Expand Up @@ -486,7 +515,31 @@ describe("WithdrawalVault.sol", () => {
expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance);
});

// ToDo: should return back the excess fee
it("Should refund excess fee", async function () {
const requestCount = 3;
const { pubkeysHexString, pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount);

const fee = 3n;
await withdrawalsPredeployed.setFee(fee);
const expectedTotalWithdrawalFee = 9n; // 3 requests * 3 gwei (fee) = 9 gwei
const excessFee = 1n;

const vebInitialBalance = await ethers.provider.getBalance(validatorsExitBus.address);

const { receipt } = await testEip7002Mock(
() =>
vault
.connect(validatorsExitBus)
.addFullWithdrawalRequests(pubkeysHexString, { value: expectedTotalWithdrawalFee + excessFee }),
pubkeys,
fullWithdrawalAmounts,
fee,
);

expect(await ethers.provider.getBalance(validatorsExitBus.address)).to.equal(
vebInitialBalance - expectedTotalWithdrawalFee - receipt.gasUsed * receipt.gasPrice,
);
});

it("Should transfer the total calculated fee to the EIP-7002 withdrawal contract", async function () {
const requestCount = 3;
Expand Down Expand Up @@ -566,18 +619,25 @@ describe("WithdrawalVault.sol", () => {
it(`Should successfully add ${requestCount} requests with extra fee ${extraFee}`, async () => {
const { pubkeysHexString, pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount);
const expectedFee = await getFee();
const withdrawalFee = expectedFee * BigInt(requestCount) + extraFee;
const expectedTotalWithdrawalFee = expectedFee * BigInt(requestCount);

const initialBalance = await getWithdrawalCredentialsContractBalance();
const vebInitialBalance = await ethers.provider.getBalance(validatorsExitBus.address);

await testEip7002Mock(
() => vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: withdrawalFee }),
const { receipt } = await testEip7002Mock(
() =>
vault
.connect(validatorsExitBus)
.addFullWithdrawalRequests(pubkeysHexString, { value: expectedTotalWithdrawalFee + extraFee }),
pubkeys,
fullWithdrawalAmounts,
expectedFee,
);

expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance);
expect(await ethers.provider.getBalance(validatorsExitBus.address)).to.equal(
vebInitialBalance - expectedTotalWithdrawalFee - receipt.gasUsed * receipt.gasPrice,
);
});
});
});
Expand Down
9 changes: 6 additions & 3 deletions test/common/lib/triggerableWithdrawals/eip7002Mock.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { expect } from "chai";
import { ContractTransactionReceipt } from "ethers";
import { ContractTransactionResponse } from "ethers";
import { ContractTransactionReceipt, ContractTransactionResponse } from "ethers";
import { ethers } from "hardhat";

import { findEventsWithInterfaces } from "lib";
Expand All @@ -25,7 +24,7 @@ export const testEip7002Mock = async (
expectedPubkeys: string[],
expectedAmounts: bigint[],
expectedFee: bigint,
) => {
): Promise<{ tx: ContractTransactionResponse; receipt: ContractTransactionReceipt }> => {
const tx = await addTriggeranleWithdrawalRequests();
const receipt = await tx.wait();

Expand All @@ -37,5 +36,9 @@ export const testEip7002Mock = async (
expect(events[i].args[1]).to.equal(expectedFee);
}

if (!receipt) {
throw new Error("No receipt");
}

return { tx, receipt };
};

0 comments on commit 89d583a

Please sign in to comment.