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 contracts2
Deploy to Polygon Mumbai testnet → run full e2e minting flow3
KMS/HSM: platformSigner key stored in AWS KMS, never in env vars4
Fund relayer wallet with MATIC for gas subsidy5
Register Gnosis Safe as contract owner for admin functions6
Verify contracts on PolygonScan for transparency7
Schedule security audit with Quantstamp / Trail of Bits before mainnet