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-worldis the main protocol contract currently used by the API.stellar-lend/contracts/lendingcontains a separate lending workspace with its own event set, including the upgrade manager.stellar-lend/contracts/ammandstellar-lend/contracts/bridgeexpose their own standalone contract events.stellar-lend/indexing_systemis not the current Soroban indexer for this repo. Its status is clarified below.
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:
#[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 asdeposit_eventorproposal_created_event.- 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 asNone.- 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.
stellar-lend/indexing_system/ should currently be treated as deprecated prototype code, not as the active Soroban indexing stack.
Why:
- It depends on
ethersand parses Ethereum-style ABI logs. - Default config points to
ws://localhost:8546andhttp://localhost:8545. - The parser is built around Ethereum
Log,Abi, and ERC-20 event signatures. - The indexer connects with
Provider<Ws>and fetchesget_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.
| 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 |
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 |
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
RiskParamsstruct - Emitted when:
set_risk_paramsupdates the persisted config
Payload fields:
min_collateral_ratioliquidation_thresholdclose_factorliquidation_incentivelast_update
The RiskParamsUpdatedEvent helper in src/events.rs should be treated as declared test/helper code, not the live runtime payload.
These modules are compiled in the crate, but the current HelloContract public surface does not expose the entrypoints that would emit them.
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 |
| 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 |
These should be treated as placeholders or test-only helpers until they are wired into reachable flows:
risk_params_updated_eventgovernance_config_updated_eventmultisig_config_updated_eventguardian_threshold_updated_eventmint_eventtransfer_eventstatus_change_eventquality_score_updated_eventapprove_eventsep41_transfer_eventsep41_burn_event
stellar-lend/contracts/lending/src/events.rs defines the event surface for the separate lending workspace.
| 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
| 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 |
| 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 |
| 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 |
| 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 |
{
"contractId": "CD6...EXAMPLE",
"topics": ["deposit_event"],
"value": {
"user": "GBZ...USER",
"asset": null,
"amount": "25000000",
"timestamp": 1735689600
}
}{
"contractId": "CD6...EXAMPLE",
"topics": ["risk_params_updated"],
"value": {
"min_collateral_ratio": "11000",
"liquidation_threshold": "10500",
"close_factor": "5000",
"liquidation_incentive": "1000",
"last_update": 1735689600
}
}{
"contractId": "CB7...UPGRADE",
"topics": ["up_exec", "GBX...APPROVER", 4],
"value": 7
}Use this when you want the smallest moving parts and you only need near-real-time data or short lookback windows.
Official reference:
- Stellar docs event reference: https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/events
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.
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:
- Mercury contract event queries: https://docs.mercurydata.app/mercury-classic/queries/contract-events
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
SubQuery is a good fit if you want your own schema, transforms, and GraphQL API on top of Soroban events.
Official references:
- Quick start: https://subquery.network/doc/indexer/quickstart/quickstart_chains/stellar.html
- Event handler mapping: https://subquery.network/doc/indexer/build/mapping/stellar.html
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
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
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.
For most deployments:
- Use RPC
getEventsfor low-latency ingest. - Persist raw topics and decoded payloads to your own database.
- Use Mercury or SubQuery if you need easier history or richer query surfaces.
- Treat
indexing_system/as deprecated until a Soroban-native replacement exists in this repo.