Spec — Token-2022 SAEP Mint (M3)
Owner: anchor-engineer + ops (multisig ceremony)
Depends on: Squads multisig v4 (4-of-7 emergency council + 6-of-9 program council per §5.2); FeeCollector deployed (TransferHook callback target + PermanentDelegate scope-enforcer + WithheldWithdraw destination); NXSStaking deployed (apy_authority PDA derived; rate update path via NXSStaking::set_apy CPI'd by GovernanceProgram); GovernanceProgram deployed (transfer_hook_authority PDA + meta-governance for extension-authority changes).
Blocks: NXSStaking M3 migration (real mint replaces pre-M3 placeholder; separate spec); TaskMarket SAEP-payment path (currently USDC/SOL only per cycle 5 scaffold); FeeCollector TransferHook activation (hook program is set at mint init — FeeCollector can be deployed before mint but TransferHook only "lights up" once mint exists); IACP fee-burn metering (Phase 3 §1.3 burn schedule depends on the mint's TransferFee surface).
References: backend PDF §1.3 (Token-2022 extensions enumeration + critical-constraint TransferHook ⊕ ConfidentialTransfer); §2.6 (deployment + upgrade tables — Token-2022 program is upstream-immutable, only mint authority handover is "deployable"); §4.3 (deploy order — mint creation lands AFTER all 6 program upgrade authorities are in Squads + FeeCollector + NXSStaking + GovernanceProgram are live); §5.1 (Token-2022 extension safety checklist: hook program whitelist, MAX_TRANSFER_FEE bound, PermanentDelegate scope enforcement, InterestBearing rate cap, mint inflation-immutability); §5.2 (multisig signer geo-distribution + HSM + ceremony controls); pre-audit-05 (TransferHook program whitelist).
Goal
The single canonical SAEP token mint. The mint is created once, on mainnet, with a fixed extension set chosen per §1.3. Extension choices are FINAL after InitializeMint per Token-2022 semantics — getting init order wrong means re-minting, which means orphaning every issued token. This spec is the executable runbook for that single moment plus the multisig ceremony around it.
The mint is initialized with 6 extensions: TransferHook, TransferFee, PermanentDelegate, InterestBearing, MetadataPointer, Pausable. ConfidentialTransfer is explicitly excluded — incompatible with TransferHook per §1.3 critical-constraint; the Privacy Escrow feature in §1.3 Phase 3 gets a separate mint, out of this spec's scope.
Initial mint authority is held by a single-sig bootstrap signer for atomic init, then transferred to multisig PDAs in a single follow-on tx (T+1, ≤1 slot after init). MintTokens authority is set to None at handover to lock supply; freeze authority is set to None (Pausable replaces it). Each extension's update authority points at the appropriate Squads multisig per §5.2: 6-of-9 program council for non-emergency (TransferFee config, PermanentDelegate, MetadataPointer); 4-of-7 emergency council for Pausable; NXSStaking PDA for InterestBearing rate; GovernanceProgram PDA for TransferHook program ID swaps.
The mint init script (scripts/init-saep-mint.ts) is idempotent, has --dry-run / --devnet / --mainnet modes, refuses mainnet without a recent successful devnet rehearsal, and emits a verification report listing on-chain authority Pubkeys post-handover. Mainnet init is a 6-of-9 ceremony per §5.2, geo-distributed, air-gapped bootstrap key, single-use signer destroyed post-handover.
This is not a program. It's an orchestration script + the human ceremony around it. The risk profile is reverse of the program-spec ones: fewer LOC, but every line is high-blast-radius — a misordered ix means re-minting, and re-minting means relaunching the protocol.
Mint configuration
Decimals
decimals: 9— matches SOL convention. InterestBearing math is identical at any decimal count (extension stores u128 internally), so decimals choice is purely display-side.
Metadata (via MetadataPointer + Token Metadata Interface)
name: "SAEP"symbol: "SAEP"uri: ipfs://<CID>— pinnedmetadata.jsonwith full token description, image, properties; CID locked at T-3d config-freezeadditional_metadata: []— reserved; governance can add fields post-launch viatoken_metadata_update_field(e.g.,governance_program_id,bookkeeping_program_id)
The MetadataPointer extension self-references (metadata_address: mint_address) — metadata stored inline rather than in a separate Metaplex account. Saves rent (~0.0014 SOL per holder) and removes one external dependency at read-time per §1.3.
Initial supply
initial_supply: 0- Mint authority transferred to None at handover (T+1) via
set_authority(MintTokens, None) - Rationale: §5.1 inflation-immutability. Future inflation requires meta-governance + a separate
EmissionsSchedulerprogram holding mint authority — neither in M3 scope. Stakers earn via InterestBearing accrual on the mint extension (no actual minting), not via supply expansion. - Migration path: if M4+ requires controlled emissions, mint authority cannot be re-acquired (one-way None). The path forward is governance-CPI-driven via a wrapper / wrap-mint pattern; documented as M4+ Open Question, not a M3 reversal.
Extensions
1. TransferHook
- hook_program_id:
FeeCollectorprogram (SAEPfee1111...per §2.1) - authority (post-handover):
GovernanceProgramPDA seeds[b"transfer_hook_authority"]. Authority can be set to None post-launch via meta-governance to make the hook program permanent. - Init pre-condition: FeeCollector MUST be deployed and the program executable BEFORE mint init; init script validates via
getAccountInfo(feeCollectorProgramId)thatexecutable == trueandowner == BPFLoaderUpgradeable. - Callback contract: Per pre-audit-05 — FeeCollector exposes
transfer_hook(source, mint, destination, owner, amount, extra_accounts); extra accounts includeHookAllowlist, optionalAgentHookAllowlist, fee-pool ATA. FeeCollector enforces hook-program-whitelist for tokens that ARE transferred (recursive case where SAEP transfers another Token-2022 mint with its own hook); collects 0.1% protocol fee per §1.3. - Failure mode: If FeeCollector reverts the hook ix, the source transfer reverts. This IS the gate; intentional. Maintenance requires governance-approved hook program upgrades via Squads (the FeeCollector program upgrade authority is the 6-of-9 program council).
- Compatibility: Self-transfers (mint → mint operations like
mint_to,burn) do NOT invoke the hook per Token-2022 spec; onlytransfer/transfer_checkedinvoke it. Verified againstspl-token-2022v6.x.
2. TransferFee (built-in)
- transfer_fee_basis_points: 10 (0.1%, matches §1.3)
- maximum_fee:
1_000_000 * 10^decimals(1M SAEP per tx; bounds whales paying 10%+ of supply per tx) - transfer_fee_config_authority (post-handover): Squads 6-of-9 program council (per §5.2 — fee parameter changes are non-emergency, take meta-governance)
- withdraw_withheld_authority (post-handover):
FeeCollectorPDA seeds[b"transfer_fee_withdraw_authority"]— FeeCollector CPI sweeps withheld fees on each settlement cycle into the protocol fee pool. - Note on double-counting: TransferHook callback collects 0.1% protocol fee at hook time (sent to fee-pool ATA inside the hook tx). TransferFee built-in collects ANOTHER 0.1% at the protocol level (held in token-account WithheldFees state, swept later). Net effective fee = 0.2% per §1.3. Confirmed not double-counting because hook fee goes to fee-pool ATA inline; withheld fee accumulates per-account until
FeeCollector::sweep_withheld_feeswithdraws.
3. PermanentDelegate
- delegate (post-handover):
FeeCollectorprogram PDA seeds[b"permanent_delegate"] - Scope: Per §1.3 "FeeCollector program as permanent delegate enables fee sweep without owner interaction" — used for stale-fee sweeps from inactive accounts.
- Critical (audit attention): PermanentDelegate is god-mode on every token account holding the mint.
FeeCollector::sweep_inactive(account, mint)MUST validate (a)account.last_active < now - inactivity_threshold(default 90 days, governance-tunable), (b) swept amount ≤withheld_fees + protocol_dust_threshold, never user balance principal. Test surface is FeeCollector spec, not this spec — but the contract is cited here as the security covenant this mint depends on. - Authority (post-handover): Squads 6-of-9 program council; can be set to None post-launch via meta-governance to remove permanent-delegate completely (one-way switch at the cost of losing inactive-account sweep).
4. InterestBearing
- rate_authority (post-handover):
NXSStakingprogram PDA seeds[b"apy_authority"]per the NXSStaking spec. NXSStaking owns the PDA;NXSStaking::set_apyis callable only from GovernanceProgram CPI (gov_program_id == config.governance_program) per cycle 64. - initial_rate: 0 bps (no APY at launch; first APY-set proposal lands via GovernanceProgram after audits clear)
- Accrual: Per §1.3 InterestBearing is a Token-2022 native feature. Yield is computed at read-time via
amount_to_ui_amount(amount, decimals, current_unix_ts); no minting happens. Stakers see growing displayed balance without claim-required. - Cap (defense-in-depth): NXSStaking::set_apy enforces
|apy_basis_points| ≤ 1000(10% max APY; ratchet via meta-governance per NXSStaking spec). The mint extension itself accepts any i16; the cap is enforced at the CPI authority level. - Pre-M3 placeholder: NXSStaking ships against a generic SPL mint without InterestBearing per its spec; M3 migration to this mint is documented in a separate NXSStaking-M3-migration spec. Out of this spec's scope; flagged so the M3 cycle plan accounts for the migration.
5. MetadataPointer
- metadata_address:
mint_address(self-referential; stores metadata inline) - authority (post-handover): Squads 6-of-9 program council (metadata updates are non-emergency)
- Initial metadata: name/symbol/uri per §Mint configuration above
- Init order constraint:
metadata_pointer_initializeMUST precedeinitialize_mint;token_metadata_initializeMUST followinitialize_mint(the metadata-init ix writes through the pointer, which requires the pointer extension to be registered AND the mint to be initialized).
6. Pausable
- pause_authority (post-handover): Squads 4-of-7 emergency council (per §1.3 + §5.2)
- State at init: unpaused
- Activation policy: Per §1.3 "Used for critical exploit response only". Pause halts all transfers protocol-wide (TransferHook, TransferFee, InterestBearing-display, NXSStaking, AgentRegistry, TaskMarket settlement all gate on pause). Pre-pause escrows remain locked (state-machine reads unaffected; mutation-instructions revert).
- Auto-thaw: Token-2022 Pausable has no auto-thaw; council must explicitly unpause. GovernanceProgram emergency category §2.6 mirrors this at the program level (14d auto-thaw), but the mint pause itself is council-revertible only — adds defense-in-depth against a compromised governance contract pause-spamming the mint.
- Why init-time even if rarely used: Token-2022 extensions are FINAL after
initialize_mint. If Pausable isn't initialized at mint creation, it can NEVER be added. Spec initializes it now to preserve the option; activation is a separate governance event.
Excluded extensions (and why)
ConfidentialTransfer
- Reason: Per §1.3 critical-constraint, ConfidentialTransfer + TransferHook CANNOT coexist in Token-2022 (mutually exclusive at extension layer). SAEP chose TransferHook (protocol fee + whitelist enforcement); confidential transfers therefore impossible on the SAEP mint.
- Privacy Escrow at M4+: Separate mint per §1.3 Phase 3 reference. That mint will have ConfidentialTransfer + NO TransferHook — enabling private balances at the cost of no per-tx fee enforcement. Out of this spec's scope; documented as Open Question for forward planning.
CpiGuard
- Reason: SAEP relies on multi-program CPIs throughout (TaskMarket → ProofVerifier → AgentRegistry → TreasuryStandard chain). Mint-level CpiGuard would block legitimate cross-program calls. Token-account-level CpiGuard remains opt-in for individual users via SAEP-agnostic Token-2022 ixs.
MemoTransfer
- Reason: No requirement; portal-level memo via SPL-Memo v2 covers IACP envelope-anchoring use case (per IACP anchor cycle 56). Adding MemoTransfer would force every transfer to carry a memo, breaking the gas profile.
NonTransferable
- Reason: SAEP is fungible governance + utility token. Soulbound semantics not in scope.
ImmutableOwner
- Reason: Owner-changes are useful for treasury rotation; immutable owner would block the use case. Per-account opt-in remains available to end-users.
Init order
Token-2022 requires extensions initialized BEFORE initialize_mint. Order of extension-inits among themselves matters less but the pre/post-mint split is hard:
system_program::create_account— allocate the mint account with size =Mint::base_size + Σ(extension sizes). Compute viaExtensionType::try_calculate_account_len::<Mint>(&[TransferHook, TransferFee, PermanentDelegate, InterestBearing, MetadataPointer, Pausable]). Pre-allocates exact rent; avoids realloc.metadata_pointer_initialize(metadata_address: mint_pubkey, authority: bootstrap_signer)— pre-minttransfer_fee_initialize(transfer_fee_config_authority: bootstrap, withdraw_withheld_authority: bootstrap, transfer_fee_basis_points: 10, maximum_fee: 1_000_000 * 10^9)— pre-minttransfer_hook_initialize(authority: bootstrap, program_id: fee_collector_program_id)— pre-mint; FeeCollector must be deployedpermanent_delegate_initialize(delegate: bootstrap)— pre-mintinterest_bearing_initialize(rate_authority: bootstrap, rate_bps: 0)— pre-mintpausable_initialize(authority: bootstrap)— pre-mintconfidential_transfer_initialize— SKIP (incompatible with TransferHook per §1.3)initialize_mint(decimals: 9, mint_authority: bootstrap, freeze_authority: bootstrap)— finalizes; extension set is now FROZENtoken_metadata_initialize(name, symbol, uri, mint_authority: bootstrap, update_authority: bootstrap)— post-mint; writes metadata inline via the self-referential pointer
Steps 1-9 MUST be a single atomic transaction. If any step fails, the partial-init mint is unusable (size + ext-state mismatch); the whole tx must revert. Step 10 can be a follow-on tx but MUST land before handover (T+1) to avoid a metadata-less mint window where indexers see an unnamed token.
The init tx is bounded at ~80k CU per ix × 9 ixs = ~720k CU baseline; with ComputeBudgetProgram::set_compute_unit_limit(1_400_000) we have headroom. Realistic measurement: ~600k CU on devnet rehearsal. Single-tx init fits the 1.4M CU per-tx ceiling.
Authority handover sequence
Init transaction (T+0)
- All 6 extension authorities + mint authority + freeze authority held by bootstrap signer
- Single-sig (bootstrap) for atomicity. Multisig signer aggregation across 6 sigs in a 9-ix-tx is feasible but operationally fragile at ceremony time; single-sig + immediate handover is the cleaner pattern.
Metadata transaction (T+0, +1 slot)
- Step 10 above; writes name/symbol/uri inline via metadata pointer
- Optional: can be bundled with handover tx if total ix count + size fits within tx limit (1232 bytes after sigs); kept separate by default for tx-size safety margin
Handover transaction (T+1, ≤1 slot after init)
All set_authority calls in a SINGLE atomic tx. Partial handover is a security incident.
set_authority(MintTokens, None)— locks initial supply at 0 forever; reverses inflation per §5.1set_authority(FreezeAccount, None)— no admin freezing; Pausable replaces (per-account freeze is a different threat model not in M3 scope)set_authority(TransferFeeConfig, squads_6of9_program_council)— meta-governance for fee changesset_authority(WithheldWithdraw, fee_collector_withdraw_pda)— FeeCollector sweeps withheld feesset_authority(TransferHookProgramId, governance_transfer_hook_authority_pda)— meta-governance can swap hookset_authority(PermanentDelegate, squads_6of9_program_council)— meta-governance can revoke (one-way to None)set_authority(InterestBearingRateAuthority, nxs_staking_apy_authority_pda)— NXSStaking owns rate; gov CPIs throughNXSStaking::set_apyset_authority(MetadataPointerAuthority, squads_6of9_program_council)— meta-governance for metadata-pointer changesset_authority(MetadataUpdateAuthority, squads_6of9_program_council)— same council for metadata-field updates (could split per Open Q #6)set_authority(PausableAuthority, squads_4of7_emergency_council)— emergency council per §5.2
Handover MUST be one atomic tx. Partial handover means bootstrap signer holds god-mode for an unbounded window — a security incident if the bootstrap key is compromised between init and handover. Mitigation per §5.2: handover tx is pre-signed at ceremony time; broadcast is automatic on init confirmation; bootstrap key is air-gapped and destroyed post-handover.
Verification transaction (T+2, ≤5 minutes after handover)
- Read-only:
getMint(mint, programId: TOKEN_2022_PROGRAM_ID)+getAccountInfo(mint)extension parse - Assert each authority Pubkey matches expected (per
state/saep-mint-mainnet-config.jsonSHA-256-attested at T-3d) - Assert MintTokens authority is None
- Assert MetadataPointer.metadata_address == mint_pubkey
- Assert TransferHook.program_id == fee_collector_program_id
- Output
reports/saep-mint-handover-mainnet.mdwith on-chain authority dump, init tx sig, handover tx sig, verification slot
Init script — scripts/init-saep-mint.ts
Modes
--dry-run(default): runs full init against an in-memory simulated cluster (e.g.,solana-test-validatorstarted in-process); prints all 9 ixs; signs locally; simulates viasimulateTransaction. Asserts post-init extension state. Does not transmit. Used in CI to catch ix-shape regressions when@solana/spl-tokenorspl-token-2022upstream changes.--devnet: full init against devnet; produces a permanent rehearsal mint. Used for portal/SDK integration testing pre-M3 + as the rehearsal-precondition for mainnet.--mainnet: full init against mainnet. Requires--confirm-mainnetflag ANDSAEP_MAINNET_INIT_KEYenv var pointing at the air-gapped bootstrap keypair. Refuses if (a) bootstrap signer balance <2 * estimated_init_cost, (b) most recent successfulstate/saep-mint-devnet.jsonis older than 7 days, (c) extension-config SHA-256 in mainnet-config.json doesn't match the council-attested hash, (d) FeeCollector / NXSStaking / GovernanceProgram are not deployed at expected pubkeys on mainnet.
Idempotence
- Script reads
state/saep-mint-{network}.jsonfirst; if mint pubkey present, refuses to re-init unless--force-reinit-rehearsalAND--devnet(mainnet re-init is impossible by design — the mint pubkey is final). - After init, writes
state/saep-mint-{network}.jsonwith mint pubkey + bootstrap signer + init tx sig + handover tx sig + slot + extension-config SHA-256.
Rehearsal contract
- Mainnet init MUST be preceded by ≥1 successful
--devnetrun within 7 days, validated by thestate/saep-mint-devnet.jsontimestamp + matching extension-config SHA-256. - Spec rationale: §1.3 "Extension choices are final" — one rehearsal cycle catches misordered ixs / version-skewed
spl-token-2022shape changes before mainnet.
Output
reports/saep-mint-init-{devnet|mainnet}.md— init tx sig, handover tx sig, verification slot, post-handover authority dump, total CU consumed, total rent, total fee.- Console: mint pubkey + Solscan link.
Multisig ceremony (mainnet only)
Per §5.2, mainnet init is a ceremony, not a script-run. Steps:
- T-7d: Devnet rehearsal run; verify report; sign-off from anchor-engineer + ops.
- T-3d: Mainnet config frozen — extension set, all 10 authority Pubkeys, metadata URI, decimals, all in
state/saep-mint-mainnet-config.json. SHA-256 of the file is signed by all 6 program-council signers (out-of-band signature, e.g., signed-message or PGP-detached) and stored in the council's air-gapped vault. The script checks this signed hash before allowing--mainnet. - T-1d: Final dry-run against devnet using mainnet-config.json (rehearsal mint pubkey replacement); validates the config still works against current Token-2022 program version (upstream may have shifted extension semantics between rehearsal and ceremony).
- T+0 (ceremony): All 6 program-council signers convene, geo-distributed per §5.2 (≥4 in different jurisdictions). Bootstrap signer keypair generated air-gapped at ceremony time (single-use). Init tx + metadata tx + handover tx broadcast in 3 consecutive blocks (or bundled atomically via Jito if the tx-size + sig-count fits — see Open Q #2). Bootstrap signer key is destroyed post-handover (memwipe of process memory + secure-delete of any keypair file + the air-gapped device is reformatted).
- T+1d: Verification report published on internal repo; signed attestation from each of the 6 signers acknowledging the on-chain authority Pubkeys match the T-3d config.
Security checks
(Per backend §5.1 checklist for Token-2022 + ceremony controls)
- Extension safety: All 10 extension authorities point to either a multisig PDA or None at handover-completion. No extension authority is an EOA at T+1.
- TransferHook authority: Settable to None post-launch via meta-governance to make hook program permanent; reviewed at M4 (out of M3 scope). Default at M3: governance can swap hook program (allows audit-driven hook upgrades).
- PermanentDelegate scope: Defense lives in FeeCollector —
FeeCollector::sweep_inactivevalidateslast_active < now - inactivity_thresholdAND swept amount ≤withheld_fees + protocol_dust. The mint extension itself trusts the delegate; this spec depends on FeeCollector correctness as the only check between protocol and user funds. - InterestBearing rate cap: NXSStaking::set_apy enforces
|apy_basis_points| ≤ 1000. Mint extension accepts arbitrary i16; cap is at the CPI authority level. Meta-governance can ratchet the cap higher per NXSStaking spec. - TransferFee maximum_fee bound: 1M SAEP per tx; meta-governance can raise. Prevents whale txs paying disproportionate fees.
- Pausable scope: Council pause halts ALL transfers including settlement releases. Pre-pause escrows are not refunded (intentional — pause is for incident response, refunds happen post-incident via separate governance proposals).
- MetadataPointer self-reference:
metadata_address == mint_address; cannot be hijacked to point at attacker-controlled metadata account post-init (would require MetadataPointerAuthority signature, held by 6-of-9). - Mint inflation immutability:
set_authority(MintTokens, None)at T+1 makes future inflation impossible at the mint level. Any future inflation requires a new mint + protocol relaunch — high enough cost to deter casual emission proposals. - Bootstrap signer destruction: memwipe + key file deletion + air-gapped device reformatted. Ceremony device NEVER touches network-connected hardware. Protects against post-ceremony key recovery from disk forensics or compromised laptop.
- Single-block init+metadata+handover: if any of the 3 txs land out of order or one fails, the mint is in an intermediate state. Bootstrap signer holds god-mode authorities until handover lands. Acceptable risk window: ≤3 slots if sequenced; ≤1 slot if Jito-bundled. Mitigation: handover tx is pre-signed at ceremony; broadcast is automatic on init confirmation; if handover fails, the council immediately re-proposes (bootstrap signer key is offline at this point per ceremony script, but exists on-disk on the air-gapped device until ceremony-end memwipe, so a re-broadcast is feasible within the ceremony window).
- No freeze authority backdoor:
set_authority(FreezeAccount, None)removes the per-account freeze surface entirely; Pausable is the only freeze-equivalent. Prevents a compromised freeze authority from selectively freezing whale accounts.
CPI contract (what this mint exposes to other programs)
- TransferHook callback:
FeeCollector::transfer_hook(source, mint, destination, owner, amount, extra_accounts)— invoked on everytransfer/transfer_checked; FeeCollector enforces 0.1% fee + hook-allowlist per pre-audit-05. - TransferFee withdraw:
FeeCollector::sweep_withheld_fees(mint, accounts[])— sweepsWithheldWithdraw-authority-locked fees from per-account state to FeeCollector pool. Authority is FeeCollector PDA; FeeCollector signs as the withdraw authority via PDA seeds. - PermanentDelegate transfer:
FeeCollector::sweep_inactive(account, mint)— uses PermanentDelegate to claim withheld fees from inactive accounts. Validation in FeeCollector per §Security checks PermanentDelegate scope. - InterestBearing rate update:
NXSStaking::set_apy(rate_bps)— CPIs Token-2022interest_bearing_update_rate(mint, rate_authority_pda, rate)via NXSStaking's apy_authority PDA. Callable only from GovernanceProgram CPI to NXSStaking per cycle 64 NXSStaking spec. - Pausable trigger: Squads 4-of-7 council directly invokes
pause(mint, pause_authority); no program-CPI; emergency human-signed. - Metadata update: Squads 6-of-9 council directly invokes
token_metadata_update_field(mint, metadata_authority, field, value); non-emergency, used for URI rotations oradditional_metadataadds. - Mint authority: None post-handover. No CPI surface. New tokens cannot be minted.
- Freeze authority: None post-handover. No CPI surface. Per-account freeze unavailable.
Devnet bring-up
Per §4.3 deploy order, mint creation is sequenced AFTER:
- All 6 program upgrade authorities migrated to Squads multisigs (program-level lockdown per §5.2).
- FeeCollector deployed +
HookAllowlistinitialized + SAEP mint pubkey pre-allocated (sotransfer_hook_initializecan reference). Pre-allocation is via the bootstrap-signer keypair: the mint pubkey is known once the keypair is generated; FeeCollector references it as a constant in its allowlist. - NXSStaking deployed +
apy_authorityPDA derived (forinterest_bearing_initializerate_authority handover). - GovernanceProgram deployed +
transfer_hook_authorityPDA derived (fortransfer_hook_initializeauthority handover).
Devnet rehearsal mint is created with all 6 extensions but bootstrap-signer-authority everywhere (no multisig handover for devnet — devnet single-sig is operationally simpler and devnet mint has no real value). NXSStaking M3 migration uses the rehearsal mint to validate the M3 mint-swap path before mainnet ceremony (separate spec).
Per §4.3 48h timelock override, devnet mint init is NOT timelock-gated — only param-changes via governance go through the override. Init is one-shot.
Open questions for reviewer
- Mint authority disable vs governance-controlled emissions. Spec defaults to disabled (None at T+1, no further supply). Alternative: hand mint authority to a governance-controlled PDA + introduce an
EmissionsSchedulerprogram at M4 for controlled emissions (e.g., 2% annual). Disable now, add later via meta-governance? Or keep the option open from day 1? Picked disable for inflation-immutability simplicity; flagged because re-acquisition is one-way-impossible. - Single-block init + metadata + handover via Jito bundle vs sequenced txs. Bootstrap signer holds god-mode authorities for ≤3 slots if sequenced; ≤1 slot if Jito-bundled. Reviewer may want bundled atomicity (eliminates the partial-handover window) at the cost of ceremony complexity (Jito searcher dependency at ceremony time).
- PermanentDelegate scope. Token-2022 PermanentDelegate is god-mode on every token account. FeeCollector enforcement is the only check. Reviewer should confirm pre-audit-05 + FeeCollector spec are sufficient defense, OR push for
set_authority(PermanentDelegate, None)at T+1 (eliminates the extension entirely) at the cost of losing inactive-account sweep capability. Trade-off: most protocol fees are recoverable via TransferFee WithheldWithdraw; PermanentDelegate is only needed for the long-tail inactive case. - TransferFee maximum_fee bound. 1M SAEP per tx may be too low or too high; depends on circulating supply at M3 launch. Reviewer to set to a sane fraction (default 0.1% of expected initial supply at M3).
- Pausable auto-thaw. Token-2022 Pausable has no auto-thaw; council can hold pause indefinitely (single point of failure if council compromised). Reviewer may want a meta-governance auto-unpause after N days, implemented via a watcher program calling
unpauseafter a timeout when the council fails to renew. Not in M3 scope; flagged for M4. - MetadataPointer + MetadataUpdate authority split. Post-launch, metadata updates require 6-of-9 sig — slow for routine URI rotations. Reviewer may want a separate
metadata_authoritySquads (e.g., 3-of-5 ops council) for ergonomic URI updates while major fields require 6-of-9. Default keeps both at 6-of-9 to avoid council proliferation. - Mint pubkey vanity prefix. Solana convention is vanity prefixes for canonical mints (e.g.,
So111...wrapped SOL,EPjFW...USDC). Bootstrap signer can grind aSAEP...prefix; takes ~1-4 hrs of CPU per character withsolana-keygen grind --starts-with SAEP:1. Spec defaults to no-vanity (mint pubkey is whatever the bootstrap keypair generates) for ceremony-time simplicity. Reviewer may want vanity grind as a pre-ceremony step (T-7d). - Decimals = 9 vs 6. SOL is 9, USDC is 6. SAEP picks 9 for SOL-convention; portal display uses scientific notation past ~10^12. Reviewer may want 6 to match USDC for cross-mint display consistency. Not reversible after init.
- InterestBearing initial rate = 0. Launch with zero APY; first APY-set proposal post-audit clears it. Alternative: bootstrap a non-zero rate at init (e.g., 5% APY) so stakers see immediate yield. Picked zero to keep mint init unentangled from APY-setting governance and to avoid pre-audit yield commitments.
- Phase 3 Privacy Escrow separate mint. Out of this spec's scope but referenced by §1.3. Should be its own spec doc when planned. Reviewer to confirm M3 scope is mint-only without committing to the Phase-3 separate-mint design (it would have ConfidentialTransfer + NO TransferHook — different threat model entirely).
Done checklist
-
scripts/init-saep-mint.tslands; supports--dry-run,--devnet,--mainnet; idempotent with state file - CI runs
--dry-runon every push to catch upstreamspl-token-2022shape regressions - FeeCollector deployed to devnet +
HookAllowlistinitialized + SAEP mint pubkey pre-allocated - NXSStaking deployed to devnet +
apy_authorityPDA derived + verified - GovernanceProgram deployed to devnet +
transfer_hook_authorityPDA derived + verified - Devnet rehearsal mint init succeeds; all 6 extensions verified post-init
- Devnet rehearsal handover succeeds (single-sig devnet variant); all authorities at expected Pubkeys
- Devnet metadata tx succeeds;
name == "SAEP",symbol == "SAEP",urimatches CID - Devnet NXSStaking M3 migration tested against rehearsal mint (separate spec, blocking)
-
state/saep-mint-mainnet-config.jsonfrozen at T-3d + 6-of-9 council SHA-256 attestation in vault - T-1d final devnet dry-run against mainnet config succeeds (validates current Token-2022 program shape)
- T+0 mainnet ceremony: init tx + metadata tx + handover tx confirmed within ≤3 slots
- T+0 bootstrap signer destroyed (memwipe + secure-delete + device reformat)
- T+1d verification report published; 6-of-9 signed attestation matches on-chain authority Pubkeys
- OtterSec / Halborn audit confirms mint config matches spec
- Mint pubkey announced in
apps/docs+ portal + IACP discovery feed - FeeCollector TransferHook callback live on devnet + 100 test transfers succeed with correct fee deduction
- Portal SDK detects mint, reads InterestBearing accrual via
amount_to_ui_amount, displays growing balance correctly