Source: pragma-org/amaru-treasury (main)
The system has three NFT-pinned UTxO families (Scopes, Registry, Treasury) plus a permissions script that holds no UTxO and is invoked through the withdraw-zero pattern. Dependencies split into bake-time (script parameters fixed at deploy) and run-time (which UTxOs must appear as input or reference input in a tx).
flowchart TD
seedS[seed UTxO for scopes]
seedR[seed UTxO for registry]
Scope{{Scope tag}}
S["scopes validator<br/>policy = SCOPES_NFT"]
P["permissions(scopes_nft, scope)"]
R["treasury_registry(seed, scope)<br/>policy = REG_NFT[scope]"]
T["Sundae treasury(cfg)<br/>cfg.registry_token = REG_NFT[scope]<br/>cfg.permissions.* = hash(P)"]
seedS --> S
seedR --> R
Scope --> P
Scope --> R
S -- SCOPES_NFT --> P
P -- "permissions script hash" --> T
R -- "REG_NFT[scope]" --> T
flowchart TD
SU["<b>Scopes UTxO</b> (NFT-pinned)<br/>Datum: Scopes {<br/> core_development,<br/> ops_and_use_cases,<br/> network_compliance,<br/> middleware : MultisigScript<br/>}"]
RU["<b>Registry UTxO</b> per scope (NFT-pinned)<br/>Datum: ScriptHashRegistry {<br/> treasury : Script h_t,<br/> vendor : Script h_v<br/>}<br/>Immutable: old == new"]
TU["<b>Treasury UTxO</b> per scope (Sundae)<br/>Datum: TreasuryConfiguration {<br/> registry_token,<br/> permissions { reorganize, sweep, fund=AnyOf[], disburse },<br/> expiration,<br/> payout_upperbound = 0<br/>}<br/>Holds: withdrawn ADA"]
P["permissions script (no UTxO)<br/>withdraw-zero gate<br/>Reorganize → owner<br/>Sweep / Disburse → owner + 1 other<br/>Fund → False"]
VU["Vendor UTxO(s)<br/>per disbursement"]
GA[(PRAGMA General Assembly multisig)]
TU -- "withdraw 0 (delegates auth)" --> P
P -- "reference input" --> SU
TU -- "reference input" --> RU
TU -- "disburse / sweep" --> VU
VU -- "reference input on cancel" --> RU
GA -- "spend (mutate)" --> SU
GA -- "spend (immutable)" --> RU
| Arrow | Direction | Reason |
|---|---|---|
| Treasury → Permissions | runtime, withdraw-zero | Sundae's treasury delegates who can sweep / disburse / reorganize to the permissions script. TreasuryConfiguration.permissions.* literally stores the permissions script hash. |
| Permissions → Scopes | runtime, reference input | expect_scopes(self.reference_inputs, scopes_nft) — owner credentials are dynamic, so permissions reads them from the Scopes UTxO each time. Decouples ownership rotation from script hashes. |
| Treasury → Registry | runtime, reference input | Sundae's vendor logic needs the treasury & vendor script hashes (e.g. for cancel-to-treasury). It locates them via the registry_token policy baked into TreasuryConfiguration. |
| Scopes UTxO ← admin | spend | Mutable but only by PRAGMA General Assembly multisig (must_be_approved_by_general_assembly). New datum must still match the Scopes shape. |
| Registry UTxO ← admin | spend | Spendable only by GA, and with_state forces old_datum == new_datum — effectively immutable until the NFT is burned. |
| Both NFT mints ← seed UTxO | one-shot | The two trap validators are parameterised by a seed OutputReference so the policy id is unique and minting can only happen once. |
| Vendor → Registry | runtime, reference input | Vendor outputs use the registry to find the treasury hash to return to on cancel. |
The treasury holds the money but knows nothing about owners; it asks permissions; permissions asks the scopes UTxO; the registry tells everyone where the treasury and vendor scripts live — all three lookups are by NFT policy id, so addresses can rotate without re-parameterising scripts.