Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 91 additions & 44 deletions src/NonceManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/Sig

import { INonceManager } from "./interfaces/INonceManager.sol";
import { EIP712 } from "./lib/EIP712.sol";
import { MerkleProof } from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

import { TreeNodeLib } from "./lib/TreeNodeLib.sol";

/**
* @title NonceManager
Expand All @@ -33,20 +34,41 @@ abstract contract NonceManager is INonceManager, EIP712 {
*/
mapping(address => mapping(bytes32 => bool)) internal usedNonces;

/**
* @notice EIP-712 typehash for single-chain signed nonce invalidation
* @dev Includes owner, deadline, and NoncesToInvalidate struct for single-chain invalidation
* @dev Parallel to PERMIT3_TYPEHASH pattern
*/
bytes32 public constant INVALIDATE_NONCES_TYPEHASH = keccak256(
"InvalidateNonces(address owner,uint48 deadline,NoncesToInvalidate noncesToInvalidate)NoncesToInvalidate(uint64 chainId,bytes32[] salts)"
);

/**
* @notice EIP-712 typehash for tree-based multi-chain nonce invalidation
* @dev Includes owner, deadline, and NonceNode tree for UI transparency and cross-chain invalidation
* @dev Binary tree structure where leaves are NoncesToInvalidate (chain-specific nonce lists)
* @dev Parallel to MULTICHAIN_PERMIT3_TYPEHASH pattern
*/
bytes32 public constant MULTICHAIN_INVALIDATE_NONCES_TYPEHASH = keccak256(
"InvalidateNonces(address owner,uint48 deadline,NonceNode nonceTree)NonceNode(NonceNode[] nodes,NoncesToInvalidate[] nonces)NoncesToInvalidate(uint64 chainId,bytes32[] salts)"
);

/**
* @notice EIP-712 typehash for NonceNode structure
* @dev Used for hashing NonceNode in tree reconstruction with MULTICHAIN_INVALIDATE_NONCES_TYPEHASH
* @dev Binary tree where leaves are NoncesToInvalidate structs, not raw bytes32 nonces
*/
bytes32 internal constant NONCE_NODE_TYPEHASH = keccak256(
"NonceNode(NonceNode[] nodes,NoncesToInvalidate[] nonces)NoncesToInvalidate(uint64 chainId,bytes32[] salts)"
);

/**
* @notice EIP-712 typehash for nonce invalidation
* @dev Includes chainId for cross-chain replay protection
*/
bytes32 public constant NONCES_TO_INVALIDATE_TYPEHASH =
keccak256("NoncesToInvalidate(uint64 chainId,bytes32[] salts)");

/**
* @notice EIP-712 typehash for invalidation signatures
* @dev Includes owner, deadline, and unbalanced root for batch operations
*/
bytes32 public constant CANCEL_PERMIT3_TYPEHASH =
keccak256("CancelPermit3(address owner,uint48 deadline,bytes32 merkleRoot)");

