diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4e99c5..06f6dfc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,3 +90,15 @@ jobs: run: | chmod +x scripts/check-wasm-size.sh ./scripts/check-wasm-size.sh + + event-shape: + name: Event shape vs schema + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check revenue_pool event shape matches EVENT_SCHEMA.md + shell: bash + run: | + chmod +x scripts/check-event-shape.sh + ./scripts/check-event-shape.sh diff --git a/EVENT_SCHEMA.md b/EVENT_SCHEMA.md index fde8b52..cd44c8f 100644 --- a/EVENT_SCHEMA.md +++ b/EVENT_SCHEMA.md @@ -1,4 +1,4 @@ -# Event Schema +# Event Schema Events emitted by all Callora contracts for indexers, frontends, and auditors. All topic/data types refer to Soroban/Stellar XDR values. @@ -675,6 +675,129 @@ three payments, three `batch_distribute` events are emitted in order. --- + + +--- + +### `pause_set` + +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 | + +```json +{ "topics": ["pause_set", "GADMIN..."], "data": true } +``` + +> While paused, `distribute` and `batch_distribute` are blocked. +> Admin rotation (`set_admin`, `claim_admin`) remains available. + +--- + +### `admin_cancelled` + +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 | + +```json +{ + "topics": ["admin_cancelled", "GCURRENT_ADMIN...", "GPENDING_ADMIN..."], + "data": null +} +``` + +> After this event `get_pending_admin()` returns `None`. The current admin remains +> unchanged and may initiate a new transfer at any time. + +--- + +### `upgraded` + +Emitted when the admin upgrades the contract WASM via `upgrade()`. The new WASM hash +is persisted to instance storage and is queryable via `get_version()`. + +| 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 | + +```json +{ + "topics": ["upgraded", "GADMIN..."], + "data": "a1b2c3d4e5f6..." +} +``` + +> `get_version()` returns this hash immediately after the transaction. Only one WASM +> version is stored; calling `upgrade()` again overwrites the previous value. +--- + +### `yield_deposited` + +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` -- current admin who 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 | + +```json +{ + "topics": ["yield_deposited", "GTREASURY..."], + "data": [5000000, "fees", 42000000] +} +``` + +> `cumulative_yield_deposited` equals `get_cumulative_yield_deposited()` immediately +> after the emitting transaction. It never decreases and panics on `i128` overflow. + +--- + +### `admin_broadcast` + +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 | + +```json +{ + "topics": ["admin_broadcast", "GADMIN..."], + "data": { "severity": "Crit", "message": "Emergency: pausing distribution pending audit." } +} +``` + +> Indexers SHOULD alert on `severity = Crit`. The `message` field is capped at +> 256 characters; longer strings are rejected before the event is emitted. ## Contract: `callora-settlement` (v0.1.0) Source: [`contracts/settlement/src/lib.rs`](contracts/settlement/src/lib.rs). @@ -928,6 +1051,11 @@ operational edge cases (off-chain payment reconciliation, dispute resolution). | `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()` | +| `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()` | diff --git a/scripts/check-event-shape.sh b/scripts/check-event-shape.sh new file mode 100644 index 0000000..c490a5b --- /dev/null +++ b/scripts/check-event-shape.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# check-event-shape.sh +# Verifies that every env.events().publish() call site in contracts/revenue_pool/src/lib.rs +# has a corresponding entry in EVENT_SCHEMA.md. +# +# Exit 0 = all events are documented. Exit 1 = undocumented event found. +set -euo pipefail + +SCHEMA="EVENT_SCHEMA.md" +LIB="contracts/revenue_pool/src/lib.rs" + +if [[ ! -f "$SCHEMA" ]]; then + echo "ERROR: $SCHEMA not found (run from repo root)" >&2 + exit 1 +fi +if [[ ! -f "$LIB" ]]; then + echo "ERROR: $LIB not found (run from repo root)" >&2 + exit 1 +fi + +# Extract event names from publish call sites: +# events::event_FOO(&env) → FOO +# Matches lines like: (events::event_admin_changed(&env), ...) +mapfile -t CODE_EVENTS < <( + grep -oP 'events::event_\K[a-z_]+(?=\(&env\))' "$LIB" | sort -u +) + +# Extract event names documented under the revenue-pool section of EVENT_SCHEMA.md. +# We look for ### `foo` headings between the revenue-pool header and the next ## header. +IN_POOL=0 +mapfile -t SCHEMA_EVENTS < <( + while IFS= read -r line; do + if [[ "$line" =~ ^##[[:space:]].*callora-revenue-pool ]]; then + IN_POOL=1; continue + fi + if [[ $IN_POOL -eq 1 && "$line" =~ ^##[[:space:]] ]]; then + IN_POOL=0; continue + fi + if [[ $IN_POOL -eq 1 && "$line" =~ ^###[[:space:]]\`([a-z_]+)\` ]]; then + echo "${BASH_REMATCH[1]}" + fi + done < "$SCHEMA" | sort -u +) + +echo "=== Revenue Pool Event Shape Check ===" +echo "Events in lib.rs : ${CODE_EVENTS[*]}" +echo "Events in schema : ${SCHEMA_EVENTS[*]}" +echo "" + +MISSING=() +for ev in "${CODE_EVENTS[@]}"; do + if ! printf '%s\n' "${SCHEMA_EVENTS[@]}" | grep -qx "$ev"; then + MISSING+=("$ev") + fi +done + +if [[ ${#MISSING[@]} -gt 0 ]]; then + echo "FAIL: The following revenue_pool events are emitted in lib.rs but not documented in EVENT_SCHEMA.md:" + for m in "${MISSING[@]}"; do + echo " - $m" + done + echo "" + echo "Add a '### \`$m\`' section under the callora-revenue-pool heading in EVENT_SCHEMA.md." + exit 1 +fi + +echo "OK: all revenue_pool event publish sites are documented in EVENT_SCHEMA.md." +exit 0