Platform: Code4rena
Start Date: 21/12/2023
Pot Size: $90,500 USDC
Total HM: 10
Participants: 39
Period: 18 days
Judge: LSDan
Total Solo HM: 5
Id: 315
League: ETH
Rank: 4/39
Findings: 2
Award: $3,690.64
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: erebus
Also found by: BugzyVonBuggernaut, hash
2460.4271 USDC - $2,460.43
liquidity_lockbox#withdraw
is at high risk of a permanent DOS due to "0" amount liquidity positions
if (positionData.liquidity > type(uint64).max) { revert("Liquidity overflow"); }
There is no minimum amount liquidity check during LP deposits.
liquidity_lockbox.sol#L223-L225
function withdraw(uint64 amount) external { address positionAddress = positionAccounts[firstAvailablePositionAccountIndex]; if (positionAddress != tx.accounts.position.key) { revert("Wrong liquidity token account"); } address pdaPositionAta = tx.accounts.pdaPositionAccount.key; if (mapPositionAccountPdaAta[positionAddress] != pdaPositionAta) { revert("Wrong position ATA"); } uint64 positionLiquidity = mapPositionAccountLiquidity[positionAddress]; // Check that the token account exists if (positionLiquidity == 0) { revert("No liquidity on a provided token account"); }
However, if the next available firstAvailablePositionAccountIndex
has zero positionLiquidity
, the position will revert.
firstAvailablePositionAccountIndex++;
The index only increments at the end of a successful withdraw
call.
invariant(liquidity.gt(new BN(0)), "liquidity must be greater than zero");
When depositing and opening a position via the Orca SDK, there is a check to ensure that the liquidity deposited is greater than 0.
However, this check is part of the SDK, not the on-chain code.
/// Open a position in a Whirlpool. A unique token will be minted to represent the position /// in the users wallet. The position will start off with 0 liquidity.
So long as the SDK is bypassed, a 0 liquidity position can be open.
Due to the existing CPI issues between Autonolas and Orca, it's very difficult to prove the DOS directly. However, it is possible to demonstrate how to open a 0 liquidity position on Orca by removing the frontend invariant check and running their test suite.
For that please refer to this gist. Note that due to the changes, various tests will fail, but the one we are interested in that does pass is: open and add liquidity to a position, then close
VSC, Anchor
The required step is to include a minimum liquidity constraint on Lockbox deposits.
An optional second step is to address the 0 position liquidity check in withdraw.
if (positionLiquidity == 0) { revert("No liquidity on a provided token account"); }
One option is to remove it entirely, or remove the revert and instead increment the index and exit the function.
if (positionLiquidity == 0) { firstAvailablePositionAccountIndex++ return }
This would remove the risk of a 0 amount DOS in the event that it somehow occurs despite the extra check on deposits.
DoS
#0 - c4-pre-sort
2024-01-10T15:18:49Z
alex-ppg marked the issue as duplicate of #341
#1 - c4-pre-sort
2024-01-10T15:18:53Z
alex-ppg marked the issue as sufficient quality report
#2 - c4-judge
2024-01-19T20:05:01Z
dmvt marked the issue as satisfactory
🌟 Selected for report: EV_om
Also found by: BugzyVonBuggernaut
1230.2135 USDC - $1,230.21
https://github.com/code-423n4/2023-12-autonolas/blob/2a095eb1f8359be349d23af67089795fb0be4ed1/lockbox-solana/solidity/liquidity_lockbox.sol#L210-L211 https://github.com/code-423n4/2023-12-autonolas/blob/2a095eb1f8359be349d23af67089795fb0be4ed1/lockbox-solana/solidity/liquidity_lockbox.sol#L320-L321
Withdrawing non-programmatically from the lockbox becomes impossible in practice due to an attacker creating many small positions.
This is due to two existing conditions: a) No deposit lower bound b) Withdrawals happen in strict FIFO order.
liquidity_lockbox.sol#L210-L211
function withdraw(uint64 amount) external { address positionAddress = positionAccounts[firstAvailablePositionAccountIndex];
As outlined in the docs, lockbox withdrawals "checks whether the liquidity L associated with the least recently deposited NFT is larger than or equals to the requested withdrawal amount X. If this condition is met, the corresponding liquidity is liquidated, and the X amount of fungible tokens is burnt"
Importantly, "multiple withdrawal calls may be necessary, contingent on whether the user's fungible token amount, X, is greater than the least recently deposited NFT".
liquidity_lockbox.sol#L320-L321
// Increase the first available position account index firstAvailablePositionAccountIndex++;
At the end of withdraw
, the index is incremented.
There is no minimum deposit amount in liquidity_lockbox::deposit
.
- const usdc_amount = DecimalUtil.toBN(new Decimal("10" /* usdc */), 6); + const usdc_amount = DecimalUtil.toBN(new Decimal("1" /* usdc */), 0);
Run the tests with the above adjustment to verify.
An attacker can grief lockbox users by creating thousands of small LP positions and forcing users to execute withdraw thousands of times in order to get their LP tokens back.
VSC
Include a minimum deposit amount in the lockbox and consider allowing users to withdraw from specific positions, or consider reordering the LP positions from large to small.
The latter two would circumvent the potential issue where even if there were many reasonable minimum deposits (1000 USD), a user dealing in much more size (1,000,000 USD) wouldn't have to call withdraw 1000 times or more.
Other
#0 - c4-pre-sort
2024-01-10T14:59:38Z
alex-ppg marked the issue as duplicate of #443
#1 - c4-pre-sort
2024-01-10T14:59:42Z
alex-ppg marked the issue as sufficient quality report
#2 - c4-judge
2024-01-19T20:09:26Z
dmvt marked the issue as satisfactory