Composing approval with other messages is subject to DoS
Summary
TOFT::sendPacket function allows the caller to specify multiple messages that are executed on the destination chain.
On the receiving side the lzCompose function in TOFT contract can be DoS-ed by front-running the approval message and causing the lzCompose to revert.
As lzCompose is supposed to process several messages, this results in lost fee paid on the sending chain for executing the subsequent messages and any value or gas airdropped to the contract.
Vulnerability Detail
TOFT::sendPacket allows the caller to specify arbitrary _composeMsg. It can be a single message or multiple composed messages.
function _lzCompose(address srcChainSender_, bytes32 _guid, bytes memory oftComposeMsg_) internal {
// Decode OFT compose message.
>>>> (uint16 msgType_,,, bytes memory tapComposeMsg_, bytes memory nextMsg_) =
>>>> TapiocaOmnichainEngineCodec.decodeToeComposeMsg(oftComposeMsg_);
// Call Permits/approvals if the msg type is a permit/approval.
// If the msg type is not a permit/approval, it will call the other receivers.
if (msgType_ == MSG_REMOTE_TRANSFER) {
_remoteTransferReceiver(srcChainSender_, tapComposeMsg_);
} else if (!_extExec(msgType_, tapComposeMsg_)) {
// Check if the TOE extender is set and the msg type is valid. If so, call the TOE extender to handle msg.
if (
address(tapiocaOmnichainReceiveExtender) != address(0)
&& tapiocaOmnichainReceiveExtender.isMsgTypeValid(msgType_)
) {
bytes memory callData = abi.encodeWithSelector(
ITapiocaOmnichainReceiveExtender.toeComposeReceiver.selector,
msgType_,
srcChainSender_,
tapComposeMsg_
);
(bool success, bytes memory returnData) =
address(tapiocaOmnichainReceiveExtender).delegatecall(callData);
if (!success) {
revert(_getTOEExtenderRevertMsg(returnData));
}
} else {
// If no TOE extender is set or msg type doesn't match extender, try to call the internal receiver.
if (!_toeComposeReceiver(msgType_, srcChainSender_, tapComposeMsg_)) {
revert InvalidMsgType(msgType_);
}
}
}
emit ComposeReceived(msgType_, _guid, tapComposeMsg_);
>>>> if (nextMsg_.length > 0) {
>>>> _lzCompose(address(this), _guid, nextMsg_);
}
}
At the beginning of the function bytes memory tapComposeMsg_ is the message being processed, while bytes memory nextMsg_ are all the other messages. lzCompose will process all the messages until nextMsg_ is empty.
A user might want to have his first message to grant approval, e.g. _extExec function call, while his second message might execute BaseTOFTReceiver::_toeComposeReceiver with _msgType == MSG_YB_SEND_SGL_BORROW.
This is a problem as there is a clear DoS attack vector on granting any approvals. A griever can observe the permit message from the user and front-run the lzCompose call and submit the approval on the user's behalf.
As permits use nonce it can't be replayed, which means if anyone front-runs the permit, the original permit will revert.
This means that lzCompose always reverts and all the gas and value to process the BaseTOFTReceiver::_toeComposeReceiver with _msgType == MSG_YB_SEND_SGL_BORROW is lost for the user.
When user is granting approvals and wants to execute any other message in the same lzCompose call, the attacker can deny the user from executing the other message by front-running the approval message and causing the lzCompose to revert.
The impact is lost fee paid on the sending chain for executing the subsequent messages and any value or gas airdropped to the contract. This is especially severe when the user wants to withdraw funds to another chain, as he needs to pay for that fee on the sending chain.
GiuseppeDeLaZara
medium
Composing approval with other messages is subject to DoS
Summary
TOFT::sendPacket
function allows the caller to specify multiple messages that are executed on the destination chain. On the receiving side thelzCompose
function inTOFT
contract can be DoS-ed by front-running the approval message and causing thelzCompose
to revert. AslzCompose
is supposed to process several messages, this results in lost fee paid on the sending chain for executing the subsequent messages and any value or gas airdropped to the contract.Vulnerability Detail
TOFT::sendPacket
allows the caller to specify arbitrary_composeMsg
. It can be a single message or multiple composed messages.If we observe the logic inside the
lzCompose
:At the beginning of the function bytes memory
tapComposeMsg_
is the message being processed, whilebytes memory nextMsg_
are all the other messages.lzCompose
will process all the messages untilnextMsg_
is empty.A user might want to have his first message to grant approval, e.g.
_extExec
function call, while his second message might executeBaseTOFTReceiver::_toeComposeReceiver
with_msgType == MSG_YB_SEND_SGL_BORROW
.This is a problem as there is a clear DoS attack vector on granting any approvals. A griever can observe the permit message from the user and front-run the
lzCompose
call and submit the approval on the user's behalf.As permits use nonce it can't be replayed, which means if anyone front-runs the permit, the original permit will revert. This means that
lzCompose
always reverts and all the gas and value to process theBaseTOFTReceiver::_toeComposeReceiver
with_msgType == MSG_YB_SEND_SGL_BORROW
is lost for the user.Permit based DoS attack is described in detail in the following article by Trust-Security: https://www.trust-security.xyz/post/permission-denied.
Impact
When user is granting approvals and wants to execute any other message in the same
lzCompose
call, the attacker can deny the user from executing the other message by front-running the approval message and causing thelzCompose
to revert. The impact is lost fee paid on the sending chain for executing the subsequent messages and any value or gas airdropped to the contract. This is especially severe when the user wants to withdraw funds to another chain, as he needs to pay for that fee on the sending chain.Code Snippet
Tool used
Manual Review
Recommendation
TOFT::sendPacket
should do extra checks to ensure if the message contains approvals, it should not allow packing several messages.