Powerful Honeysuckle Anteater
High
The current design of cross-messaging results in overriding omniChain data, which could lead to catastrophic inconsistencies.
This issue occurs in multiple parts of the codebase, specifically every time omniChain data is transferred. The data is simply overridden, leading to potential conflicts.
We have two chains, X
and Y
(in reality, Optimism
and Mode L2
). Initially, both chains start with the same omniChain data.
X
gets a new dCDS deposit and needs to sendY
the updated omniChain data.X
-->LayerZero
LayerZero
-->Y
- Now both chains have the same updated omniChain data.
- Both chains frequently interact and simultaneously send
LayerZero
requests to update omniChain data on the other chain. This creates a situation similar to race conditions in concurrency, where atomic operations are not used. X
-->LayerZero
Y
-->LayerZero
Collisions occur because X
will receive Y
's data, and Y
will receive X
's data, but neither will have the combined updated state.
For example, assume both X
and Y
have a totalDeposit value of 1
.
X
updates it to2
and sends a request viaLayerZero
toY
.- At the same time,
Y
updates its totalDeposit to4
and sends a request viaLayerZero
toX
.
The resulting state:
X
has totalDeposit set to4
.Y
has totalDeposit set to2
.
The correct state should have been totalDeposit set to 6
on both chains, reflecting the total increase of 4
.
This issue occurs because values are directly overridden instead of tracking deltas in the changes.
Example & code reference:
- We override it here when the destination chain receives the message GlobalVariables.sol#L637-L638
function _lzReceive(
Origin calldata /*_origin*/,
bytes32 /*_guid*/,
bytes calldata payload,
address /*_executor*/,
bytes calldata /*_extraData*/
) internal override {
// Decoding the message from src
OAppData memory oappData = abi.decode(payload, (OAppData));
....Skipping Code....
// Update the global omnichain data struct
@>> omniChainData = oappData.message;
// Update the individual collateral data
s_collateralData[oappData.assetName] = oappData.collateralData;
}
- We are sending a message for every state-changing interaction, such as Borrowers deposit/withdraw/renew/liquidate and dCDS users deposit/withdraw/redeem. Example: At the end of a deposit() call in CDSLib.sol#L594 here we just override the global data, before sending the cross-chain message.
function deposit(
CDSInterface.DepositUserParams memory params,
mapping(address => CDSInterface.CdsDetails) storage cdsDetails,
CDSInterface.Interfaces memory interfaces
) public returns (CDSInterface.DepositResult memory) {
.....Skipping code.....
@>> interfaces.globalVariables.setOmniChainData(omniChainData);
return CDSInterface.DepositResult(
params.usdtAmountDepositedTillNow,
params.totalCdsDepositedAmount,
params.totalCdsDepositedAmountWithOptionFees,
params.totalAvailableLiquidationAmount,
params.cdsCount
);
}
and then after the call to the library, in deposit()
function in CDS.sol we call the globalVariables to send the omnichain message:
function deposit(
uint128 usdtAmount,
uint128 usdaAmount,
bool liquidate,
uint128 liquidationAmount,
uint128 lockingPeriod
) public payable nonReentrant whenNotPaused(IMultiSign.Functions(4)) {
// Get eth price
uint128 ethPrice = getLatestData();
// Check the eth price is non zero
if (ethPrice == 0) revert CDS_ETH_PriceFeed_Failed();
DepositResult memory result = CDSLib.deposit(
DepositUserParams(
usdtAmount,
usdaAmount,
liquidate,
liquidationAmount,
ethPrice,
lastEthPrice,
usdaLimit,
usdtLimit,
usdtAmountDepositedTillNow,
totalCdsDepositedAmount,
totalCdsDepositedAmountWithOptionFees,
totalAvailableLiquidationAmount,
cdsCount,
lockingPeriod
),
cdsDetails,
Interfaces(
treasury,
globalVariables,
usda,
usdt,
borrowing,
CDSInterface(address(this))
)
);
...Skipping Code...
// getting options since,the src don't know the dst state
bytes memory _options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(400000, 0);
// calculting fee
MessagingFee memory fee = globalVariables.quote(
IGlobalVariables.FunctionToDo(1),
IBorrowing.AssetName(0),
_options,
false
);
// Calling Omnichain send function
@>> globalVariables.send{value: fee.nativeFee}(
IGlobalVariables.FunctionToDo(1),
IBorrowing.AssetName(0),
fee,
_options,
msg.sender
);
}
No response
No response
The issue described in the scenarios above can occur through normal interactions. Additionally, a malicious party could exploit this to take arbitrage opportunities or deliberately disrupt the state, which could have catastrophic consequences.
Given the protocol manages numerous cross-chain interactions, including transferring ETH tokens and tracking global variables, failing to maintain a consistent state and instead overriding it could easily result in loss of funds, protocol insolvency, and other severe issues.
Instead of directly overriding the omniChain data, implement a system to send only deltas. This approach would prevent collisions and ensure state consistency.