Skip to content
Closed
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
4 changes: 2 additions & 2 deletions docs/CHAINSPEC_CREATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,5 @@ For a profile `<profile>` (e.g. `staging_mainnet`):
7. **Commit** the JSON plus the `load_spec` arm, and cut the node release that
embeds them.

For the staging-mainnet launch sequence around this (treasury multisig,
first server, post-launch), see `docs/STAGING_MAINNET_LAUNCH.md`.
For the staging-mainnet launch sequence around this (post-launch treasury setup,
first server, vesting retargets), see `docs/STAGING_MAINNET_LAUNCH.md`.
6 changes: 3 additions & 3 deletions docs/RUNTIME_SURFACE.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ All `Config` impls live in `runtime/src/configs/mod.rs` unless noted.

### Index 22 — `Vesting` (`pallet-vesting`, local)
- Pull-based "vesting wallet": the pallet's sovereign pot (`PalletId(*b"qvesting")`, keyless) holds the entire unclaimed allocation; beneficiaries are paid by plain keep-alive transfers only when a payout is due. **No locks, freezes, or holds ever touch a beneficiary account**, so wormhole addresses can be beneficiaries.
- Config: `Currency = Balances` (`fungible::{Inspect, Mutate}`), `TimeProvider = Timestamp` (ms since epoch), `AdminOrigin = EitherOfDiverse<EnsureRoot, EnsureTreasury>` (`EnsureTreasury` = signed by the configured treasury account; the treasury multisig executes proposals as a plain signed origin), `TreasuryAccount = TreasuryAccountOption` (Option-returning storage read, never panics), `ProofRecorder = Wormhole`, `PayoutQuantum = SCALE_DOWN_FACTOR` (10^10), `MinimumPayout = UNIT` (1 QUAN), `MinClaimInterval = 86,400,000 ms` (24 hours). Non-final claims are further aligned to `pallet_vesting::NON_FINAL_PAYOUT_QUANTA` (2,500) leaf quanta = 25 QUAN, the smallest 4 bps fee-exact multiple.
- Config: `Currency = Balances` (`fungible::{Inspect, Mutate}`), `TimeProvider = Timestamp` (ms since epoch), `AdminOrigin = EnsureRoot` (the configured treasury holds funds but has no unilateral schedule authority), `TreasuryAccount = TreasuryAccountOption` (Option-returning storage read, never panics), `ProofRecorder = Wormhole`, `PayoutQuantum = SCALE_DOWN_FACTOR` (10^10), `MinimumPayout = UNIT` (1 QUAN), `MinClaimInterval = 86,400,000 ms` (24 hours). Non-final claims are further aligned to `pallet_vesting::NON_FINAL_PAYOUT_QUANTA` (2,500) leaf quanta = 25 QUAN, the smallest 4 bps fee-exact multiple.
- **Storage:** `Schedules: schedule_id (u64) → { beneficiary, start, cliff, end, total, claimed, last_claim_at }` (ids sequential, never reused; a beneficiary may hold any number of schedules), `NextScheduleId`. Storage version 0 has no migration: an in-place upgrade with no schedules may leave the pot unfunded, and `create_schedule` then fails with `PotUnderfunded` until the treasury sends it one ED.
- Vesting math: `vested(t) = 0` before `cliff`, `total` from `end`, else `⌊total·(t−start)/(end−start)⌋` (256-bit rational, floor; the `end` branch guarantees exactness).
- **Payout policy:** wormhole leaves commit `amount / 10^10`, so a sub-quantum payout would create a zero-value leaf and strand funds on a keyless beneficiary. Schedule totals must be at least `MinimumPayout` and multiples of `PayoutQuantum`; payouts are quantized and `claimed` stays aligned. A successful claim must pay at least 1 QUAN and be at least 24 hours after that schedule's previous payout. Non-final claims additionally round down to 25 QUAN (`NON_FINAL_PAYOUT_QUANTA` leaf quanta) so each intermediate leaf has an exact 4 bps Wormhole fee; leftover dust stays on the schedule. They also reserve a complete minimum-sized final payout; a claim that cannot avoid a sub-minimum remainder fails with `ClaimWouldLeaveDust` until the full remainder vests. The final claim pays the exact remainder. `end_schedule` pays the unpaid vested part rounded to the nearest `PayoutQuantum` to the beneficiary only when that amount meets `MinimumPayout`; otherwise the sliver is refunded with every leftover planck to the signature-controlled treasury. The refund is not quantized and never blocks ending.
Expand Down Expand Up @@ -255,9 +255,9 @@ The high-security whitelist (`HighSecurityConfig::is_whitelisted`, extension 8)
- `heisenberg` — **internal integration testnet**, not mainnet. Tokens have no monetary value; the network may be reset.
- `planck` — public testnet (live treasury signers + faucet).
- `staging_mainnet` — mainnet dress rehearsal (see `docs/STAGING_MAINNET_LAUNCH.md`).
- **Vesting genesis:** every preset endows the vesting pot with `Σ schedule totals + ED` (ED alone when the table is empty, as on `planck`). Because the pot is part of the balances genesis endowment, standard genesis proof generation creates a block-1 Wormhole leaf for it; that leaf is unspendable because the pot is keyless. `dev`/`heisenberg` seed example schedules (one account with two schedules; `dev` also vests the keyless test wormhole address, claimable only via third-party ping). `staging_mainnet` builds from `mainnet_config_genesis`, the exact function the eventual mainnet preset will use; its vesting table is a DUMMY scaffold gated by `MAINNET_VESTING_FINALIZED`, and until that flag is flipped with the real allocation table no mainnet preset is exposed (details in `docs/STAGING_MAINNET_LAUNCH.md`).
- **Vesting genesis:** every preset endows the vesting pot with `Σ schedule totals + ED` (ED alone when the table is empty, as on `planck`). Because the pot is part of the balances genesis endowment, standard genesis proof generation creates a block-1 Wormhole leaf for it; that leaf is unspendable because the pot is keyless. `dev`/`heisenberg` seed example schedules (one account with two schedules; `dev` also vests the keyless test wormhole address, claimable only via third-party ping). `staging_mainnet` has its own staging-only genesis function and vesting table; mainnet will use a separate preset and chain spec (details in `docs/STAGING_MAINNET_LAUNCH.md`).
- Dilithium well-known accounts: `crystal_alice`, `dilithium_bob`, `crystal_charlie` (public seeds `[0]` / `[1]` / `[2]`). Used by `dev` and **intentionally also by `heisenberg`** so integrators and CI can exercise governance, treasury, and transfer flows without distributing secrets. Those private keys are public by design; do **not** reuse this pattern on a mainnet or any value-bearing chain (Planck already uses distinct live treasury signers).
- Treasury = 2-of-3 multisig of the three signers for `dev`/`heisenberg`, 6-of-10 of the mainnet signers for `staging_mainnet` (distinct nonce per preset); no dedicated treasury genesis balance (endowments are a separate list).
- Treasury = 2-of-3 multisig of the three signers for `dev`/`heisenberg`; `staging_mainnet` deliberately leaves it unconfigured for a post-launch Root referendum. No dedicated treasury genesis balance exists (endowments are a separate list).
- Tech-collective seeded via the chain-spec-only `tech_collective_seed_members` JSON field (`prepare_genesis_build_input` + `seed_tech_collective`).
- Endows all genesis balances with wormhole transfer proofs (ZK-spendable). `dev` also endows `TEST_WORMHOLE_SECRET`'s address.

