lez-programs/artifacts/twap_oracle-idl.json
r4bbit c528d85a2b feat(twap-oracle): implement PublishPrice with tick-to-price conversion and tail extrapolation
Add PublishPrice — a permissionless instruction that computes the TWAP over a
PriceObservations buffer, extrapolated to the current time, and writes it to the
consumer-facing OraclePriceAccount.

The stored body averages [t1, t2] (t1 = oldest valid entry, t2 = most recent),
needing no boundary search since each buffer is calibrated to one window_duration.
The final segment from t2 to `now` is extrapolated from the live tick in the
CurrentTickAccount (added as a fourth account), mirroring Uniswap's
OracleLibrary.consult. This keeps the published timestamp = now truthful: an
unchanged price yields a fresh stamp and the correct value, and a republish picks
up a since-reported move instead of freezing the pre-move average.

The live tick is only credited since it was written, so the tail is split at the
current tick's last_updated:

    boundary     = clamp(current_tick.last_updated, t2.ts, now)
    clamped_tick = last_recorded_tick + clamp(current_tick - last_recorded_tick, ±MAX_TICK_DELTA)
    cum_now      = t2.tick_cumulative
                 + last_recorded_tick * (boundary - t2.ts)   // before the live tick took effect
                 + clamped_tick       * (now - boundary)      // live tick, only since last_updated
    twap_tick    = (cum_now - t1.tick_cumulative) / (now - t1.ts)   // floor (div_euclid)

Splitting at last_updated stops a tick written moments before publish from being
smeared across a stale gap and inflating a supposedly fresh TWAP. The live-tick
segment is clamped against last_recorded_tick by MAX_TICK_DELTA — the same bound
RecordTick applies — capping how far a current-tick move can shift the result. A
zero-length tail (now == t2.ts) leaves the pure stored-window average.

If fewer than two observations exist the call is a silent no-op, leaving the price
account at timestamp = 0 (the uninitialized signal consumers reject). While young,
the TWAP covers the available span, which may be shorter than the window.

The TWAP tick is converted to a price ratio via the Uniswap v3 sqrtPriceX96
representation (pure integer, zkVM-safe), stored as a Q64.64 in
OraclePriceAccount.price — source-agnostic, no tick framing leaks into the standard.
Out-of-range ticks clamp; ratios above 2^64 saturate at u128::MAX. Adds
PRICE_FRACTIONAL_BITS = 64; removes the placeholder TWAP_PRICE_BIAS encoding.

Closes #117
2026-06-23 16:12:12 +02:00

310 lines
6.1 KiB
JSON

{
"version": "0.1.0",
"name": "twap_oracle",
"instructions": [
{
"name": "create_price_observations",
"accounts": [
{
"name": "price_observations",
"writable": false,
"signer": false,
"init": false
},
{
"name": "price_source",
"writable": false,
"signer": false,
"init": false
},
{
"name": "clock",
"writable": false,
"signer": false,
"init": false
}
],
"args": [
{
"name": "initial_tick",
"type": "i32"
},
{
"name": "window_duration",
"type": "u64"
}
]
},
{
"name": "create_oracle_price_account",
"accounts": [
{
"name": "oracle_price_account",
"writable": false,
"signer": false,
"init": false
},
{
"name": "price_source",
"writable": false,
"signer": false,
"init": false
},
{
"name": "clock",
"writable": false,
"signer": false,
"init": false
}
],
"args": [
{
"name": "base_asset",
"type": "account_id"
},
{
"name": "quote_asset",
"type": "account_id"
},
{
"name": "initial_price",
"type": "u128"
},
{
"name": "window_duration",
"type": "u64"
}
]
},
{
"name": "create_current_tick_account",
"accounts": [
{
"name": "current_tick_account",
"writable": false,
"signer": false,
"init": false
},
{
"name": "price_source",
"writable": false,
"signer": false,
"init": false
},
{
"name": "clock",
"writable": false,
"signer": false,
"init": false
}
],
"args": [
{
"name": "initial_price",
"type": "u128"
}
]
},
{
"name": "publish_price",
"accounts": [
{
"name": "price_observations",
"writable": false,
"signer": false,
"init": false
},
{
"name": "oracle_price_account",
"writable": false,
"signer": false,
"init": false
},
{
"name": "current_tick_account",
"writable": false,
"signer": false,
"init": false
},
{
"name": "clock",
"writable": false,
"signer": false,
"init": false
}
],
"args": [
{
"name": "price_source_id",
"type": "account_id"
},
{
"name": "window_duration",
"type": "u64"
}
]
},
{
"name": "record_tick",
"accounts": [
{
"name": "price_observations",
"writable": false,
"signer": false,
"init": false
},
{
"name": "current_tick_account",
"writable": false,
"signer": false,
"init": false
},
{
"name": "clock",
"writable": false,
"signer": false,
"init": false
}
],
"args": [
{
"name": "price_source_id",
"type": "account_id"
},
{
"name": "window_duration",
"type": "u64"
}
]
},
{
"name": "update_current_tick",
"accounts": [
{
"name": "current_tick_account",
"writable": false,
"signer": false,
"init": false
},
{
"name": "price_source",
"writable": false,
"signer": false,
"init": false
},
{
"name": "clock",
"writable": false,
"signer": false,
"init": false
}
],
"args": [
{
"name": "price",
"type": "u128"
}
]
}
],
"accounts": [
{
"name": "PriceObservations",
"type": {
"kind": "struct",
"fields": [
{
"name": "price_source_id",
"type": "account_id"
},
{
"name": "write_index",
"type": "u32"
},
{
"name": "total_entries",
"type": "u64"
},
{
"name": "last_recorded_tick",
"type": "i32"
},
{
"name": "entries",
"type": {
"vec": {
"defined": "ObservationEntry"
}
}
}
]
}
},
{
"name": "OraclePriceAccount",
"type": {
"kind": "struct",
"fields": [
{
"name": "base_asset",
"type": "account_id"
},
{
"name": "quote_asset",
"type": "account_id"
},
{
"name": "price",
"type": "u128"
},
{
"name": "timestamp",
"type": "u64"
},
{
"name": "source_id",
"type": "account_id"
},
{
"name": "confidence_interval",
"type": "u128"
}
]
}
},
{
"name": "CurrentTickAccount",
"type": {
"kind": "struct",
"fields": [
{
"name": "tick",
"type": "i32"
},
{
"name": "last_updated",
"type": "u64"
}
]
}
}
],
"types": [
{
"name": "ObservationEntry",
"kind": "struct",
"fields": [
{
"name": "timestamp",
"type": "u64"
},
{
"name": "tick_cumulative",
"type": "i64"
}
]
}
],
"instruction_type": "twap_oracle_core::Instruction"
}