Platform: Code4rena
Start Date: 03/07/2023
Pot Size: $40,000 USDC
Total HM: 14
Participants: 74
Period: 7 days
Judge: alcueca
Total Solo HM: 9
Id: 259
League: ETH
Rank: 43/74
Findings: 1
Award: $17.52
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xprinc
Also found by: 0x11singh99, 0xAnah, 0xWaitress, 0xkazim, 2997ms, 33audits, 404Notfound, 8olidity, CRIMSON-RAT-REACH, CyberPunks, DanielWang888, Deekshith99, Eeyore, Eurovickk, Inspecktor, JGcarv, John, Jorgect, Kaysoft, LosPollosHermanos, MohammedRizwan, Qeew, QiuhaoLi, Rolezn, TheSavageTeddy, Topmark, Trust, Udsen, a3yip6, alexzoid, bigtone, codegpt, erebus, fatherOfBlocks, ginlee, glcanvas, hunter_w3b, josephdara, kaveyjoe, kutugu, mahdirostami, max10afternoon, oakcobalt, peanuts, pfapostol, ptsanev, qpzm, radev_sw, ravikiranweb3, sces60107, seth_lawson, te_aut, twcctop, zhaojie, ziyou-
17.5208 USDC - $17.52
In the dual token pool, the first liquidity provider may inadvertently lose tokens when they assign a value to one token and set the second token amount to zero. Notably, in the _addLiquidity()
function on line https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L441 when the _mint()
method is invoked, there is no validation check for the lpAmountOut
variable to ensure it is not zero.
Please find below a Proof of Concept. Create a new file named like Well.AddLiquidityAttack.t.sol
and include the following context.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; import {console} from "forge-std/Test.sol"; import {TestHelper, IERC20, Call, Balances} from "test/TestHelper.sol"; import {AddLiquidityAction, RemoveLiquidityAction, LiquidityHelper} from "test/LiquidityHelper.sol"; import {Aquifer} from "src/Aquifer.sol"; import {SwapHelper, SwapAction, Snapshot} from "test/SwapHelper.sol"; contract WellAddLiquidityTest is LiquidityHelper { function _setupWellWithoutAddingLiquidity(Call memory _wellFunction, Call[] memory _pumps, IERC20[] memory _tokens) internal { tokens = _tokens; wellFunction = _wellFunction; for (uint256 i; i < _pumps.length; i++) { pumps.push(_pumps[i]); } initUser(); wellImplementation = deployWellImplementation(); aquifer = new Aquifer(); well = encodeAndBoreWell(address(aquifer), wellImplementation, tokens, _wellFunction, _pumps, bytes32(0)); // Mint mock tokens to user mintTokens(user, initialLiquidity); mintTokens(user2, initialLiquidity); approveMaxTokens(user, address(well)); approveMaxTokens(user2, address(well)); // Mint mock tokens to TestHelper mintTokens(address(this), initialLiquidity); approveMaxTokens(address(this), address(well)); // Do not add initial liquidity from TestHelper // addLiquidityEqualAmount(address(this), initialLiquidity); } function setUp() public { _setupWellWithoutAddingLiquidity(deployWellFunction(), deployPumps(1), deployMockTokens(2)); } function _addLiq(address targetUser, uint256 token0, uint256 token1) internal returns(uint256 minted) { uint256[] memory amounts = new uint256[](tokens.length); amounts[0] = token0; amounts[1] = token1; minted = well.addLiquidity(amounts, 0, targetUser, type(uint256).max); } function testZeroMint() public { vm.startPrank(user); uint256 userBalanceToken0Before = IERC20(tokens[0]).balanceOf(address(user)); _addLiq(user, 1e18, 0); uint256 userBalanceToken0After = IERC20(tokens[0]).balanceOf(address(user)); assert( // Spent `1e18` token0 userBalanceToken0Before - userBalanceToken0After == 1e18 // Got zero LP tokens && well.balanceOf(user) == 0 ); } }
Begin by executing forge test -vv --match-test testZeroMint
. Here is an example output:
Running 1 test for test/Well.AddLiquidityAttack.t.sol:WellAddLiquidityTest [PASS] testZeroMint() (gas: 138179) Test result: ok. 1 passed; 0 failed; finished in 4.36ms
I advise that transactions be reverted when zero LP tokens are being minted.
MINIMUM_LIQUIDITY
tokens to the zero addressThe practice of minting the first MINIMUM_LIQUIDITY
tokens (1000 tokens in the case of Uniswap) to the zero address(0x0)
when initializing a new liquidity pool is a design choice utilized by the Uniswap protocol. This mechanism is used to prevent anyone from ever owning a 100% share of the liquidity pool, which could potentially manipulate the price of tokens in the pool.
Implementing this feature can bring a number of benefits including increased robustness against potential pricing manipulation.
I recommend that the _addLiquidity()
function at line https://github.com/code-423n4/2023-07-basin/blob/main/src/Well.sol#L413 be updated to mint the first MINIMUM_LIQUIDITY
tokens to the zero address, as is done in Uniswap.
#0 - c4-pre-sort
2023-07-13T14:19:53Z
141345 marked the issue as high quality report
#1 - 141345
2023-07-13T14:21:50Z
dup of https://github.com/code-423n4/2023-07-basin-findings/issues/241 might need escalate to medium
#2 - c4-sponsor
2023-08-02T23:10:46Z
publiuss marked the issue as sponsor acknowledged
#3 - c4-judge
2023-08-05T10:29:28Z
alcueca changed the severity to 2 (Med Risk)
#4 - c4-judge
2023-08-05T10:29:28Z
alcueca changed the severity to 2 (Med Risk)
#5 - c4-judge
2023-08-05T10:30:00Z
alcueca marked the issue as satisfactory
#6 - lokithe5th
2023-08-11T07:02:20Z
Although the PoC is correct here, this only holds true in the case where the first liquidity provider has specifically stated that their minLpAmountOut
is allowed to be 0
, in their call to addLiquidity(uint256[] memory tokenAmountsIn, uint256 minLpAmountOut, address recipient, uint256 deadline)
This is due to this check:
if (lpAmountOut < minLpAmountOut) { revert SlippageOut(lpAmountOut, minLpAmountOut); }
The warden's submission as QA severity seems appropriate here because:
There is no unexpected loss of funds and the contract behaves as expected, as the caller has to have acknowledged the possibility of getting 0
tokens out.
This requires the first liquidity provider to add only one-sided liquidity (which doesn't make sense for initializing an LP)
#7 - alcueca
2023-08-15T20:56:18Z
I didn't notice the detail of minLpAmountOut
. Thanks for pointing it out. I agree that if the LP accepts the result might be zero, it can't complain if it actually is.
#8 - c4-judge
2023-08-15T20:57:14Z
alcueca changed the severity to QA (Quality Assurance)
#9 - c4-judge
2023-08-15T20:57:27Z
alcueca marked the issue as grade-a