Treasury Expansion & Vesting
This document covers the treasury canister's expanded account model (BL-075–079), cross-entity transfer mechanics, and the vesting system added in BL-088.
Canisters:
- Staging:
xe7g5-4aaaa-aaaao-a7byq-cai(dao-admin — treasury admin UI) - Production (Tier 1):
m7xyj-kaaaa-aaaac-behcq-cai
Account Types
The treasury canister manages multiple account types, each with its own thresholds and withdrawal rules.
Chart of Accounts (BL-075.1–075.3)
| Account Type | Code | Purpose | Withdrawal Rule |
|---|---|---|---|
| Operating | OP | Day-to-day expenses, vendor payouts | Immediate, role-gated |
| Reserve | RES | Emergency fund, rainy-day buffer | Requires governance vote |
| Endowment | END | Long-term mission capital | Restricted — interest only |
| Project Escrow | ESC | Milestone-based project funding | Released on milestone approval |
| Vesting Pool | VEST | Contributor token grants | Released per vesting schedule |
Minimum threshold balances are configured at canister init and can be updated via SetThreshold governance proposals.
Operating ── immediate payout ──► recipient
Reserve ── governance vote ──► Operating ── payout ──► recipient
Endowment ── interest accrual only ──► Operating
Project ESC ── milestone approval ──► recipient
Vesting Pool ── schedule release ──► recipientThreshold Configuration
// Candid
type AccountThreshold = record {
account_type : AccountType;
min_balance : nat; // minimum DOM tokens (e8s)
max_single_withdrawal : opt nat;
};If a payout would bring an account below min_balance, the canister rejects the transfer with InsufficientReserve. Admins with the Treasurer role can override with an explicit force: true flag (audited via AdminEvent).
Cross-Entity Transfers (BL-075.2)
Cross-entity transfers move funds between account types within the same treasury canister, or between treasury canisters of affiliated entities (federated DAO partners).
Internal Transfers
// Move from Reserve to Operating (requires Treasurer role)
transfer_internal : (TransferInternalArgs) -> (Result<nat64, TreasuryError>);
type TransferInternalArgs = record {
from : AccountType;
to : AccountType;
amount : nat; // DOM tokens in e8s
memo : opt text;
require_vote : bool; // if true, creates a governance proposal instead
};Cross-Entity Transfers (BL-075.2)
Transfers between DAO entities use a two-phase commit:
- Initiate — source treasury locks funds in escrow and records a
CrossEntityTransferProof - Confirm — destination treasury verifies the proof via inter-canister call and credits the account
initiate_cross_entity_transfer : (CrossEntityArgs) -> (Result<TransferProof, TreasuryError>);
confirm_cross_entity_transfer : (TransferProof) -> (Result<nat64, TreasuryError>);
type CrossEntityArgs = record {
destination_canister : principal;
destination_account : AccountType;
amount : nat;
expires_at : opt nat64; // Unix timestamp, default 24h
};If confirmation is not received before expires_at, the source treasury releases the escrow and refunds the originating account.
Price Oracle Integration (BL-076)
The treasury uses price feeds from the oracle-bridge to convert USD amounts to DOM token quantities. This enables USD-denominated budget proposals and campaign goals.
Oracle Feed
Oracle-bridge exposes a price endpoint:
GET /api/price/dom-usd{
"price_usd": 0.0142,
"timestamp": 1711900800,
"source": "aggregate",
"confidence": "high"
}The treasury canister stores the most recent price tick received from a trusted oracle principal. Price is updated via governance (SetDomPrice proposal type — see Governance & Price Oracle).
USD-Denominated Payouts
// Propose a payout in USD (converted to DOM at current oracle price)
propose_payout_usd : (PayoutUsdArgs) -> (Result<nat64, TreasuryError>);
type PayoutUsdArgs = record {
recipient : principal;
amount_usd : float64; // e.g. 250.00
account : AccountType;
memo : opt text;
slippage_bps : nat16; // basis points, e.g. 50 = 0.5% tolerance
};If the DOM/USD price moves beyond slippage_bps between proposal creation and execution, the payout is rejected and a new proposal must be submitted.
Vesting Schedules (BL-088)
Vesting locks contributor grants in the treasury VEST account and releases them incrementally over time.
VestingSchedule Struct
type ReleaseType = variant {
Linear; // pro-rata, released per block/time interval
Cliff; // nothing until lock_until, then all at once
Stepped; // tranches: e.g. 25% at 6mo, 25% at 12mo, 50% at 24mo
};
type VestingSchedule = record {
beneficiary : principal;
total_amount : nat; // total DOM in e8s
lock_until : nat64; // Unix timestamp (earliest any release occurs)
release_type : ReleaseType;
start_at : nat64; // vesting period start
end_at : nat64; // vesting period end (for Linear/Stepped)
tranches : opt vec Tranche; // required for Stepped
memo : opt text;
};
type Tranche = record {
release_at : nat64; // Unix timestamp
bps : nat16; // basis points of total_amount (must sum to 10000)
};lock_until vs. start_at
lock_until— absolute earliest any tokens can leave the canister. Even for Linear schedules, no release happens before this timestamp.start_at— when the vesting clock begins. For Linear schedules, the pro-rata calculation starts here.end_at— when 100% of the grant is fully vested.
A typical 1-year cliff + 3-year linear grant:
VestingSchedule {
beneficiary: "<contributor principal>",
total_amount: 100_000_00000000, // 100,000 DOM (8 decimals)
lock_until: 1743465600, // 2025-04-01 (1 year cliff)
release_type: ReleaseType::Linear,
start_at: 1711929600, // 2024-04-01
end_at: 1806537600, // 2027-04-01
tranches: None,
memo: Some("Contributor grant — FAS sprint team"),
}Vesting API
// Create a new vesting schedule (Treasurer role required)
create_vesting_schedule : (VestingSchedule) -> (Result<nat64, TreasuryError>);
// Query unvested + vested balances for a beneficiary
get_vesting_balance : (principal) -> (VestingBalance) query;
// Claim vested tokens (called by beneficiary)
claim_vested : (schedule_id : nat64) -> (Result<nat64, TreasuryError>);
// List all schedules for a beneficiary
list_vesting_schedules : (principal) -> (vec VestingScheduleView) query;
// Cancel unvested portion (Treasurer role, emits AdminEvent)
cancel_vesting_schedule : (schedule_id : nat64, reason : text) -> (Result<nat64, TreasuryError>);VestingBalance Response
type VestingBalance = record {
schedule_id : nat64;
total_amount : nat;
vested_amount : nat; // calculated at query time
claimed_amount: nat;
claimable_now : nat; // vested - claimed
next_release : opt nat64; // next unlock timestamp (Stepped schedules)
};Treasury Canister API Reference
Payout Flows
// Propose a new payout (starts voting window if amount > threshold)
propose_payout : (PayoutArgs) -> (Result<nat64, TreasuryError>);
// Execute an approved payout proposal
execute_payout : (proposal_id : nat64) -> (Result<nat64, TreasuryError>);
// Query payout status
get_payout : (proposal_id : nat64) -> (opt PayoutRecord) query;
// List recent payouts
list_payouts : (ListPayoutsArgs) -> (vec PayoutRecord) query;Admin Events
All treasury mutations emit AdminEvent records (State V2 pattern from dom-token, BL-040). These are queryable for audit purposes:
list_admin_events : (ListAdminEventsArgs) -> (vec AdminEvent) query;
type AdminEvent = record {
id : nat64;
timestamp : nat64;
actor : principal;
event_type : AdminEventType;
details : text; // JSON-encoded diff
};Error Types
type TreasuryError = variant {
NotAuthorized;
InsufficientFunds;
InsufficientReserve;
ProposalNotFound;
ProposalNotApproved;
SlippageExceeded;
VestingScheduleNotFound;
AlreadyClaimed;
LockPeriodActive;
TransferFailed : text;
};Role Requirements
| Action | Required Role |
|---|---|
| View balances and payouts | Member |
| Propose payouts (below threshold) | Treasurer |
| Approve payouts | Council |
| Execute approved payouts | Treasurer |
| Create/cancel vesting schedules | Treasurer |
| Transfer between account types | Treasurer |
| Override minimum balance | Treasurer + force: true |
Roles are verified via inter-canister call to membership (production: tx4wx-iqaaa-aaaah-avegq-cai).
Integration with dom-token
Treasury payout execution calls icrc1_transfer on the dom-token ledger (production: ybuho-zyaaa-aaaal-qwsfq-cai). The treasury canister holds an allowance approved by the DAO wallet.
Treasury.execute_payout()
└─► dom-token.icrc1_transfer({ from: treasury_account, to: recipient, amount })
└─► ICRC-1 ledger records the transferBurn-path integrations (marketplace fee burns, donation burns) use a separate burn wallet architecture — see DOM Token V2.