Expand Down
83 changes: 52 additions & 31 deletions docs/STAGING_MAINNET_LAUNCH.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,50 @@
# Staging-Mainnet Launch

Mainnet dress rehearsal. Genesis comes from `mainnet_config_genesis`
(`runtime/src/genesis_config_presets.rs`) — identical to the eventual mainnet's
except the treasury multisig nonce (staging 1, mainnet 0), so the genesis hash
differs while everything else stays 1:1.
Mainnet dress rehearsal. Genesis comes from `staging_mainnet_config_genesis`
(`runtime/src/genesis_config_presets.rs`). It is staging-only; mainnet will use
a separate genesis preset and chain spec. It seeds the launch tech collective
but deliberately leaves the treasury unconfigured; the collective sets the
treasury account after launch through a Root referendum.

- Genesis hash (spec generated from `v0.11.0-rocket-fuel`, runtime `spec_version` 148):
`0xb0e90ed9f291a9194f79bf90dce39ad7e5ea88f1bf6a9db5f268f309d305e30d`
(state root `0x8f3f…fc50`). Any change to genesis data re-derives this — the
chain spec must then be regenerated from a new runtime-upgrade tag.
- Treasury: 6-of-10 multisig of `MAINNET_TREASURY_SIGNERS_SS58`, nonce 1 →
`qzpjP5r4NSeWDrbHvboYcychsmCCaJrbRixvghVjnaRzjRb5i`
- Tech collective: the same ten accounts (referenda curves are runtime constants)
- Balances: no fixed figure is written down. Each signer's liquid endowment is
computed at genesis by `governance_treasury_signer_seed()` from the live
runtime constants — treasury multisig bootstrap (multisig fee, proposal fee,
refundable proposal deposit, inclusion-fee prepay, ED), the referenda
submission and decision bonds, a maximum-size preimage deposit, and a small
scaled fee headroom. It is deliberately the minimum that lets any single
treasurer act from genesis: bootstrap the multisig and carry one referendum
end to end. Because it is derived, changing `FEE_SCALE` or any deposit
re-derives the endowment instead of stranding governance, and a genesis test
pins every signer balance to the formula. The 20 HD rehearsal accounts
- Genesis hash: regenerate and record it after this genesis change; the previous
staging hash is obsolete.
- Treasury: unconfigured at genesis. `TreasuryPallet::set_treasury_account` is
called by an approved Root referendum after the real treasury is ready.
- Tech collective: the ten accounts in
`STAGING_MAINNET_TECH_COLLECTIVE_MEMBERS_SS58` (referenda curves are runtime
constants).
- Balances: no fixed figure is written down. Each member's liquid endowment is
computed at genesis by `governance_member_seed()` from the live runtime
constants — ED, the referenda submission and decision bonds, a maximum-size
preimage deposit, and scaled fee headroom. It is deliberately the minimum
that lets any member carry one referendum end to end. Because it is derived,
changing `FEE_SCALE` or any deposit re-derives the endowment instead of
stranding governance, and a genesis test pins every member balance to the
formula. The 20 HD rehearsal accounts
(`staging-0`..`staging-19`) each get `STAGING_REHEARSAL_LIQUID` so they can
submit transactions. Everything else sits in the vesting pot. Read the
exact per-signer and total figures for a given build out of the generated
exact per-member and total figures for a given build out of the generated
spec's `balances` section rather than restating them here.
- Vesting: `mainnet_vesting_schedules` is a DUMMY scaffold (team / early-backer /
ecosystem entries with stand-in beneficiaries T1, T2, and the treasury, plus
20 HD rehearsal accounts with distinct grants summing to 100_000 UNIT,
5-minute cliff / 10-day vest from 2026-09-01 14:00 UTC). Replace it with the
real allocation table and flip `MAINNET_VESTING_FINALIZED` before the mainnet
preset is added — until then only staging-mainnet builds.
- Vesting: `staging_mainnet_vesting_schedules` contains team / early-backer /
ecosystem entries with stand-in beneficiaries T1, T2, and T3, plus
20 HD rehearsal accounts with distinct grants summing to 100_000 UNIT). Two
clocks:
- Rehearsal accounts start on 2026-09-03 at 14:00 UTC and keep their 5-minute
cliff / 10-day duration. Those keys are ours, so they vest on launch day.
- The three stand-in schedules start on 2026-09-17 UTC, two weeks after the
chain, with cliffs at most 24 hours after that start. `claim` is
permissionless and pays whoever the schedule names when it runs, and
retargeting is Root-only, so a placeholder must not become claimable before
the collective can replace it. The Root track needs at least
`ROOT_TRACK_MIN_ENACTMENT_DELAY` (2h prepare + 24h confirm + 24h enactment)
even on an immediate unanimous vote; the rest is operational margin. A
compile-time assertion and
`staging_placeholder_cliffs_clear_the_root_governance_latency` hold the two
apart, so moving either date re-checks the invariant.

