# Learning RW CTF 2024 - SafeBridge [toc] ## Intro https://github.com/farazsth98/CTF/tree/master/realworldctf-2024/safebridge <br> ## Bridge ### Happy flow 1. User initiate bridge request - `L1ERC20Bridge.depositERC20To` - `Emit` message 2. Relayer pick up the message (off-chain) 3. Send message to L2 - `L1ERC20Bridge.finalizeDeposit` >![圖片](https://hackmd.io/_uploads/Sk3OeG46A.png) > Source: https://ctftime.org/writeup/38600 <br> ### `L1ERC20Bridge` - `depositERC20To` -> `_initiateERC20Deposit` -> `emit ERC20DepositInitiated` ```solidity= function depositERC20(address _l1Token, address _l2Token, uint256 _amount) external virtual { _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount); } /** * @inheritdoc IL1ERC20Bridge */ function depositERC20To(address _l1Token, address _l2Token, address _to, uint256 _amount) external virtual { _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount); } function _initiateERC20Deposit(address _l1Token, address _l2Token, address _from, address _to, uint256 _amount) internal { IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount); bytes memory message; if (_l1Token == weth) { message = abi.encodeWithSelector( IL2ERC20Bridge.finalizeDeposit.selector, address(0), Lib_PredeployAddresses.L2_WETH, _from, _to, _amount ); } else { message = abi.encodeWithSelector(IL2ERC20Bridge.finalizeDeposit.selector, _l1Token, _l2Token, _from, _to, _amount); } sendCrossDomainMessage(l2TokenBridge, message); deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount; emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount); } ``` <br> ### **Relayer** Relayer pickup the `SentMessage` and relay the message ```python= ... def _relayer_worker( self, src_web3: Web3, src_messenger: Contract, dst_messenger: Contract ): _src_chain_id = src_web3.eth.chain_id _last_processed_block_number = 0 while True: try: latest_block_number = src_web3.eth.block_number if _last_processed_block_number > latest_block_number: _last_processed_block_number = latest_block_number print( f"chain {_src_chain_id} syncing {_last_processed_block_number + 1} {latest_block_number}" ) for i in range( _last_processed_block_number + 1, latest_block_number + 1 ): _last_processed_block_number = i logs = src_messenger.events.SentMessage().get_logs( fromBlock=i, toBlock=i ) for log in logs: print(f"chain {_src_chain_id} got log {src_web3.to_json(log)}") try: tx_hash = dst_messenger.functions.relayMessage( log.args["target"], log.args["sender"], log.args["message"], log.args["messageNonce"], ).transact() dst_messenger.w3.eth.wait_for_transaction_receipt(tx_hash) print( f"chain {_src_chain_id} relay message hash: {tx_hash.hex()} src block number: {i}" ) time.sleep(1) except Exception as e: print(e) ``` <br> ### L2ERC20Bridge - Finalize the deposit ```solidity= function finalizeDeposit( address _l1Token, address _l2Token, address _from, address _to, uint256 _amount ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) { // Check the target token is compliant and // verify the deposited token on L1 matches the L2 deposited token representation here if ( ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) && _l1Token == IL2StandardERC20(_l2Token).l1Token() ) { IL2StandardERC20(_l2Token).mint(_to, _amount); emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount); } else { emit DepositFailed(_l1Token, _l2Token, _from, _to, _amount); } } ``` <br> ## Issue - Mapping mismatch with the emit message At `L1ERC20Bridge._initiateERC20Deposit()` - If L1 token is weth, it updates the following mapping `deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount;` Source code ```solidity= function _initiateERC20Deposit(address _l1Token, address _l2Token, address _from, address _to, uint256 _amount) internal { IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount); bytes memory message; if (_l1Token == weth) { message = abi.encodeWithSelector( IL2ERC20Bridge.finalizeDeposit.selector, address(0), Lib_PredeployAddresses.L2_WETH, _from, _to, _amount ); } else { message = abi.encodeWithSelector(IL2ERC20Bridge.finalizeDeposit.selector, _l1Token, _l2Token, _from, _to, _amount); } sendCrossDomainMessage(l2TokenBridge, message); deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount; emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount); } ``` <br> However, at the message it sends to L2 - the `l2Token` been set to the `weth` This results in an inconsistent mapping between L1 and L2 chain ```solidity abi.encodeWithSelector( IL2ERC20Bridge.finalizeDeposit.selector, address(0), Lib_PredeployAddresses.L2_WETH, _from, _to, _amount ) ``` <br> ## Attack Case Normal Case 1. L1 -> L2 (Deposit WETH) - `WETH : WETH` * L1 Mapping:[L1WETH][L2WETH] += 2 * L2 Mapping:[L1WETH][L2WETH] += 2 2. L2 -> L1 (Withdrawal WETH): * L1 Mapping:[L1WETH][L2WETH] -= 2 * L2 Mapping:[L1WETH][L2WETH] -= 2 <br> Attack Case 1. L1 -> L2 (Deposit WETH):`WETH : FAKE Coin` * L1 Mapping:[L1WETH][L2FAKE] += 2 (Wrong mapping) * L2 Mapping:[L1WETH][L2WETH] += 2 (Because of the wrong `abi.encodeWithSelector` message) 2. L2 -> L1 (Withdrawal WETH): Wrong update deposits[L1_WETH][L2_WETH] -= 2 deposits[L1_WETH][L2_WETH] -= 2 Actual balance * L1 Mapping:[L1_WETH][FAKE_TOKEN] = 2 * L1 Mapping:[L1WETH][L2WETH] = 0 * L2 Mapping:[L1WETH][L2WETH] = 0