Beginner FriendlyFoundryBridge
100 EXP
View results
Submission Details
Severity: high
Valid

Malicious signer can drain all the funds from the vault by signing the withdrawal message hash for self.

Summary

A malicious signer can potentially withdraw all the funds to themselves as they have full control over the funds transfer and can drain all the funds from the vault. The bridge is meant to allow the user to deposit and withdraw their bridged funds, but it can misused by the signer to drain all the funds, as instead of calling transfer for the user, they can call it for themselves with the full amount inside the vault.

Vulnerability Details

  • There is no mechanism inside L1BossBridge contract to track the withdrawal request being signed by signer actually exists or not due to which a malicious signer can sign for any withdrawal request that can be on their address and
    can potentially drain all the funds by signing the withdrawal message hash for themselves and drain all the funds from the Vault by calling L1BossBridge::withdrawTokensToL1 with their own signature containing the withdrawal request to send funds to their own address.

  • Also, a malicious signer can call L1BossBridge::depositTokensToL2 and after successful withdrawal on L2, the signer can then withdraw the token that was deposited on L1 by approving the withdrawal request for themselves.

Impact

High. A malicious signer can drain all funds.

Tools Used

Manual Review, Foundry Test

PoC

Paste the below test in test/L1TokenBridge.t.sol and run the test: forge test --mt test_MaliciousSignerCanDrainAllFunds

function test_MaliciousSignerCanDrainAllFunds() public {
uint256 depositAmount = 10e18;
vm.startPrank(user);
token.approve(address(tokenBridge), depositAmount);
tokenBridge.depositTokensToL2(user, userInL2, depositAmount);
vm.stopPrank();
uint256 vaultMaxBalance = token.balanceOf(address(vault));
bytes memory maliciousWithdrawalMessage = _getTokenWithdrawalMessage(operator.addr, vaultMaxBalance);
uint256 maliciousOperatorInitBalance = token.balanceOf(operator.addr);
(uint8 v, bytes32 r, bytes32 s) = _signMessage(maliciousWithdrawalMessage, operator.key);
tokenBridge.withdrawTokensToL1(operator.addr, vaultMaxBalance, v, r, s);
uint256 maliciousOperatorFinalBalance = token.balanceOf(operator.addr);
assert(vaultMaxBalance != 0);
assertEq(maliciousOperatorFinalBalance, maliciousOperatorInitBalance + vaultMaxBalance);
}

Recommendations

When deposit is made on a layer 1 to bridge tokens to layer 2 or vice-versa, then a off-chain event is triggered, so consider adding a nonce with that event which will uniquely identify that particular bridge request and store that withdrawal request corresponding to the nonce, which will make sure that a txn being signed by a signer actually exists.
So, when the off-chain mechanism picks up the event mints the corresponding tokens on L2, along with that the particular withdrawal request should be stored on-chain.

Updates

Lead Judging Commences

0xnevi Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

withdrawTokensToL1()/sendToL1(): signature replay

shikhar229169 Submitter
over 1 year ago
0xnevi Lead Judge
over 1 year ago
0xnevi Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

withdrawTokensToL1(): No check for deposits amount

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.