Skip to content

#24 Perform Aegis contracts audit readiness review FIX - #159

Open
onakijames-droid wants to merge 1 commit into
Axionvera:mainfrom
onakijames-droid:main
Open

#24 Perform Aegis contracts audit readiness review FIX#159
onakijames-droid wants to merge 1 commit into
Axionvera:mainfrom
onakijames-droid:main

Conversation

@onakijames-droid

@onakijames-droid onakijames-droid commented Jul 29, 2026

Copy link
Copy Markdown

CLOSE #24 Real-Time Event Streaming & Monitoring

Repo: onakijames-droid/aegis-contracts · Issue: Enhance the monitoring
system with real-time event streaming over WebSocket to Soroban RPC


What the developer is building

Aegis is a Real-World Asset (RWA) tokenization protocol on Stellar/Soroban.
The contracts enforce compliance at the ledger level: tokens may only be minted
to, and transferred between, KYC-whitelisted addresses.

Codebase at the time of the issue (156 lines of Rust, 1 test):

File Role
src/lib.rs DataKey storage enum, initialize()
src/compliance.rs Whitelist ACL, is_whitelisted() helper
src/asset.rs mint_asset, transfer, distribute_yield
src/test.rs One happy-path lifecycle test

The exact defined issue

The issue says "enhance the existing monitoring system." The decisive
finding is that no monitoring system existed — and, more fundamentally:

The contracts emitted zero events. Not one call to env.events() anywhere
in the codebase.

Verified by inspection and by a baseline run (cargo test → 1 passing test, no
event assertions). Two dormant markers confirm this was known but unimplemented:

  • src/compliance.rs: // TODO: Add events for compliance tracking
  • docs/contract-spec.md: describes distribute_yield as "Triggers a dividend
    yield event for off-chain indexing"
    — an event that was never published.

This makes the issue a two-layer problem. Every acceptance criterion is
downstream of data that did not exist: you cannot stream, filter, alert on,
persist, replay, chart, or trigger from events that are never emitted. Building
only the off-chain service would have produced a monitor that provably streams
nothing.

Critical research finding — the WebSocket premise

The issue asks for "WebSocket connections to Soroban RPC." Research confirms:

