csanuragjain
medium
It is possible to steal all ERC20 tokens which are lying on CrossDomainMessenger. This is possible due to unsafe withdrawTo function in L2StandardBridge
-
Assume L1 CrossDomainMessenger has X amount of WETH and we want to steal all those WETH
-
Attacker sends message using L2 CrossDomainMessenger using function sendMessage
_sendMessage(
OTHER_MESSENGER,
baseGas(_message, _minGasLimit),
msg.value,
abi.encodeWithSelector(
this.relayMessage.selector,
messageNonce(),
msg.sender,
_target,
msg.value,
_minGasLimit,
_message
)
);
- Lets say below params were passed for Step 1
_target = L1StandardBridge.sol contract
_message = Call function bridgeERC20To with Arguments (WETH, WethWrappedRemote, Attacker_Address, X, _minGasLimit, "")
-
Eventually this transaction is finalized and executed by Portal.
-
This calls the relayMessage function of CrossDomainMessenger with _message (from step 2)
-
relayMessage makes call to target with _message
bool success = SafeCall.call(_target, gasleft() - RELAY_GAS_BUFFER, _value, _message);
-
This makes call to _target which is L1StandardBridge.sol. calldata _message points to function bridgeERC20To with Arguments (WETH, WethWrappedRemote, Attacker_Address, X, _minGasLimit, "")
-
So finally bridgeERC20To function is called. msg.sender will be L1 CrossDomainMessenger as this is called by CrossDomainMessenger
function bridgeERC20To(
address _localToken,
address _remoteToken,
address _to,
uint256 _amount,
uint32 _minGasLimit,
bytes calldata _extraData
) public virtual {
_initiateBridgeERC20(
_localToken,
_remoteToken,
msg.sender,
_to,
_amount,
_minGasLimit,
_extraData
);
}
- _initiateBridgeERC20 is called like below:
_initiateBridgeERC20(
WETH,
WethWrappedRemote,
L1CrossDomainMessenger,
Attacker_Address,
X,
_minGasLimit,
""
);
- Finally _initiateBridgeERC20 is triggered which is defined as:
function _initiateBridgeERC20(
address _localToken,
address _remoteToken,
address _from,
address _to,
uint256 _amount,
uint32 _minGasLimit,
bytes calldata _extraData
) internal {
if (_isOptimismMintableERC20(_localToken)) {
...
} else {
IERC20(_localToken).safeTransferFrom(_from, address(this), _amount);
deposits[_localToken][_remoteToken] = deposits[_localToken][_remoteToken] + _amount;
}
emit ERC20BridgeInitiated(_localToken, _remoteToken, _from, _to, _amount, _extraData);
MESSENGER.sendMessage(
address(OTHER_BRIDGE),
abi.encodeWithSelector(
this.finalizeBridgeERC20.selector,
// Because this call will be executed on the remote chain, we reverse the order of
// the remote and local token addresses relative to their order in the
// finalizeBridgeERC20 function.
_remoteToken,
_localToken,
_from,
_to,
_amount,
_extraData
),
_minGasLimit
);
}
- The below statement transfers X WETH from CrossDomainMessenger to L1 StandardBridge
IERC20(_localToken).safeTransferFrom(_from, address(this), _amount);
-
Finally message is sent to L2 this.finalizeBridgeERC20.selector
-
This message reaches L2 and finalizeBridgeERC20 function is executed
function finalizeBridgeERC20(
address _localToken,
address _remoteToken,
address _from,
address _to,
uint256 _amount,
bytes calldata _extraData
) public onlyOtherBridge {
if (_isOptimismMintableERC20(_localToken)) {
require(
_isCorrectTokenPair(_localToken, _remoteToken),
"StandardBridge: wrong remote token for Optimism Mintable ERC20 local token"
);
OptimismMintableERC20(_localToken).mint(_to, _amount);
}
...
}
-
As we can see the WethWrappedRemote gets minted to Attacker address (to address)
-
So finally ERC20 held by CrossDomainMessenger is now stolen by Attacker and attacker can simply withdraw this and get the WETH back in L1
Any ERC20 token which is lying in CrossDomainMessenger can be stolen. The proxy for L1 CrossDomainMessenger (0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1) shows User mistake where SHIB token has been transferred to the proxy. This balance or any future coming balance on CrossDomainMessenger can be stolen by Attacker
Manual Review
Revert if msg.sender is CrossDomainMessenger in any public Bridge contract function