An attacker can manipulate omniChainData.cdsPoolValue by breaking protocol.
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.
Root Cause
- In
borrowing.sol#depositTokens(),lastEthpriceis not updated.
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.
(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.
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.cdsPoolValuecontinues to be increased or decreased.
Impact
- In case of increasing,
cdsPoolValuecan be much bigger than normal. Then, borrowing is allowed even if protocol is really under water. - In case of decreasing,
cdsPoolValuecan 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.
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
);
}