EIP712ContractSpec

Smart Contract Spec · EIP-712 · Polygon PoS · OpenZeppelin

EIP-712 Contract Spec

Production-ready Solidity contracts for minting VOLTS certificates, anchoring proofRefs, and distributing royalties — with EIP-712 signed intent relay pattern

ERC-721 + EIP-712 mint flow for VOLTS award certificates with embedded provenance anchor
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/**
 * VOLTSCertificate — ERC-721 + EIP-712 award certificate
 * Anchors: contentHash + proofRef + genotypeId on-chain per mint
 * Supports batch minting with relayer-subsidy (meta-tx) pattern
 */
contract VOLTSCertificate is ERC721, EIP712, Ownable {
    using ECDSA for bytes32;

    struct Anchor {
        bytes32 contentHash;   // SHA-256 of canonical genotype
        bytes32 proofRef;      // IPFS CID (bytes32 truncated) or ZK proof ref
        bytes32 genotypeId;    // Factorizer genotype identifier
        uint8   circuitVersion;
        uint64  voltsAmount;   // VOLTS rewarded (scaled ×100 for precision)
        address creator;
    }

    // EIP-712 typehash
    bytes32 public constant MINT_TYPEHASH = keccak256(
        "MintIntent(address recipient,bytes32 contentHash,bytes32 proofRef,"
        "bytes32 genotypeId,uint8 circuitVersion,uint64 voltsAmount,uint256 nonce,uint256 deadline)"
    );

    mapping(uint256 => Anchor) public anchors;
    mapping(address => uint256) public nonces;
    address public platformSigner;
    uint256 private _tokenIdCounter;

    event CertificateMinted(
        uint256 indexed tokenId,
        address indexed recipient,
        bytes32 contentHash,
        bytes32 genotypeId,
        uint64  voltsAmount
    );

    constructor(address _platformSigner)
        ERC721("VOLTS Certificate", "VOLTSC")
        EIP712("VOLTSCertificate", "1")
        Ownable(msg.sender)
    {
        platformSigner = _platformSigner;
    }

    /**
     * @dev Mint with platform-signed EIP-712 intent
     * Relayer pays gas; platform pre-signs the typed struct.
     */
    function mintWithIntent(
        address     recipient,
        bytes32     contentHash,
        bytes32     proofRef,
        bytes32     genotypeId,
        uint8       circuitVersion,
        uint64      voltsAmount,
        uint256     deadline,
        bytes calldata platformSignature
    ) external returns (uint256 tokenId) {
        require(block.timestamp <= deadline, "Intent expired");

        bytes32 structHash = keccak256(abi.encode(
            MINT_TYPEHASH, recipient, contentHash, proofRef,
            genotypeId, circuitVersion, voltsAmount,
            nonces[recipient]++, deadline
        ));
        bytes32 digest = _hashTypedDataV4(structHash);
        address signer = ECDSA.recover(digest, platformSignature);
        require(signer == platformSigner, "Invalid platform signature");

        tokenId = ++_tokenIdCounter;
        _safeMint(recipient, tokenId);
        anchors[tokenId] = Anchor(
            contentHash, proofRef, genotypeId,
            circuitVersion, voltsAmount, recipient
        );

        emit CertificateMinted(tokenId, recipient, contentHash, genotypeId, voltsAmount);
    }

    /**
     * @dev Batch mint — gas-efficient multi-award for tournament winners
     */
    function batchMint(
        address[] calldata recipients,
        bytes32[] calldata contentHashes,
        bytes32[] calldata proofRefs,
        bytes32[] calldata genotypeIds,
        uint64[]  calldata voltsAmounts
    ) external onlyOwner {
        require(recipients.length == contentHashes.length, "Length mismatch");
        for (uint i; i < recipients.length; ) {
            uint256 tid = ++_tokenIdCounter;
            _safeMint(recipients[i], tid);
            anchors[tid] = Anchor(
                contentHashes[i], proofRefs[i], genotypeIds[i],
                1, voltsAmounts[i], recipients[i]
            );
            emit CertificateMinted(tid, recipients[i], contentHashes[i], genotypeIds[i], voltsAmounts[i]);
            unchecked { ++i; }
        }
    }

    function setPlatformSigner(address _signer) external onlyOwner {
        platformSigner = _signer;
    }
}
EIP-712 Typed Data Payload— Platform signs off-chain; relayer submits on-chain
// EIP-712 typed data payload (JavaScript/ethers.js)
const domain = {
  name: "VOLTSCertificate",
  version: "1",
  chainId: 137,                          // Polygon PoS
  verifyingContract: VOLTS_CONTRACT_ADDR,
};

const types = {
  MintIntent: [
    { name: "recipient",      type: "address" },
    { name: "contentHash",    type: "bytes32" },
    { name: "proofRef",       type: "bytes32" },
    { name: "genotypeId",     type: "bytes32" },
    { name: "circuitVersion", type: "uint8"   },
    { name: "voltsAmount",    type: "uint64"  },
    { name: "nonce",          type: "uint256" },
    { name: "deadline",       type: "uint256" },
  ],
};

const value = {
  recipient:      winnerAddress,
  contentHash:    ethers.utils.keccak256(ethers.utils.toUtf8Bytes(canonicalJSON)),
  proofRef:       ethers.utils.formatBytes32String(ipfsCID.slice(0, 31)),
  genotypeId:     ethers.utils.formatBytes32String(genotypeId.slice(0, 31)),
  circuitVersion: 1,
  voltsAmount:    BigInt(Math.round(computedVolts * 100)),
  nonce:          await contract.nonces(winnerAddress),
  deadline:       Math.floor(Date.now() / 1000) + 3600,  // 1hr
};

// Platform signs (backend, never expose private key in frontend)
const platformSignature = await platformWallet._signTypedData(domain, types, value);

// Relayer submits on-chain (subsidizes gas)
const tx = await contract.mintWithIntent(
  value.recipient, value.contentHash, value.proofRef,
  value.genotypeId, value.circuitVersion, value.voltsAmount,
  value.deadline, platformSignature
);
🔐
EIP-712 Intent
Platform signs a typed struct off-chain. Relayer submits to chain. User never pays gas. Signature verified in contract.
⛓️
On-Chain Anchor
Compact struct: {contentHash, proofRef, genotypeId, circuitVersion, voltsAmount} stored per token. Explorer-queryable.
💎
Royalty Split
EIP-2981 + multi-recipient BPS array. Default: 70% creator / 20% platform / 10% stewardship. Merkle-drop for large cohorts.
Deploy Checklist
1
Run Slither + MythX static analysis on contracts
2
Deploy to Polygon Mumbai testnet → run full e2e minting flow
3
KMS/HSM: platformSigner key stored in AWS KMS, never in env vars
4
Fund relayer wallet with MATIC for gas subsidy
5
Register Gnosis Safe as contract owner for admin functions
6
Verify contracts on PolygonScan for transparency
7
Schedule security audit with Quantstamp / Trail of Bits before mainnet
🍌

Install Factorizer

Access blueprints offline and get a faster experience