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: 29/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/491c7f63e5799d95a181be4a978b2f074dc219a5/src/V3FactoryOwner.sol#L189-L195 https://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/UniswapV3Pool.sol#L857 https://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/UniswapV3Pool.sol#L862
When the user collects the fees collected by the current protocol, the pool.collectProtocol
method will automatically reduce the fee amount by one instead of clearing the fee storage in order to save gas, which causes the verification of V3FactoryOwner.claimFees
to fail and triggers the V3FactoryOwner__InsufficientFeesCollected
error.
This results in auctions that may often revert griefly, affecting the interests of the auctioneer.
This is an interactive combination error involving two points:
UniswapV3Pool.collectProtocol
will automatically reduce the fee amount by one instead of clearing the storage.V3FactoryOwner.claimFees
will verify the collect fee amount, and the reduced amount will cause the transaction to be reverted.Why does this error often trigger griefly and affect the interests of the auctioneer? Letβs take a look at the execution process:
UniswapV3Pool.protocolFees
, but is not aware of the existence of this issue. When the fee value exceeds payoutAmount
, the auctioneer passes the amount of fees obtained.The above process violates the principle of auction. A's tx is executed first but lose, while B's tx is executed after the fee is increased and wins the auction. This is a specific example that affects the interests of the auctioneer. Due to the timing of transaction execution, the error is implicit and will not be discovered for a period of time, affecting the revenue of the auctioneer.
The following is the specific POC:
diff --git a/test/V3FactoryOwner.t.sol b/test/V3FactoryOwner.t.sol index 2db6623..2c38861 100644 --- a/test/V3FactoryOwner.t.sol +++ b/test/V3FactoryOwner.t.sol @@ -477,4 +477,27 @@ contract ClaimFees is V3FactoryOwnerTest { factoryOwner.claimFees(pool, _recipient, _amount0Requested, _amount1Requested); vm.stopPrank(); } + + function testUserClaimAllFeesRevertGriefly() public { + uint128 payoutAmount = 1 ether; + uint128 amount0Collected = 1 ether; + uint128 amount1Collected = 2 ether; + _deployFactoryOwnerWithPayoutAmount(payoutAmount); + pool.setNextReturn__collectProtocol(amount0Collected, amount1Collected); + (uint128 amount0, uint128 amount1) = pool.protocolFees(); + assertEq(amount0, amount0Collected); + assertEq(amount1, amount1Collected); + + address user = makeAddr("User"); + payoutToken.mint(user, payoutAmount); + vm.startPrank(user); + payoutToken.approve(address(factoryOwner), payoutAmount); + // @audit The user queries the fees collected by the protocol and passes them in as parameters. + // @audit As a result, the transaction fails due to uniswap's pool gas saving mechanism. + vm.expectRevert(V3FactoryOwner.V3FactoryOwner__InsufficientFeesCollected.selector); + factoryOwner.claimFees(pool, user, amount0, amount1); + // @audit The user can only reduce the amount by one + factoryOwner.claimFees(pool, user, amount0 - 1, amount1 - 1); + vm.stopPrank(); + } } diff --git a/test/mocks/MockUniswapV3Pool.sol b/test/mocks/MockUniswapV3Pool.sol index 1977a8d..fb78794 100644 --- a/test/mocks/MockUniswapV3Pool.sol +++ b/test/mocks/MockUniswapV3Pool.sol @@ -26,14 +26,37 @@ contract MockUniswapV3Pool is IUniswapV3PoolOwnerActions { mockFeesAmount1 = amount1; } + function protocolFees() public view returns (uint128 amount0, uint128 amount1) { + if (useMockProtocolFeeAmounts) { + amount0 = mockFeesAmount0; + amount1 = mockFeesAmount1; + } + } + function collectProtocol(address recipient, uint128 amount0Requested, uint128 amount1Requested) external - returns (uint128, uint128) + returns (uint128 amount0, uint128 amount1) { lastParam__collectProtocol_recipient = recipient; lastParam__collectProtocol_amount0Requested = amount0Requested; lastParam__collectProtocol_amount1Requested = amount1Requested; - if (useMockProtocolFeeAmounts) return (mockFeesAmount0, mockFeesAmount1); - return (amount0Requested, amount1Requested); + amount0 = amount0Requested; + amount1 = amount1Requested; + + if (useMockProtocolFeeAmounts) { + amount0 = amount0Requested > mockFeesAmount0 ? mockFeesAmount0 : amount0Requested; + amount1 = amount1Requested > mockFeesAmount1 ? mockFeesAmount1 : amount1Requested; + + if (amount0 > 0) { + if (amount0 == mockFeesAmount0) amount0--; // ensure that the slot is not cleared, for gas savings + mockFeesAmount0 -= amount0; + // TransferHelper.safeTransfer(token0, recipient, amount0); + } + if (amount1 > 0) { + if (amount1 == mockFeesAmount1) amount1--; // ensure that the slot is not cleared, for gas savings + mockFeesAmount1 -= amount1; + // TransferHelper.safeTransfer(token1, recipient, amount1); + } + } } }
Foundry
The verification of V3FactoryOwner.claimFees
should take into account the gas saving mechanism of UniswapV3Pool.collectProtocol
and be consistent with it to eliminate interactive combination errors.
Context
#0 - c4-judge
2024-03-07T22:45:56Z
MarioPoneder marked the issue as duplicate of #34
#1 - c4-judge
2024-03-14T01:39:42Z
MarioPoneder marked the issue as satisfactory
#2 - c4-judge
2024-03-26T22:58:51Z
MarioPoneder marked the issue as grade-b