Skip to content
Request an audit

‹ All findings

An attacker can manipulate omniChainData.cdsPoolValue by breaking protocol.

MediumAutonomint·Sherlock · Hedged stablecoin · 4th December 2024CodebaseM-6

Summary

Missing update of lastEthprice in borrowing.sol#depositTokens() will cause manipulation of omniChainData.cdsPoolValue as an attaker replays borrowing.sol#depositTokens() by breaking protocol.

https://github.com/sherlock-audit/2024-11-autonomint/blob/main/Blockchain/Blockchian/contracts/Core_logic/borrowing.sol#L258

Root Cause

  • In borrowing.sol#depositTokens(), lastEthprice is not updated.
solidity
    function depositTokens(
        BorrowDepositParams memory depositParam
    ) external payable nonReentrant whenNotPaused(IMultiSign.Functions(0)) {
        // Assign options for lz contract, here the gas is hardcoded as 400000, we got this through testing by iteration
        bytes memory _options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(400000, 0);
        // calculting fee
        MessagingFee memory fee = globalVariables.quote(
            IGlobalVariables.FunctionToDo(1),
            depositParam.assetName,
            _options,
            false
        );
        // Get the exchange rate for a collateral and eth price
        (uint128 exchangeRate, uint128 ethPrice) = getUSDValue(assetAddress[depositParam.assetName]);

        totalNormalizedAmount = BorrowLib.deposit(
            BorrowLibDeposit_Params(
                LTV,
                APR,
                lastCumulativeRate,
                totalNormalizedAmount,
                exchangeRate,
                ethPrice,
                lastEthprice
            ),
            depositParam,
            Interfaces(treasury, globalVariables, usda, abond, cds, options),
            assetAddress
        );

        //Call calculateCumulativeRate() to get currentCumulativeRate
        calculateCumulativeRate();
        lastEventTime = uint128(block.timestamp);

        // Calling Omnichain send function
        globalVariables.send{value: fee.nativeFee}(
            IGlobalVariables.FunctionToDo(1),
            depositParam.assetName,
            fee,
            _options,
            msg.sender
        );
    }
  • In BorrowLib.sol:661, omniChainData is updated with new cdsPoolValue.
solidity
    (ratio, omniChainData) = calculateRatio(
        params.depositingAmount,
        uint128(libParams.ethPrice),
        libParams.lastEthprice,
        omniChainData.totalNoOfDepositIndices,
        omniChainData.totalVolumeOfBorrowersAmountinWei,
        omniChainData.totalCdsDepositedAmount - omniChainData.downsideProtected,
        omniChainData
    );
    // Check whether the cds have enough funds to give downside prottection to borrower
    if (ratio < (2 * RATIO_PRECISION)) revert IBorrowing.Borrow_NotEnoughFundInCDS();
  • In BorrowLib.sol#calculateRatio(), cdsPoolValue is updated by difference between lastEthprice and currentEthprice.
