Maverick contest - rvierdiiev's results

DeFi infrastructure.

General Information

Platform: Code4rena

Start Date: 01/12/2022

Pot Size: $60,500 USDC

Total HM: 8

Participants: 27

Period: 11 days

Judge: kirk-baird

Total Solo HM: 6

Id: 187

League: ETH

Maverick

Findings Distribution

Researcher Performance

Rank: 3/27

Findings: 2

Award: $5,995.91

QA:
grade-b

🌟 Selected for report: 1

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: rvierdiiev

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
selected for report
sponsor disputed
M-01

Awards

5936.0731 USDC - $5,936.07

External Links

Lines of code

https://github.com/code-423n4/2022-12-Stealth-Project/blob/main/router-v1/contracts/Router.sol#L277-L287

Vulnerability details

Impact

Router.getOrCreatePoolAndAddLiquidity can be frontrunned which allows to set different initial price.

Proof of Concept

When new Pool is created it is already initialized with active tick. This allows the pool creator to provide price for assets. So when first LP calls Pool.addLiquidity, this active tick [is used]( to determine how many assets LP should deposit.

Router.getOrCreatePoolAndAddLiquidity function allows caller to add liquidity to the pool that can be not created yet. In such case it will create it with initial active tick provided by sender and then it will provide user's liquidity to the pool. In case if pool already exists, function will just add liquidity to it.

https://github.com/code-423n4/2022-12-Stealth-Project/blob/main/router-v1/contracts/Router.sol#L277-L287

    function getOrCreatePoolAndAddLiquidity(
        PoolParams calldata poolParams,
        uint256 tokenId,
        IPool.AddLiquidityParams[] calldata addParams,
        uint256 minTokenAAmount,
        uint256 minTokenBAmount,
        uint256 deadline
    ) external payable checkDeadline(deadline) returns (uint256 receivingTokenId, uint256 tokenAAmount, uint256 tokenBAmount, IPool.BinDelta[] memory binDeltas) {
        IPool pool = getOrCreatePool(poolParams);
        return addLiquidity(pool, tokenId, addParams, minTokenAAmount, minTokenBAmount);
    }

1.Pool of tokenA:tokenB doens't exist. 2.User calls Router.getOrCreatePoolAndAddLiquidity function to create pool with initial active tick and provide liquidity in same tx. 3.Attacker frontruns tx and creates such pool with different active tick. 4.Router.getOrCreatePoolAndAddLiquidity is executed and new Pool was not created, and liquidity was added to the Pool created by attacker. 5.Attacker swapped provided by first depositor tokens for a good price. 6.First depositor lost part of funds.

Tools Used

VsCode

Do not allow to do such actions together in one tx. Do it in 2 tx. First, create Pool. And in second tx add liquidity.

#0 - gte620v

2022-12-16T01:16:55Z

This is a dup of https://github.com/code-423n4/2022-12-Stealth-Project-findings/issues/92

A user can protect against this by setting isDelta=false in AddLiquidityParams.

This comment is also relevant: https://github.com/code-423n4/2022-12-Stealth-Project-findings/issues/83#issuecomment-1353917297

#1 - c4-sponsor

2022-12-16T01:17:18Z

gte620v marked the issue as sponsor disputed

#2 - kirk-baird

2023-01-03T09:40:14Z

This is an interesting one. I'm inclined to side with the warden on this. Although it is possible to protect against this attack it's not immediately clear this will always happen.

My thoughts are that if you are calling getOrCreatePoolAndAddLiquidity() for a pool you don't believe exists you may set poolParams.activeTick and then isDelta = true with the expectation you'll be setting a delta from poolParams.activeTick.

If the front-runner has creates a new pool with a different active tick then the user will be vulnerable since they have isDelta = true.

However, since protection does exist although optional I think the severity should be downgraded to Medium.

#3 - c4-judge

2023-01-03T09:40:19Z

kirk-baird changed the severity to 2 (Med Risk)

#4 - c4-judge

2023-01-12T22:01:08Z

kirk-baird marked the issue as satisfactory

Awards

59.8382 USDC - $59.84

Labels

bug
downgraded by judge
grade-b
QA (Quality Assurance)
Q-04

External Links

Lines of code

https://github.com/code-423n4/2022-12-Stealth-Project/blob/main/router-v1/contracts/Router.sol#L132-L140 https://github.com/code-423n4/2022-12-Stealth-Project/blob/main/router-v1/contracts/Router.sol#L122

Vulnerability details

Impact

If user calls Router.exactInputSingle swap with params.recipient == 0 by mistake, all swapping funds will be sent to Router and anyone can claim them to himself.

Proof of Concept

Router.exactInputSingle function calls exactInputInternal function and pases recipient param. https://github.com/code-423n4/2022-12-Stealth-Project/blob/main/router-v1/contracts/Router.sol#L121-L129

    function exactInputInternal(uint256 amountIn, address recipient, uint256 sqrtPriceLimitD18, SwapCallbackData memory data) private returns (uint256 amountOut) {
        if (recipient == address(0)) recipient = address(this);


        (IERC20 tokenIn, IERC20 tokenOut, IPool pool) = data.path.decodeFirstPool();


        bool tokenAIn = tokenIn < tokenOut;


        (, amountOut) = pool.swap(recipient, amountIn, tokenAIn, false, sqrtPriceLimitD18, abi.encode(data));
    }

As you can see if recipient is 0, then it is set to address(this). That means that receiver of trade will be Router. Router contract has several methods that allow anyone to transfer controlled by Router tokens to himself, like refundETH, sweepToken, unwrapWETH9. https://github.com/code-423n4/2022-12-Stealth-Project/blob/main/router-v1/contracts/Router.sol#L70-L77

    function sweepToken(IERC20 token, uint256 amountMinimum, address recipient) public payable {
        uint256 balanceToken = token.balanceOf(address(this));
        require(balanceToken >= amountMinimum, "Insufficient token");


        if (balanceToken > 0) {
            TransferHelper.safeTransfer(address(token), recipient, balanceToken);
        }
    }

No restrictions here, so anyone can call.

This creates possiblity that after user will make exactInputSingle swap with recipient 0 address and in another tx will try to sweep those tokens, someone will frontrun user and will send tokens to himself. User will lost swapped funds.

This test will show that if recipient is 0 then after the swap Router controls swapped tokens. You can run it inside Router.ts.

it.only("swap recipient 0", async () => {
      let _fee = 0.01 / 100;
      let pool: string = await getPool(_fee, tokenA, tokenB, 500, 500);
      const prePoolBalance = await balances(pool);
      const preRouterBalance = await balances(router.address);
      //Router balance of b token is 0
      expect(preRouterBalance.tokenB).to.be.eq(0);
      console.log(preRouterBalance);
      expect(
        await router.exactInputSingle([
          tokenA.address,
          tokenB.address,
          pool,
          ethers.constants.AddressZero,
          maxDeadline,
          floatToFixed(10),
          0,
          0,
        ])
      )
        .to.emit(tokenA, "Transfer")
        .to.emit(tokenB, "Transfer");
      const postPoolBalance = await balances(pool);
      expect(postPoolBalance.tokenA - prePoolBalance.tokenA).to.eq(10);
      expect(postPoolBalance.tokenB - prePoolBalance.tokenB).to.be.lessThan(-4);

      const postRouterBalance = await balances(router.address);
      console.log(postRouterBalance);
      //Router balance of b token is not 0 anymore
      //now anyone can sweep it
      expect(postRouterBalance.tokenB).to.be.not.eq(0);
    });

Tools Used

VsCode

Add check to exactInputSingle and exactOutputSingle that params.recipient is not 0.

#0 - Jeiwan

2022-12-13T00:46:32Z

This is an expected behavior. It makes swaps calldata portable and Router-agnostic, which allows to seamlessly migrate to a new Router. This was initially introduced in Uniswap V3: https://github.com/Uniswap/v3-periphery/commit/a0e0e5817528f0b810583c04feea17b696a16755

It also gives a chance to withdraw tokens thanks to the public sweepTokens function: https://github.com/code-423n4/2022-12-Stealth-Project/blob/main/router-v1/contracts/Router.sol#L70 Otherwise, tokens would've been sent to the zero address and lost forever.

#1 - kirk-baird

2022-12-13T10:51:53Z

This is clearly a design feature to use recipient = address(0) to have funds transferred to the Router. I'm going to mark this as a QA.

#2 - c4-judge

2022-12-13T10:52:00Z

kirk-baird changed the severity to QA (Quality Assurance)

#3 - c4-judge

2023-01-18T22:07:02Z

kirk-baird marked the issue as grade-b

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter