Status: Draft
Version: 0.5.0
Depends on: SPEC-CORE.md (Structural Authorization Pattern)
Related: structural-authorization-ckb.md
This document specifies the treasury pattern — a shared, donatable pool built on the structural authorization pattern defined in SPEC-CORE. The pool's capacity can only move when a valid condition cell of an authorized type appears in the same transaction.
Treasury-pattern applications (governance, pooled dev funds, …) are plug-ins that implement the condition script interface. Each is defined in a separate SPEC-<APPLICATION>.md. Applications that only need structural authorization (bounties, bonds, escrows, …) are specified against SPEC-CORE.md directly — see POSSIBILITIES.md.
- Treasury cell (guarded cell specialized as a shared pool)
- Donation mechanism
- Lifecycle: anchor, execute, abort transaction classes
- Capacity accounting for pooled funds
- Self-replenishment (for temporary-claim applications)
- Condition cell ("proof cell") requirements specific to treasury use
- Application plug-in checklist
- The structural authorization mechanism itself (see
SPEC-CORE.md) - Application-specific condition logic (see
SPEC-<APPLICATION>.md) - Concrete cell data layouts for applications
- Frontend / mempool coordination
- Mainnet deployment checklist
All terms from SPEC-CORE apply. Additional terms:
| Term | Definition |
|---|---|
| Treasury cell | A guarded cell (per SPEC-CORE) used as a shared pool. Holds donatable, pooled capacity. |
| Proof cell | A condition cell (per SPEC-CORE) that represents an on-chain claim funded from the treasury. |
| Anchor TX | Transaction that spends the treasury to create a proof cell. |
| Execute TX | Transaction that consumes a proof cell after conditions are met, producing a result and (for temporary-claim applications) returning capacity to the treasury. |
| Abort TX | Transaction that consumes a proof cell without executing the success path, returning capacity to the treasury. |
| Authorized proof type | The authorized condition type (per SPEC-CORE) that a treasury instance accepts. Fixed in treasury args. |
| Result | Application-defined outputs produced on successful execute (e.g. updated registry, payout cell). |
The treasury pattern is one application of the structural authorization pattern:
┌──────────────────────────────────────────────────────────┐
│ CKB primitives: type scripts, cells, since, capacity │
├──────────────────────────────────────────────────────────┤
│ SPEC-CORE: Structural Authorization Pattern │
│ (guarded cell + condition cell + mutual validation) │
├──────────────────────────────────────────────────────────┤
│ SPEC-TREASURY (this document): Shared Pool Pattern │
│ (donation, lifecycle, replenishment, abort) │
├──────────────────────────────────────────────────────────┤
│ Applications: governance, bounty, grants, … │
├──────────────────────────────────────────────────────────┤
│ Implementations: concrete schemas, scripts, TX builders │
└──────────────────────────────────────────────────────────┘
The treasury pattern adds to SPEC-CORE:
- A donation mechanism (anyone can add capacity to the pool)
- A multi-phase lifecycle (anchor → delay → execute/abort)
- Capacity replenishment for temporary-claim applications
- An abort path for conditions that can't be fulfilled
SPEC-CORE's structural authorization mechanism also supports applications that don't need the treasury pattern — single-phase conditional payments, escrows, proof-of-work rewards, etc. Those applications reference SPEC-CORE directly.
These extend SPEC-CORE's invariants (§4). All SPEC-CORE invariants remain in force.
| ID | Invariant |
|---|---|
| INV-TS1 | No transaction may reduce treasury capacity except via an anchor TX that creates one or more authorized proof outputs. |
| INV-TS2 | A treasury input may appear only in anchor, execute, or abort transactions that satisfy this spec. |
| INV-TS3 | A proof cell may be created only in an anchor TX that passes both the guard script and the condition script. |
| INV-TS4 | A proof cell may be consumed only in an execute or abort TX that passes both scripts. |
| INV-TS5 | An execute or abort TX MUST NOT create new authorized proof outputs. Transactions with authorized proof cells in both inputs and outputs MUST be rejected, unless the application spec explicitly defines a rotation mode with its own capacity accounting. |
Circuit breaker: treasury guards protecting significant value SHOULD implement the outflow throttle defined in SPEC-CORE §7.5 (
max_outflow_per_spend,min_spend_interval). A throttled treasury bounds the worst-case drain from a condition-script bug to a survivable rate without reintroducing a trusted party. Anchor is the throttled class (it is the only class that reduces treasury capacity, INV-TS1); donation inflow is never throttled.
| ID | Invariant |
|---|---|
| INV-TA1 | On anchor: Σ(treasury_in.capacity) = treasury_out.capacity + Σ(proof_out.capacity) + fee. Additional fee-paying inputs may contribute. |
| INV-TA2 | On execute: proof_in.capacity + treasury_in.capacity = treasury_out.capacity + Σ(result.capacity) + tip + fee (tip MAY be 0; see INV-TA7). |
| INV-TA3 | On abort: proof_in.capacity + treasury_in.capacity = treasury_out.capacity + tip + fee. All proof capacity returns except the bounded tip and fee. |
| INV-TA4 | Treasury and proof outputs MUST meet minimum CKB occupancy for their byte size. |
| INV-TA5 | For temporary-claim applications (e.g., governance): net treasury loss per anchor→execute cycle equals transaction fees only. For permanent-transfer applications (e.g., bounty payout): proof capacity is consumed by result outputs. Application spec MUST declare which class it belongs to. |
| INV-TA6 | The fee term in INV-TA1–TA3 MUST be bounded. Each application MUST define a MAX_FEE, enforced by the guard, on any transaction class where fees can be drawn from treasury or proof capacity — or require fees to be paid entirely from separate fee-paying inputs (in which case treasury/proof capacity balances exactly). Rationale: these transactions carry no signatures, so with an unbounded fee anyone can construct an execute/abort that siphons proof capacity to miners as fee. |
| INV-TA7 | Executor tip (liveness incentive). Execute and abort transactions MAY include a single tip output whose recipient is deliberately unpinned — claimable by whoever submits the transaction. The tip amount MUST be bounded by an application-defined MAX_TIP enforced by the guard. Rationale: "anyone may submit" (INV-TL1) guarantees liveness only if someone actually watches chain state and builds the transaction; a bounded tip makes triggering execute/abort incentive-compatible rather than altruistic. The unpinned recipient is safe only because the amount is capped and everything else is pinned — a front-runner who replaces the tip recipient changes who earns the tip, not what the transaction does. |
| ID | Invariant |
|---|---|
| INV-TT1 | If an application requires a delay between anchor and execute, it MUST be enforced via the CKB since field on the proof input (relative or absolute MTP as defined by the application spec). |
| INV-TT2 | Execute/abort MUST NOT be includable before since is satisfied (consensus-enforced, not application-enforced). |
| ID | Property |
|---|---|
| INV-TL1 | Anyone may submit a valid anchor, execute, or abort TX (no privileged relayer). |
| INV-TL2 | Donations MUST succeed by creating separate UTXOs at the treasury address. The guard type script executes on the donation output (creation mode) and MUST permit it without a proof cell or signature (per SPEC-CORE INV-L2). |
| INV-TL3 | Treasury must have sufficient capacity (≥ minimum proof capacity + fees) to create a proof; otherwise, anchor creation halts until donations replenish the pool. |
Treasury is not spent. A user sends capacity to the treasury address, creating a new, separate UTXO.
inputs: [ donor_cell ]
outputs: [ donation_cell (treasury lock + treasury type) ]
Donation UTXOs are later merged during an anchor transaction.
The guard type script does run on donation — on CKB, type scripts execute for cells in outputs as well as inputs — but in creation mode (treasury cell in outputs only, no treasury input), which it MUST permit without a proof cell (SPEC-CORE §5.4, INV-L2). No signature is required either.
Because creation is permissionless, anyone can mint treasury-typed cells carrying arbitrary data. To keep malformed donation cells from complicating later anchor merges, the guard SHOULD enforce that treasury cells created in donation mode carry empty data (0x); if it does not, the anchor path MUST treat merged donation cells' data as meaningless and rely only on their capacity.
Economic note — idle-capacity dilution: pooled capacity earns nothing while it sits in the treasury, whereas NervosDAO depositors are compensated for secondary issuance. Donations therefore dilute against the CKB supply over time. Applications MUST document this cost to donors; whether treasury cells can layer over NervosDAO deposits to offset it is an open question (§10).
Creates one or more proof cells funded from the treasury, merging any donation UTXOs.
inputs:
- treasury_cell(s) (one or more existing treasury/donation cells)
- (optional) fee_paying_cell
outputs:
- proof_cell(s) (authorized type, each ≥ application-defined minimum capacity)
- treasury_cell (single consolidated change output, reduced capacity)
witnesses:
- treasury: "0x" (no signature — per SPEC-CORE INV-S3)
cell_deps:
- guard script binary
- condition script binary
Guard script (anchor mode):
- One or more treasury inputs (merging donations).
- Exactly one treasury output (change).
- One or more proof outputs matching
authorized_condition_script_hash. - Capacity conservation per INV-TA1.
Condition script (creation mode):
- Validates proof output structure, data, and any creation conditions.
- Application-specific rules in
SPEC-<APPLICATION>.md.
Consumes proof after conditions are met; produces result; returns capacity to treasury (for temporary-claim applications) or sends it to result outputs (for permanent-transfer).
inputs:
- treasury_cell (consumed and recreated for replenishment)
- proof_cell (since: per application, if required)
- ... application inputs (e.g. registry cell, vote cells)
outputs:
- result_cell(s) (application-defined)
- treasury_cell (replenished, or same capacity if permanent-transfer)
- (optional) executor_tip_cell (≤ MAX_TIP, recipient unpinned — INV-TA7)
witnesses:
- per condition / application requirements (signatures, preimages, etc.)
cell_deps:
- guard script binary
- condition script binary
- application deps
Guard script (execute mode):
- Detects authorized proof cell consumed as input.
- Verifies treasury output receives appropriate capacity (INV-TA2).
- Does not re-validate application logic beyond proof type identity and accounting.
Condition script (consumption mode):
- Validates all application success conditions.
- Validates result outputs.
- Enforces
sinceif applicable.
Returns proof capacity to treasury when success conditions will not be met (e.g. failed governance vote, expired deadline).
inputs:
- treasury_cell (consumed and recreated)
- proof_cell (since: per application abort rules)
outputs:
- treasury_cell (replenished with proof capacity)
- (optional) executor_tip_cell (≤ MAX_TIP, recipient unpinned — INV-TA7)
- (optional) tombstone / null result
witnesses:
- per application abort rules
Whether abort exists and its conditions are application-specific but MUST be defined if proof cells can become stranded (no valid execute path). All proof capacity MUST return to the treasury on abort (INV-TA3).
Note: Abort is strongly recommended for any application where proof cells persist on-chain before final approval (e.g. governance proposals awaiting votes).
The guard script detects its execution context from the transaction structure:
| Mode | Condition |
|---|---|
| ANCHOR | Treasury cell in inputs AND authorized proof cell in outputs (but not in inputs) |
| EXECUTE / ABORT | Treasury cell in inputs AND authorized proof cell in inputs (and NO authorized proof cell in outputs — see below) |
| DONATE | Treasury cell in outputs only (no treasury input) — permit without proof |
Proof cells on both sides: a transaction with authorized proof cells in both inputs and outputs MUST be rejected by the guard (INV-TS5), unless the application spec explicitly defines a rotation mode with its own accounting. Without this rule, mode detection that stops at "proof in inputs" lets an attacker smuggle newly created proof cells into an execute/abort transaction, bypassing anchor accounting (INV-TA1). The guard MUST scan inputs and outputs for authorized proof cells independently, in every mode.
Distinguishing Execute from Abort is delegated to the condition script or handled via an explicit mode indicator in the proof cell's data. The guard script's only concern for Execute vs. Abort is capacity accounting: abort requires full capacity return (INV-TA3); execute allows capacity to flow to result outputs (INV-TA2). In both cases the fee is bounded per INV-TA6 and any executor tip per INV-TA7.
This document specifies an adaptation of the structural authorization pattern (SPEC-CORE.md), not the pattern itself. Use SPEC-TREASURY only when all of the following apply:
- Multiple parties need to spend from a common, donatable pool
- No single party should be custodian of that pool
- Claims are temporary — capacity leaves the pool to fund proof cells and should return after execute or abort
- A multi-phase lifecycle (anchor → delay → execute/abort) is required
- Throughput requirements are low (~1 operation per CKB block)
If any of these do not apply, use SPEC-CORE directly.
| Application | Why the treasury layer is needed |
|---|---|
| Governance treasury | Shared pool funds proposal proof cells; capacity returns after execute or abort |
| Pooled dev / grant funds | Donations accumulate; milestone proofs trigger disbursement; pool replenishes or tracks balance across claims |
| Insurance / mutual pools | Shared premiums; claims consume proof cells; pool must persist across many policyholders |
Most applications in POSSIBILITIES.md use structural authorization only. They do not need donation, anchor/execute/abort, or replenishment machinery.
| Application | Why SPEC-TREASURY is unnecessary |
|---|---|
| Bug bounties | One guarded cell per bounty; single-phase claim; capacity transfers to claimant permanently |
| Accountability bonds | 1:1 bond cell; referee attestation condition; no shared pool |
| Dominant assurance / crowdfund | Guarded pool cell with condition-script modes; no treasury proof lifecycle |
| Bridge escrows | 1:1 escrow cell; light-client proof condition; single-phase release |
| Vesting / escrow | 1:1 relationship; time-locked guarded cell suffices |
| Salary / periodic payments | 1:1 relationship; no multi-party pool |
| Personal savings vault | Single party; defeats the structural authorization premise |
Note: A program that funds many individual bounties from one pool could use SPEC-TREASURY. An individual bounty cell does not — it is SPEC-CORE only.
Each SPEC-<APPLICATION>.md MUST define:
- Application class: temporary-claim or permanent-transfer
- Proof cell
dataschema (versioned) - Proof
argsschema - Creation rules (anchor — condition script
on_create) - Success rules (execute — condition script
on_consume) - Abort rules (if applicable — condition script abort path)
- Result output constraints
- Recipient binding mode for every payout path — identity-pinned or role-pinned with its binding mechanism (SPEC-CORE §5.5)
- Input-class audit: every fact each condition branch reads, classified per SPEC-CORE §5.6; any foreign or off-chain input documented as an explicit trust assumption
-
sinceencoding (relative MTP seconds, absolute, units) - Witness layout
- State machine diagram — every state has an exit; complete transition table with one accounting equation per transaction class
- Branch enumeration with default-reject (
else => ERR) in every script - Risk-model placement (SPEC-CORE §7.6): which low-risk properties hold, which risk multipliers apply, and position on the deployment ladder
- Mapping to transaction classes (§5)
- Edge-case / negative test matrix
- MIN_PROOF_CAPACITY for this application
- MAX_FEE for each transaction class drawing on treasury/proof capacity (INV-TA6)
- MAX_TIP, or an explicit statement that no executor tip is offered (INV-TA7)
- Outflow throttle parameters (
max_outflow_per_spend,min_spend_interval), or an explicit justification for omitting the throttle (SPEC-CORE §7.5) - Donor documentation of idle-capacity dilution (§5.1)
These applications use SPEC-CORE and the treasury adaptation defined in this document.
| Application | Spec document | Status |
|---|---|---|
| Governance (Transaction Firewall) | SPEC-GOVERNANCE.md |
TBD |
Applications that use structural authorization only (bounties, bonds, assurance contracts, …) are catalogued in POSSIBILITIES.md and specified against SPEC-CORE.md, not here.
These extend SPEC-CORE's tests (§8).
| ID | Test |
|---|---|
| TT+01 | Donate creates separate UTXO at treasury address |
| TT+02 | Valid anchor creates proof + treasury change |
| TT+03 | Anchor merges multiple donation UTXOs into one treasury output |
| TT+04 | Valid execute consumes proof, replenishes treasury, produces result |
| TT+05 | Valid abort returns all proof capacity to treasury |
| TT+06 | Full cycle: donate → anchor → execute; net treasury delta = fees |
| TT+07 | Empty witness on treasury input (anchor) |
| TT+08 | Execute/abort with executor tip ≤ MAX_TIP succeeds; treasury replenishment reduced by exactly the tip (INV-TA7) |
| TT+09 | Anchor within throttle limits passes; treasury change output records header median time (if throttle adopted) |
| ID | Test |
|---|---|
| TT-01 | Anchor without proof output |
| TT-02 | Anchor with wrong proof code_hash |
| TT-03 | Anchor with correct layout but wrong proof args |
| TT-04 | Anchor violating capacity conservation |
| TT-05 | Execute before since elapsed |
| TT-06 | Execute without proof consumption |
| TT-07 | Execute with insufficient treasury replenishment |
| TT-08 | Extra attacker-controlled outputs failing condition script |
| TT-09 | Proof from foreign deployment (wrong script hash) |
| TT-10 | Direct treasury spend with signature witness (no proof) |
| TT-11 | Abort that doesn't return full proof capacity |
| TT-12 | Execute or abort TX that also creates a new authorized proof output (smuggled anchor — INV-TS5) |
| TT-13 | Execute or abort with fee exceeding MAX_FEE drawn from treasury/proof capacity (INV-TA6) |
| TT-14 | Donation output with non-empty data (if the canonical-data rule of §5.1 is adopted) |
| TT-15 | Execute or abort with tip exceeding MAX_TIP (INV-TA7) |
| TT-16 | Anchor exceeding max_outflow_per_spend, or before min_spend_interval has elapsed (if throttle adopted) |
- Randomized output injection on anchor/execute
- Capacity sum invariant after every TX
- Fuzz proof
datanear boundary lengths - Invariant: treasury capacity after full anchor→execute cycle = initial − fees
- Abort in guard vs condition script: Should the guard script expose an explicit abort mode, or rely entirely on the condition script's branching?
- Outstanding proof cap: Enforce on-chain in the guard script (count proof cells) or off-chain via coordinator?
- Proof capacity sizing: Fixed per application or dynamic from proof data?
- Treasury UTXO set: For higher throughput, should the spec define merge/split rules for multiple treasury cells (relaxing the singleton constraint)?
- MAX_FEE sizing (INV-TA6): Fixed per application, or derived (e.g., percentage of proof capacity with an absolute cap)? A value too low strands transactions during fee spikes; too high re-opens the siphoning margin. The same sizing question applies to MAX_TIP (INV-TA7): the tip must exceed an executor's marginal cost to run the watcher, while staying small enough that racing to claim it doesn't become its own MEV game.
- Idle-capacity dilution: treasury capacity earns nothing while NervosDAO depositors are compensated for secondary issuance, so pooled donations dilute over time (§5.1). Can treasury cells layer over NervosDAO deposits to offset this (complicated by the DAO's deposit/withdrawal lifecycle interacting with anchor timing), or is documenting the donor-side cost the honest answer?
These SHOULD be resolved before first mainnet deployment.
- Core mechanism:
SPEC-CORE.md - Pattern overview:
structural-authorization-ckb.md - Discussion / prior art:
structural-authorization-ckb-comments.md - CKB
since/ transaction structure: https://docs.nervos.org/docs/tech-explanation/since - Live governance implementation: https://github.com/digitaldrreamer/ckb-transaction-firewall
- UTXO output injection: arXiv:2406.07700
This extends SPEC-CORE's guard pseudocode with treasury-specific lifecycle logic.
fn treasury_guard_main():
authorized = load_args().authorized_condition_script_hash
if treasury_in_inputs():
mode = detect_mode() // ANCHOR | EXECUTE | ABORT (see §6)
match mode:
ANCHOR =>
assert at_least_one_treasury_input()
assert single_treasury_output()
proofs = find_proof_outputs(authorized)
assert len(proofs) >= 1
assert each proof.capacity >= MIN_PROOF_CAPACITY
assert capacity_conserved(treasury_ins, treasury_out, proofs)
assert fee <= MAX_FEE // INV-TA6 — anchors are signature-free too
EXECUTE =>
proof_in = find_consumed_proof_input(authorized)
assert no_proof_outputs(authorized) // no smuggled anchors — INV-TS5
treasury_out = find_treasury_output()
// capacity accounting per INV-TA2
// result outputs may consume some proof capacity
assert capacity_balanced(proof_in, treasury_in, treasury_out, results, tip)
assert fee <= MAX_FEE // INV-TA6
assert tip <= MAX_TIP // INV-TA7 (tip may be 0)
ABORT =>
proof_in = find_consumed_proof_input(authorized)
assert no_proof_outputs(authorized) // no smuggled anchors — INV-TS5
treasury_out = find_treasury_output()
// all proof capacity returns — INV-TA3; fee and tip bounded — INV-TA6/TA7
assert fee <= MAX_FEE
assert tip <= MAX_TIP
assert treasury_out.capacity >= treasury_in.capacity + proof_in.capacity - fee - tip
else:
// Treasury in outputs only — donation / creation
return OK