solidity
    function calculateRatio(
        uint256 amount,
        uint128 currentEthPrice,
        uint128 lastEthprice,
        uint256 noOfDeposits,
        uint256 totalCollateralInETH,
        uint256 latestTotalCDSPool,
        IGlobalVariables.OmniChainData memory previousData
    ) public pure returns (uint64, IGlobalVariables.OmniChainData memory) {
        uint256 netPLCdsPool;

        // Calculate net P/L of CDS Pool
        // if the current eth price is high
        if (currentEthPrice > lastEthprice) {
            // profit, multiply the price difference with total collateral
@>          netPLCdsPool = (((currentEthPrice - lastEthprice) * totalCollateralInETH) / USDA_PRECISION) / 100;
        } else {
            // loss, multiply the price difference with total collateral
@>          netPLCdsPool = (((lastEthprice - currentEthPrice) * totalCollateralInETH) / USDA_PRECISION) / 100;
        }

        uint256 currentVaultValue;
        uint256 currentCDSPoolValue;

        // Check it is the first deposit
        if (noOfDeposits == 0) {
            // Calculate the ethVault value
            previousData.vaultValue = amount * currentEthPrice;
            // Set the currentEthVaultValue to lastEthVaultValue for next deposit
            currentVaultValue = previousData.vaultValue;

            // Get the total amount in CDS
            // lastTotalCDSPool = cds.totalCdsDepositedAmount();
            previousData.totalCDSPool = latestTotalCDSPool;

            // BAsed on the eth prices, add or sub, profit and loss respectively
            if (currentEthPrice >= lastEthprice) {
@>              currentCDSPoolValue = previousData.totalCDSPool + netPLCdsPool;
            } else {
@>              currentCDSPoolValue = previousData.totalCDSPool - netPLCdsPool;
            }

            // Set the currentCDSPoolValue to lastCDSPoolValue for next deposit
@>          previousData.cdsPoolValue = currentCDSPoolValue;
@>          currentCDSPoolValue = currentCDSPoolValue * USDA_PRECISION;
        } else {
            // find current vault value by adding current depositing amount
            currentVaultValue = previousData.vaultValue + (amount * currentEthPrice);
            previousData.vaultValue = currentVaultValue;

            // BAsed on the eth prices, add or sub, profit and loss respectively
            if (currentEthPrice >= lastEthprice) {
                previousData.cdsPoolValue += netPLCdsPool;
            } else {
                previousData.cdsPoolValue -= netPLCdsPool;
            }
@>          previousData.totalCDSPool = latestTotalCDSPool;
@>          currentCDSPoolValue = previousData.cdsPoolValue * USDA_PRECISION;
        }

        // Calculate ratio by dividing currentEthVaultValue by currentCDSPoolValue,
        // since it may return in decimals we multiply it by 1e6
@>      uint64 ratio = uint64((currentCDSPoolValue * CUMULATIVE_PRECISION) / currentVaultValue);
        return (ratio, previousData);
    }

External pre-conditions

  • Eth price changed a little from borrowing.sol#lastEthprice.

Attack Path

  • We say that current ratio is bigger than 2 * RATIO_PRECISION.
  • An attaker continues to replay borrowing.sol#depositTokens.
  • Then, omniChainData.cdsPoolValue continues to be increased or decreased.

Impact

  • In case of increasing, cdsPoolValue can be much bigger than normal. Then, borrowing is allowed even if protocol is really under water.
  • In case of decreasing, cdsPoolValue can be decreased so that ratio is small enough to touch (2 * RATIO_PRECISION). Then, borrowing can be DOSed even if cds have enough funds. And withdrawal from CDS can be DOSed.

Mitigation

borrowing.sol#depositTokens() function has to be modified as follows.

solidity
    function depositTokens(
        BorrowDepositParams memory depositParam
    ) external payable nonReentrant whenNotPaused(IMultiSign.Functions(0)) {
        // Assign options for lz contract, here the gas is hardcoded as 400000, we got this through testing by iteration
        bytes memory _options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(400000, 0);
        // calculting fee
        MessagingFee memory fee = globalVariables.quote(
            IGlobalVariables.FunctionToDo(1),
            depositParam.assetName,
            _options,
            false
        );
        // Get the exchange rate for a collateral and eth price
        (uint128 exchangeRate, uint128 ethPrice) = getUSDValue(assetAddress[depositParam.assetName]);

        totalNormalizedAmount = BorrowLib.deposit(
            BorrowLibDeposit_Params(
                LTV,
                APR,
                lastCumulativeRate,
                totalNormalizedAmount,
                exchangeRate,
                ethPrice,
                lastEthprice
            ),
            depositParam,
            Interfaces(treasury, globalVariables, usda, abond, cds, options),
            assetAddress
        );

        //Call calculateCumulativeRate() to get currentCumulativeRate
        calculateCumulativeRate();
        lastEventTime = uint128(block.timestamp);
++      lastEthprice = ethPrice;

        // Calling Omnichain send function
        globalVariables.send{value: fee.nativeFee}(
            IGlobalVariables.FunctionToDo(1),
            depositParam.assetName,
            fee,
            _options,
            msg.sender
        );
    }