For the complete documentation index, see llms.txt. This page is also available as Markdown.

Events

Reference for every integrator-facing event that SwapCore (or a library it delegatecalls into) emits. Use these to drive indexers, subgraphs, monitoring dashboards, and UI feeds.

Sources:

  • lib/SwapEvents.sol — the canonical declaration of every event on this page (SwapCore inherits this).

  • lib/Utils.sol — emits BuyerTransferFailed, ExpiryIndexExtrapolated, ProtocolFeeCollected, CreatorFeeCollected, ProtocolFeeForegone, and MarketAdminChange from the settlement / liquidation / fee paths, via local mirror declarations of the SwapEvents originals.

  • lib/PoolInternalsLib.sol — declares (again mirroring SwapEvents) and emits EarlyExitValueVested, LiquidationValueVested, and SettlementVestingClipped.

  • lib/RateIndexLib.sol — declares and emits IndexClamped.

The mirror declarations are signature-identical to the SwapEvents originals, so each event has one topic signature no matter which file emits it. Utils, PoolInternalsLib, and RateIndexLib are Solidity libraries that SwapCore delegatecalls, so their events are emitted in SwapCore's execution context and logged under the SwapCore address. Subscribing to the SwapCore address alone captures every event on this page — you do not need to watch the library addresses, and watching them would yield nothing.

One footnote on that address: the deployed protocol address is the SwapStore proxy, which also logs the standard ERC-1967 Upgraded(address) event whenever the implementation is repointed. Filter by the topics on this page rather than assuming every log at the address is one of these events.

Scope. This page covers the SwapCore event surface only. Other production contracts emit their own events at their own addresses — Admin (governance / timelock events), SwapStoreGovernor (upgrade-governance events), the position wrapper (wrap / unwrap / settle / redeem), the oracle contracts (poke, clamp, premium-posting, and timelock events), and the v1.1 adapters and their factories. Those are not documented here.

Event topic signatures below are the canonical keccak256("EventName(type1,type2,...)") — use them as the topic[0] filter in log queries.


Lifecycle events

MarketCreated

event MarketCreated(
    bytes32 indexed marketId,
    address indexed creator,
    address         referenceRateOracle,
    address         baseSwapRateOracle,
    address indexed swapToken,
    uint64          leverageMultiplier,
    uint32          swapTerm,
    Types.RateType  rateType,
    bytes32         correspondingMarketId
);

Emitted once per side of the pair from createMarket_initializeMarket. A single createMarket call therefore produces two MarketCreated events, one with rateType = 0 (BUY_FIXED) and one with rateType = 1 (BUY_FLOATING). Use correspondingMarketId to stitch them together into a pair.

Indexed topics: marketId, creator, swapToken.

MarketConfigured

Emitted alongside MarketCreated from _initializeMarket, carrying the rest of the config that didn't fit in MarketCreated. Use this to snapshot the full market configuration at creation time. The economic parameters (fee curve, buckets, oracles, term, collateral) are immutable — nothing in SwapCore rewrites them — so one MarketConfigured is all an indexer needs for those. The one field here that can change later is marketOwner: track MarketOwnerChanged to keep it current.

MarketCreationFeeCollected

Emitted once per createMarket call if a creation fee was configured in Admin at call time. feeToken == address(0) means the fee was paid in ETH; otherwise it's the ERC20 token address. Paired with MarketCreated events on the two market IDs.

MarketCreationFeeForegone

Emitted by createMarket in place of MarketCreationFeeCollected when the creation fee was charged but could not be delivered to the multisig — a blacklisted or reverting recipient, a paused fee token, or a fee token whose transfer is expensive enough that the creator can starve it of gas under the 63/64 rule. The fee is refunded to the creator and market creation succeeds.

This fail-open is deliberate: creation must not be brickable by a fee token the multisig cannot receive. For accounting, treat this as "fee waived — creator made whole," not "fee collected." Mirrors MarketCreationFeeCollected's scope exactly (both paired market IDs, same fee denomination), so a revenue indexer should watch both events on the same footing or it will over-count creation fees.


Ownership & admin

MarketOwnershipTransferStarted

Emitted by transferMarketOwnership. Marks the start of the 2-step handoff. newOwner == address(0) means an in-flight transfer was cancelled.

