Skip to content
Merged
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
78 changes: 77 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- [Getting Started](#getting-started)
- [Usage Examples](#usage-examples)
- [Security Posture](#security-posture)
- [Observability](#observability)
- [Testing & Verification](#testing-and-verification)
- [Migrations & Versioning](#migrations-and-versioning)
- [Operational Runbook](#operational-runbook)
Expand Down Expand Up @@ -170,7 +171,8 @@ PeerX-Contracts/
│ ├── staking_bonus.rs # Locked staking & distributions
│ ├── emergency.rs # Pause / freeze / snapshot
│ ├── migration.rs # Versioned upgrades
│ ├── events.rs # Batched on-chain events
│ ├── events.rs # Structured events, routed through observability::log
│ ├── observability.rs # LogLevel enum, admin-settable log threshold
│ └── risk_management/ # Circuit breaker, concentration, limits
├── soroban-ping/ # Hello-world contract, smoke test
├── trading_test_runner/ # Off-chain trading test driver
Expand Down Expand Up @@ -307,6 +309,80 @@ See [`SECURITY.md`](SECURITY.md) for the full vulnerability checklist, invariant

---

## Observability {#observability}

Every structured event the contract emits (`src/events.rs`, plus the ad-hoc
diagnostics previously gated behind `#[cfg(feature = "logging")]` in
`lib.rs`) is routed through a single choke point in
[`src/observability.rs`](peerx-contracts/counter/src/observability.rs):
`observability::log(env, level, topics, data)`. It publishes exactly what
`env.events().publish(topics, data)` would have published — same event
names, same payloads — but only if `level` clears the currently configured
threshold.

### Log levels

```rust
pub enum LogLevel { Debug, Info, Warn, Error }
```

Ordered from most to least verbose. A configured threshold of `Warn` means
only `Warn` and `Error` events are published; `Debug` and `Info` are
silently dropped before the event is ever written.

### Per-network defaults

Until an admin overrides it, the threshold falls back to a compiled default
selected by cargo feature:

| Build | Feature | Default level |
| --- | --- | --- |
| Dev | *(none)* | `Debug` |
| Testnet | `testnet` | `Info` |
| Mainnet | `mainnet` | `Warn` |

```bash
# Mainnet build — quiet by default, no Debug/Info noise
cargo build --release --target wasm32-unknown-unknown \
--manifest-path peerx-contracts/counter/Cargo.toml --features mainnet
```

### Admin override

The effective threshold can be changed at runtime — no redeploy required —
and is stored in **persistent** storage, so it survives across calls and
contract upgrades:

```bash
soroban contract invoke --id <CONTRACT_ID> --network mainnet \
--source <ADMIN_SECRET> \
-- set_log_level --caller <ADMIN_ADDRESS> --level Error

soroban contract invoke --id <CONTRACT_ID> --network mainnet \
-- get_log_level
```

`set_log_level` requires `caller.require_auth()` plus `require_admin` — the
same admin gate every other privileged entry point uses. Setting `Error`
silences every event except `Error`-level ones (see
`observability_tests.rs::error_level_silences_everything_except_error_events`
for the event-count assertion this guarantees).

### Adding a new event

New helpers in `events.rs` should call `observability::log` instead of
`env.events().publish` directly, and pick the lowest level that's still
useful in production:

- `Debug` — high-frequency, per-operation detail (e.g. raw swap amounts).
- `Info` — normal business events (swaps, LP changes, tier changes).
- `Warn` — degraded conditions worth an operator's attention (admin
pause/resume, failed orders, congestion changes).
- `Error` — extreme conditions (emergency overrides) that must never be
silenced.

---

## Testing &amp; Verification {#testing-and-verification}

```bash
Expand Down
6 changes: 6 additions & 0 deletions peerx-contracts/counter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,9 @@ proptest = "1.4"
# Off by default to avoid overhead in production.
logging = []
experimental = []

# Selects the compiled default for observability::LogLevel (overridable at
# runtime by the admin via set_log_level). Mutually exclusive; omitting both
# yields the dev default (Debug). See src/observability.rs.
mainnet = []
testnet = []
90 changes: 68 additions & 22 deletions peerx-contracts/counter/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use soroban_sdk::{contracttype, Address, Env, Symbol, Vec};

use crate::observability::{log, LogLevel};

#[contracttype]
#[derive(Clone)]
pub struct BadgeEvent {
Expand All @@ -22,7 +24,9 @@ impl Events {
user: Address,
timestamp: i64,
) {
env.events().publish(
log(
env,
LogLevel::Info,
(Symbol::new(env, "SwapExecuted"), user, from_token, to_token),
(from_amount, to_amount, timestamp),
);
Expand All @@ -36,7 +40,9 @@ impl Events {
user: Address,
timestamp: i64,
) {
env.events().publish(
log(
env,
LogLevel::Info,
(Symbol::new(env, "LiquidityAdded"), user),
(xlm_amount, usdc_amount, lp_tokens_minted, timestamp),
);
Expand All @@ -50,7 +56,9 @@ impl Events {
user: Address,
timestamp: i64,
) {
env.events().publish(
log(
env,
LogLevel::Info,
(Symbol::new(env, "LiquidityRemoved"), user),
(xlm_amount, usdc_amount, lp_tokens_burned, timestamp),
);
Expand All @@ -74,8 +82,12 @@ impl Events {
let buffer: Option<Vec<BadgeEvent>> = env.storage().temporary().get(&EVENT_BUFFER_KEY);
if let Some(events) = buffer {
if !events.is_empty() {
env.events()
.publish((Symbol::new(env, "BadgesAwarded"),), events);
log(
env,
LogLevel::Info,
(Symbol::new(env, "BadgesAwarded"),),
events,
);
env.storage().temporary().remove(&EVENT_BUFFER_KEY);
}
}
Expand All @@ -88,20 +100,30 @@ impl Events {
new_tier: crate::tiers::UserTier,
timestamp: i64,
) {
env.events().publish(
log(
env,
LogLevel::Info,
(Symbol::new(env, "UserTierChanged"), user),
(old_tier, new_tier, timestamp),
);
}

pub fn admin_paused(env: &Env, admin: Address, timestamp: i64) {
env.events()
.publish((Symbol::new(env, "AdminPaused"), admin), (timestamp,));
log(
env,
LogLevel::Warn,
(Symbol::new(env, "AdminPaused"), admin),
(timestamp,),
);
}

pub fn admin_resumed(env: &Env, admin: Address, timestamp: i64) {
env.events()
.publish((Symbol::new(env, "AdminResumed"), admin), (timestamp,));
log(
env,
LogLevel::Warn,
(Symbol::new(env, "AdminResumed"), admin),
(timestamp,),
);
}
}

Expand All @@ -125,7 +147,9 @@ pub fn alert_triggered(
notification_method_tag: Symbol,
timestamp: u64,
) {
env.events().publish(
log(
env,
LogLevel::Warn,
(Symbol::new(env, "AlertTriggered"), owner, alert_id),
(kind_tag, notification_method_tag, timestamp),
);
Expand All @@ -137,7 +161,9 @@ pub fn alert_triggered(
/// Topic : ("AlertCreated", owner_address, alert_id)
/// Payload: (kind_tag, expires_at)
pub fn alert_created(env: &Env, owner: Address, alert_id: u64, kind_tag: Symbol, expires_at: u64) {
env.events().publish(
log(
env,
LogLevel::Info,
(Symbol::new(env, "AlertCreated"), owner, alert_id),
(kind_tag, expires_at),
);
Expand All @@ -158,7 +184,9 @@ pub fn performance_metrics_calculated(
max_drawdown: u128,
timestamp: i64,
) {
env.events().publish(
log(
env,
LogLevel::Debug,
(Symbol::new(env, "PerformanceMetricsCalculated"), user),
(time_window, sharpe_ratio, max_drawdown, timestamp),
);
Expand All @@ -178,7 +206,9 @@ pub fn asset_allocation_analyzed(
diversification_score: u128,
timestamp: i64,
) {
env.events().publish(
log(
env,
LogLevel::Debug,
(Symbol::new(env, "AssetAllocationAnalyzed"), user),
(total_assets, diversification_score, timestamp),
);
Expand All @@ -199,7 +229,9 @@ pub fn benchmark_comparison_calculated(
beta: u128,
timestamp: i64,
) {
env.events().publish(
log(
env,
LogLevel::Debug,
(
Symbol::new(env, "BenchmarkComparisonCalculated"),
user,
Expand All @@ -224,7 +256,9 @@ pub fn period_returns_calculated(
time_weighted_return: i128,
timestamp: i64,
) {
env.events().publish(
log(
env,
LogLevel::Debug,
(Symbol::new(env, "PeriodReturnsCalculated"), user),
(
start_timestamp,
Expand All @@ -247,7 +281,9 @@ pub fn network_congestion_changed(
capacity_utilization: u32,
timestamp: u64,
) {
env.events().publish(
log(
env,
LogLevel::Warn,
(Symbol::new(env, "NetworkCongestionChanged"),),
(previous_level, new_level, capacity_utilization, timestamp),
);
Expand All @@ -266,7 +302,9 @@ pub fn fee_adjustment_applied(
congestion_level: Symbol,
timestamp: u64,
) {
env.events().publish(
log(
env,
LogLevel::Info,
(Symbol::new(env, "FeeAdjustmentApplied"),),
(
previous_fee_bps,
Expand All @@ -289,7 +327,9 @@ pub fn emergency_fee_override_activated(
reason: Symbol,
timestamp: u64,
) {
env.events().publish(
log(
env,
LogLevel::Error,
(Symbol::new(env, "EmergencyFeeOverrideActivated"),),
(fee_cap_bps, reason, timestamp),
);
Expand All @@ -301,7 +341,9 @@ pub fn emergency_fee_override_activated(
/// Topic : ("EmergencyFeeOverrideDeactivated",)
/// Payload: (timestamp,)
pub fn emergency_fee_override_deactivated(env: &Env, timestamp: u64) {
env.events().publish(
log(
env,
LogLevel::Warn,
(Symbol::new(env, "EmergencyFeeOverrideDeactivated"),),
(timestamp,),
);
Expand All @@ -313,7 +355,9 @@ pub fn emergency_fee_override_deactivated(env: &Env, timestamp: u64) {
/// Topic : ("FeeConfigurationUpdated",)
/// Payload: (admin_address, config_change_tag, timestamp)
pub fn fee_configuration_updated(env: &Env, admin: Address, change_type: Symbol, timestamp: u64) {
env.events().publish(
log(
env,
LogLevel::Info,
(Symbol::new(env, "FeeConfigurationUpdated"), admin),
(change_type, timestamp),
);
Expand All @@ -332,7 +376,9 @@ pub fn fee_statistics_report(
volatility: u32,
timestamp: u64,
) {
env.events().publish(
log(
env,
LogLevel::Debug,
(Symbol::new(env, "FeeStatisticsReport"),),
(avg_fee_bps, min_fee_bps, max_fee_bps, volatility, timestamp),
);
Expand Down
Loading
Loading