# 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`
>
> 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