Possible DOS on cross-chain messages
Description
The LayerZero cross-chain messaging application requires paying a fee for each message sent. The fee is usually paid in the native token of the source chain. Currently, the CrossChainRelayUpgradeable contract does not check the msg.value
of the call against the necessary fee required for performing the cross-chain message. This means that an attacker can send a cross-chain message without paying the fee, which leads to the funds being taken from the relayer's account.
function sendMessage(OrderlyCrossChainMessage.MessageV1 memory data, bytes memory payload)
public
payable
override
onlyCaller
{
bytes memory lzPayload = data.encodeMessageV1AndPayload(payload);
uint16 lzDstChainId = _chainIdMapping[data.dstChainId];
require(lzDstChainId != 0, "CrossChainRelay: invalid dst chain id");
uint16 version = 1;
uint256 gasLimit = _flowGasLimitMapping[data.method];
if (gasLimit == 0) {
gasLimit = 3000000;
}
bytes memory adapterParams = abi.encodePacked(version, gasLimit);
(uint256 nativeFee,) = lzEndpoint.estimateFees(lzDstChainId, address(this), lzPayload, false, adapterParams);
_lzSend(
lzDstChainId, // _dstChainId
lzPayload, // _payload
payable(address(this)), // _refundAddress @audit why refund to this contract? users could then maybe have 0 as msg.value
// and send msges with what's already in this contract
address(0), // _zroPaymentAddress
adapterParams, // _adapterParams
nativeFee // _nativeFee
);
emit MessageSent(data, payload);
}
Impact
An attacker may drain the relayer's account by performing a large number of cross-chain messages with msg.value
set to zero. This could in turn lead to a temporary denial-of-service attack on the relayer, as they would no longer be able to send cross-chain messages until they replenish their account.
Recommendations
We recommend checking the msg.value
of the call against the necessary fee required for performing the cross-chain message.
function sendMessage(OrderlyCrossChainMessage.MessageV1 memory data, bytes memory payload)
public
payable
override
onlyCaller
{
// ...
(uint256 nativeFee,) = lzEndpoint.estimateFees(lzDstChainId, address(this), lzPayload, false, adapterParams);
+ require(msg.value >= nativeFee, "CrossChainRelay: insufficient msg.value");
// ...
}
Moreover, we also recommend changing the _refundAddress
to one of the user's choosing, instead of the contract's address, such that the user can receive the refund of the msg.value
if the cross-chain message fails.
function sendMessage(OrderlyCrossChainMessage.MessageV1 memory data, bytes memory payload)
public
payable
override
onlyCaller
{
// ...
_lzSend(
lzDstChainId, // _dstChainId
lzPayload, // _payload
- payable(address(this)), // _refundAddress
+ payable(userAddress), // _refundAddress
address(0), // _zroPaymentAddress
adapterParams, // _adapterParams
nativeFee // _nativeFee
);
}
Remediation
This issue has been acknowledged by Orderly Network, and a fix was implemented in commit 10991618↗.