Skip to content

Latest commit

 

History

History
441 lines (330 loc) · 17.2 KB

File metadata and controls

441 lines (330 loc) · 17.2 KB

Smart Contract Event Indexing Guide

This guide documents the event surfaces that exist in the current StellarLend workspace and how to consume them safely.

It is written around the code that exists today:

  • stellar-lend/contracts/hello-world is the main protocol contract currently used by the API.
  • stellar-lend/contracts/lending contains a separate lending workspace with its own event set, including the upgrade manager.
  • stellar-lend/contracts/amm and stellar-lend/contracts/bridge expose their own standalone contract events.
  • stellar-lend/indexing_system is not the current Soroban indexer for this repo. Its status is clarified below.

How Soroban Events Look

Soroban event consumers should expect three logical pieces:

  • contractId: the emitting contract.
  • topics: up to four indexed values used for filtering.
  • value: the non-indexed payload.

For this repo there are two emission styles:

  1. #[contractevent] structs. The first topic is the event name generated by Soroban. In practice this is the snake_case form of the struct name, such as deposit_event or proposal_created_event.
  2. Manual env.events().publish(...) calls. For these, the topic tuple shown in the source is the source of truth.

Important decoding notes:

  • Option<Address> means the native asset can appear as None.
  • Amounts are raw integer amounts in the token's smallest unit.
  • Risk parameters and reserve factors are basis points unless the source struct says otherwise.
  • Analytics values are raw counters, not formatted percentages.

Current Status of indexing_system/

stellar-lend/indexing_system/ should currently be treated as deprecated prototype code, not as the active Soroban indexing stack.

Why:

  • It depends on ethers and parses Ethereum-style ABI logs.
  • Default config points to ws://localhost:8546 and http://localhost:8545.
  • The parser is built around Ethereum Log, Abi, and ERC-20 event signatures.
  • The indexer connects with Provider<Ws> and fetches get_logs, which is not the Soroban event model.

Recommendation:

  • Do not build new Soroban integrations on top of indexing_system/.
  • For current StellarLend deployments, use Stellar RPC getEvents, Mercury, SubQuery, or a custom ingestion service built on those APIs.

Event Surface Summary

Contract area Status Notes
contracts/hello-world public entrypoints Active Primary protocol event surface today
contracts/hello-world internal modules not exposed by HelloContract Implemented but not public Documented below so indexers are not surprised if wired later
contracts/lending main/data-store/upgrade/pause Separate active workspace Useful if you deploy that contract family directly
contracts/amm Standalone Separate AMM contract events
contracts/bridge Standalone Separate bridge contract events
contracts/hello-world/src/monitor.rs Not currently wired File exists, but hello-world/src/lib.rs does not export it today

hello-world Events Exposed by the Current Public Contract

These are the events reachable through the public methods in stellar-lend/contracts/hello-world/src/lib.rs.

Topic Payload fields Emitted when
("admin_changed") [("new_admin", address), ("caller", address)?] initialize and transfer_admin update admin state
admin_action_event actor, action, timestamp initialize completes risk-management setup
governance_initialized_event admin, vote_token, voting_period, quorum_bps, timestamp gov_initialize succeeds
deposit_event user, asset, amount, timestamp deposit_collateral succeeds
borrow_event user, asset, amount, timestamp borrow_asset succeeds
repay_event user, asset, amount, timestamp repay_debt succeeds
withdrawal_event user, asset, amount, timestamp withdraw_collateral succeeds
liquidation_event liquidator, borrower, debt_asset, collateral_asset, debt_liquidated, collateral_seized, incentive_amount, timestamp liquidate succeeds
pause_state_changed_event actor, operation, paused, timestamp set_emergency_pause or other pause helpers succeed
flash_loan_initiated_event user, asset, amount, fee, callback, timestamp execute_flash_loan succeeds
flash_loan_repaid_event user, asset, amount, fee, timestamp repay_flash_loan succeeds
treasury_set_event admin, treasury, timestamp set_treasury succeeds
reserves_claimed_event admin, asset, recipient, amount, timestamp claim_reserves succeeds
fee_config_updated_event admin, interest_fee_bps, liquidation_fee_bps, timestamp set_fee_config succeeds
position_updated_event user, collateral, debt Position-changing flows update cached user position
analytics_updated_event user, activity_type, amount, timestamp Deposit, borrow, repay, withdraw, and liquidate update analytics
user_activity_tracked_event user, operation, amount, timestamp Deposit, borrow, repay, withdraw, and liquidate update the activity log
liquidation_fee_collected_event asset, fee_amount, timestamp Liquidation fee is routed to reserves or treasury

Special Case: Risk Parameters

The runtime risk-parameter update event does not currently use the helper struct in src/events.rs.

Actual runtime emission:

  • Topic: risk_params_updated
  • Value: full RiskParams struct
  • Emitted when: set_risk_params updates the persisted config

Payload fields:

  • min_collateral_ratio
  • liquidation_threshold
  • close_factor
  • liquidation_incentive
  • last_update

