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

The claimFees caller attempting to claim all protocol fees is reverted #345

Open
c4-bot-6 opened this issue Mar 4, 2024 · 3 comments
Open
Labels
bug Something isn't working duplicate-45 edited-by-warden grade-b Q-14 QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax 🤖_34_group AI based duplicate group recommendation satisfactory satisfies C4 submission criteria; eligible for awards

Comments

@c4-bot-6
Copy link
Contributor

c4-bot-6 commented Mar 4, 2024

Lines of code

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

Vulnerability details

Description

The claimFees function is tasked with claiming protocol fees by paying out PAYOUT_TOKEN instead of fees collected by the pool. It includes validation to protect the caller from receiving less amount of tokens than what was requested.

if (_amount0 < _amount0Requested || _amount1 < _amount1Requested) {
    revert V3FactoryOwner__InsufficientFeesCollected();
}

Due to the UniswapV3Pool collecting protocol fees, it transfers 1 wei less if the caller attempts to collect all protocol fees. At this point, this could potentially disrupt the claimFees function in V3FactoryOwner.

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);
}
Exmaple
  1. We assume there are 1000 tokens of USDC and 1000 tokens of USDT in protocol fees.
  2. Someone is attempting to claim all fees, which amounts to 1000 USDC and 1000 USDT, by paying 1 WETH.
  3. The collectProtocol function in UniswapV3Pool returns 1000e6 - 1 USDC and 1000e6 - 1 USDT, and the transaction is reverted because the returned amount is less than the requested amount.

Impact

  • The caller is reverted if attempting to collect the maximum amount of fees.

Proof of Concept

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.23;

import {Vm, Test, console} from "forge-std/Test.sol";
import {UniStaker} from "src/UniStaker.sol";
import {IUniswapV3FactoryOwnerActions} from "src/interfaces/IUniswapV3FactoryOwnerActions.sol";
import {IUniswapV3PoolOwnerActions} from "src/interfaces/IUniswapV3PoolOwnerActions.sol";
import {INotifiableRewardReceiver} from "src/interfaces/INotifiableRewardReceiver.sol";
import {IERC20Delegates} from "src/interfaces/IERC20Delegates.sol";
import {ERC20, IERC20} from "openzeppelin/token/ERC20/ERC20.sol";
import {V3FactoryOwner} from "src/V3FactoryOwner.sol";
import {TransferHelper} from "v3-core/libraries/TransferHelper.sol";

contract PausableToken is ERC20{
    bool public isPaused = false;
    address public owner;

    constructor(address _owner, string memory _name, string memory _symbol) ERC20(_name, _symbol){
        owner = _owner;
    }

    function transfer(address to, uint256 value) public override returns (bool) {
        require(!isPaused, "Contract is paused!");
        return super.transfer(to, value);
    }

    function transferFrom(address from, address to, uint256 value) public override returns (bool){
        require(!isPaused, "Contract is paused!");
        return super.transferFrom(from, to, value);
    }

    function setIsPaused(bool _status) public {
        require(owner == msg.sender, "Only owner");
        isPaused = _status;
    }

    function mint(address _account, uint256 _amount) public {
        _mint(_account, _amount);
    }
}

contract MockERC20 is ERC20{
    constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol){}

    function mint(address _account, uint256 _amount) public {
        _mint(_account, _amount);
    }
}

contract UniswapV3PoolMock{
    struct ProtocolFees {
        uint128 token0;
        uint128 token1;
    }

    ProtocolFees public protocolFees;
    address public token0;
    address public token1;

    event CollectProtocol(address indexed sender, address indexed recipient, uint128 amount0, uint128 amount1);

    constructor(address _token0, address _token1) {
        token0 = _token0;
        token1 = _token1;
    }

    function setProtocolFees(uint128 _amount0, uint128 _amount1) public {
        protocolFees = ProtocolFees({token0: _amount0, token1: _amount1});
    }

    function collectProtocol(address recipient, uint128 amount0Requested, uint128 amount1Requested) public returns (uint128 amount0, uint128 amount1){
        amount0 = amount0Requested > protocolFees.token0 ? protocolFees.token0 : amount0Requested;
        amount1 = amount1Requested > protocolFees.token1 ? protocolFees.token1 : amount1Requested;

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

        emit CollectProtocol(msg.sender, recipient, amount0, amount1);
    }
}