MarketOwnerChanged

Emitted by acceptMarketOwnership when the pending owner accepts the role. newOwner is the new owner (msg.sender of acceptMarketOwnership).

MarketLpWhitelistUpdated

Emitted by setMarketLpWhitelist. Only meaningful on markets created with lpWhitelistEnabled = true.

MarketAdminChange

Emitted by terminateMarket with controlType = "terminateMarket" and isPaused = true. The string and bool arguments exist for forward-compatibility with future admin levers, but today this event only ever fires for termination.


Liquidity

CollateralSupplied

Emitted by supplyCollateral. onBehalfOf is the address that got the shares; caller is msg.sender (same unless a bundler is acting).

This event carries nine arguments — note mintPrice between sharePrice and entryPrice. Compute topic[0] from the declaration above; an argument list that omits mintPrice hashes to a different topic and matches nothing.

The two prices are distinct and both are needed:

  • sharePrice — the fair (vesting-adjusted) mark.

  • mintPrice — the raw price the shares were actually minted at (amount / sharesMinted). This is the one to use for reconciling a deposit against shares received.

mintPrice >= sharePrice always holds. They diverge while the market's vesting reserve is draining — the reserve is fed by both early-exit and liquidation vesting (see EarlyExitValueVested / LiquidationValueVested) — but they are also equal in several ordinary states, so a gap between them is not by itself a signal of anything: treat mintPrice as the reconciliation price and sharePrice as the mark, and don't alert on their difference alone. entryPrice and vestEndTimestamp are the LP's profit-vest anchor and window end set by this deposit; entryPrice is the burn mark, not sharePrice (see Liquidity provision).

CollateralTokenWithdrawn

Emitted by withdrawCollateral. amount is in swapToken decimals and was delivered to receiver (which may differ from both onBehalfOf and caller when a bundler passes itself as receiver to forward the redeemed tokens elsewhere). capApplied is true when the profit-vest cap bound the burn (shares priced at entryPrice rather than the live sharePrice).

EarlyExitValueVested

Emitted when an early exit diverts a NAV crystallization into the market's vesting reserve, so the settlement jump can't be captured just-in-time by a deposit-then-withdraw sandwich.

No tokens move. amount stays in pool.totalCollateral; the reserve is a bookkeeping withholding applied only where vesting-adjusted prices are computed — computeVirtualSharePrice, poolSharePrice, and the anchor (burn) leg of poolMintAndAnchorPrices. The mint leg and every other read are undeducted. The withholding decays linearly to zero by dripEnd.

Indexers that reconstruct pool NAV from pool.totalCollateral must account for this — but in the direction the mechanics imply: during the drip, raw totalCollateral over-reports vesting-adjusted NAV by the outstanding reserve, converging back as it drains. Two further caveats: amount is the clamped booked amount, which can be less than the full crystallization jump when the solvency clamp binds (see SettlementVestingClipped); and dripEnd can be superseded — a later crystallization re-bases the schedule, so track the most recent *Vested event per market, not the first.

LiquidationValueVested

The liquidation-side twin of EarlyExitValueVested, with the same no-tokens-move mechanics and the same NAV caveat. A buyer-side liquidation force-settles the swap and realizes the full buyer collateral into the pool, beyond the swap's fair mark; amount is that excess as actually booked (the same solvency clamp applies). dripEnd is the swap's original expiry, or the later horizon when the new reserve blends with a still-live one.

Both events fire from the same site in PoolInternalsLib and are mutually exclusive per closure — but do not rely on one of them always firing. When the solvency clamp fully clips the jump, neither fires and only SettlementVestingClipped is emitted. To attribute a reserve credit to early exit vs. liquidation in all cases, use whichever *Vested event fires, falling back to SettlementVestingClipped.isLiquidation when neither does.

SettlementVestingClipped

Emitted alongside — or instead of — the *Vested events when the vesting reserve's solvency clamp withheld less than the full crystallization jump: the pool was underwater relative to the live reserve, so only booked entered the reserve and clipped (the shortfall) stayed in pool NAV. No tokens move; the event is observational only. isLiquidation distinguishes the liquidation path (true) from early exit (false).