The RiskParamsUpdatedEvent helper in src/events.rs should be treated as declared test/helper code, not the live runtime payload.

hello-world Events Implemented but Not Exposed by HelloContract Today

These modules are compiled in the crate, but the current HelloContract public surface does not expose the entrypoints that would emit them.

Governance and Recovery Events

Typed #[contractevent] forms:

Topic Payload fields Source module
proposal_created_event proposal_id, proposer, proposal_type, description, start_time, end_time, created_at governance.rs
vote_cast_event proposal_id, voter, vote_type, voting_power, timestamp governance.rs
proposal_queued_event proposal_id, execution_time, for_votes, against_votes, quorum_reached, threshold_met governance.rs
proposal_executed_event proposal_id, executor, timestamp governance.rs
proposal_failed_event proposal_id, for_votes, against_votes, quorum_reached, threshold_met governance.rs
proposal_cancelled_event proposal_id, caller, timestamp governance.rs
proposal_approved_event proposal_id, approver, timestamp governance.rs
guardian_added_event guardian, added_by, timestamp governance.rs
guardian_removed_event guardian, removed_by, timestamp governance.rs
recovery_started_event old_admin, new_admin, initiator, expires_at, timestamp governance.rs
recovery_approved_event approver, current_approvals, threshold, timestamp governance.rs
recovery_executed_event old_admin, new_admin, executor, timestamp governance.rs

Manual compatibility topics also exist in the same area:

Topic tuple Value Notes
("proposal_created", proposal_id, proposer) () Used by admin and emergency proposal helper paths
("proposal_queued", proposal_id) execution_time Used by admin and emergency proposal helper paths
("vote_cast", proposal_id, voter) (vote_type, voting_power) Helper exists for manual publishing
("proposal_executed", proposal_id, executor) () Helper exists for manual publishing
("proposal_failed", proposal_id) () Helper exists for manual publishing
("guardian_added", guardian) () Used by recovery.rs helpers
("guardian_removed", guardian) () Used by recovery.rs helpers
("recovery_started", old_admin, new_admin) initiator Used by recovery.rs helpers
("recovery_approved", approver) () Used by recovery.rs helpers
("recovery_executed", old_admin, new_admin) executor Used by recovery.rs helpers

Admin, Bridge, and Reserve Topics

Topic tuple Value Emitted when
("role_granted", caller, role) [("account", address)] Admin grants a role
("role_revoked", caller, role) [("account", address)] Admin revokes a role
("bridge", "deposit", network_id) (user, deposit_amount, fee) Internal bridge deposit flow
("bridge", "withdraw", network_id) (user, withdraw_amount, fee) Internal bridge withdrawal flow
("reserve_initialized") (asset, reserve_factor_bps) Reserve config initialized
("reserve_factor_updated", caller) (asset, reserve_factor_bps) Reserve factor updated
("reserve_accrued") (asset, reserve_amount, new_balance) Interest accrues to reserves
("treasury_address_set", caller) treasury Treasury address set
("reserve_withdrawn", caller) (asset, treasury, amount, new_balance) Reserve withdrawal to treasury
price_updated_event actor, asset, price, decimals, oracle, timestamp Oracle module updates a price feed

Declared in src/events.rs but Not Emitted by Runtime Today

These should be treated as placeholders or test-only helpers until they are wired into reachable flows:

  • risk_params_updated_event
  • governance_config_updated_event
  • multisig_config_updated_event
  • guardian_threshold_updated_event
  • mint_event
  • transfer_event
  • status_change_event
  • quality_score_updated_event
  • approve_event
  • sep41_transfer_event
  • sep41_burn_event

contracts/lending Event Catalog

stellar-lend/contracts/lending/src/events.rs defines the event surface for the separate lending workspace.

Main Lending Contract

Topic Payload fields
borrow_event user, asset, amount, collateral, timestamp
repay_event user, asset, amount, timestamp
withdraw_event user, asset, amount, remaining_balance, timestamp
flash_loan_event receiver, asset, amount, fee, timestamp
deposit_event user, asset, amount, timestamp for borrow-collateral deposits
deposit_event user, asset, amount, new_balance, timestamp for vault deposits
pause_event pause_type, paused, admin

deposit_event is intentionally overloaded in that contract family. Distinguish the two variants by payload shape:

  • borrow-collateral deposit has no new_balance
  • vault deposit includes new_balance

Data Store Contract

Topic tuple Value
("ds_init", admin) single-value event
("writer", caller, writer) single-value event
("ds_save", caller, key) value_len
("ds_bkup", caller, backup_name) key_count
("ds_rest", caller, backup_name) entry_count
("ds_migr", caller, new_version) memo

Upgrade Manager Contract

Topic tuple Value
("up_init", admin) required_approvals
("up_apadd", caller, approver) single-value event
("up_prop", caller, id) new_version
("up_appr", caller, proposal_id) approval_count
("up_exec", caller, proposal_id) new_version
("up_roll", caller, proposal_id) prev_version

Other Standalone Workspace Contracts

contracts/amm

