DOM Token V2
DOM (Decentralized Otter Money) is the utility token of the Hello World DAO, implemented as an ICRC-1/2 fungible token on the Internet Computer. Version 2 of the canister adds admin event logging, a USD-value burn path, and a structured donation burn record system.
Canister IDs:
- Staging:
njo7k-3qaaa-aaaau-aed4a-cai - Production:
ybuho-zyaaa-aaaal-qwsfq-cai
Epic: BL-040 upgraded canister to State V2. BL-078.1 added donation burn records.
What Changed in V2
| Feature | V1 | V2 |
|---|---|---|
| Admin event log | No | Yes — all admin mutations recorded |
| Burn by USD value | No | Yes — burn_usd_value field on BurnArgs |
| Donation burn records | No | Yes — structured DonationBurnRecord |
| Burn wallet | Implicit | Explicit burn_wallet_principal config |
required_approvals | Hardcoded | Runtime-configurable |
The V2 state struct is backward-compatible. New fields use Option<T> to survive stable_restore from V1 WASM.
AdminEvent Logging
Every privileged mutation now emits an AdminEvent that is persisted in stable memory and queryable via Candid.
AdminEvent Types
type AdminEventType = variant {
BurnExecuted;
BurnPolicyUpdated;
MintExecuted;
ThresholdUpdated;
BurnWalletUpdated;
ControllerAdded;
ControllerRemoved;
RequiredApprovalsUpdated;
DonationBurnRecorded;
};
type AdminEvent = record {
id : nat64;
timestamp : nat64; // IC time in nanoseconds
actor : principal; // caller who triggered the event
event_type : AdminEventType;
details : text; // JSON-encoded payload (varies by type)
};Querying Admin Events
// Get all events (admin only)
list_admin_events : (ListAdminEventsArgs) -> (vec AdminEvent) query;
type ListAdminEventsArgs = record {
from_id : opt nat64; // pagination cursor
limit : opt nat32; // default 50, max 200
filter : opt AdminEventType;
};Example: audit all burns since a given event ID:
dfx canister call ybuho-zyaaa-aaaal-qwsfq-cai list_admin_events \
'(record { from_id = opt 1500; limit = opt 100; filter = opt variant { BurnExecuted } })' \
--network icBurn Paths
DOM tokens are designed to deflate over time. V2 adds a USD-value burn path to enable fiat-equivalent burn amounts, used primarily by the donation and marketplace flows.
Standard Burn (by token amount)
type BurnArgs = record {
amount : nat; // DOM tokens in e8s (8 decimal places)
memo : opt vec nat8;
from_subaccount : opt vec nat8;
};
burn : (BurnArgs) -> (Result<nat64, BurnError>);USD-Value Burn (V2, BL-075.3)
type BurnUsdArgs = record {
amount_usd : float64; // burn equivalent of this USD value
slippage_bps : nat16; // max price deviation tolerance (basis points)
memo : opt vec nat8;
from_subaccount : opt vec nat8;
};
burn_usd_value : (BurnUsdArgs) -> (Result<BurnUsdReceipt, BurnError>);
type BurnUsdReceipt = record {
tx_id : nat64;
dom_burned : nat; // actual DOM tokens burned (e8s)
price_used : float64; // DOM/USD price at execution time
usd_value : float64; // confirmed USD equivalent
};The canister reads the current DOM/USD price from the latest oracle tick stored by the governance canister (SetDomPrice proposals). If the price has moved beyond slippage_bps since the burn was initiated, BurnError::SlippageExceeded is returned.
Donation Burn Records (BL-078.1)
When a member donates to an Otter Camp campaign, the oracle-bridge coordinates a burn that converts part of the donation to DOM and burns it. This deflationary mechanism is recorded as a DonationBurnRecord.
DonationBurnRecord Struct
type DonationBurnRecord = record {
id : nat64;
campaign_id : text; // Otter Camp campaign identifier
donor : principal;
donation_usd : float64; // total donation in USD
dom_burned : nat; // DOM tokens burned (e8s)
burn_price_usd : float64; // DOM/USD price at burn time
tx_id : nat64; // dom-token ledger transaction ID
timestamp : nat64; // IC nanoseconds
memo : opt text;
};Recording a Donation Burn
Donation burns are initiated by the oracle-bridge (which processes the fiat payment confirmation) and submitted to the canister via an authenticated inter-canister call or oracle-bridge signed request:
record_donation_burn : (DonationBurnArgs) -> (Result<nat64, BurnError>);
type DonationBurnArgs = record {
campaign_id : text;
donor : principal;
donation_usd : float64;
burn_pct_bps : nat16; // basis points of donation to burn, e.g. 1000 = 10%
slippage_bps : nat16;
memo : opt text;
};Querying Donation Burns
// Get donation burn history for a campaign
list_donation_burns_by_campaign : (campaign_id : text, limit : opt nat32) -> (vec DonationBurnRecord) query;
// Get donation burn history for a donor
list_donation_burns_by_donor : (principal, limit : opt nat32) -> (vec DonationBurnRecord) query;
// Get a single burn record
get_donation_burn : (id : nat64) -> (opt DonationBurnRecord) query;Burn Wallet Architecture
The burn wallet is a dedicated principal that receives burned tokens before they are permanently removed from circulation. Using an explicit burn wallet (rather than the zero address) enables on-chain auditing of total burned supply.
Configuration
// Set the burn wallet principal (controller only)
set_burn_wallet : (principal) -> (Result<(), BurnError>);
// Query the current burn wallet
get_burn_wallet : () -> (principal) query;
// Query total burned supply (sum of all transfers to burn wallet)
total_burned : () -> (nat) query;Burn Flow
caller.burn(amount)
│
▼
dom-token validates balance
│
▼
dom-token.icrc1_transfer({ to: burn_wallet, amount })
│
▼
AdminEvent { BurnExecuted } persisted
│
▼
burn_wallet accumulates tokens
(balance visible on-chain, tokens unspendable — no private key)The burn wallet principal is set to a canister-controlled address with no spend capability. Total circulating supply = total_minted - total_burned.
Burn Policy Hooks
The dom-token canister supports policy hooks that run before a burn is finalized. The otter-camp canister's DonationProcessor uses these hooks to trigger campaign contribution logic.
type BurnPolicy = variant {
AllowAll;
RequireMinimum : nat; // reject burns below this amount
RequireApproval : principal; // call this canister for approval
};
set_burn_policy : (BurnPolicy) -> (Result<(), BurnError>);
get_burn_policy : () -> (BurnPolicy) query;When RequireApproval is set, the canister makes a synchronous inter-canister call to the approval canister before finalizing the burn. If the approval canister returns false, the burn is rejected.
ICRC-1/2 Standard Compliance
The dom-token canister implements the full ICRC-1 and ICRC-2 token standards in addition to V2 extensions.
Key ICRC-1 Methods
icrc1_name : () -> (text) query;
icrc1_symbol : () -> (text) query;
icrc1_decimals : () -> (nat8) query; // returns 8
icrc1_total_supply : () -> (nat) query;
icrc1_balance_of : (Account) -> (nat) query;
icrc1_transfer : (TransferArgs) -> (Result<nat, TransferError>);Key ICRC-2 Methods (Approvals)
icrc2_approve : (ApproveArgs) -> (Result<nat, ApproveError>);
icrc2_transfer_from : (TransferFromArgs) -> (Result<nat, TransferError>);
icrc2_allowance : (AllowanceArgs) -> (Allowance) query;The treasury canister uses icrc2_approve to grant itself an allowance from the DAO wallet, then calls icrc2_transfer_from when executing approved payouts.
Canister Upgrade Safety
Critical: Any new fields added to the State struct must be Option<T>. Non-optional fields cause stable_restore to fail on upgrade, wiping all canister state.
// CORRECT — safe upgrade
pub struct State {
pub balances: HashMap<Principal, u128>,
pub admin_events: Vec<AdminEvent>,
pub burn_wallet: Option<Principal>, // <-- Option<T>
pub new_field: Option<String>, // <-- Option<T>
}
// INCORRECT — will wipe state on upgrade from older WASM
pub struct State {
pub balances: HashMap<Principal, u128>,
pub new_required_field: String, // <-- breaks stable_restore
}#[serde(default)] does NOT help here — Candid deserialization ignores serde attributes. Always use Option<T> for new fields.
Local Development
cd /home/coby/git/dom-token
# Run PocketIC integration tests
cargo test
# Build WASM
cargo build --release --target wasm32-unknown-unknown
# Deploy to local PocketIC
dfx deploy dom-token --network local
# Check current burn wallet on staging
dfx canister call njo7k-3qaaa-aaaau-aed4a-cai get_burn_wallet --network ic
# Query total burned
dfx canister call njo7k-3qaaa-aaaau-aed4a-cai total_burned --network ic