ladboy233
medium
Lack of expiration time for cross-chain message passing
In the current implementation, the L1CrossDomainMessagern.sol inherits from CrossDomainMessager.sol and L2CrossDomainMessenger.sol inherits from CrossMainMessager.sol as well.
In CrossDomainMessager.sol, we have this function:
function sendMessage(
address _target,
bytes calldata _message,
uint32 _minGasLimit
) external payable {
// Triggers a message to the other messenger. Note that the amount of gas provided to the
// message is the amount of gas requested by the user PLUS the base gas value. We want to
// guarantee the property that the call to the target contract will always have at least
// the minimum gas limit specified by the user.
_sendMessage(
OTHER_MESSENGER,
baseGas(_message, _minGasLimit),
msg.value,
abi.encodeWithSelector(
this.relayMessage.selector,
messageNonce(),
msg.sender,
_target,
msg.value,
_minGasLimit,
_message
)
);
emit SentMessage(_target, msg.sender, _message, messageNonce(), _minGasLimit);
emit SentMessageExtension1(msg.sender, msg.value);
unchecked {
++msgNonce;
}
}
The parent can other overwrite the _sendMessage method.
In L1CrossDomainMessagern.sol, the _sendMessage is overwrite to:
/**
* @inheritdoc CrossDomainMessenger
*/
function _sendMessage(
address _to,
uint64 _gasLimit,
uint256 _value,
bytes memory _data
) internal override {
PORTAL.depositTransaction{ value: _value }(_to, _value, _gasLimit, false, _data);
}
In L2CrossDomainMessagern.sol, the _sendMessage is overwrite to:
/**
* @inheritdoc CrossDomainMessenger
*/
function _sendMessage(
address _to,
uint64 _gasLimit,
uint256 _value,
bytes memory _data
) internal override {
L2ToL1MessagePasser(payable(Predeploys.L2_TO_L1_MESSAGE_PASSER)).initiateWithdrawal{
value: _value
}(_to, _gasLimit, _data);
}
The user can specify the target address with customized call data and attach ETH.
However, If we looked into the message data encoded format:
_sendMessage(
OTHER_MESSENGER,
baseGas(_message, _minGasLimit),
msg.value,
abi.encodeWithSelector(
this.relayMessage.selector,
messageNonce(),
msg.sender,
_target,
msg.value,
_minGasLimit,
_message
)
);
We see that there is no deadline and timestamp or expiration time encoded into the message.
For example, user A has a contract B in ethereum mainnet and contract C in optimism network.
Contract C in optimism is already paused by User A from operating because the User A identify issue in off-chain code that decode the transaction event from contract C.
After user A think he identify and fix the issue, User A performs a cross-chain call from ethereum network and try to deposit 0.1 ETH to contract C and try to unpause the contract C.
However, there is gas spike in ethereum network and the User A's deposit call with unpause instruction has low gas payment and the transaction is pending in the mempool.
Later, A hacker figured out a way to withdraw fund from contract C, the User A monitored on-chain activity for contract C in optimism network. User A fire up a deposit call with pause contract instruction and try to pause the contract C immediately.
The transaction get executed and Contract C is paused.
Later, the gas spike faded and the gas price from User A's previous depositTo with unpause instruction transation becomes profitable for miner to include the transction in the mainnet.
The unpause transaction get executed and the vulnerable contract C is unpaused after being paused because of the lack of expiration time in cross-chain message passing. The hacker is able to drain more fund from contract C because the contract is unexpectedly unpaused after paused.
The lack of deadline and timestamp in the cross-chain message allows outdated message and transaction to be maliciously executed.
Manual Review
We recommend the protocol add deadline and expiration timestamp when doing message passing.