When the jump is fully clipped (booked == 0), this is the sole event for the closure — no EarlyExitValueVested or LiquidationValueVested fires. An indexer tracking the vesting reserve should treat the three events as one family: booked from a *Vested event enters the reserve; clipped here never does.


Swap lifecycle

SwapCreated

Emitted by buySwap. The indexer reconstructs a position from this event alone — every field that makePayment will need later is included. swapRate is the all-in rate locked by the buyer; baseRate, utilFee, and riskPremium are broken out for analytics.

SwapClosed

This declaration has 16 arguments — liquidatorReward was appended, which changed the event's topic[0]. An integration that computed its filter from the earlier 15-argument declaration matches nothing on current deployments. Recompute the topic hash from the declaration above and re-check any hardcoded signature constants.

Emitted once per swap from every closure path: makePayment, exitSwapEarly, and liquidateSwap. Branch on closureType to distinguish.

For normal expiry and early exit, floatingPayment / fixedPayment carry the absolute leg amounts over the settled period and floatingRate is the realized annualized floating rate that drove them. For liquidations, floatingPayment, fixedPayment, and earlyExitFee are all 0 (not computed — liquidation uses an accrual model rather than full settlement math), but floatingRate is populated: it's the rate that drove the Utils.calculateLiquidationObligation decision, reused here rather than recomputed.

netSettlement is the signed realized net transfer from the buyer's perspective — positive when the buyer received, negative when the buyer paid, with magnitude equal to amountSettled. It encodes direction and magnitude in one field, so an indexer can use netSettlement alone instead of combining amountSettled with netRecipient.

liquidatorReward is the amount actually transferred to the liquidator, in swap token decimals. It is 0 for every non-liquidation closure — at normal expiry the prefunded bounty is returned to the buyer, not paid out. On a buyer-side liquidation it is the prefunded liquidationBounty (funded by the buyer at entry and never part of amountSettled). On a pool-side liquidation it is the incentive carved out of the gross LP backing, i.e. lpCollateralReleased - amountSettled — so the pool's gross outflow is amountSettled + liquidatorReward.

SwapLiquidated

Emitted by liquidateSwap in addition to SwapClosed. Use this when you need the liquidator identity or the boolean flags distinguishing a buyer-side vs. pool-side liquidation. liquidatedParty is the buyer's address (swap.userAddress) on a buyer-side liquidation, and address(0) on a pool-side liquidation — the liquidated party is the LP pool, which has no single address, so the field is left zero and you read poolLiquidated == true instead. Exactly one of buyerLiquidated / poolLiquidated is true in a real liquidation; both being false is a defensive path that shouldn't occur in production.

SwapTransferred

Emitted by transferSwapPosition. The caller field captures whether the transfer was done directly (caller == previousOwner) or via a delegate (caller != previousOwner, typically the wrapper / bundler).

PoolCollateralZeroed

Emitted when settlement or a pool-side liquidation drives the LP pool's collateral to zero (the pool lost its entire position). Rare — the last-LP guard and bucket accounting usually prevent this from happening except in extreme-loss scenarios. Monitor this event for risk dashboards; it's the canonical "pool wiped" signal.


Settlement escrow

BuyerTransferFailed

Emitted inside Utils._transferOrEscrow (declared in SwapEvents and mirrored in Utils). Because Utils is a Solidity library, the emit happens in SwapCore's execution context and indexers should subscribe to the SwapCore address.

Fires when SwapCore couldn't deliver amount of swapToken to recipient during _settleSwaps or liquidateSwap (typically a blacklisted recipient). The amount is then held in escrowedCollateral[swapId] for later retrieval via claimEscrow. Pair with EscrowClaimed for the full failed-then-recovered cycle.

EscrowClaimed

Emitted by claimEscrow. recipient is always the original swap.userAddress regardless of who called claimEscrow — the caller field captures the delegate (or the owner themselves).


Fees

ProtocolFeeCollected

Emitted by buySwap when protocolFee > 0 and the protocol's share was successfully delivered to the multisig.

feeAmount is the multisig's portion net of any creator share already paid out — not the gross fee charged to the buyer. Utils.distributeProtocolFee transfers the creator's cut first, subtracts it, and emits what remains. So:

