Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Due to the gas saving mechanism of the uniswap pool’s collectProtocol method, the auctions may often revert griefly. #372

Open
c4-bot-8 opened this issue Mar 5, 2024 · 3 comments
Labels
bug Something isn't working duplicate-45 grade-b Q-12 QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax satisfactory satisfies C4 submission criteria; eligible for awards

Comments

@c4-bot-8
Copy link
Contributor

c4-bot-8 commented Mar 5, 2024

Lines of code

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

Vulnerability details

Impact

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.

Proof of Concept

This is an interactive combination error involving two points:

  1. In order to save gas, UniswapV3Pool.collectProtocol will automatically reduce the fee amount by one instead of clearing the storage.
  2. 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:

  1. The fees in the pool increase from zero. The relevant auctioneer has been monitoring the amount of fees through 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.
  2. At this time, there are two auction parties A and B competing. A and B initiated transactions in block N at the same time. A's tx was executed first with a high gasPrice, but the transaction failed due to the existence of this issue; while B's tx is delayed until block N + 1 due to the lower gasPrice.
  3. And there is a swap of the pool between the transactions of A and B, which increases the protocolFees of the pool, so B's tx can be executed normally and wins the auction.

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);
+      }
+    }
   }
 }

Tools Used

Foundry

Recommended Mitigation Steps

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.

Assessed type

Context

@c4-bot-8 c4-bot-8 added 2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working labels Mar 5, 2024
c4-bot-10 added a commit that referenced this issue Mar 5, 2024
@c4-judge
Copy link
Contributor

c4-judge commented Mar 7, 2024

MarioPoneder marked the issue as duplicate of #34

@c4-judge
Copy link
Contributor

MarioPoneder marked the issue as satisfactory

@c4-judge c4-judge added the satisfactory satisfies C4 submission criteria; eligible for awards label Mar 14, 2024
@CloudEllie CloudEllie added grade-a QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax and removed 2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value labels Mar 26, 2024
@c4-judge c4-judge added grade-b and removed grade-a labels Mar 26, 2024
@c4-judge
Copy link
Contributor

MarioPoneder marked the issue as grade-b

@C4-Staff C4-Staff reopened this Mar 27, 2024
@C4-Staff C4-Staff added the Q-12 label Mar 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working duplicate-45 grade-b Q-12 QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax satisfactory satisfies C4 submission criteria; eligible for awards
Projects
None yet
Development

No branches or pull requests

4 participants