Topic Payload fields
swap_executed_event user, protocol, amount_in, amount_out, effective_price
liquidity_added_event user, protocol, amount_a, amount_b, lp_tokens
liquidity_removed_event user, protocol, lp_tokens
amm_operation_event user, operation, amount_in, amount_out, timestamp
callback_validated_event caller, user, operation, nonce

contracts/bridge

Topic Payload fields
bridge_registered_event bridge_id, fee_bps, min_amount
bridge_fee_updated_event bridge_id, fee_bps
bridge_active_updated_event bridge_id, active
bridge_deposit_event bridge_id, amount, fee, net
bridge_withdrawal_event bridge_id, amount

Example Decoded Events

Example: deposit_event from hello-world

{
  "contractId": "CD6...EXAMPLE",
  "topics": ["deposit_event"],
  "value": {
    "user": "GBZ...USER",
    "asset": null,
    "amount": "25000000",
    "timestamp": 1735689600
  }
}

Example: live risk_params_updated

{
  "contractId": "CD6...EXAMPLE",
  "topics": ["risk_params_updated"],
  "value": {
    "min_collateral_ratio": "11000",
    "liquidation_threshold": "10500",
    "close_factor": "5000",
    "liquidation_incentive": "1000",
    "last_update": 1735689600
  }
}

Example: up_exec from the lending upgrade manager

{
  "contractId": "CB7...UPGRADE",
  "topics": ["up_exec", "GBX...APPROVER", 4],
  "value": 7
}

Option 1: Index Directly from Stellar RPC

Use this when you want the smallest moving parts and you only need near-real-time data or short lookback windows.

Official reference:

Important limitation:

  • RPC event history is ephemeral. The Stellar docs currently warn that providers usually retain less than a week of history.

Example using the JavaScript RPC client:

import { rpc, scValToNative, nativeToScVal } from "@stellar/stellar-sdk";

const server = new rpc.Server("https://soroban-testnet.stellar.org");

const page = await server.getEvents({
  startLedger: 0,
  filters: [
    {
      type: "contract",
      contractIds: ["YOUR_CONTRACT_ID"],
      topics: [[nativeToScVal("deposit_event", { type: "symbol" }).toXDR("base64")]],
    },
  ],
  limit: 100,
});

for (const event of page.events) {
  console.log(event.topic.map((topic) => scValToNative(topic)));
  console.log(scValToNative(event.value));
}

Recommended usage:

  • testnet development
  • real-time alerting
  • small integrations that already persist every event they see

Not recommended as the only production source for dashboards that need deep history.

Option 2: Index with Mercury

Mercury is a managed indexing layer for Stellar and Soroban and is the best fit when you want historical queries without running your own archival pipeline.

Official reference:

Useful Mercury patterns:

  • query by contract
  • query by topics
  • query by ledger range
  • query by transaction

Example by contract:

curl -X GET \
  "$BASE/events/by-contract/YOUR_CONTRACT_ID?limit=100&offset=0" \
  -H "Authorization: $AUTH"

Example by contract and topic:

curl -X GET \
  "$BASE/events/by-contract/YOUR_CONTRACT_ID?topics=deposit_event&limit=100&offset=0" \
  -H "Authorization: $AUTH"

Recommended usage:

  • production UIs
  • analytics backfills
  • historical compliance or monitoring queries

Option 3: Index with SubQuery

SubQuery is a good fit if you want your own schema, transforms, and GraphQL API on top of Soroban events.

Official references:

Minimal handler configuration:

{
  kind: StellarDatasourceKind.Runtime,
  startBlock: 1700000,
  mapping: {
    file: "./dist/index.js",
    handlers: [
      {
        handler: "handleEvent",
        kind: StellarHandlerKind.Event,
        filter: {
          contractId: "YOUR_CONTRACT_ID",
          topics: ["deposit_event"],
        },
      },
    ],
  },
}

Recommended usage:

  • custom data models
  • GraphQL-first apps
  • multi-contract analytics

Testnet and Mainnet Guidance

Testnet

Use testnet for:

  • schema design
  • decoder validation
  • replay testing
  • upgrade rehearsals

Good starting point:

  • official Stellar testnet RPC example endpoint: https://soroban-testnet.stellar.org

Mainnet

Use mainnet for:

  • production UI reads
  • analytics
  • alerting
  • audit trails

Production guidance:

  • do not rely on short-retention RPC alone for backfills
  • use Mercury, SubQuery, or your own durable event sink
  • if you use raw RPC on mainnet, confirm the provider's retention window before launch

The Stellar documentation currently uses mainnet.sorobanrpc.com as a mainnet RPC example in the Laboratory, but production teams should still verify provider retention and SLA before depending on it.

Recommended Production Pattern

For most deployments:

  1. Use RPC getEvents for low-latency ingest.
  2. Persist raw topics and decoded payloads to your own database.
  3. Use Mercury or SubQuery if you need easier history or richer query surfaces.
  4. Treat indexing_system/ as deprecated until a Soroban-native replacement exists in this repo.