Platform: Code4rena
Start Date: 23/02/2024
Pot Size: $92,000 USDC
Total HM: 0
Participants: 47
Period: 10 days
Judge: 0xTheC0der
Id: 336
League: ETH
Rank: 30/47
Findings: 1
Award: $694.30
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: CodeWasp
Also found by: 0xdice91, 0xlemon, Aamir, Al-Qa-qa, AlexCzm, BAHOZ, Bauchibred, Breeje, DadeKuma, Fassi_Security, PetarTolev, Shield, SpicyMeatball, Trust, ZanyBonzy, cheatc0d3, gesha17, haxatron, imare, jesjupyter, kutugu, lsaudit, marchev, merlinboii, nnez, osmanozdemir1, peanuts, radev_sw, twicek, visualbits
694.2987 USDC - $694.30
https://github.com/code-423n4/2024-02-uniswap-foundation/blob/5298812a129f942555466ebaa6ea9a2af4be0ccc/src/V3FactoryOwner.sol#L193-L195 https://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/UniswapV3Pool.sol#L856-L865
In V3FactoryOwner::claimFees
, a caller calls the function to claim the protocol fees accrued by a given Uniswap v3 pool contract. However, in UniswapV3Pool
, the input amount0Requested
and amount1Requested
are properly handled to reduced by 1
to ensure that the slot is not cleared for gas savings
. This case will cause unexpected revert if the caller wants to claim all fees accrued due to the requirement _amount0 < _amount0Requested || _amount1 < _amount1Requested
and the lack of document nor comment on the input range of _amount0Requested
, _amount1Requested
.
In V3FactoryOwner::claimFees
, a caller calls the function to claim the protocol fees accrued by a given Uniswap v3 pool contract.
(uint128 _amount0, uint128 _amount1) = _pool.collectProtocol(_recipient, _amount0Requested, _amount1Requested); // // Protect the caller from receiving less than requested. See `collectProtocol` for context. if (_amount0 < _amount0Requested || _amount1 < _amount1Requested) { // @audit will revert, shouldn't be --? if (amount0 == protocolFees.token0) amount0--; revert V3FactoryOwner__InsufficientFeesCollected(); }
There is a check to ensure that the caller won't receive less than requested
.
However, in the context of UniswapV3Pool::collectProtocol, when amount0 == protocolFees.token0
or amount1 == protocolFees.token1
, they are deducted by 1 so that the slot is not cleared.
if (amount0 > 0) { if (amount0 == protocolFees.token0) amount0--; // ensure that the slot is not cleared, for gas savings protocolFees.token0 -= amount0; TransferHelper.safeTransfer(token0, recipient, amount0); } if (amount1 > 0) { if (amount1 == protocolFees.token1) amount1--; // ensure that the slot is not cleared, for gas savings protocolFees.token1 -= amount1; TransferHelper.safeTransfer(token1, recipient, amount1); }
Since there is no explicit doc nor comment of the range of input _amount0Requested
, _amount1Requested
, this will cause unexpected revert if the caller wants to claim all fees accrued but ends up with _amount0Requested-1
and _amount1Requested-1
which is an edge case and should not revert anyway.
The PoC is shown below (added in UniStaker.integration.t.sol
):
function test_UnexpectedRevert( ) public { uint128 _amountDai = 1 ether; uint128 _amountWeth = 2 ether; IERC20 weth = IERC20(payable(WETH_ADDRESS)); IERC20 dai = IERC20(payable(DAI_ADDRESS)); IUniswapPool daiWethPool = IUniswapPool(DAI_WETH_3000_POOL); // Amount should be high enough to generate fees _amountDai = uint128(bound(_amountDai, 1e18, 1_000_000e18)); _amountWeth = uint128(bound(_amountWeth, 1e18, 1_000_000e18)); uint256 totalDai = _amountDai; uint256 totalWeth = _amountWeth + PAYOUT_AMOUNT; vm.deal(address(this), totalWeth); deal(address(dai), address(this), totalDai, true); deal(address(weth), address(this), totalWeth); dai.approve(address(UNISWAP_V3_SWAP_ROUTER), totalDai); weth.approve(address(UNISWAP_V3_SWAP_ROUTER), totalWeth); weth.approve(address(v3FactoryOwner), PAYOUT_AMOUNT); _passQueueAndExecuteProposals(); _swapTokens(DAI_ADDRESS, WETH_ADDRESS, totalDai); _swapTokens(WETH_ADDRESS, DAI_ADDRESS, _amountWeth); uint256 originalDaiBalance = IERC20(DAI_ADDRESS).balanceOf(address(this)); uint256 originalWethBalance = IERC20(WETH_ADDRESS).balanceOf(address(this)) - PAYOUT_AMOUNT; (uint128 token0Fees, uint128 token1Fees) = daiWethPool.protocolFees(); vm.expectRevert(); v3FactoryOwner.claimFees( IUniswapV3PoolOwnerActions(DAI_WETH_3000_POOL), address(this), token0Fees, token1Fees ); // <= issue here }
The trace shows:
V3FactoryOwner__InsufficientFeesCollected()
Foundry
Before _pool.collectProtocol
, call _pool.protocolFees()
to get the current fees. The function should not revert under this case.
(uint128 token0Fees, uint128 token1Fees) = _pool.protocolFees(); ... if ((_amount0 < _amount0Requested && _amount0Requested != token0Fees) || (_amount1 < _amount1Requested && _amount1Requested != token1Fees)) { revert V3FactoryOwner__InsufficientFeesCollected(); }
DoS
#0 - c4-judge
2024-03-07T12:42:53Z
MarioPoneder marked the issue as duplicate of #34
#1 - c4-judge
2024-03-14T01:38:45Z
MarioPoneder marked the issue as satisfactory
#2 - c4-judge
2024-03-26T23:00:32Z
MarioPoneder marked the issue as grade-b