F-2025-0003·signature-replay-protection

Replay protection in mint() lacks nonce, preventing legitimate repeated mints with same parameters

Acknowledgednftstakingeip-712
TL;DR

Mint signatures are hashed only over (to, value, stakingPool). Repeated legitimate mints with identical parameters share the same hash and are blocked as replays after the first.

Severity
MEDIUM
Impact
MEDIUM
Likelihood
MEDIUM
Method
MManual review
CAT.
Complexity
LOW
Exploitability
MEDIUM
02Section · Description

Description

The mint() function in GenesisLicense.sol uses a signature hash composed of (to_, value_, stakingPool_) to prevent replay attacks:

solidity
bytes32 structHash = keccak256(abi.encode(MINT_INFO_TYPEHASH, to_, value_, stakingPool_));
bytes32 hash = _hashTypedDataV4(structHash);
require(!$._mintHashes[hash], "Used hash"); // Prevent replay
$._mintHashes[hash] = true;

Because this hash does not include a nonce, multiple mint attempts with the same parameters produce the same hash, causing valid mints to be rejected as replays.

03Section · Impact

Impact

A user who legitimately needs to mint the same (value, stakingPool) combination more than once (for example two NFTs at the same price into the same pool) cannot do so without obtaining a different signature. This forces operational workarounds and creates silent failures from the user perspective.

04Section · Recommendation

Recommendation

Implement an internal nonce per user (or per address) that is included in the signed data and incremented on every successful mint. This ensures every signature is unique even if the other parameters are the same, preventing replay attacks effectively while allowing repeated mints with identical parameters:

solidity
mapping(address => uint256) private _nonces;
function mint(
address to_,
uint256 value_,
address stakingPool_,
bytes calldata signature_
) external nonReentrant {
uint256 nonce = _nonces[to_];
bytes32 structHash = keccak256(
abi.encode(MINT_INFO_TYPEHASH, to_, value_, stakingPool_, nonce)
);
bytes32 hash = _hashTypedDataV4(structHash);
require(SignatureChecker.isValidSignatureNow($._verifier, hash, signature_), "Invalid signature");
require(!$._mintHashes[hash], "Used hash");
$._mintHashes[hash] = true;
_nonces[to_] += 1;
// Mint logic...
}
F-2025-0003

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx