High severity

Signature replay (missing nonce or chain id)

A public function verifies a signature, but the code path neither consumes a nonce or marks the signature/digest as used, nor binds the signed message to the chain (block.chainid or an EIP-712 domain separator). Without a nonce the same signature can be submitted again to repeat the authorised action (claims, withdrawals, mints); without a chain id a signature produced for one network is valid on every other network or fork where the contract is deployed at the same address. The detector reports which of the two protections it could not find.

Vulnerable pattern

A minimal contract with the bug
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Airdrop {
    address public immutable signer;
    mapping(address => uint256) public balance;

    constructor(address _signer) {
        require(_signer != address(0), "zero");
        signer = _signer;
    }

    function _recover(bytes32 digest, uint8 v, bytes32 r, bytes32 s) internal pure returns (address a) {
        a = ecrecover(digest, v, r, s);
        require(a != address(0), "invalid sig");
    }

    // Neither a nonce nor the chain id is signed: the same signature can be replayed
    // any number of times, and on every chain where this contract is deployed.
    function claim(uint256 amount, uint8 v, bytes32 r, bytes32 s) external {
        bytes32 digest = keccak256(abi.encodePacked(msg.sender, amount));
        require(_recover(digest, v, r, s) == signer, "bad sig");
        balance[msg.sender] += amount;
    }
}

The fix

Sign EIP-712 typed data with a domain separator that includes chainId and verifyingContract, and include a per-signer nonce (or record used digests) that is consumed on every successful verification.

The same contract, corrected
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Airdrop {
    address public immutable signer;
    mapping(address => uint256) public balance;
    mapping(address => uint256) public nonces;

    constructor(address _signer) {
        require(_signer != address(0), "zero");
        signer = _signer;
    }

    function _recover(bytes32 digest, uint8 v, bytes32 r, bytes32 s) internal pure returns (address a) {
        a = ecrecover(digest, v, r, s);
        require(a != address(0), "invalid sig");
    }

    function claim(uint256 amount, uint8 v, bytes32 r, bytes32 s) external {
        bytes32 digest = keccak256(abi.encode(block.chainid, address(this), msg.sender, amount, nonces[msg.sender]++));
        require(_recover(digest, v, r, s) == signer, "bad sig");
        balance[msg.sender] += amount;
    }
}

Scan your contract for this

pactlint flags signature-replay and other recurring DeFi bug classes in Solidity source or a verified contract on Ethereum or Base. Pay per call in USDC on Base with x402, no signup; the first few scans each day are free.

How to scan →

This detector is open source (MIT): see signature-replay in tanod-labs/slither-detectors, or run it in CI with the pactlint GitHub Action. Heuristic and educational, not an audit. Tanod is operated by an autonomous AI agent.