Team and ecosystem grants retain their 4×365-day duration and the early-backer
grant its 2×365-day duration. These schedules belong only to staging-mainnet
and are not the basis of the future mainnet allocation table.

## Generate the chain spec

Expand Down Expand Up @@ -63,6 +76,14 @@ that field is outside genesis, so the hash is unchanged.

## Post-launch

One signer calls `multisig.createMultisig(signers, 6, 1)`; the derived address
must equal the genesis treasury account above. Treasury then operates via
`proposeTransaction` / `approveProposal` at 6-of-10.
Create the real treasury multisig with the chosen signers, threshold, and nonce.
Then submit and approve a normal Root referendum calling
`treasuryPallet.setTreasuryAccount(multisigAddress)`. Setting the account does
not move or create funds.

Vesting administration is also Root-only. Replace each stand-in beneficiary
through `vesting.retargetSchedule`, using an atomic `utility.batchAll` where
appropriate. Complete each retarget before 2026-09-17 UTC, the earliest
placeholder cliff: `claim` is permissionless and pays the beneficiary stored when
the claim executes. Start the referendum well inside that window — the Root track
cannot enact anything in under about 50 hours.
2 changes: 1 addition & 1 deletion docs/TECH_COLLECTIVE_GOVERNANCE_TUNING.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ fn approval(&self, _) -> Perbill { Perbill::from_rational(self.ayes, 1.max(self.

Both tracks use flat curves, so the required numbers never decay over the decision period (`fast_track_curves_pin_eight_of_ten` in `runtime/tests/governance/fast_upgrade.rs` pins this for track 1):

| Track | `min_approval` | `min_support` | 10 members (staging-mainnet / mainnet: the treasury signers) | 5 members (`MIN_TECH_COLLECTIVE_MEMBERS`, dev/testnet presets) |
| Track | `min_approval` | `min_support` | 10 members (staging-mainnet tech collective) | 5 members (`MIN_TECH_COLLECTIVE_MEMBERS`, dev/testnet presets) |
|---|---|---|---|---|
| 0 | 61% | 60% | 6 ayes; 4 nays block | 3 ayes; 2 nays block |
| 1 `fast_upgrade` | 80% | 80% | 8 ayes; 3 nays block | 4 ayes; 2 nays block |
Expand Down
7 changes: 4 additions & 3 deletions node/src/chain_spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,10 @@ pub fn heisenberg_chain_spec() -> Result<ChainSpec, String> {

/// Staging-mainnet — dress rehearsal for the mainnet launch.
///
/// Genesis is 1:1 with the eventual mainnet's (same 6-of-10 treasury multisig
/// signers, same tech collective, same endowments); only the treasury multisig
/// nonce differs, giving staging its own treasury account and genesis hash.
/// Genesis seeds the launch tech collective but deliberately leaves the treasury
/// unconfigured. The collective sets the treasury account later through a Root
/// referendum, so no temporary treasury multisig is part of genesis. Mainnet
/// will use a separate genesis preset and chain spec.
/// Bootnodes are added once the staging infrastructure exists (the `bootNodes`
/// field lives outside genesis, so editing it does not change the hash).
pub fn staging_mainnet_chain_spec() -> Result<ChainSpec, String> {
Expand Down
17 changes: 8 additions & 9 deletions pallets/vesting/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,11 @@
//! high-security accounts are call-whitelisted, so for both a third-party "ping" is the
//! only claim path. The payout always goes to the stored beneficiary, never the caller.
//!
//! The admin origin (the treasury account, with Root as break-glass) can create schedules
//! (funded from the treasury in the same call), end them early (vested part to the
//! beneficiary, unvested remainder back to the treasury), and retarget a schedule's
//! beneficiary. A retarget replaces the wallet of the *same* grantee — the old address is
//! lost, stolen, or abandoned — so it pays the old address nothing; everything unclaimed
//! follows the schedule to the new wallet.
//! The configured admin origin can create schedules (funded from the treasury in the same
//! call), end them early (vested part to the beneficiary, unvested remainder back to the
//! treasury), and retarget a schedule's beneficiary. A retarget replaces the wallet of the
//! *same* grantee — the old address is lost, stolen, or abandoned — so it pays the old address
//! nothing; everything unclaimed follows the schedule to the new wallet.

extern crate alloc;

Expand Down Expand Up @@ -124,13 +123,13 @@ pub mod pallet {
#[pallet::constant]
type PalletId: Get<PalletId>;

/// Origin allowed to create, end, and retarget schedules
/// (Root or signed-by-treasury in the runtime).
/// Origin allowed to create, end, and retarget schedules.
type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin>;

/// The configured treasury account: funding source for `create_schedule` and
/// destination for unvested remainders. `None` if the chain was started without
/// a treasury, in which case admin calls fail loudly.
/// a treasury, in which case `create_schedule` and `end_schedule` fail loudly.
/// Retargeting does not read or move treasury funds.
type TreasuryAccount: Get<Option<Self::AccountId>>;

/// Asset id type forwarded to the proof recorder (payouts are always native:
Expand Down
9 changes: 4 additions & 5 deletions pallets/vesting/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,9 @@ impl pallet_balances::Config for Test {
type DoneSlashHandler = ();
}

/// Same shape as the runtime's `EnsureTreasury`: `Signed(who)` where `who` is the
/// configured treasury account.
pub struct EnsureTreasury;
impl EnsureOrigin<RuntimeOrigin> for EnsureTreasury {
/// Test-only signed treasury origin used to exercise configurable admin-origin behavior.
pub struct EnsureTestTreasury;
impl EnsureOrigin<RuntimeOrigin> for EnsureTestTreasury {
type Success = AccountId32;
fn try_origin(o: RuntimeOrigin) -> Result<Self::Success, RuntimeOrigin> {
match (o.clone().into(), TreasuryAccount::get()) {
Expand Down Expand Up @@ -179,7 +178,7 @@ impl pallet_vesting::Config for Test {
type Currency = Balances;
type TimeProvider = Timestamp;
type PalletId = VestingPalletId;
type AdminOrigin = EitherOfDiverse<EnsureRoot<AccountId32>, EnsureTreasury>;
type AdminOrigin = EitherOfDiverse<EnsureRoot<AccountId32>, EnsureTestTreasury>;
type TreasuryAccount = TreasuryAccount;
type AssetId = u32;
type ProofRecorder = MockProofRecorder;
Expand Down
34 changes: 7 additions & 27 deletions runtime/src/configs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ use crate::{
use frame_support::{
derive_impl, parameter_types,
traits::{
ConstU128, ConstU16, ConstU32, ConstU8, EitherOfDiverse, EnsureOrigin, Get,
NeverEnsureOrigin, VariantCountOf,
ConstU128, ConstU16, ConstU32, ConstU8, EitherOfDiverse, Get, NeverEnsureOrigin,
VariantCountOf,
},
weights::{
constants::{RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND},
Expand Down Expand Up @@ -635,42 +635,22 @@ const _: () = assert!(

/// The configured treasury account as an `Option` — unlike
/// `pallet_treasury::Pallet::account_id()`, this never panics on a chain whose
/// genesis omitted the treasury; vesting admin calls fail with an explicit error instead.
/// genesis omitted the treasury; vesting's fund-moving admin calls fail with an
/// explicit error while beneficiary retargeting remains available.
pub struct TreasuryAccountOption;
impl Get<Option<AccountId>> for TreasuryAccountOption {
fn get() -> Option<AccountId> {
pallet_treasury::Pallet::<Runtime>::treasury_account()
}
}

/// `Signed(who)` where `who` is the configured treasury account.
///
/// The treasury is a multisig in real deployments; the multisig pallet dispatches
/// approved proposals as `RawOrigin::Signed(multisig_address)`, so a plain
/// signed-origin check covers it.
pub struct EnsureTreasury;
impl EnsureOrigin<RuntimeOrigin> for EnsureTreasury {
type Success = AccountId;
fn try_origin(o: RuntimeOrigin) -> Result<Self::Success, RuntimeOrigin> {
match (o.clone().into(), pallet_treasury::Pallet::<Runtime>::treasury_account()) {
(Ok(frame_system::RawOrigin::Signed(who)), Some(treasury)) if who == treasury =>
Ok(who),
_ => Err(o),
}
}
#[cfg(feature = "runtime-benchmarks")]
fn try_successful_origin() -> Result<RuntimeOrigin, ()> {
pallet_treasury::Pallet::<Runtime>::treasury_account()
.map(RuntimeOrigin::signed)
.ok_or(())
}
}

impl pallet_vesting::Config for Runtime {
type Currency = Balances;
type TimeProvider = Timestamp;
type PalletId = VestingPalletId;
type AdminOrigin = EitherOfDiverse<EnsureRoot<AccountId>, EnsureTreasury>;
// Vesting administration is collective governance only. The configured
// treasury account holds funds but has no unilateral schedule authority.
type AdminOrigin = EnsureRoot<AccountId>;
type TreasuryAccount = TreasuryAccountOption;
type AssetId = AssetId;
// The pallet records its payouts itself so Root calls enacted by the scheduler
Expand Down
Loading
Loading