Protected downside is not updated when cds.getTotalCdsDepositedAmount() < downsideProtected
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.
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]()
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
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
- A condition arrives for a user where the downside protection of the user is greater than total CDS deposited Amount.
- The user calls
CDS::withdraw, where the downside protection shortfall is fulfilled via layer zero call. - 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
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();
}