Skip to main content
This catalog covers custom events from the controller, pool, governance, price aggregator, and DeFindex adapter. Decode each event by its emitting contract address and exact ordered topics. The position NFT also emits standard token events for mint, transfer, and burn. Library ownership and access-control events need separate decoders.
Subscribe to both the controller and pool. The pool publishes market state and full market parameters.

Reading the payloads

Custom events below use fixed Soroban Symbol topics. Match each symbol in order. Standard library events can also place indexed fields in topics. Payload encoding is one of three shapes: UpdatePositionBatchEvent uses vec. PoolMarketStateBatchEvent and PoolMarketParamsBatchEvent use single-value. All other events below use 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. Field tables use the numeric scales defined in Data types. Raw asset units use token decimals; read those from the token contract or 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 is withheld in shares. Subtracting the two native-unit amount fields is not an exact fee calculation because gross and net shares are rounded separately. 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 publishes the liquidated account’s batch first, then a receiving account batch only if there are net credit updates. 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: u32, asset: Address, receiver: Address, caller: Address, amount: i128 (raw), fee: i128 (raw).

FlashPositionEvent

  • Topics: ["position", "flash_position"] · Format: map
  • Fields: account_id: u64, hub_id: u32, asset: Address, receiver: Address, caller: Address, amount: i128 (requested debt), amount_received: i128 (measured receiver receipt), fee: i128 (always 0). Amounts use raw token units. The account keeps the minted debt.
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.

ClaimRevenueEvent

  • Topics: ["revenue", "claim"] · Format: map
  • Fields: hub_id: u32, asset: Address, caller: Address, accumulator: Address, amount: i128 (raw token units).
  • Emitted by claim_revenue when the controller receives a positive payout.
amount is the controller’s measured receipt forwarded to the accumulator. It does not measure the accumulator’s final receipt if that transfer under-delivers.

AccountDelegateEvent

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

CleanBadDebtEvent

  • Topics: ["debt", "bad_debt"] · Format: map
  • Fields: account_id: u64, total_borrow_usd_wad: i128, total_collateral_usd_wad: i128. Values describe the account before cleanup.
  • 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 are maps. They carry hub_id: u32, max_borrow_rate: i128, base_borrow_rate: i128, slope1: i128, slope2: i128, slope3: i128, mid_utilization: i128, optimal_utilization: i128, max_utilization: i128, and reserve_factor: u32. Rates and utilization use RAY; reserve factor uses BPS. CreateMarketEvent identifies its token as base_asset: Address and adds market_address: Address. UpdateMarketParamsEvent uses asset: Address. They omit flash-loan settings and asset decimals. Read those fields from PoolMarketParamsBatchEvent.

Strategy events

Configuration events

EventSpoke is a map with spoke_id: u32, is_deprecated: bool, liquidation_target_hf_wad: i128, hf_for_max_bonus_wad: i128, and liquidation_bonus_factor_bps: u32. UpdateSpokeAssetEvent carries the full post-change config, including paused, frozen, and no_seize. It does not carry the flags epoch. Read get_spoke_asset_flags_epoch before proposing RelaxSpokeAssetFlags recovery. A later flag write invalidates the old recovery operation (#319).

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 PoolMarketParamsEvent row is a map with hub_id: u32, asset: Address, 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: u32, asset: Address, amount: i128 (gross principal), fee: i128 (withheld), amount_sent: i128 (amount − fee). Amounts use raw token units. amount_sent is the transfer request, not a measured receipt.
  • 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: Address, amount: i128 (always 0; harvest moves no funds), and price_per_share: i128 (12 decimals, floor-rescaled from the RAY supply index).

Indexing rules

  • Match emitter address and ordered topics. A topic rename requires a decoder update. Persist successful contract events; failed diagnostic events are not committed position 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.
  • Credit(0) has no controller-specific account-creation event. Its receiving batch carries account_attributes when net credits exist, and the call returns the id. Track standard NFT mint events for token creation and read account metadata when the lending batch is absent.
  • 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.