Skip to main content
See /concepts/lifecycle for the end-to-end flow (policy → trigger → bond → wait/sell decision).
A trigger is the on-chain proof that a policy’s parametric condition fired. In V5.4 the source of truth is Chainlink BTC/USD + ETH/USD on Base mainnet; the shields require 3 confirmations 60 seconds apart and enforce a time window and a drop calculation vs the strike snapshotted at purchase.

The trigger condition

For every active flash shield, the drop is measured against the strike the policy stored at purchase:
drop_bps = (strike - currentPrice) / strike  × 10_000
fired    = drop_bps >= TRIGGER_DROP_BPS AND
           block.timestamp <= policy.startedAt + durationSeconds
Per-product TRIGGER_DROP_BPS:
ProductTRIGGER_DROP_BPSWindow
Flash BTC 1h250 (2.5%)1 h
Flash BTC 24h600 (6%)24 h
Flash BTC 48h1000 (10%)48 h
Flash ETH 1h400 (4%)1 h
Flash ETH 24h850 (8.5%)24 h
Flash ETH 48h1400 (14%)48 h
If the cover window closes before a valid trigger is accepted, the policy expires — no refund.

Oracle: 3 confirmations 60s apart

LuminaOracleV2 reads the Chainlink feed three times, at least 60 seconds between reads, and only signs a trigger payload if all three reads agree the threshold has been crossed. This filters out single-block spikes (printer errors, sandwich attacks against the feed, momentary flash-loan oracle distortions). The oracle also checks the Base L2 sequencer uptime feed — if the sequencer is down or in the grace period, no trigger is signed. Users can’t react during sequencer outages, so triggering during one would be unfair.

Sequence diagram

What’s signed (EIP-712)

struct PriceProof {
  bytes32 asset;            // keccak256("BTC") or keccak256("ETH")
  uint256 strike;           // policy.strike (18 decimals)
  uint256 price;            // observed price (18 decimals)
  uint256 timestamp;        // unix seconds; ≤ MAX_PROOF_AGE old
  uint256 nonce;            // per-asset replay counter
  uint256 policyId;         // the policy this proof targets
}
The shield verifies:
  1. The signature recovers to LuminaOracleV2.oracleKey() (the only allowed signer).
  2. block.timestamp - payload.timestamp ≤ MAX_PROOF_AGE (24 hours, audit fix M-8).
  3. The nonce is strictly greater than the last accepted nonce for the asset (replay protection).
  4. strike == policy.strike (no oracle drift between purchase and trigger).
  5. (strike - price) / strike ≥ TRIGGER_DROP_BPS (drop condition).
  6. block.timestamp ≤ policy.startedAt + durationSeconds (window).

Who can submit a trigger

Anyone — the signature is what gives the trigger weight, not the submitter. In practice:
  • ShieldKeeper via Chainlink Automation. A registered keeper that scans active policies, fetches signed payloads from the oracle, and submits triggers in batches. Hard cap: 10 policies per upkeep call (gas guardrail; large epochs are paginated across multiple upkeeps).
  • Direct agent submission. An agent watching the feed can submit their own trigger if they prefer.
  • API convenience. POST /api/v1/policies/{id}/trigger wraps the oracle request + submission.
Two reasons:
  1. Aggregation. The “3 confirmations 60s apart” rule needs three reads spaced in time; doing that purely in the shield contract would require keeping state for partial reads across blocks. Cleaner to aggregate off-chain and sign the result.
  2. Cost. Pulling a Chainlink price into shield storage on every trigger would inflate gas. EIP-712 proofs are ~200 bytes of calldata.
The oracle still uses Chainlink BTC/USD + ETH/USD on Base mainnet as its upstream feeds — agents just see a signed proof of the already-aggregated value.

See also

  • Shields — per-product drop thresholds + addresses.
  • Adapters — why submitTrigger goes through the adapter first.