Skip to content
Request an audit

‹ All findings

Protected downside is not updated when cds.getTotalCdsDepositedAmount() < downsideProtected

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

Summary

The borrowing::_withdraw is used to withdraw collateral from the protocol and burn USDa. Here, borrowing::_getDownsideFromCDS is called to update the downside protected if cds.getTotalCdsDepositedAmount() >= downsideProtected otherwise fetch the shortage from other chain via layerzero call.

solidity
    function _getDownsideFromCDS(
        uint128 downsideProtected,
        uint256 feeForOFT
    ) internal {
        if (cds.getTotalCdsDepositedAmount() < downsideProtected) {
            // Call the oftOrCollateralReceiveFromOtherChains function in global variables
            globalVariables.oftOrCollateralReceiveFromOtherChains{value: feeForOFT}(
                IGlobalVariables.FunctionToDo(3),
                IGlobalVariables.USDaOftTransferData(address(treasury),downsideProtected - cds.getTotalCdsDepositedAmount()),
                // Since we don't need ETH, we have passed zero params
                IGlobalVariables.CollateralTokenTransferData(address(0),0,0,0),
                IGlobalVariables.CallingFunction.BORROW_WITHDRAW,
                msg.sender
            );
        } else {
            // updating downside protected from this chain in CDS
            cds.updateDownsideProtected(downsideProtected);
        }
        // Burn the borrow amount
        treasury.approveTokens(IBorrowing.AssetName.USDa, address(this), downsideProtected);
        bool success = usda.contractBurnFrom(address(treasury), downsideProtected);
        if (!success) revert Borrow_BurnFailed();
    }

As we can see cds.updateDownsideProtected(downsideProtected) is called to update the entire downsideProtected atCDS::downsideProtected variable. However, in case of cds.getTotalCdsDepositedAmount() < downsideProtected, we get the lacking USDa from other chain via layerzero call, here we fail to update the CDS::downsideProtected. Hence, the downsideProtected will be short, which will eventually reduce less downside protected when _updateCurrentTotalCdsDepositedAmount is called in [CDS::deposit]()

solidity
    function _updateCurrentTotalCdsDepositedAmount() private {
        if (downsideProtected > 0) {
            totalCdsDepositedAmount -= downsideProtected;                                          <@
            totalCdsDepositedAmountWithOptionFees -= downsideProtected;                 <@
            downsideProtected = 0;
        }
    }

This totalCdsDepositedAmount is used for calculating calculateCumulativeValue inside the CDSLib::deposit function, that will inflate the omnichain cumulative value, this will lead to CDS withdrawers will get a higher optionFees

solidity
function withdraw(
        uint64 index,
        uint256 excessProfitCumulativeValue,
        uint256 nonce,
        bytes memory signature
    ) external payable nonReentrant whenNotPaused(IMultiSign.Functions(5)) {
       // rest of the code
        IGlobalVariables.OmniChainData memory omniChainData = globalVariables.getOmniChainData();

        // Calculate current value
        CalculateValueResult memory result = _calculateCumulativeValue(
            omniChainData.totalVolumeOfBorrowersAmountinWei,
            omniChainData.totalCdsDepositedAmount,
            ethPrice
        );

        // Set the cumulative vaue
        (omniChainData.cumulativeValue, omniChainData.cumulativeValueSign) = getCumulativeValue(
            omniChainData,
            result.currentValue,
            result.gains
        );

        // updating totalCdsDepositedAmount by considering downsideProtected
        _updateCurrentTotalCdsDepositedAmount();

        // updating global data
        if (omniChainData.downsideProtected > 0) {
            omniChainData.totalCdsDepositedAmount -= omniChainData.downsideProtected;
            omniChainData.totalCdsDepositedAmountWithOptionFees -= omniChainData.downsideProtected;
            omniChainData.downsideProtected = 0;
        }

        // Calculate return amount includes eth Price difference gain or loss option fees
        uint256 optionFees = ((cdsDepositDetails.normalizedAmount * omniChainData.lastCumulativeRate) /  <@
            (CDSLib.PRECISION * CDSLib.NORM_PRECISION)) - cdsDepositDetails.depositedAmount;

        // Calculate the options fees to get from other chains
        uint256 optionsFeesToGetFromOtherChain = getOptionsFeesProportions(optionFees);

        // Update user deposit data
        cdsDepositDetails.withdrawedTime = uint64(block.timestamp);
        cdsDepositDetails.ethPriceAtWithdraw = ethPrice;
        uint256 currentValue = cdsAmountToReturn(
            msg.sender,
            index,
            omniChainData.cumulativeValue,
            omniChainData.cumulativeValueSign,
            excessProfitCumulativeValue
        ) - 1; //? subtracted extra 1 wei

        cdsDepositDetails.depositedAmount = currentValue;
        uint256 returnAmount = cdsDepositDetails.depositedAmount + optionFees;    <@
        cdsDepositDetails.withdrawedAmount = returnAmount;
       // rest of the code
    }

Root Cause

In borrowing.sol:726, there is a missing updateDownsideProtected call. Allowing to inflate the cumulative value.

Impact

  1. A condition arrives for a user where the downside protection of the user is greater than total CDS deposited Amount.
  2. The user calls CDS::withdraw, where the downside protection shortfall is fulfilled via layer zero call.
  3. However, due to no downside protection update, the cumulative value will get artificially inflated, allowing CDS withdrawers gain higher option fees than intended

Mitigation

It is recommended to update downside protection for the amount not borrowed from other chain

diff
    function _getDownsideFromCDS(
        uint128 downsideProtected,
        uint256 feeForOFT
    ) internal {
        if (cds.getTotalCdsDepositedAmount() < downsideProtected) {
            // Call the oftOrCollateralReceiveFromOtherChains function in global variables
+         cds.updateDownsideProtected(downsideProtected); 
            globalVariables.oftOrCollateralReceiveFromOtherChains{value: feeForOFT}(
                IGlobalVariables.FunctionToDo(3),
                IGlobalVariables.USDaOftTransferData(address(treasury),downsideProtected - cds.getTotalCdsDepositedAmount()),
                // Since we don't need ETH, we have passed zero params
                IGlobalVariables.CollateralTokenTransferData(address(0),0,0,0),
                IGlobalVariables.CallingFunction.BORROW_WITHDRAW,
                msg.sender
            );
        } else {
            // updating downside protected from this chain in CDS
            cds.updateDownsideProtected(downsideProtected);
        }
        // Burn the borrow amount
        treasury.approveTokens(IBorrowing.AssetName.USDa, address(this), downsideProtected);
        bool success = usda.contractBurnFrom(address(treasury), downsideProtected);
        if (!success) revert Borrow_BurnFailed();
    }