Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions EVENT_SCHEMA.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,108 @@ Emitted when the nominee accepts the admin role.

---

### `upgraded`

Emitted by `upgrade()` after the WASM swap succeeds. Retained for backward
compatibility with indexers already parsing this event.

| 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..."
}
```

> **Deprecation note:** New indexers should prefer `upgrade_started` and
> `upgrade_completed` which carry structured payloads and support lifecycle
> correlation. `upgraded` will remain in every upgrade transaction for
> backward compatibility.

---

### `upgrade_started`

Emitted **before** `env.deployer().update_current_contract_wasm()` executes.
Carries the intended WASM hash and the previous version so indexers can
reconstruct full version history and detect failed upgrades (an `upgrade_started`
without a subsequent `upgrade_completed` in the same transaction indicates a
failure before the WASM swap completed).

| Index | Location | Type | Description |
|-------------------|----------|--------------------|------------------------------------------------------------------|
| topic 0 | topics | Symbol | `"upgrade_started"` |
| topic 1 | topics | Address | `caller` -- admin who initiated the upgrade |
| `new_wasm_hash` | data | BytesN<32> | hash of the WASM intended to be installed |
| `previous_version`| data | Option<BytesN<32>> | hash from the last `upgrade()` call, or `None` on first upgrade |

```json
{
"topics": ["upgrade_started", "GADMIN..."],
"data": {
"new_wasm_hash": "b2c3d4e5f6a1...",
"previous_version": "a1b2c3d4e5f6..."
}
}
```

**First upgrade (no prior version):**

```json
{
"topics": ["upgrade_started", "GADMIN..."],
"data": {
"new_wasm_hash": "a1b2c3d4e5f6...",
"previous_version": null
}
}
```

**Indexer guidance.**
- `upgrade_started` and `upgrade_completed` share the same `new_wasm_hash`
value. Correlate them by matching that value within the same transaction.
- Track `previous_version` across consecutive upgrades to build a complete
on-chain version timeline.

---

### `upgrade_completed`

Emitted immediately **after** `env.deployer().update_current_contract_wasm()`
returns successfully, confirming the WASM swap took effect.

| Index | Location | Type | Description |
|-----------------|----------|------------|-------------------------------------------|
| topic 0 | topics | Symbol | `"upgrade_completed"` |
| topic 1 | topics | Address | `caller` -- admin who executed the upgrade|
| `new_wasm_hash` | data | BytesN<32> | hash of the WASM that was installed |

```json
{
"topics": ["upgrade_completed", "GADMIN..."],
"data": {
"new_wasm_hash": "b2c3d4e5f6a1..."
}
}
```

**Indexer guidance.**
- `upgrade_completed` is always preceded by `upgrade_started` and `upgraded`
in the same transaction when upgrade succeeds.
- The emitted order within a successful `upgrade()` call is:
1. `upgrade_started` (pre-swap)
2. `upgrade_completed` (post-swap)
3. `upgraded` (backward-compat)
- Absence of `upgrade_completed` after `upgrade_started` means the WASM swap
was rejected by the host (bad hash, auth failure, or host error).

---

## Contract: `callora-revenue-pool` (v0.0.1)

The revenue pool receives USDC forwarded by the vault on every `deduct` / `batch_deduct`
Expand Down Expand Up @@ -920,6 +1022,9 @@ operational edge cases (off-chain payment reconciliation, dispute resolution).
| `metadata_updated` | vault | `update_metadata()` |
| `metadata_removed` | vault | `remove_metadata()` |
| `distribute` | vault | `distribute()` |
| `upgraded` | vault | `upgrade()` -- backward-compat |
| `upgrade_started` | vault | `upgrade()` -- emitted before WASM swap |
| `upgrade_completed` | vault | `upgrade()` -- emitted after WASM swap |
| `init` | revenue-pool | `init()` |
| `admin_changed` | revenue-pool | `set_admin()` |
| `admin_transfer_started` | revenue-pool | `set_admin()` |
Expand All @@ -946,3 +1051,4 @@ operational edge cases (off-chain payment reconciliation, dispute resolution).
| 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 | `upgraded` (documented), `upgrade_started`, `upgrade_completed` -- lifecycle events (Issue #528) |
22 changes: 21 additions & 1 deletion contracts/settlement/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#![no_std]

use soroban_sdk::{contract, contractimpl, contracttype, token, Address, BytesN, Env, Symbol, Vec};
use soroban_sdk::{contract, contractimpl, contracttype, token, Address, BytesN, Env, String, Symbol, Vec};

mod errors;
pub use errors::SettlementError;
Expand All @@ -11,6 +11,26 @@ pub const MAX_BATCH_SIZE: u32 = 50;
/// Maximum number of developer balances returned per page in paginated queries.
pub const MAX_DEVELOPER_BALANCES_PAGE_SIZE: u32 = 100;

/// Maximum length in bytes for admin broadcast messages.
pub const MAX_MESSAGE_LEN: u32 = 256;

/// Severity level for admin broadcast messages.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Severity {
Info,
Warn,
Crit,
}

/// Payload for admin broadcast events.
#[contracttype]
#[derive(Clone, Debug)]
pub struct AdminBroadcast {
pub severity: Severity,
pub message: String,
}

/// Persistent storage keys for settlement contract
#[contracttype]
#[derive(Clone, Debug, PartialEq)]
Expand Down
101 changes: 101 additions & 0 deletions contracts/vault/EVENT_SCHEMA.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Vault Upgrade Event Schema

This document describes the three events emitted by `upgrade()` in the Callora Vault contract,
their topic layout, data payloads, and guidance for indexers.

---

## Events emitted (in order, within the same transaction)

| # | Topic 0 | Topic 1 | Data type | Emitted when |
|---|---------|---------|-----------|--------------|
| 1 | `upgrade_started` | admin `Address` | `UpgradeStartedData` | Before `env.deployer().update_current_contract_wasm()` |
| 2 | `upgrade_completed` | admin `Address` | `UpgradeCompletedData` | After the WASM swap succeeds |
| 3 | `upgraded` | admin `Address` | `BytesN<32>` (new hash) | After the version marker is persisted (backward-compat) |

---

## `upgrade_started`

**Purpose:** signals upgrade intent. An `upgrade_started` without a subsequent `upgrade_completed`
in the same transaction indicates the WASM swap was never executed (e.g., authorization failure
or WASM validation error).

### Payload — `UpgradeStartedData`

```rust
#[contracttype]
pub struct UpgradeStartedData {
pub new_wasm_hash: BytesN<32>,
pub previous_version: Option<BytesN<32>>,
}
```

| Field | Type | Description |
|-------|------|-------------|
| `new_wasm_hash` | `BytesN<32>` | WASM hash to be installed |
| `previous_version` | `Option<BytesN<32>>` | Hash from the previous upgrade, or `None` on first upgrade |

### JSON example

```json
{
"topics": ["upgrade_started", "<admin-address>"],
"data": {
"new_wasm_hash": "aabbcc...3232",
"previous_version": null
}
}
```

---

## `upgrade_completed`

**Purpose:** confirms the WASM swap succeeded. The `new_wasm_hash` matches the value carried
in the corresponding `upgrade_started` event from the same transaction.

### Payload — `UpgradeCompletedData`

```rust
#[contracttype]
pub struct UpgradeCompletedData {
pub new_wasm_hash: BytesN<32>,
}
```

| Field | Type | Description |
|-------|------|-------------|
| `new_wasm_hash` | `BytesN<32>` | WASM hash that was successfully installed |

### JSON example

```json
{
"topics": ["upgrade_completed", "<admin-address>"],
"data": {
"new_wasm_hash": "aabbcc...3232"
}
}
```

---

## `upgraded` (backward compatibility)

Retained from before this PR. Emitted after `upgrade_completed` with the same `new_wasm_hash`
as a bare `BytesN<32>` in the data position. Indexers already consuming `upgraded` require
no changes.

---

## Indexer guidance

- **Event ordering guarantee:** `upgrade_started` always precedes `upgrade_completed` which always
precedes `upgraded` within the same ledger close.
- **Detecting failed upgrades:** if `upgrade_started` appears without `upgrade_completed` in the
same transaction, the upgrade was initiated but the WASM swap did not complete.
- **Version chain reconstruction:** collect all `upgrade_started` events and follow
`previous_version` links to reconstruct the full upgrade history for a contract instance.
- **Deduplication:** correlate `upgrade_started` and `upgrade_completed` by matching
`new_wasm_hash` within the same transaction hash.
34 changes: 34 additions & 0 deletions contracts/vault/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,24 @@ pub fn event_admin_broadcast(env: &Env) -> Symbol {
Symbol::new(env, "admin_broadcast")
}

/// Returns the Symbol for the `"upgrade_started"` event topic.
///
/// Emitted before `env.deployer().update_current_contract_wasm()` executes,
/// recording the intended WASM hash and the previous version. Indexers can
/// use the absence of `upgrade_completed` in the same transaction to detect
/// failed upgrades.
pub fn event_upgrade_started(env: &Env) -> Symbol {
Symbol::new(env, "upgrade_started")
}

/// Returns the Symbol for the `"upgrade_completed"` event topic.
///
/// Emitted after `env.deployer().update_current_contract_wasm()` returns
/// successfully, confirming the WASM swap took effect.
pub fn event_upgrade_completed(env: &Env) -> Symbol {
Symbol::new(env, "upgrade_completed")
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -436,4 +454,20 @@ mod tests {
let sym = event_admin_broadcast(&env);
assert_eq!(sym, Symbol::new(&env, "admin_broadcast"));
}

/// Snapshot: proves event_upgrade_started maps to exactly the bytes for "upgrade_started".
#[test]
fn test_event_upgrade_started_bytes() {
let env = soroban_sdk::Env::default();
let sym = event_upgrade_started(&env);
assert_eq!(sym, Symbol::new(&env, "upgrade_started"));
}

/// Snapshot: proves event_upgrade_completed maps to exactly the bytes for "upgrade_completed".
#[test]
fn test_event_upgrade_completed_bytes() {
let env = soroban_sdk::Env::default();
let sym = event_upgrade_completed(&env);
assert_eq!(sym, Symbol::new(&env, "upgrade_completed"));
}
}
Loading
Loading