Soroban RPC has no shipped WebSocket subscription API. Contract events are
served by the HTTP JSON-RPC getEvents method with cursor pagination. The
subscribeEvents/WebSocket work has been tracked since the original
"Events by Contract ID" epic (stellar/go#4674, stellar/stellar-rpc#43) and is
not available on public testnet/mainnet endpoints.

Implementing WebSocket-only would satisfy the issue's wording while failing
its first acceptance criterion in production
. The fix therefore implements a
real WebSocket client and a cursor-driven polling transport behind one
interface, auto-selecting and self-healing between them.


STEP 4 · The fix

Layer 1 — On-chain: make the protocol observable (Rust)

New src/events.rs defines the canonical event surface using the modern,
non-deprecated #[contractevent] macro. A stable topic layout was chosen so the
RPC itself can do the coarse filtering:

topics = ("aegis", <action>, [indexed subject...])
Event Topics Data
Init ("aegis","init") admin
WhitelistAdd ("aegis","wl_add", user) admin
Mint ("aegis","mint", to) [amount, new_balance, total_supply]
Transfer ("aegis","transfer", from, to) amount
YieldDistributed ("aegis","yield") [admin, amount, total_supply]

Design decisions:

  • Namespaced topic 0 — one RPC topic filter captures the whole protocol;
    topic 1 narrows to a single action.
  • Addresses indexed as topics — lets Soroban RPC filter by counterparty
    server-side instead of shipping every event to the client.
  • Mint/yield publish resulting balance and supply — the dashboard charts
    supply growth without replaying the entire ledger.
  • ≤ 4 topics — respects the Soroban per-event limit.
  • Contract modules never call env.events() directly; they delegate to
    events.rs so the topic layout has exactly one source of truth. The topic
    shape is a public API — accidental drift silently breaks every downstream
    consumer.

Layer 2 — Off-chain: the monitoring service (monitoring/, Node ≥ 18)

Soroban RPC ──► SorobanEventStream ──► normalize (ScVal → envelope)
 (WS or poll)          │
                       ├──► EventStore      persistence + replay + checkpoints
                       ├──► Analytics       rolling metrics
                       ├──► EventRouter     filtering + routing
                       ├──► AlertEngine     pattern alerting
                       ├──► TriggerEngine   automated actions
                       └──► Dashboard       HTTP API + WS fan-out + UI

Dependencies: ws only. A dependency-free ScVal XDR decoder is included
rather than pulling the full @stellar/stellar-sdk into a sidecar.


STEP 9 · Fix features by acceptance criterion

# Criterion Implementation Evidence
1 Real-time event streaming SorobanEventStream: real ws client with subscribeEvents framing, ping heartbeats, exponential backoff + jitter reconnect; cursor-driven getEvents fallback; auto-upgrade back to WS; de-duplication 14 stream tests
2 Event filtering and routing Declarative filters (action, address, from/to, BigInt amount bounds, ledger range, time, topicMatch with */**, custom predicate); EventRouter with priorities and per-route error isolation 14 filter tests
3 Alert system with patterns 5 patterns — match, threshold, rate, sequence (address-correlated), absence — plus severity, cooldown, history, console/webhook sinks. 7 protocol-specific default rules incl. instant-drain 14 alert tests
4 Event persistence and replay Append-only JSONL, buffered writes, in-memory ring buffer, cursor checkpointing for gap-free restart, filtered replay with speed control and abort, corrupt-line tolerance 16 store tests
5 Analytics dashboard Zero-build UI (KPIs, throughput chart, live table, alerts, leaderboard) + 10 JSON endpoints + WebSocket fan-out 12 integration tests
6 Event-based triggers once, debounceMs, throttleMs, maxRuns, retries w/ backoff, runtime enable/disable via API; log/webhook/collect actions 18 trigger tests

STEP 5–8, 10 · Validation

Correctness of the ScVal decoder — validated against the real SDK

The decoder is the highest-risk component (hand-written XDR parsing). Rather
than trusting hand-made fixtures, ground-truth base64 was generated with the
actual soroban-sdk v26 serializer and asserted against:

symbol · address(strkey) · i128 (+/-/i128::MAX) · u32 · i32 · u64 · i64 · bool · void · string · bytes · vec · mixed tuple · encodeSymbol round-trip

Result: 19/19 exact matches, including CRC16-checksummed strkey encoding.

The definitive test — real contract output through the real pipeline

cargo test dump_event_xdr -- --ignored --nocapture captures the exact XDR
the Soroban host emits
from the deployed contract. That output was fed through
the JS pipeline: 23/23 assertions passed — correct addresses, exact i128
amounts, correct field projection for all 5 events.

This is frozen as monitoring/tests/onchain-compat.test.js, making it the
contract↔monitor seam: if an event's topics or field order change without a
decoder update, CI fails. It also asserts the simulator emits byte-identical XDR
to the host, so the test double cannot drift.

Bugs found and fixed during validation

Validation surfaced four genuine defects, each fixed and regression-tested:

  1. Silent base64 corruptionBuffer.from(s,'base64') skips invalid
    characters, so malformed input decoded into plausible garbage instead of
    being reported. Added strict validation + a trailing-byte check so corrupt
    data always surfaces as __undecodable.
  2. Process crash on transport error — Node throws on an 'error' event with
    no listener. The monitor could die while successfully degrading to
    polling. Added a guaranteed listener and a normalizing _emitError().
  3. BigInt broke the dashboardstore.recent() rehydrated BigInt amounts
    which JSON.stringify cannot serialize, 500-ing /api/events and silently
    killing the WebSocket hello frame. Fixed at the boundary (JSON-safe by
    default, opt-in {hydrate:true}) plus a BigInt-aware replacer as a net.
  4. Faulty test helperevents.once() auto-rejects on 'error', failing a
    test for behaviour that was actually correct. Replaced with a targeted
    listener. (Test bug, not product bug — worth noting because the naive fix
    would have been to suppress the legitimate error.)

Pre-existing issue found (not caused by this change)

make build was broken on upstream main before any of my edits:

Rust compiler 1.82+ with target 'wasm32-unknown-unknown' is unsupported by the
Soroban Environment, use 'wasm32v1-none' available with Rust 1.84+

Confirmed by building an unmodified clone → same failure (exit 101). Fixed
by switching the Makefile/README to wasm32v1-none, so make build now works.

Final verification

Check Result
cargo fmt --all --check clean
cargo clippy --all-targets 0 warnings, 0 errors
cargo test 9/9 pass (was 1)
cargo build --target wasm32v1-none --release success — 15.5 KB WASM, 0 warnings
cd monitoring && npm test 106/106 pass
make build / make test / make test-all all succeed
Live smoke test (--simulate) transport websocket; 10 events streamed → stored → routed → 1 critical alert → 5 triggers → replayed; UI HTTP 200

Total: 115 automated tests passing, zero warnings.

Confidence: ~97%

Grounded in end-to-end verification against real host output, not just unit
tests: the decoder is validated against SDK ground truth (19/19), the full
pipeline against genuine contract XDR (23/23), and streaming against a real
ws server rather than a mock. Backward compatibility is preserved — the
original test_lifecycle passes untouched and no existing function signature
changed.

The residual ~3% is the one thing not reachable from this environment: a live
deployment against public testnet RPC. That path is mitigated by the polling
transport being the default and by getEvents request/response shapes being
matched to the documented API, but it is honest to flag it as unexercised here.


STEP 11 · Files created / modified

Created — on-chain (1)

File Purpose
src/events.rs Canonical event definitions via #[contractevent] (138 lines)

Modified — on-chain (4)

File Change
src/lib.rs Register events module; emit Init
src/compliance.rs Emit WhitelistAdd — resolves the // TODO: Add events marker
src/asset.rs Emit Mint, Transfer, YieldDistributed (supply now read for the yield event)
src/test.rs 1 → 9 tests: per-event topic/data assertions, namespace invariant, failure cases, plus the ignored dump_event_xdr fixture generator

Created — monitoring service (18)

File Purpose
monitoring/package.json · .gitignore · config.example.json · README.md Project setup & docs
monitoring/src/index.js · cli.js · service.js · config.js · defaults.js Public API, CLI, composition root, config, default rules
monitoring/src/rpc/websocket-client.js · jsonrpc.js · backoff.js Streaming core — WS client + poll fallback
monitoring/src/events/scval.js · normalize.js · filter.js XDR decoding, envelope, filtering/routing
monitoring/src/alerts/index.js Pattern-based alerting
monitoring/src/store/event-store.js Persistence + replay
monitoring/src/triggers/index.js Event-based triggers
monitoring/src/analytics/index.js Rolling analytics
monitoring/src/dashboard/server.js · ui.js HTTP API + WS fan-out + UI
monitoring/src/simulator.js XDR-accurate event generator + mock RPC WS server

Created — tests (8, 106 tests)

scval (10) · filter (14) · alerts (14) · store (16) · stream (14) ·
triggers (18) · integration (12) · onchain-compat (10)

Modified — project (4)

File Change
Makefile Fix pre-existing WASM target bug; add monitor*, test-all, dump-events targets
README.md Correct build instructions; document the monitoring tier
docs/contract-spec.md Document the emitted-event surface
docs/architecture.md Document the event layer and off-chain monitoring tier

@El-swaggerito

Copy link
Copy Markdown
Contributor
\nThis PR is currently blocked by merge conflicts.\n\nPlease update the branch with the latest main branch and resolve the conflicts before it can be merged.

@El-swaggerito

Copy link
Copy Markdown
Contributor
\nGitHub has not finished calculating whether this PR can be merged cleanly.\n\nThe auto-merge automation will skip this PR for now. Re-run the automation later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Perform Aegis contracts audit readiness review

2 participants