Events emitted by all Callora contracts for indexers, frontends, and auditors. All topic/data types refer to Soroban/Stellar XDR values.
The workspace-members-dedup hardening patch does not introduce event additions, removals, or payload shape changes.
Event topic centralization (PR: task/event-symbol-catalog).
All inline Symbol::new(&env, "...") event topic literals have been extracted from
lib.rs call sites into dedicated src/events.rs modules per crate:
contracts/vault/src/events.rs— 23 topicscontracts/settlement/src/events.rs— 8 topicscontracts/revenue_pool/src/events.rs— 12 topics
Each module exports one pub fn event_*(&env) -> Symbol function per topic and includes
a #[cfg(test)] snapshot block asserting byte-level identity to the original literal.
No topic strings were renamed; this refactor is a zero-semantic-change migration.
Emitted once when the vault is initialized.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "init" |
| topic 1 | topics | Address | vault owner |
| data | data | i128 | initial balance |
{
"topics": ["init", "GOWNER..."],
"data": 1000000
}Emitted when a depositor increases the vault balance.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "deposit" |
| topic 1 | topics | Address | caller (depositor) |
| data | data | (i128, i128) | (amount, new_balance) |
{
"topics": ["deposit", "GDEPOSITOR..."],
"data": [500000, 1500000]
}Emitted on each deduction — once per deduct() call and once per item in batch_deduct().
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "deduct" |
| topic 1 | topics | Address | caller |
| topic 2 | topics | Symbol | request_id (empty Symbol if not provided) |
| data | data | (i128, i128) | (amount, new_balance) |
{
"topics": ["deduct", "GCALLER...", "req_abc123"],
"data": [100000, 900000]
}request_id encoding (indexer contract):
- Topic is always present: the vault always emits exactly 3 topics for
deduct. - No optional topic: Soroban events do not carry an
Optiontopic value; instead the vault uses a sentinel. - Sentinel for “no request_id�: when the input
request_idisNone, topic 2 isSymbol("")(an empty symbol). - Indexer rule: treat
Symbol("")as “no request_id provided�. - Ambiguity note:
Some(Symbol(""))is indistinguishable fromNoneon-chain. Clients SHOULD NOT intentionally pass an empty symbol as a real request id.
Precondition (Issue #263): deduct / batch_deduct require a settlement
address to be configured via set_settlement. If the settlement address is
not set, the call panics with "settlement address not set" before any
deduct event is emitted — indexers will therefore never observe a deduct
event for a call that lacked a configured settlement destination.
Idempotency guard (Issue #249): when request_id is Some(Symbol), the
value is single-use across successful deduct and batch_deduct calls.
Reusing a previously accepted value, or repeating the same value twice inside
one batch, panics with "duplicate request_id" before any balance update,
transfer, or deduct event is emitted.
Emitted when the vault owner withdraws to their own address.
| Field | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "withdraw" |
| topic 1 | topics | Address | vault owner |
amount |
data | i128 | amount withdrawn in USDC micro-units |
new_balance |
data | i128 | vault balance after withdrawal |
| Index | Location | Type | Description |
| --------- | ---------- | -------------- | ----------------------- |
| topic 0 | topics | Symbol | "withdraw" |
| topic 1 | topics | Address | vault owner |
| data | data | (i128, i128) | (amount, new_balance) |
{
"topics": ["withdraw", "GOWNER..."],
"data": [200000, 700000]
}Emitted when the vault owner withdraws to a designated recipient.
| Field | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "withdraw_to" |
| topic 1 | topics | Address | vault owner |
| topic 2 | topics | Address | recipient to |
amount |
data | i128 | amount withdrawn in USDC micro-units |
new_balance |
data | i128 | vault balance after withdrawal |
| Index | Location | Type | Description |
| --------- | ---------- | -------------- | ----------------------- |
| topic 0 | topics | Symbol | "withdraw_to" |
| topic 1 | topics | Address | vault owner |
| topic 2 | topics | Address | recipient |
| data | data | (i128, i128) | (amount, new_balance) |
{
"topics": ["withdraw_to", "GOWNER...", "GRECIPIENT..."],
"data": [150000, 550000]
}Emitted when the vault is paused by the admin or owner.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "vault_paused" |
| topic 1 | topics | Address | caller (admin/owner) |
| data | data | () | empty |
{
"topics": ["vault_paused", "GADMIN..."],
"data": null
}Emitted when the vault is unpaused by the admin or owner.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "vault_unpaused" |
| topic 1 | topics | Address | caller (admin/owner) |
| data | data | () | empty |
{
"topics": ["vault_unpaused", "GADMIN..."],
"data": null
}Emitted when the owner starts a two-step ownership transfer.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "ownership_nominated" |
| topic 1 | topics | Address | current owner |
| topic 2 | topics | Address | nominee |
| data | data | () | empty |
{
"topics": ["ownership_nominated", "GOWNER...", "GNOMINEE..."],
"data": null
}Emitted when the nominee accepts ownership.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "ownership_accepted" |
| topic 1 | topics | Address | old owner |
| topic 2 | topics | Address | new owner |
| data | data | () | empty |
{
"topics": ["ownership_accepted", "GOWNER...", "GNEWOWNER..."],
"data": null
}Emitted when the admin starts a two-step admin transfer.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "admin_nominated" |
| topic 1 | topics | Address | current admin |
| topic 2 | topics | Address | nominee |
| data | data | () | empty |
{
"topics": ["admin_nominated", "GADMIN...", "GNOMINEE..."],
"data": null
}- OwnershipTransfer: not present in current vault; would list old_owner, new_owner.
Emitted when the vault circuit-breaker is activated by admin or owner.
| Field | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "vault_paused" |
| topic 1 | topics | Address | caller — admin or owner who triggered pause |
| data | data | () | empty |
Indexer Note: After this event is emitted, is_paused() view function returns true.
The following operations are blocked until unpause: deposit(), deduct(), batch_deduct().
Emitted when the vault circuit-breaker is deactivated by admin or owner.
| Field | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "vault_unpaused" |
| topic 1 | topics | Address | caller — admin or owner who triggered unpause |
| data | data | () | empty |
Indexer Note: After this event is emitted, is_paused() view function returns false.
All vault operations are restored: deposit(), deduct(), batch_deduct().
The vault exposes a read-only view function for off-chain systems to query the current pause state.
Signature: pub fn is_paused(env: Env) -> bool
Return Value:
true— Vault is currently paused (circuit-breaker active)false— Vault is operational (normal state)
Safety Guarantees:
- Read-only: No state mutation or side effects
- Deterministic: Identical state always produces identical output
- Non-panicking: Never panics, even before initialization
- Safe default: Returns
falsewhen pause state is unset
Indexer Usage:
// Check if vault is paused before processing transactions
const isPaused = await vault.isPaused();
if (isPaused) {
// Vault is paused - deposits and deductions are blocked
// Only admin/owner operations like withdraw() are allowed
} else {
// Vault is operational - all functions available
}Consistency with Events:
vault_pausedevent emitted →is_paused()returnstruevault_unpausedevent emitted →is_paused()returnsfalse
Indexers should use is_paused() for current state queries and subscribe to
vault_paused/vault_unpaused events for state change notifications.
Emitted when the admin sets a revenue pool address.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "set_revenue_pool" |
| topic 1 | topics | Address | caller (admin) |
| data | data | Address | new revenue pool |
{
"topics": ["set_revenue_pool", "GADMIN..."],
"data": "GPOOL..."
}Emitted when the admin clears the revenue pool address.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "clear_revenue_pool" |
| topic 1 | topics | Address | caller (admin) |
| data | data | () | empty |
{
"topics": ["clear_revenue_pool", "GADMIN..."],
"data": null
}Emitted when offering metadata is stored for the first time.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "metadata_set" |
| topic 1 | topics | String | offering_id |
| topic 2 | topics | Address | caller (owner) |
| data | data | String | metadata (IPFS CID / URI) |
{
"topics": ["metadata_set", "offering-001", "GOWNER..."],
"data": "ipfs://bafybeigdyrzt..."
}Emitted when existing offering metadata is replaced.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "metadata_updated" |
| topic 1 | topics | String | offering_id |
| topic 2 | topics | Address | caller (owner) |
| data | data | (String, String) | (old_metadata, new_metadata) |
{
"topics": ["metadata_updated", "offering-001", "GOWNER..."],
"data": ["ipfs://old...", "ipfs://new..."]
}Emitted when the owner deletes a stale offering's metadata from instance storage.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "metadata_removed" |
| topic 1 | topics | String | offering_id |
| topic 2 | topics | Address | caller (owner) |
| data | data | () | empty |
{
"topics": ["metadata_removed", "offering-001", "GOWNER..."],
"data": null
}Indexer note: After this event, get_metadata(offering_id) returns None.
The call is idempotent — removing a key that was never set (or was already removed)
emits the event and returns Ok(()) without error.
Emitted when the owner updates the authorized caller address.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "set_authorized_caller" |
| topic 1 | topics | Address | vault owner |
| data | data | (Option, Option) | (old_authorized_caller, new_authorized_caller) |
{
"topics": ["set_authorized_caller", "GOWNER..."],
"data": [null, "GCALLER..."]
}Emitted when the current admin nominates a successor.
| Field | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "admin_nominated" |
| topic 1 | topics | Address | current admin |
| topic 2 | topics | Address | nominee |
| data | data | () | empty |
Emitted when the nominee accepts the admin role.
| Field | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "admin_accepted" |
| topic 1 | topics | Address | old admin |
| topic 2 | topics | Address | new admin |
| data | data | () | empty |
The revenue pool receives USDC forwarded by the vault on every deduct / batch_deduct
call and lets the admin distribute those funds to developers.
Emitted once when the revenue pool is initialized.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "init" |
| topic 1 | topics | Address | admin — initial admin address |
| data | data | Address | usdc_token — token contract address |
{
"topics": ["init", "GADMIN..."],
"data": "GUSDC_TOKEN..."
}Security note:
usdc_tokenis immutable afterinit. Verify it matches the canonical Stellar USDC contract before deployment.
Emitted when the current admin nominates a successor (step 1 of 2).
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "admin_transfer_started" |
| topic 1 | topics | Address | current_admin — the nominator |
| data | data | Address | pending_admin — nominee who must accept |
{
"topics": ["admin_transfer_started", "GCURRENT_ADMIN..."],
"data": "GPENDING_ADMIN..."
}Indexers should treat funds as still under
current_admincontrol untiladmin_transfer_completedis observed.
Emitted when set_admin() is called to record the requested admin change.
This event is emitted immediately before admin_transfer_started.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "admin_changed" |
| topic 1 | topics | Address | current_admin — caller/admin |
| data | data | (Address, Address) | (old_admin, new_admin) |
{
"topics": ["admin_changed", "GCURRENT_ADMIN..."],
"data": ["GCURRENT_ADMIN...", "GPENDING_ADMIN..."]
}Emitted when the nominee accepts the admin role (step 2 of 2).
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "admin_transfer_completed" |
| topic 1 | topics | Address | new_admin — the accepted admin |
| data | data | () | empty |
{
"topics": ["admin_transfer_completed", "GNEW_ADMIN..."],
"data": null
}After this event, only
new_admincan calldistribute,batch_distribute,receive_payment, andset_admin.
Emitted when the admin sets or replaces the emergency pause guardian.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "pause_guardian_set" |
| topic 1 | topics | Address | caller — current admin |
| data | data | Address | guardian — address allowed to pause |
{
"topics": ["pause_guardian_set", "GADMIN..."],
"data": "GGUARDIAN..."
}Emitted when the admin clears the emergency pause guardian role.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "pause_guardian_cleared" |
| topic 1 | topics | Address | caller — current admin |
| data | data | Address | previous guardian address |
{
"topics": ["pause_guardian_cleared", "GADMIN..."],
"data": "GOLD_GUARDIAN..."
}Emitted when the admin logs an inbound payment from the vault.
Note: This is an event-only helper — it does not move tokens. USDC arrives via a direct token transfer from the vault. Call
receive_paymentto emit this event for indexer alignment.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "receive_payment" |
| topic 1 | topics | Address | caller — typically admin |
| data | data | (i128, bool) | (amount, from_vault) — amount in stroops; from_vault=true when source is the vault |
{
"topics": ["receive_payment", "GADMIN..."],
"data": [5000000, true]
}Example — manual top-up (not from vault):
{
"topics": ["receive_payment", "GADMIN..."],
"data": [1000000, false]
}Indexers tracking total inflows should subscribe to this event and filter on
from_vaultto distinguish vault-originated payments from manual top-ups.
Emitted when the admin distributes USDC to a single developer.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "distribute" |
| topic 1 | topics | Address | to — developer address |
| data | data | i128 | amount in stroops |
{
"topics": ["distribute", "GDEVELOPER..."],
"data": 2500000
}A
distributeevent guarantees the token transfer succeeded — the USDC has left the pool contract and arrived atto.
Emitted when the admin updates the per-leg distribution cap.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "set_max_distribute" |
| topic 1 | topics | Address | admin address |
| data | data | (i128, i128) | (old_max, new_max) |
{
"topics": ["set_max_distribute", "GADMIN..."],
"data": [9223372036854775807, 500]
}Emitted once per payment during a batch_distribute() call. If a batch has
three payments, three batch_distribute events are emitted in order.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "batch_distribute" |
| topic 1 | topics | Address | to — developer address |
| data | data | i128 | amount in stroops |
{
"topics": ["batch_distribute", "GDEVELOPER_A..."],
"data": 1000000
}Example — 3-payment batch produces 3 events:
[
{ "topics": ["batch_distribute", "GDEV_A..."], "data": 1000000 },
{ "topics": ["batch_distribute", "GDEV_B..."], "data": 2000000 },
{ "topics": ["batch_distribute", "GDEV_C..."], "data": 500000 }
]
batch_distributeis atomic — either all payments succeed and all events are emitted, or none are. Indexers can verify atomicity by checking that all events share the same ledger sequence number.
Emitted by both pause() (data = true) and unpause() (data = false) to signal
a change in the pool's pause state. Only the admin may trigger either function.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "pause_set" |
| topic 1 | topics | Address | caller -- the admin who called pause/unpause |
| data | data | bool | true = pool is now paused; false = unpaused |
{ "topics": ["pause_set", "GADMIN..."], "data": true }While paused,
distributeandbatch_distributeare blocked. Admin rotation (set_admin,claim_admin) remains available.
Emitted when the current admin cancels a pending two-step admin transfer via
cancel_admin_transfer(). Both the current and the pending admin are recorded as topics
so indexers can link the cancellation to the in-flight handover without a data decode.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "admin_cancelled" |
| topic 1 | topics | Address | current_admin -- admin who issued the cancel |
| topic 2 | topics | Address | pending_admin -- nominee whose claim is revoked |
| data | data | () | empty |
{
"topics": ["admin_cancelled", "GCURRENT_ADMIN...", "GPENDING_ADMIN..."],
"data": null
}After this event
get_pending_admin()returnsNone. The current admin remains unchanged and may initiate a new transfer at any time.
A successful upgrade() call publishes three events in this order:
upgrade_started— published before the host swaps the contract WASM.upgrade_completed— published after the WASM swap and theContractVersionstorage write.upgraded— legacy single-event shape retained for backwards compatibility with off-chain subscribers written against the pre-lifecycle schema. New indexers should subscribe to the structured pair above.
Receipt of upgrade_started without a matching upgrade_completed at the
same (ledger, timestamp) means the host trapped between the two emits;
the WASM swap and ContractVersion write were rolled back.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "upgrade_started" or "upgrade_completed" |
| topic 1 | topics | Address | caller -- the address that authorized upgrade() |
| data | data | UpgradeEvent |
structured payload (see below) |
UpgradeEvent fields:
| Field | Type | Description |
|---|---|---|
caller |
Address |
Same as topic 1; included in data for indexers that store the payload only. |
previous_wasm |
Option<BytesN<32>> |
Hash recorded by the prior upgrade(). None on the first upgrade. |
new_wasm |
BytesN<32> |
Hash being deployed by this call. |
ledger |
u32 |
env.ledger().sequence() captured before the WASM swap. |
timestamp |
u64 |
env.ledger().timestamp() captured before the WASM swap. |
{
"topics": ["upgrade_completed", "GADMIN..."],
"data": {
"caller": "GADMIN...",
"previous_wasm": "a1b2c3d4...",
"new_wasm": "f0e1d2c3...",
"ledger": 1234567,
"timestamp": 1700000000
}
}| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "upgraded" |
| topic 1 | topics | Address | caller -- admin who executed the upgrade |
| data | data | BytesN<32> | new_wasm_hash -- hash of the deployed WASM blob |
{
"topics": ["upgraded", "GADMIN..."],
"data": "a1b2c3d4e5f6..."
}
get_version()returns the new hash immediately after the transaction. Only one WASM version is stored; callingupgrade()again overwrites the previous value (which is then visible to consumers as the next event'sprevious_wasm).
Emitted when the admin nominates a new treasury via set_treasury(). The nominee
must call accept_treasury() before it becomes authorized to call deposit_yield().
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "treasury_transfer_started" |
| topic 1 | topics | Address | caller -- current admin that nominated the treasury |
| data[0] | data | Address | old_treasury -- currently active treasury |
| data[1] | data | Address | new_treasury -- nominated treasury |
Emitted when the pending treasury accepts the nomination via accept_treasury().
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "treasury_transfer_completed" |
| topic 1 | topics | Address | new_treasury -- accepting treasury |
| data | data | Address | old_treasury -- previously active treasury |
Emitted when the admin cancels a pending treasury nomination via
cancel_treasury_transfer().
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "treasury_cancelled" |
| topic 1 | topics | Address | caller -- current admin that cancelled |
| data | data | Address | pending_treasury -- cancelled nominee |
Emitted when the treasury deposits accumulated protocol yield into the revenue pool
via deposit_yield(). The cumulative tracker is updated atomically with the transfer.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "yield_deposited" |
| topic 1 | topics | Address | treasury -- configured treasury that called deposit_yield |
| data[0] | data | i128 | amount -- USDC deposited in this call (stroops) |
| data[1] | data | Symbol | source -- short label, e.g. "fees" or "yield" |
| data[2] | data | i128 | cumulative_yield_deposited -- running total after deposit |
{
"topics": ["yield_deposited", "GTREASURY..."],
"data": [5000000, "fees", 42000000]
}
cumulative_yield_depositedequalsget_cumulative_yield_deposited()immediately after the emitting transaction. It never decreases and panics oni128overflow.
Emitted when the admin publishes an emergency message via broadcast().
No tokens are moved; this is an out-of-band signaling channel for indexers and frontends.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "admin_broadcast" |
| topic 1 | topics | Address | caller -- must be current admin |
| data | data | AdminBroadcast |
struct with severity and message fields |
AdminBroadcast struct fields:
| Field | Type | Description |
|---|---|---|
severity |
Severity | One of Info, Warn, or Crit |
message |
String | Broadcast text; max 256 characters, never empty |
{
"topics": ["admin_broadcast", "GADMIN..."],
"data": { "severity": "Crit", "message": "Emergency: pausing distribution pending audit." }
}Indexers SHOULD alert on
severity = Crit. Themessagefield is capped at 256 characters; longer strings are rejected before the event is emitted.
Emitted when the vault owner sweeps surplus USDC to a sibling contract via
sweep_idle_balance(). Tokens are moved and meta.balance is decremented
atomically in the same transaction.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "swept" |
| topic 1 | topics | Address | owner — vault owner who called sweep_idle_balance |
| topic 2 | topics | SweepDestination | Settlement or RevenuePool variant |
| data | data | (i128, i128) | (amount, new_balance) after sweep |
{
"topics": ["swept", "GOWNER...", "Settlement"],
"data": [300000, 700000]
}SweepDestination encoding:
Settlement— USDC was forwarded to the address stored underStorageKey::Settlement.RevenuePool— USDC was forwarded to the address stored underStorageKey::RevenuePool.
Preconditions (no event emitted if these fail):
- Vault must not be paused (
VaultError::Paused). - Caller must be the vault owner (
VaultError::Unauthorized). amount > 0(VaultError::AmountNotPositive).amount ≤ meta.balance(VaultError::InsufficientBalance).- For
Settlement:set_settlementmust have been called (VaultError::SettlementNotSet). - For
RevenuePool: a revenue pool must be configured (VaultError::NotInitialized).
Indexer note: After this event, balance() returns new_balance. The USDC
has left the vault on-ledger; sweep_idle_balance does not call
settlement.receive_payment() — it is a raw token transfer only.
Source: contracts/settlement/src/lib.rs.
Amount units. All amount / new_balance fields are i128 in USDC
micro-units (7-decimal scaled integers), matching the Stellar USDC contract.
Legacy text elsewhere in this document calls this "stroops" — same scalar type,
same integer semantics; the settlement contract never handles native XLM.
Data payload encoding. The data column describes the Soroban
contracttype struct published by env.events().publish(...). On the wire
each struct is a single XDR value whose field names match the Rust struct;
the JSON examples below are the logical field view an indexer sees after
decoding, not a raw array. The struct layouts live in lib.rs:
PaymentReceivedEvent and BalanceCreditedEvent.
Emit atomicity and ordering. Both events originate inside one
receive_payment() call, so they share the same transaction and ledger
sequence. When to_pool = false, payment_received is always emitted
before balance_credited. If any guard panics (see "Panic modes" below)
no events are emitted and state is rolled back.
Panic modes (no events emitted).
- Caller is not the registered vault or admin (
require_authorized_caller). amount <= 0—"amount must be positive".to_pool = truewithdeveloper = Some(_)—"developer address must be None when to_pool=true".to_pool = falsewithdeveloper = None—"developer address required when to_pool=false".- Arithmetic overflow on pool or developer balance —
"pool balance overflow"/"developer balance overflow".
Emitted by receive_payment() for every successful inbound payment,
regardless of routing.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "payment_received" |
| topic 1 | topics | Address | caller — authorized vault or admin address (same as from_vault field) |
from_vault |
data | Address | originator of the payment; duplicates topic 1 for indexers that key by data only |
amount |
data | i128 | payment amount in USDC micro-units; invariant amount > 0 |
to_pool |
data | bool | true → credited to global pool; false → credited to an individual developer |
developer |
data | Option<Address> | None when to_pool = true; Some(address) when to_pool = false |
Example — global pool credit (to_pool = true):
{
"topics": ["payment_received", "GCALLER..."],
"data": {
"from_vault": "GCALLER...",
"amount": 5000000,
"to_pool": true,
"developer": null
}
}Side effect: GlobalPool.total_balance += amount and
GlobalPool.last_updated = env.ledger().timestamp().
Example — developer credit (to_pool = false):
{
"topics": ["payment_received", "GCALLER..."],
"data": {
"from_vault": "GCALLER...",
"amount": 2500000,
"to_pool": false,
"developer": "GDEV..."
}
}Side effect: developer balance map entry for GDEV... is incremented by
amount. GlobalPool.last_updated is not touched on developer credits.
Indexer guidance.
topic 1is always the caller; filter on it to isolate payments from a specific vault or admin.developeris the only field that distinguishes pool vs. developer credits in the data payload; theto_poolboolean is redundant but stable and cheaper to filter on.- A
payment_receivedwithto_pool = falseis always paired with exactly onebalance_creditedevent in the same transaction.
Emitted by receive_payment() only when to_pool = false, immediately
after the matching payment_received event.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "balance_credited" |
| topic 1 | topics | Address | developer — address whose balance was updated |
developer |
data | Address | same as topic 1; duplicated for data-only indexers |
amount |
data | i128 | amount credited to the developer in USDC micro-units |
new_balance |
data | i128 | developer's cumulative balance after this credit (post-state) |
{
"topics": ["balance_credited", "GDEV..."],
"data": {
"developer": "GDEV...",
"amount": 2500000,
"new_balance": 7500000
}
}Invariants.
new_balance = prior_balance + amount, checked fori128overflow; overflow panics and rolls back both events.new_balanceequalsCalloraSettlement::get_developer_balance(developer)immediately after the emitting transaction.amountinbalance_creditedequalsamountin the pairedpayment_received.
Indexer guidance.
- Track developer earnings by subscribing to
balance_credited— it already carries the post-credit balance, so no separate read is required. - Track total protocol inflow by summing
payment_received.amountacross both routing modes, or filterto_pool = truefor pool-only inflow. balance_creditedis never emitted whento_pool = true; do not wait for one on pool credits.
Emitted by set_vault() when the admin updates the registered vault address.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "vault_changed" |
| topic 1 | topics | Address | caller — admin who performed update |
| data | data | (Address, Address) | (old_vault, new_vault) |
{
"topics": ["vault_changed", "GADMIN..."],
"data": ["GOLDVAULT...", "GNEWVAULT..."]
}Emitted by withdraw_developer_balance() when a developer withdraws their balance as USDC.
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "developer_withdraw" |
| topic 1 | topics | Address | developer — address withdrawing the balance |
developer |
data | Address | same as topic 1; duplicated for data-only indexers |
amount |
data | i128 | amount withdrawn in USDC micro-units |
remaining_balance |
data | i128 | developer's cumulative balance after this withdrawal (post-state) |
to |
data | Address | recipient address the funds were sent to (defaults to developer) |
{
"topics": ["developer_withdraw", "GDEV..."],
"data": {
"developer": "GDEV...",
"amount": 2000000,
"remaining_balance": 0,
"to": "GRECIPIENT..."
}
}Invariants.
remaining_balance = prior_balance - amount, checked for underflow.tocannot be the contract's own address.- If
tois not provided (None), it defaults todeveloper.
Emitted by force_credit_developer() when an admin manually credits a developer balance (escape hatch).
This is an admin-authorized inflow — no on-ledger USDC is moved. It is designed for operational edge cases (off-chain payment reconciliation, dispute resolution).
| Index | Location | Type | Description |
|---|---|---|---|
| topic 0 | topics | Symbol | "developer_force_credited" |
| topic 1 | topics | Address | developer — address whose balance was updated |
developer |
data | Address | same as topic 1; duplicated for data-only indexers |
amount |
data | i128 | amount credited to the developer in USDC micro-units |
reason |
data | Symbol | on-chain reason code for the manual credit |
new_balance |
data | i128 | developer's cumulative balance after this credit (post-state) |
{
"topics": ["developer_force_credited", "GDEV..."],
"data": {
"developer": "GDEV...",
"amount": 5000000,
"reason": "offline_settlement",
"new_balance": 7500000
}
}Invariants.
new_balance = prior_balance + amount, checked fori128overflow.- Only the contract admin may call
force_credit_developer. - This is an audit-only path; every credit includes an on-chain
reasonSymbol.
Indexer guidance.
- Subscribe to
developer_force_creditedto track admin-initiated manual credits. - The
reasonfield distinguishes different operational scenarios (e.g.,"dispute_resolution","offline_settlement","bulk_reconciliation"). - This event is never paired with a
payment_receivedevent. - For full accounting, sum
balance_credited.amount+developer_force_credited.amountto compute total developer inflows.
| Event | Contract | Trigger |
|---|---|---|
init |
vault | init() |
deposit |
vault | deposit() |
deduct |
vault | deduct() / each item in batch_deduct() |
withdraw |
vault | withdraw() |
withdraw_to |
vault | withdraw_to() |
vault_paused |
vault | pause() |
vault_unpaused |
vault | unpause() |
ownership_nominated |
vault | transfer_ownership() |
ownership_accepted |
vault | accept_ownership() |
admin_nominated |
vault | set_admin() |
admin_accepted |
vault | accept_admin() |
set_revenue_pool |
vault | set_revenue_pool(Some(addr)) |
clear_revenue_pool |
vault | set_revenue_pool(None) |
set_max_deduct |
vault | set_max_deduct() |
set_authorized_caller |
vault | set_authorized_caller() |
metadata_set |
vault | set_metadata() |
metadata_updated |
vault | update_metadata() |
metadata_removed |
vault | remove_metadata() |
distribute |
vault | distribute() |
swept |
vault | sweep_idle_balance() |
init |
revenue-pool | init() |
admin_changed |
revenue-pool | set_admin() |
admin_transfer_started |
revenue-pool | set_admin() |
set_max_distribute |
revenue-pool | set_max_distribute() |
admin_transfer_completed |
revenue-pool | claim_admin() |
receive_payment |
revenue-pool | receive_payment() |
distribute |
revenue-pool | distribute() |
batch_distribute |
revenue-pool | each payment in batch_distribute() |
pause_set |
revenue-pool | pause() / unpause() |
admin_cancelled |
revenue-pool | cancel_admin_transfer() |
upgraded |
revenue-pool | upgrade() |
treasury_transfer_started |
revenue-pool | set_treasury() |
treasury_transfer_completed |
revenue-pool | accept_treasury() |
treasury_cancelled |
revenue-pool | cancel_treasury_transfer() |
yield_deposited |
revenue-pool | deposit_yield() |
admin_broadcast |
revenue-pool | broadcast() |
payment_received |
settlement | receive_payment() |
balance_credited |
settlement | receive_payment() with to_pool=false |
vault_changed |
settlement | set_vault() |
developer_force_credited |
settlement | force_credit_developer() |
| Version | Contract | Change |
|---|---|---|
| 0.0.1 | vault | Initial vault events |
| 0.0.1 | vault | Added set_authorized_caller event with old/new value payload (Issue #256) |
| 0.0.1 | vault | Added metadata_removed event on remove_metadata() for stale-entry cleanup |
| 0.0.1 | revenue-pool | Full revenue pool event suite with JSON examples |
| 0.0.1 | revenue-pool | Added admin_changed event on set_admin for explicit old/new admin intent |
| 0.1.0 | settlement | payment_received, balance_credited |
| 0.1.0 | settlement | developer_force_credited (admin escape hatch) |
| 0.2.0 | vault | Added swept event on sweep_idle_balance() (Issue #415) |