Skip to content

Checking access...

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 TypeCodePurposeWithdrawal Rule
OperatingOPDay-to-day expenses, vendor payoutsImmediate, role-gated
ReserveRESEmergency fund, rainy-day bufferRequires governance vote
EndowmentENDLong-term mission capitalRestricted — interest only
Project EscrowESCMilestone-based project fundingReleased on milestone approval
Vesting PoolVESTContributor token grantsReleased 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 ──► recipient

Threshold Configuration

rust
// 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

rust
// 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:

  1. Initiate — source treasury locks funds in escrow and records a CrossEntityTransferProof
  2. Confirm — destination treasury verifies the proof via inter-canister call and credits the account
rust
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:

http
GET /api/price/dom-usd
json
{
  "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

rust
// 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

rust
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:

rust
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

rust
// 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

rust
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

rust
// 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:

rust
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

rust
type TreasuryError = variant {
  NotAuthorized;
  InsufficientFunds;
  InsufficientReserve;
  ProposalNotFound;
  ProposalNotApproved;
  SlippageExceeded;
  VestingScheduleNotFound;
  AlreadyClaimed;
  LockPeriodActive;
  TransferFailed : text;
};

Role Requirements

ActionRequired Role
View balances and payoutsMember
Propose payouts (below threshold)Treasurer
Approve payoutsCouncil
Execute approved payoutsTreasurer
Create/cancel vesting schedulesTreasurer
Transfer between account typesTreasurer
Override minimum balanceTreasurer + 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 transfer

Burn-path integrations (marketplace fee burns, donation burns) use a separate burn wallet architecture — see DOM Token V2.

Hello World Co-Op DAO