Liquid Staking

Stakelink
DeFiHardhatOracle
50,000 USDC
View results
Submission Details
Severity: low
Invalid

Batch ID Retrieval Fails for Partially Finalized Withdrawals in `WithdrawalPool`

Summary

The WithdrawalPool::getBatchIds function returns the wrong batch ID for partially finalized withdrawals, preventing users from retrieving the correct batch ID needed for withdrawal.

Vulnerability Details

The WithdrawalPool::getBatchIds function is crucial for users to determine which batch a fully or partially finalized withdrawal ID belongs to. This batch ID is required to successfully withdraw using the WithdrawalPool::withdraw function. However, the current implementation of getBatchIds consistently returns the wrong batch ID when the withdrawal ID is only partially finalized.

The test below shows two implementations of getBatchIds: my recommended fix and the current, faulty implementation.

Please add the recommended getBatchIds fix from the "Recommendations" section as getBatchIdsMIne in the WithdrawalPool contract, then add the following test to test/core/priorityPool/withdrawal-pool.test.ts:

it('MINE withdraw should work correctly', async () => {
const { signers, accounts, withdrawalPool, token } = await loadFixture(deployFixture)
await withdrawalPool.queueWithdrawal(accounts[0], toEther(500))
await withdrawalPool.queueWithdrawal(accounts[1], toEther(200))
await withdrawalPool.queueWithdrawal(accounts[0], toEther(500))
await withdrawalPool.queueWithdrawal(accounts[1], toEther(300))
console.log(
'account0 WithdrawalIDs:',
await withdrawalPool.getWithdrawalIdsByOwner(accounts[0])
)
console.log(
'account1 WithdrawalIDs:',
await withdrawalPool.getWithdrawalIdsByOwner(accounts[1])
)
await withdrawalPool.deposit(toEther(500))
await withdrawalPool.deposit(toEther(500))
await withdrawalPool.deposit(toEther(100))
await withdrawalPool.deposit(toEther(100))
await withdrawalPool.deposit(toEther(100))
console.log('account0 BatchIds:', await withdrawalPool.getBatchIds([1, 3]))
console.log('account0 getBatchIdsMIne:', await withdrawalPool.getBatchIdsMIne([1, 3]))
console.log('account1 getBatchIds:', await withdrawalPool.getBatchIds([2, 4]))
console.log('account1 getBatchIdsMIne:', await withdrawalPool.getBatchIdsMIne([2, 4]))
// test
let startingBalance = await token.balanceOf(accounts[0])
await withdrawalPool.withdraw([1], [1])
assert.equal(fromEther((await token.balanceOf(accounts[0])) - startingBalance), 500)
// test
let startingBalance = await token.balanceOf(accounts[1])
await withdrawalPool.connect(signers[1]).withdraw([2], [2])
assert.equal(fromEther((await token.balanceOf(accounts[1])) - startingBalance), 200)
// should be able to withdraw the partially filled amount when given
//the correct batch id
let startingBalance = await token.balanceOf(accounts[1])
await withdrawalPool.connect(signers[1]).withdraw([4], [5])
assert.equal(fromEther((await token.balanceOf(accounts[1])) - startingBalance), 100)
})

Run:

yarn test --grep "MINE withdraw should work correctly"

Here are the logs:

$ npx hardhat test --network hardhat --grep 'MINE withdraw should work correctly'
Compiled 1 Solidity file successfully (evm target: london).
WithdrawalPool
account0 WithdrawalIDs: Result(2) [ 1n, 3n ]
account1 WithdrawalIDs: Result(2) [ 2n, 4n ]
account0 BatchIds: Result(2) [ 1n, 4n ]
account0 getBatchIdsMIne: Result(2) [ 1n, 4n ]
account1 getBatchIds: Result(2) [ 2n, 0n ]
account1 getBatchIdsMIne: Result(2) [ 2n, 5n ]
MINE withdraw should work correctly (10076ms)
1 passing (10s)
Done in 22.47s.

In the test, I created two queued withdrawals for account0 and two for account1, totaling 1,500 tokens in queued withdrawals. Then, I made deposits of up to 1,300 tokens, leaving the last queued withdrawal only partially finalized. The current implementation incorrectly returns a default batch ID of 0, while the fixed version correctly returns a batch ID of 5.

Impact

Users will be unable to retrieve batch IDs for partially finalized queued withdrawals, making it nearly impossible to withdraw from these partially finalized withdrawal Ids.

Tools Used

Manual Review

Recommendations

Update the current WithdrawalPool::getBatchIds function to:

function getBatchIds(
uint256[] memory _withdrawalIds
) public view returns (uint256[] memory) {
uint256[] memory batchIds = new uint256[]();
for (uint256 i = 0; i < _withdrawalIds.length; ++i) {
uint256 batchId;
uint256 withdrawalId = _withdrawalIds[i];
for (uint256 j = withdrawalBatchIdCutoff; j < withdrawalBatches.length; ++j) {
uint256 indexOfLastWithdrawal = withdrawalBatches[j].indexOfLastWithdrawal;
if (withdrawalId <= indexOfLastWithdrawal) {
batchId = j;
break;
}
if (
indexOfLastWithdrawal + 1 >= withdrawalId &&
queuedWithdrawals[withdrawalId].partiallyWithdrawableAmount != 0
) {
batchId = j;
continue;
}
}
batchIds[i] = batchId;
}
return batchIds;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge
about 1 year ago
inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice
Assigned finding tags:

[INVALID]Batch ID Retrieval Fails for Partially Finalized Withdrawals in `WithdrawalPool`

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.