Add the two events; do not subtract one from the other. Treating ProtocolFeeCollected.feeAmount as the gross and deducting the creator share double-counts the split and under-reports the multisig take on every market that has a creator fee configured. When no creator fee applies (or the transfer to the market owner failed), CreatorFeeCollected is absent and feeAmount happens to equal the gross — which is why this error can hide until the first creator-fee market goes live.

If the multisig can't receive the fee, SwapCore emits ProtocolFeeForegone instead of ProtocolFeeCollected (even though protocolFee > 0) and refunds the payer, so don't treat the absence of ProtocolFeeCollected on a swap as "no fee was charged."

ProtocolFeeForegone

Emitted from a single path: Utils.distributeProtocolFee on buySwap, in place of ProtocolFeeCollected, when the protocol fee was charged but could not be delivered to the multisig (e.g. the multisig is a contract that reverts on receive, or a blacklisted recipient). The amount was refunded to payer (msg.sender) rather than collected. A market-creation fee that fails delivery emits MarketCreationFeeForegone instead — this event never fires from createMarket. For accounting, treat a swap with ProtocolFeeForegone as "fee waived — payer made whole," not "fee collected."

CreatorFeeCollected

Emitted by buySwap when a creator fee split is configured (Admin.creatorFeeShare > 0) and the graceful transfer to market.marketOwner succeeded. feeAmount here is the creator's portion only.

The multisig's portion is already reported directly by ProtocolFeeCollected.feeAmount in the same tx — it needs no adjustment, because the creator share was subtracted on-chain before that event was emitted. Sum the two to recover the gross fee the buyer paid.

If the transfer to the market owner failed (rare — happens when the owner is a contract that reverts on receive), this event is not emitted and the full protocol fee is sent to the multisig.


Oracle & index

ExpiryIndexExtrapolated

The dead-oracle settlement signal, and the highest-severity event on this page for a risk dashboard.

Normal expiry resolves the floating index from a real snapshot at or after maturity. If none exists, settlement reverts E450 — but only until expiry + Admin.settlementGracePeriodSec() (default 14d, bounds [7d, 90d], timelocked). Past that, the failed index update is retried once with a guaranteed gas budget (E451 if the caller can't fund it, so gas starvation can never force this path). Only if that retry also fails — a genuinely dead, invalid, or garbage oracle — does settlement proceed against an extrapolation of the frozen snapshot history, and this event fires.

expiryIndex was not observed. It is projected from the trailing slope behind lastUpdate, so the settlement price is a deterministic estimate, not market data. expiryTime - lastUpdate is the size of the gap being papered over — surface it, because it is the direct measure of how far the estimate reaches.

This event is always accompanied by market termination. The same code path sets terminated = true on the settling market and on its paired twin (the twin prices new swaps off the same scarred index history), emitting MarketAdminChange(marketId, "terminateMarket", true) for each. So a single ExpiryIndexExtrapolated implies up to two MarketAdminChange logs in the same tx, and the market pair is closed to new swaps from that point on. Alert on this event directly rather than inferring it from the terminations.

IndexClamped

Emitted from RateIndexLib.update when a Cumulative-convention oracle reports an index below its last recorded value. The Cumulative convention promises a monotonic non-decreasing index, so the reading is clamped up to floor and settlement proceeds on the clamped value — this prevents a phantom negative floating rate, but it deliberately masks a genuine decrease.

That masking is the reason to monitor it. A recurring IndexClamped on the same oracle means the feed is not behaving as a cumulative index and the market pair on it is accruing float against a frozen floor — the protocol keeps working while the underlying signal is wrong. reported vs. floor gives the size of the suppressed decrease.

Because indices are global per oracle, not per market, one clamp affects every market sharing that oracle. Key any alerting on the oracle address and fan out to the affected markets, not the reverse.

Note that the oracle contracts IndexBaseRateV1 and MorphoBaseRateV1 declare their own two-argument IndexClamped(uint256,uint256). That is a different event with a different signature, emitted from the oracle's own address rather than SwapCore's — deliberately named to mirror this one so monitoring is symmetric across the stack. Don't mix the two in one filter.


Authorization

AuthorizationSet

Emitted by setAuthorization. authorizer is the account whose authorization list changed; authorized is the delegate; isAuthorized is the new state.

Last updated