contract UniStakerPOC is Test {
  // Addresses
  address public admin = makeAddr("ADMIN");
  address public pausableTokenOwner = makeAddr("PAUSABLE_OWNER");
  address public uniV3Factory = makeAddr("UniswapV3Factory");

  // Values
  uint256 public payoutAmount = 1e18;

  PausableToken pausableToken;
  MockERC20 weth;
  MockERC20 uni;
  UniStaker uniStaker;
  V3FactoryOwner factoryOwner;
  UniswapV3PoolMock uniV3Pool;

  function setUp() public {
      // Deploy contracts
      pausableToken = new PausableToken(pausableTokenOwner, "Pausable Token", "PT");
      weth = new MockERC20("WETH token", "WETH");
      uni = new MockERC20("Uniswap Gov token", "UNI");
      uniStaker = new UniStaker(IERC20(address(weth)), IERC20Delegates(address(uni)), address(admin));
      factoryOwner = new V3FactoryOwner(admin, IUniswapV3FactoryOwnerActions(address(uniStaker)), IERC20(weth),
          payoutAmount, INotifiableRewardReceiver(address(uniStaker)));
      // Deploy a pool
      uniV3Pool = new UniswapV3PoolMock(address(pausableToken), address(weth));

      // Allow the V3FactoryOwner contract to call the `setRewardNotifier` function.
      vm.prank(admin);
      uniStaker.setRewardNotifier(address(factoryOwner), true);
  }

  function test_claimFeesRevertIfRequestedMaximumAmount() public {
      // Protocol fees: 100 PT + 1 WETH
      pausableToken.mint(address(uniV3Pool), 100e18);
      weth.mint(address(uniV3Pool), 1e18);
      uniV3Pool.setProtocolFees(100e18, 1e18);
      (uint128 token0Fees, uint128 token1Fees) = uniV3Pool.protocolFees();

      // Attempting to claim all 100 PT and 1 WETH.
      address caller = makeAddr("CALLER");
      weth.mint(address(caller), 1e18);
      vm.startPrank(caller);
      weth.approve(address(factoryOwner), payoutAmount);
      // The transaction was reverted with `V3FactoryOwner__InsufficientFeesCollected()`
      factoryOwner.claimFees(IUniswapV3PoolOwnerActions(address(uniV3Pool)), caller, token0Fees, token1Fees);
      vm.stopPrank();
  }
}
POC setup

foundry.toml:

[profile.default]
  optimizer = true
  optimizer_runs = 10_000_000
  remappings = [
    "openzeppelin/=lib/openzeppelin-contracts/contracts",
    "v3-periphery=node_modules/@uniswap/v3-periphery/contracts",
    "v3-core=node_modules/@uniswap/v3-core/contracts"
  ]
  solc_version = "0.8.23"
forge test --mt test_claimFeesRevertIfRequestedMaximumAmount

Tools Used

Manual Review
Foundry

Recommended Mitigation Steps

- if (_amount0 < _amount0Requested || _amount1 < _amount1Requested) {
-    revert V3FactoryOwner__InsufficientFeesCollected();
- }
+ if (_amount0 < _amount0Requested - 1 || _amount1 < _amount1Requested - 1) {
+    revert V3FactoryOwner__InsufficientFeesCollected();
+ }

Assessed type

DoS

@c4-bot-6 c4-bot-6 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 4, 2024
c4-bot-10 added a commit that referenced this issue Mar 4, 2024
@c4-bot-1 c4-bot-1 changed the title The claimFees caller attempting to claim all protocol fees is reverted. The claimFees caller attempting to claim all protocol fees is reverted Mar 5, 2024
@c4-bot-11 c4-bot-11 added the 🤖_34_group AI based duplicate group recommendation label Mar 5, 2024
@c4-judge c4-judge closed this as completed Mar 7, 2024
@c4-judge
Copy link
Contributor

c4-judge commented Mar 7, 2024

MarioPoneder marked the issue as duplicate of #34

@c4-judge c4-judge added duplicate-34 satisfactory satisfies C4 submission criteria; eligible for awards labels Mar 7, 2024
@c4-judge
Copy link
Contributor

MarioPoneder marked the issue as satisfactory

@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
Copy link
Contributor

MarioPoneder marked the issue as grade-b

@c4-judge c4-judge added grade-b and removed grade-a labels Mar 26, 2024
@C4-Staff C4-Staff reopened this Mar 27, 2024
@C4-Staff C4-Staff added the Q-14 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 edited-by-warden grade-b Q-14 QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax 🤖_34_group AI based duplicate group recommendation satisfactory satisfies C4 submission criteria; eligible for awards
Projects
None yet
Development

No branches or pull requests

6 participants