Platform: Code4rena
Start Date: 13/03/2023
Pot Size: $72,500 USDC
Total HM: 33
Participants: 35
Period: 7 days
Judge: Dravee
Total Solo HM: 16
Id: 222
League: ETH
Rank: 34/35
Findings: 1
Award: $78.86
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: bytes032
Also found by: 0xbepresent, PaludoX0, juancito, peanuts, sorrynotsorry
78.8601 USDC - $78.86
https://github.com/code-423n4/2023-03-polynomial/blob/aeecafc8aaceab1ebeb94117459946032ccdff1e/src/LiquidityPool.sol#L200-L215 https://github.com/code-423n4/2023-03-polynomial/blob/aeecafc8aaceab1ebeb94117459946032ccdff1e/src/LiquidityPool.sol#L264-L280
Processing deposits and withdrawals can be griefed, possbile DoS
LiquidityPool
contract utilizes queueDeposit()
and queueWithdraw()
functions to slate the liquidity deposits and withdrawals.
Accordingly, the users are given Deposit and Withdraw Id's which are to be processed by calling processDeposits()
and processWithdraws()
Both queueDeposit()
and queueWithdraw()
functions are as below;
function queueDeposit(uint256 amount, address user) external override nonReentrant whenNotPaused("POOL_QUEUE_DEPOSIT") { QueuedDeposit storage newDeposit = depositQueue[nextQueuedDepositId]; newDeposit.id = nextQueuedDepositId++; newDeposit.user = user; newDeposit.depositedAmount = amount; newDeposit.requestedTime = block.timestamp; totalQueuedDeposits += amount; SUSD.safeTransferFrom(msg.sender, address(this), amount); emit InitiateDeposit(newDeposit.id, msg.sender, user, amount); }
And queueWithdraw();
function queueWithdraw(uint256 tokens, address user) external override nonReentrant whenNotPaused("POOL_QUEUE_WITHDRAW") { require(liquidityToken.balanceOf(msg.sender) >= tokens); QueuedWithdraw storage newWithdraw = withdrawalQueue[nextQueuedWithdrawalId]; newWithdraw.id = nextQueuedWithdrawalId++; newWithdraw.user = user; newWithdraw.withdrawnTokens = tokens; newWithdraw.requestedTime = block.timestamp; totalQueuedWithdrawals += tokens; liquidityToken.burn(msg.sender, tokens); emit InitiateWithdrawal(newWithdraw.id, msg.sender, user, tokens); }
And the ID's distributed in these functions are handled in processDeposits()
and processWithdraws()
functions as below;
function processDeposits(uint256 count) external override nonReentrant whenNotPaused("POOL_PROCESS_DEPOSITS") { assert(queuedDepositHead + count - 1 < nextQueuedDepositId); uint256 tokenPrice = getTokenPrice(); for (uint256 i = 0; i < count; i++) { QueuedDeposit storage current = depositQueue[queuedDepositHead]; if (current.requestedTime == 0 || block.timestamp < current.requestedTime + minDepositDelay) { return; } uint256 tokensToMint = current.depositedAmount.divWadDown(tokenPrice); current.mintedTokens = tokensToMint; totalQueuedDeposits -= current.depositedAmount; totalFunds += current.depositedAmount; liquidityToken.mint(current.user, tokensToMint); emit ProcessDeposit(current.id, current.user, current.depositedAmount, tokensToMint, current.requestedTime); current.depositedAmount = 0; queuedDepositHead++; } }
And processWithdraws();
function processWithdraws(uint256 count) external override nonReentrant whenNotPaused("POOL_PROCESS_WITHDRAWS") { assert(queuedWithdrawalHead + count - 1 < nextQueuedWithdrawalId); for (uint256 i = 0; i < count; i++) { uint256 tokenPrice = getTokenPrice(); QueuedWithdraw storage current = withdrawalQueue[queuedWithdrawalHead]; if (current.requestedTime == 0 || block.timestamp < current.requestedTime + minWithdrawDelay) { return; } uint256 availableFunds = uint256(int256(totalFunds) - usedFunds); if (availableFunds == 0) { return; } uint256 susdToReturn = current.withdrawnTokens.mulWadDown(tokenPrice); // Partial withdrawals if not enough available funds in the vault // Queue head is not increased if (susdToReturn > availableFunds) { current.returnedAmount = availableFunds; uint256 tokensBurned = availableFunds.divWadUp(tokenPrice); totalQueuedWithdrawals -= tokensBurned; current.withdrawnTokens -= tokensBurned; totalFunds -= availableFunds; SUSD.safeTransfer(current.user, availableFunds); emit ProcessWithdrawalPartially( current.id, current.user, tokensBurned, availableFunds, current.requestedTime ); return; } else { // Complete full withdrawal current.returnedAmount = susdToReturn; totalQueuedWithdrawals -= current.withdrawnTokens; totalFunds -= susdToReturn; SUSD.safeTransfer(current.user, susdToReturn); emit ProcessWithdrawal( current.id, current.user, current.withdrawnTokens, susdToReturn, current.requestedTime ); current.withdrawnTokens = 0; } queuedWithdrawalHead++; } }
As can be seen in queueDeposit()
and queueWithdraw()
functions, both functions accept Zero amounts.
Accordingly one can queue a deposit of 0 tokens. And if it's intended to attack the protocol, this action can be done multiple times to inflate the deposit queue at the cost of transaction gas fees. Same can be done to withdraw queue while holding some liquidity tokens. As a result, the process of deposits and withdrawals will be griefed since nextQueuedDepositId
and nextQueuedWithdrawalId
will reach higher numbers.
Manual Review
Do not allow queueing zero-amount deposits and withdrawals.
#0 - c4-judge
2023-03-22T17:45:07Z
JustDravee marked the issue as duplicate of #122
#1 - JustDravee
2023-03-22T17:58:33Z
Note:
Marking as duplicate for now but this mentions both queueWithdraw
and queueDeposit
compared to https://github.com/code-423n4/2023-03-polynomial-findings/issues/122 which only mentions queueWithdraw
.
This one is more complete but lacks a coded POCs compared to the other.
Still valid.
#2 - c4-judge
2023-05-03T01:29:36Z
JustDravee marked the issue as satisfactory
#3 - c4-judge
2023-05-16T00:04:39Z
JustDravee changed the severity to 2 (Med Risk)