Skip to main content
The protocol publishes contract events from five of its eight contracts: controller (20), pool (3), governance (2), price aggregator (1), and the DeFindex strategy adapter (1). The position-NFT, swap-aggregator, and XOXNO oracle contracts publish none.
Subscribe to the controller and the pool. Market state and market parameters are published by the pool, not the controller.

Reading the payloads

Each event’s topics list is a constant vector of Soroban Symbols. No event in this protocol promotes a field into the topic vector, so topics carry no data — match on them exactly, in order. Payload encoding is one of three shapes: Only UpdatePositionBatchEvent uses vec; only PoolMarketStateBatchEvent and PoolMarketParamsBatchEvent use single-value; everything else uses map. A nested #[contracttype] struct with named fields encodes as an alphabetically-keyed map; one with unnamed fields (a tuple struct) encodes as a vector in declaration order. Scales. Never assume a scale. RAY is 1e27 (indexes, rates, utilization, scaled share balances), WAD is 1e18 (USD values, health factors, sanity bounds), bps is 10_000. “Raw asset units” means the token’s own decimals, which are not carried in the event — read them from the token contract or from PoolMarketParamsBatchEvent. Pool timestamps are milliseconds.

PositionAction values

PositionAction is a unit-only enum with #[repr(u32)], so it appears on the wire as a plain u32. It tags each leg inside UpdatePositionBatchEvent.
LiqSeize is gross, LiqCredit is net. In share-credit mode the protocol fee equals LiqSeize.amount − LiqCredit.amount; summing both tags as the same quantity double-counts. In SeizeMode::Transfer only LiqSeize is emitted, also gross — the fee is withheld from the outbound transfer. Reading a LiqSeize amount as the liquidator’s proceeds overstates them by the fee.

Shared payload types

EventAccountAttributes

A tuple struct — a 3-entry vector: [owner: Address, spoke_id: u32, mode: EventPositionMode]. EventPositionMode is a u32 enum: None = 0 (a normal, non-strategy position), Multiply = 1, Long = 2, Short = 3. It is the wire form of PositionMode, with Normal mapped to None.

EventDepositDelta

A tuple struct — a 10-entry vector:

EventBorrowDelta

A tuple struct — a 6-entry vector: [action, hub_id, asset, scaled_amount (RAY), index_ray (RAY), amount (raw asset units)].

Controller events

UpdatePositionBatchEvent

  • Topics: ["position", "batch_update"]
  • Format: vec — a 4-entry vector, not a map.
  • Emitted by: supply, withdraw, borrow, repay, liquidate, multiply, flash_position, swap_debt, swap_collateral, repay_debt_with_collateral, migrate_from_blend, update_account_threshold.
The event is not published when both lists are empty.
One operation can publish more than one batch. A SeizeMode::Credit liquidation writes two accounts and publishes the liquidated account’s batch first, the receiving account’s second. Key on account_id; never assume one operation yields one batch. The receiver’s batch is supply-side only and omits any leg whose net credit is zero.

LiquidationEvent

  • Topics: ["position", "liquidation"] · Format: map
This event carries no seizure or protocol-fee figure. Those live in the accompanying batch’s LiqSeize legs (gross) and, in share-credit mode, its LiqCredit legs (net).

FlashLoanEvent

  • Topics: ["position", "flash_loan"] · Format: map
  • Fields: hub_id, asset, receiver, caller, amount (raw), fee (raw).

FlashPositionEvent

  • Topics: ["position", "flash_position"] · Format: map
  • Fields: account_id, hub_id, asset, receiver, caller, amount (principal requested from the debt mint), amount_received (tokens the receiver actually got, measured by balance delta), fee (always 0 — a flash position is zero-fee; the cost is the strategy debt it mints).
The debt legs appear in the accompanying batch as FlashPos; collateral deposited from the callback is tagged Supply, identically to an ordinary deposit. Do not read FlashPos as the collateral leg.

AccountDelegateEvent

  • Topics: ["account", "delegate"] · Format: map
  • Fields: account_id, owner, delegate, granted (bool).
  • Published only when the delegate list actually changed.

CleanBadDebtEvent

  • Topics: ["debt", "bad_debt"] · Format: map
  • Fields: account_id, total_borrow_usd_wad, total_collateral_usd_wad.
  • Emitted by clean_bad_debt, force_socialize_bad_debt, and by liquidate when the post-liquidation account still holds eligible bad debt.
This event records no position deltas: the positions are seized on the pool and the account entry is removed, with no accompanying UpdatePositionBatchEvent for the cleanup itself.

Market events

Both carry the flat rate-curve fields (max_borrow_rate, base_borrow_rate, slope1slope3, mid_utilization, optimal_utilization, max_utilization, all RAY; reserve_factor in bps) plus hub_id and the asset. CreateMarketEvent also carries market_address. Neither copies the flash-loan flag, the flash-loan fee, or asset decimals — read those from PoolMarketParamsBatchEvent.

Strategy events

Configuration events

UpdateSpokeAssetEvent carries the asset’s full post-change SpokeAssetConfig map, including all three halt flags (paused, frozen, no_seize).

Pool events

PoolMarketStateBatchEvent

  • Topics: ["market", "batch_state_update"]
  • Format: single-value — the payload is the vector of rows. There is no wrapping map and no updates key.
  • Emitted by: supply, borrow, withdraw, repay, seize_positions, net_settle, update_indexes, claim_revenue, flash_loan, recapitalize, and create_strategy. Not published when the snapshot list is empty.
Each row is a 9-entry vector:

PoolMarketParamsBatchEvent

  • Topics: ["market", "batch_params_update"]
  • Format: single-value — the payload is the vector of rows.
  • Emitted by: create_market and update_params; both publish exactly one row.
Each row is a map with hub_id, asset, and params: MarketParamsRaw — the market’s full post-change parameters, including flash-loan settings and asset decimals.

StrategyFeeEvent

  • Topics: ["strategy", "fee"] · Format: map
  • Fields: hub_id, asset, amount (gross strategy principal), fee (withheld), amount_sent (amount − fee).
  • Published only when the fee is non-zero.

Governance events

Price-aggregator events

UpdateAssetOracleEvent — topics ["config", "asset_oracle"], format map, fields key: PriceKey and oracle: AssetOracle. Emitted by set_oracle, set_sanity_band, and set_tolerance.

DeFindex-strategy events

HarvestEvent — topics ["strategy", "harvest"], format map, fields from, amount (always 0; harvest moves no funds), and price_per_share (12 decimals, floor-rescaled from the RAY supply index).

Indexing notes

  • Topic names are contract API. Treat renames as breaking changes.
  • Store HubAssetKey fields together; do not index by token address alone — the same token on two hubs is two markets.
  • Key swap_debt opens on SwDebtR, which tags both legs.
  • Do not assume bad-debt cleanup precedes the position batch: every UpdatePositionBatchEvent is published before CleanBadDebtEvent.
  • A Credit(0) account has no creation event. It is announced only through the second batch’s account_attributes, and returned to the caller. An indexer that discovers accounts from a creation event alone will miss it.
  • flash_position refunds are silent: no refund event exists anywhere, and FlashPositionEvent carries no refund field. The only on-chain trace is the token contract’s own transfer event, controller to caller.
  • Position ownership changes are observable only through the position-NFT contract’s standard non-fungible transfer semantics; this protocol defines no event for them.