/**
* @notice Initialize EIP-712 domain separator
* @param name Contract name for EIP-712 domain
Expand Down Expand Up @@ -89,73 +111,98 @@ abstract contract NonceManager is INonceManager, EIP712 {

/**
* @notice Invalidate nonces using a signed message
* @param owner Address that signed the invalidation
* @param deadline Timestamp after which signature is invalid
* @param salts Array of nonce salts to invalidate
* @param signature EIP-712 signature authorizing invalidation
* @param sig Signature data (owner, deadline, signature)
*/
function invalidateNonces(
address owner,
uint48 deadline,
bytes32[] calldata salts,
bytes calldata signature
NonceSignature calldata sig
) external {
if (block.timestamp > deadline) {
revert SignatureExpired(deadline, uint48(block.timestamp));
if (block.timestamp > sig.deadline) {
revert SignatureExpired(sig.deadline, uint48(block.timestamp));
}

NoncesToInvalidate memory invalidations = NoncesToInvalidate({ chainId: uint64(block.chainid), salts: salts });

// Hash the NoncesToInvalidate struct according to EIP-712
bytes32 invalidationsHash = keccak256(
abi.encode(
NONCES_TO_INVALIDATE_TYPEHASH, invalidations.chainId, keccak256(abi.encodePacked(invalidations.salts))
)
);

bytes32 signedHash =
keccak256(abi.encode(CANCEL_PERMIT3_TYPEHASH, owner, deadline, hashNoncesToInvalidate(invalidations)));
keccak256(abi.encode(INVALIDATE_NONCES_TYPEHASH, sig.owner, sig.deadline, invalidationsHash));

_verifySignature(owner, signedHash, signature);
_verifySignature(sig.owner, signedHash, sig.signature);

_processNonceInvalidation(owner, salts);
_processNonceInvalidation(sig.owner, salts);
}

/**
* @notice Cross-chain nonce invalidation using the Unbalanced Merkle Tree approach
* @param owner Token owner
* @param deadline Signature expiration
* @param proof Unbalanced Merkle Tree invalidation proof
* @param signature Authorization signature
* @notice Invalidate multiple nonces using tree structure with UI transparency
* @dev User signs complete NonceNode showing all nonces being invalidated
* @dev Reconstructs NonceNode hash from compact encoding for signature verification
* @param tree NonceTree containing proofStructure, currentChainInvalidations, and proof
* @param sig Signature data (owner, deadline, signature)
*/
function invalidateNonces(
address owner,
uint48 deadline,
NoncesToInvalidate calldata invalidations,
bytes32[] calldata proof,
bytes calldata signature
NonceTree calldata tree,
NonceSignature calldata sig
) external {
if (block.timestamp > deadline) {
revert SignatureExpired(deadline, uint48(block.timestamp));
// Validate deadline
if (block.timestamp > sig.deadline) {
revert SignatureExpired(sig.deadline, uint48(block.timestamp));
}
if (invalidations.chainId != uint64(block.chainid)) {
revert WrongChainId(uint64(block.chainid), invalidations.chainId);

// Validate chain ID matches current chain
if (tree.currentChainInvalidations.chainId != uint64(block.chainid)) {
revert WrongChainId(uint64(block.chainid), tree.currentChainInvalidations.chainId);
}

// Calculate the root from the invalidations and proof
// processProof performs validation internally and provides granular error messages
bytes32 invalidationsHash = hashNoncesToInvalidate(invalidations);
bytes32 merkleRoot = MerkleProof.processProof(proof, invalidationsHash);
// Hash the current chain's NoncesToInvalidate as a leaf
bytes32 currentChainHash = hashNoncesToInvalidate(tree.currentChainInvalidations);

bytes32 signedHash = keccak256(abi.encode(CANCEL_PERMIT3_TYPEHASH, owner, deadline, merkleRoot));
// Reconstruct the NonceNode hash from the proof and tree structure
bytes32 nonceNodeHash =
TreeNodeLib.computeTreeHash(NONCE_NODE_TYPEHASH, tree.proofStructure, tree.proof, currentChainHash);

_verifySignature(owner, signedHash, signature);
// Verify signature with MULTICHAIN_INVALIDATE_NONCES_TYPEHASH
bytes32 signedHash =
keccak256(abi.encode(MULTICHAIN_INVALIDATE_NONCES_TYPEHASH, sig.owner, sig.deadline, nonceNodeHash));

_verifySignature(sig.owner, signedHash, sig.signature);

_processNonceInvalidation(owner, invalidations.salts);
// Process nonce cancellation
_processNonceInvalidation(sig.owner, tree.currentChainInvalidations.salts);
}

/**
* @notice Generate EIP-712 hash for nonce invalidation data
* @param invalidations Struct containing chain ID and nonces
* @return bytes32 Hash of the invalidation data
* @notice Generate EIP-712 hash for NoncesToInvalidate struct
* @dev Hashes the struct for use as a leaf in tree reconstruction
* @dev Uses single-nonce optimization for gas efficiency
* @dev Preserves salt order - no sorting (order matters for hash)
* @param invalidations Struct containing chain ID and nonces to invalidate
* @return bytes32 Hash suitable for tree leaf
*/
function hashNoncesToInvalidate(
NoncesToInvalidate memory invalidations
) public pure returns (bytes32) {
return keccak256(abi.encode(NONCES_TO_INVALIDATE_TYPEHASH, invalidations.chainId, invalidations.salts));
if (invalidations.salts.length == 0) {
return bytes32(0);
}

// Single nonce optimization - use the nonce directly as hash
if (invalidations.salts.length == 1) {
return invalidations.salts[0];
}

// Multiple nonces - hash as EIP-712 NoncesToInvalidate struct
return keccak256(
abi.encode(
NONCES_TO_INVALIDATE_TYPEHASH, invalidations.chainId, keccak256(abi.encodePacked(invalidations.salts))
)
);
}

/**
Expand Down
63 changes: 46 additions & 17 deletions src/interfaces/INonceManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,44 @@ interface INonceManager is IPermit {
bytes32[] salts;
}

/**
* @notice Nested structure for batch nonce invalidation with UI transparency
* @dev Similar to PermitNode - enables tree-based nonce invalidation
* @dev Binary tree structure where leaves are NoncesToInvalidate (chain-specific nonce lists)
* @param nodes Child nonce tree nodes (nested structures)
* @param nonces Leaf invalidations (NoncesToInvalidate for each chain)
*/
struct NonceNode {
NonceNode[] nodes;
NoncesToInvalidate[] nonces;
}

/**
* @notice Signature data for nonce invalidation operations
* @dev Parallel to IPermit3.Signature but without salt and timestamp fields
* @param owner Address that owns the nonces being invalidated
* @param deadline Timestamp after which signature expires
* @param signature EIP-712 signature bytes
*/
struct NonceSignature {
address owner;
uint48 deadline;
bytes signature;
}

/**
* @notice Input struct for tree-based nonce invalidation containing tree structure data
* @dev Parallel to IPermit3.PermitTree struct
* @param currentChainInvalidations Nonces to invalidate for the current chain
* @param proofStructure Compact tree encoding (position + type flags)
* @param proof Array of sibling hashes for tree reconstruction
*/
struct NonceTree {
NoncesToInvalidate currentChainInvalidations;
bytes32 proofStructure;
bytes32[] proof;
}

/**
* @notice Export EIP-712 domain separator
* @return bytes32 domain separator hash
Expand Down Expand Up @@ -93,32 +131,23 @@ interface INonceManager is IPermit {

/**
* @notice Mark nonces as used with signature authorization
* @param owner Token owner address
* @param deadline Signature expiration timestamp
* @param salts Array of nonce salts to invalidate
* @param signature EIP-712 signature authorizing the invalidation
* @param sig Signature data (owner, deadline, signature)
*/
function invalidateNonces(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question here. Can't we just treat single-chain cancellation as a sing-node tree?

address owner,
uint48 deadline,
bytes32[] calldata salts,
bytes calldata signature
NonceSignature calldata sig
) external;

/**
* @notice Cross-chain nonce invalidation using Merkle Tree
* @param owner Token owner address
* @param deadline Signature expiration timestamp
* @param invalidations Current chain invalidation data
* @param proof Merkle proof array for verification
* @param signature EIP-712 signature authorizing the invalidation
* @notice Invalidate multiple nonces using tree structure with UI transparency
* @dev User signs complete NonceNode showing all nonces being invalidated
* @param tree NonceTree containing proofStructure, currentChainInvalidations, and proof
* @param sig Signature data (owner, deadline, signature)
*/
function invalidateNonces(
address owner,
uint48 deadline,
NoncesToInvalidate memory invalidations,
bytes32[] memory proof,
bytes calldata signature
NonceTree calldata tree,
NonceSignature calldata sig
) external;

/**
Expand Down
Loading