Skip to content

Contracts Overview

Onchain architecture

NiceTry's onchain footprint is deliberately small: three stateless verifiers and two independent account families, each with its own deterministic factory. The main family, SimpleAccount, is a minimal proxy that tracks per-key authorisation state for its rotating FORS+C signers and holds one static SPHINCS backup key. The second family, SphincsAccount, holds a single standard SPHINCS key and nothing else.

Contract map

ContractFileRole
ForsVerifiersrc/Verifiers/ForsVerifier.solStateless FORS+C verifier: recover(sig, digest) → address
SphincsVerifiersrc/Verifiers/SphincsVerifier.solStateless SPHINCS backup verifier (FORS+C under WOTS+C, 3,688 B): verify(pkSeed, pkRoot, digest, sig) → bool
SphincsStandardVerifiersrc/Verifiers/SphincsStandardVerifier.solStateless standard SPHINCS verifier (plain FORS + WOTS+, 6,176 B), same verify ABI
SimpleAccountsrc/SimpleAccount.solERC-4337 account: FORS+C primary, SPHINCS backup, per-key authState, addSigner
SimpleAccountFactorysrc/SimpleAccountFactory.solCREATE2 clone factory; address commits to root and backup key
SphincsAccountsrc/SphincsAccount.solERC-4337 account with a single, never-rotated SPHINCS key
SphincsAccountFactorysrc/SphincsAccountFactory.solCREATE2 clone factory; address commits to the key
InitialSignerCommitmentsrc/InitialSignerCommitment.solDomain separators for the SimpleAccount salt, the activation leaf and the backup-key leaf
ISignatureVerifiersrc/Interfaces/ISignatureVerifier.solFORS+C verifier interface (recover)
ISphincsVerifiersrc/Interfaces/ISphincsVerifier.solSPHINCS verifier interface (verify), implemented by both SPHINCS verifiers

The two families never share an address, an implementation or a salt domain. Everything below describes SimpleAccount; the much smaller SphincsAccount picture is on its own page.

How they fit together

SimpleAccountFactory  --(CREATE2 clone)-->  SimpleAccount (EIP-1167 proxy)
                                                  |
                                                  | validateUserOp: dispatch on signature length
                                                  |
                          +-----------------------+-----------------------+
                          | 2,448 B               | 2,451 + 32n B         | 3,688 B
                          v                       v                       v
                 ForsVerifier.recover()   recover() + MerkleProofLib   SphincsVerifier.verify()
                          |                       |                       |
                          v                       v                       v
                 authState[key] ACTIVE?   leaf in initialSignerRoot?  (backupPkSeed, backupPkRoot)
                          |                       |                       |
                          +-----------------------+-----------------------+
                                                  |
                                                  v
                                   burn current key, activate nextOwner

The account talks to the verifiers only through their interfaces, so the signing schemes are pluggable dependencies rather than hard-wired logic:

interface ISignatureVerifier {
    /// @return signer The recovered signer address, or address(0) on failure.
    function recover(bytes calldata sig, bytes32 digest) external view returns (address signer);
}
 
interface ISphincsVerifier {
    /// @return valid True iff sig verifies over message under (pkSeed, pkRoot).
    function verify(bytes32 pkSeed, bytes32 pkRoot, bytes32 message, bytes calldata sig)
        external view returns (bool valid);
}

All verifiers are immutable and shared: one deployment of each serves every account. SimpleAccount holds its two in immutable VERIFIER and immutable SPHINCS_VERIFIER; SphincsAccount holds the standard verifier in immutable VERIFIER.

Conventions shared across the contracts

  • EntryPoint: the canonical ERC-4337 EntryPoint v0.7, identical-address across mainnet, Sepolia, and major rollups.
  • Rotation at validation time: key state advances inside validateUserOp, so a key is retired even if the inner call reverts. See Standards → ERC-4337.
  • Next key in calldata: every UserOp's callData ends with bytes20(nextOwner) (FORS+C and activation) or bytes20(currentKey) ‖ bytes20(nextOwner) (SPHINCS), so the signed userOpHash commits to the rotation.
  • Dispatch by length, no type tag: 2,448 bytes is FORS+C, 3,688 is SPHINCS, anything of the form 2,451 + 32·n is an activation envelope. The classes are disjoint by construction, and the 6,176-byte SphincsAccount blob is disjoint from all of them. See Signature dispatch.
  • Deterministic addresses: verifiers, factories, and account implementations are deployed via CREATE2 so they share addresses across chains. See Multichain addresses.
  • Toolchain: solc 0.8.30, optimizer (200 runs), via-ir; account-abstraction (v0.7 BaseAccount), OpenZeppelin (Initializable), and Solady (LibClone, MerkleProofLib) as dependencies. lib/kernel is kept as a submodule only because it vendors Solady.

What is no longer in the main line

The ECDSA and WOTS+C accounts, the ZeroDev Kernel / Nexus ERC-7579 validator modules, the EIP-8141 FrameAccount draft, and the stateful SphincsIndexedVerifier were removed from main and are preserved on the archive/dev-pre-cleanup branch. There is currently no ERC-7579 module for the FORS+C path: it ships only as the standalone SimpleAccount. See Archived designs.