44 Commits

Author SHA1 Message Date
bristinWild
aee5372f2a fix(lp-0013): address review feedback — docs alignment and missing rotation test
- Add integration test `token_rotate_authority_then_new_authority_can_mint`:
  create with self-authority, rotate to external key, verify new authority
  mints as rest account, verify old authority is rejected (RFP-001 end-to-end)
- Fix README error table: 'must sign' -> 'must authorize' (matches mint.rs:36)
- Fix guest doc comments for mint/set_authority to describe the 0-or-1
  external authority model correctly
- Fix example scripts: new-fungible-definition-with-authority -> new-fungible-definition,
  --initial-supply -> --total-supply (align to token-idl.json and demo-full-flow.sh)
2026-07-02 01:20:33 +05:30
bristinWild
686a7d066a fix(token): owner-guard set_authority; align demo script to token IDL
Second review round on PR #125 (LP-0013):

- set_authority now rejects foreign-owned definitions. It takes the
  ProgramContext and asserts definition_account.program_owner ==
  self_program_id, matching mint and initialize_account. Without this a
  foreign-owned account with token-shaped data could have its authority
  field rewritten. Added test_set_authority_rejects_foreign_owned_definition.

- demo-full-flow.sh now calls instruction and flag names that exist in
  the regenerated token IDL: new-fungible-definition (was the nonexistent
  new-fungible-definition-with-authority), --total-supply (was
  --initial-supply), and drops --authority-account for the self-authority
  mint/set-authority path (the rest account is --authority-accounts and is
  empty when the definition is its own authority).

- Stripped a trailing-space lint nit in docs/LP-0013-README.md.
2026-07-02 01:20:32 +05:30
bristinWild
160ff8ee4a fix(token): support external mint authority; intentional metadata supply; demo path resolver
Addresses review on PR #125 (LP-0013):

#1 authority transfer now hands control to the new signer. mint and
set_authority take a trailing authority_accounts (Vec<AccountWithMetadata>):
empty preserves the original self/PDA-authority behavior (AMM unchanged),
one entry lets an external/rotated authority actually mint or rotate again.
Tests: rotated_authority_can_mint, rotated_authority_old_key_cannot_mint.

#2 metadata-backed fungibles take a real mint_authority instead of a
hardcoded Authority::renounced(), matching the plain-fungible supply model.
Test: test_metadata_fungible_with_authority_is_mintable.

#3 demo-full-flow.sh resolves TOKEN_BIN from the README-documented
cargo risczero build output, falling back to the workspace build, with an
explicit TOKEN_BIN override still respected.

Regenerated token-idl.json for the new trailing authority_accounts.
2026-07-02 01:20:32 +05:30
bristinWild
dd8328cf9f fix: update AMM unit test fixtures for LP token authority
pool_lp_created() and pool_lp_created_after_lock() fixtures now use
Authority::new(token_lp_definition_id) matching the production change
where the pool-definition PDA is the LP token's mint authority.
2026-07-02 01:20:32 +05:30
bristinWild
d5837aa0f6 style: cargo fmt after authority redesign 2026-07-02 01:20:31 +05:30
bristinWild
83df2037ef refactor(authority): embed Authority type in TokenDefinition; fix AMM LP minting
Addresses @0x-r4bbit's review:

- lez-authority now provides an Authority(Option<[u8;32]>) newtype and an
  Ownable trait (require_owner / transfer_ownership / renounce_ownership);
  programs embed the authority slot in their account type instead of calling
  a wrapper. Replaces the old AuthoritySlot.
- TokenDefinition::Fungible embeds authority: Authority; TokenDefinition
  implements Ownable.
- Fold mint authority into NewFungibleDefinition { mint_authority: Option<AccountId> };
  remove the separate NewFungibleDefinitionWithAuthority instruction.
- mint/set_authority authorize against the definition account itself (its id
  must match the stored authority and be authorized in the tx), restoring the
  2-account mint shape and supporting PDA authorities.
- Fix AMM: the pool-definition PDA is now the LP token's mint authority, so the
  AMM mints LP at creation and on add-liquidity (was permanently revoked).
- Instruction params use AccountId; remove LP-0013-specific comments.
- Regenerate token/amm/ata/stablecoin IDLs.

Tests: lez-authority 8, token unit 56, token/amm/stablecoin/ata integration all
green under RISC0_DEV_MODE=1; fmt + clippy clean.
2026-07-02 01:18:39 +05:30
bristinWild
c2a7d753d7 fix: address Copilot review round 2
- set_authority rejects all-zero new_authority on rotation (matches creation guard)
- SetAuthority/Mint doc comments now list the required authority signer account
- README: add --authority-account to mint/set-authority CLI examples,
  correct error-code table to actual panic strings, make program ID build-dependent
2026-07-02 01:17:06 +05:30
bristinWild
1c41d19a51 feat: reject all-zero mint authority; restore 7-step demo-full-flow.sh
- new_fungible_definition_with_authority rejects all-zero mint_authority (RFP-001 reliability)
- add test_new_fungible_definition_with_authority_rejects_zero_authority
- restore demo-full-flow.sh (had been overwritten with example content); now
  uses the correct account parsing, base58->hex authority, and --authority-account flag
- commit updated Cargo.lock files for the lez-authority dependency
2026-07-02 01:16:49 +05:30
bristinWild
175c9d256c refactor: gate mint/set_authority via lez-authority with explicit signer account 2026-07-02 01:15:03 +05:30
bristinWild
029f617737 style: fix rustfmt trailing newline and replace unwrap with expect for clippy 2026-07-02 01:11:53 +05:30
bristinWild
16738c7def fix: enforce mint authority key validation in mint and set_authority
- mint.rs: validate caller account_id matches stored mint_authority key
- set_authority.rs: validate caller matches mint_authority before rotation/revoke
- tests.rs: align AUTHORITY constant and fixtures to account_id [15; 32]
- integration_tests/token.rs: derive authority_key from Ids::token_definition()
  so stored key matches actual signer account ID; update all affected asserts
- demo-full-flow.sh: fix --public flag, remove || true from spel commands,
  update test count to 60

60 unit tests + 16 integration tests passing (RISC0_DEV_MODE=1)
2026-07-02 01:11:53 +05:30
bristinWild
b561f91db2 fix: enforce mint authority key validation in mint and set_authority
- mint.rs: validate caller account_id matches stored mint_authority key
- set_authority.rs: validate caller matches mint_authority before rotation/revoke
- tests.rs: align AUTHORITY constant and fixtures to match account_id [15; 32]
- demo-full-flow.sh: fix --public flag, remove || true from spel commands,
  update test count to 60
2026-07-02 01:11:53 +05:30
bristinWild
cdf6d8fc54 feat: add E2E integration tests for authority lifecycle 2026-07-02 01:11:53 +05:30
bristinWild
0ae30c98ae fix: update all programs for mint_authority field, regenerate token IDL 2026-07-02 01:11:53 +05:30
bristinWild
cc70332293 fix: add mint_authority to amm and fix nightly fmt 2026-07-02 01:11:53 +05:30
bristinWild
a8863ab7ea feat(LP-0013): add mint authority model to token program
- Add lez-authority crate: agnostic AuthoritySlot library (RFP-001)
- Add mint_authority field to TokenDefinition::Fungible
- Add NewFungibleDefinitionWithAuthority instruction
- Add SetAuthority instruction (rotation + permanent revocation)
- Update Mint to enforce authority guard
- Wire new instructions into guest binary
- Add 8 authority unit tests (53 total passing)
- Add LP-0013 README, IDL, demo script, and example scripts
2026-07-02 01:11:53 +05:30
r4bbit
0a120bd42c docs: add a testnet run book to show how to deploy and use the programs 2026-06-30 15:13:47 +02:00
r4bbit
0fa2b49880 chore: add helper examples to calculate program PDAs
These is needed because SPEL currently doesn't support PDA calculation
the way our programs do (we wrap our seeds in SHA256)
2026-06-30 15:13:47 +02:00
r4bbit
2308681dcf fix(amm): compute pool arithmetic in u256 to avoid u128 overflow
The AMM multiplied amounts in u128 — `token_a * token_b` for the initial
LP in `new_definition`, `reserve * amount` in swaps, and the mul/div steps
in add/remove liquidity. For realistic 18-decimal token amounts the
intermediate product exceeds `u128::MAX` (~3.4e38): opening a pool with
100/200 tokens is `1e20 * 2e20 = 2e40`, which panicked and caused the
sequencer to skip the transaction.

Widen the intermediate arithmetic, not the stored types. Add
`mul_div_floor`, `mul_div_ceil`, and `isqrt_product` to `amm_core` (using
`alloy_primitives::U256`, as `spot_price_q64_64` already does): they
compute the product/division/sqrt in U256 and downcast the result back to
u128. Route `new_definition`, `swap_exact_input`/`swap_exact_output`,
`add_liquidity`, and `remove_liquidity` through them. `swap_exact_output`
keeps its ceil rounding (required input rounded up, in the pool's favour)
via `mul_div_ceil`.

Balances, reserves, and LP supply stay u128, so account data formats,
IDLs, and the token/ata/stablecoin programs are unchanged. This lifts the
usable amount range to the full u128.
2026-06-30 15:13:47 +02:00
r4bbit
091ea5a5d0 chore: update to LEZ v0.2.0-rc6
Bump the LEZ dependency from the `lez-core-v0.2.0` tag to `v0.2.0-rc6` across
the workspace and all guest manifests (still resolving via the renamed
`lee_core`/`lee` packages), and regenerate the lockfiles to match.

rc6 moved the clock program out of `nssa` into a separate system-programs crate
(gated behind the guest-building `artifacts` feature), so adapt the tests:

- Import `ClockAccountData` and `CLOCK_01_PROGRAM_ACCOUNT_ID` from `clock_core`
  instead of `nssa`, and build clock data via `ClockAccountData::to_bytes()`
  rather than hand-encoding the Borsh layout.
- `V03State::new()` no longer auto-creates the clock account, so AMM tests seed
  the canonical 1-block clock explicitly before ops that read it.
- `advance_clock` now writes the clock account directly via
  `force_insert_account` (the clock can no longer be ticked with a real
  transaction), matching how upstream rc6 state-machine tests seed accounts.
- Add the `clock_core` dependency to integration_tests/benchmark.
2026-06-30 15:13:47 +02:00
r4bbit
c42d4b6c07 refactor: migrate programs to LEZ lez-core-v0.2.0
Bump the LEZ dependency from the `v0.2.0-rc3` tags to the released
`lez-core-v0.2.0` tag across the workspace and all guest manifests. The crate
was renamed upstream, so `nssa_core`/`nssa` now resolve via the `lee_core`/`lee`
packages, and spel-framework points at the `refactor/lez-v020-compat` fork
branch for compatibility.

Adapt the integration tests to the new API surface:

- `NssaError` is now `LeeError` (error variants unchanged).
- Account inputs move from numeric mask vectors (`vec![2, 0, 0]`) to typed
  `InputAccountIdentity` values (e.g. `PrivateUnauthorized { epk, view_tag,
  npk, ssk, identifier }`).
- `ViewingPublicKey::from_scalar` → `from_seed(d, z)`; `AccountId::from(&npk)`
  → `AccountId::for_regular_private_account(&npk, 0)`; ephemeral-key/shared-
  secret setup → `SharedSecretKey::encapsulate_deterministic(...)` with the
  circuit filling the EPK.

Regenerate all guest Cargo.lock files and the workspace lockfile to match.
2026-06-30 15:13:47 +02:00
r4bbit
c9fbb626ea refactor(twap_oracle): match instruction function order to Instruction enum
`idl-gen` emits IDL instructions in source order, and `spel` uses each
instruction's IDL position as its serde variant index. When the
`#[instruction]` function order diverges from the `twap_oracle_core::Instruction`
enum order, spel addresses the wrong instruction.

Move `update_current_tick` ahead of the TWAP-computation instruction so the
function order in twap_oracle.rs lines up with the enum variant order, and
regenerate artifacts/twap_oracle-idl.json to match.

No behavioral change — the instruction bodies are unchanged, only reordered.
2026-06-30 15:13:47 +02:00
r4bbit
c8f061e4a8 fix(amm): require signer on user token holdings in swap and add-liquidity
The swap and add-liquidity instructions debited user-owned token holdings
without requiring those accounts to be signers. Mark them `signer` so a
transaction can't move a user's tokens without their authorization:

- add liquidity: `user_holding_lp` is now `#[account(mut, signer)]`
- swap (both directions): `user_holding_a` and `user_holding_b` are now
  `#[account(mut, signer)]`

Regenerate artifacts/amm-idl.json to reflect the new signer metadata.

Update integration tests accordingly: swaps now sign and supply nonces for
both user holdings (incrementing both nonces), and
`amm_new_definition_precreated_zero_balance_user_lp` becomes
`amm_new_definition_precreated_user_lp_unsigned_fails`, asserting an unsigned
pre-existing LP holding is rejected and the transaction reverts.
2026-06-30 15:13:47 +02:00
Ricardo Guilherme Schmidt
497e13db85 build(guest): strip release symbols
Configure guest release profiles with debug = 0 and strip = "symbols" so deployed RISC Zero artifacts use stripped binaries.

Document that release-profile ImageIDs are canonical for testnet and mainnet deployments and dependent values must be refreshed.
2026-06-28 20:57:02 -03:00
Ricardo Guilherme Schmidt
255f87f38f fix(idl): align LEZ account metadata
Mark unconditional signer, init, and mutable accounts in guest entrypoints.

Regenerate IDL artifacts for token, ATA, stablecoin, TWAP oracle, and AMM.
2026-06-28 15:24:29 +02:00
r4bbit
e4447617f6 chore: pin ruint dep 2026-06-26 14:33:47 -03:00
Andrea Franz
e93db419c4 chore(stablecoin): use alloy primitives 2026-06-25 15:52:03 +02:00
Andrea Franz
3eab96a217 feat(stablecoin): add fixed-point math utilities
closes #157
2026-06-25 15:52:03 +02:00
r4bbit
bd8064a587 test(twap): cover CreateOraclePriceAccount and PublishPrice end-to-end
Add the first zkVM-path coverage of the oracle's price-account output, which
previously existed only as native unit tests:

- amm_twap_create_oracle_price_account: creates the OraclePriceAccount via a
  signing price source and checks the initialized state (price, timestamp,
  source/base/quote, confidence).
- amm_twap_publish_price_publishes_window_average: full pipeline — real swaps +
  RecordTick build the observations, then PublishPrice consumes them. With the
  clock at the newest observation (empty tail) the published price is the
  stored-window average tick converted to a Q64.64 price, stamped with now.
- amm_twap_publish_price_extrapolates_tail_to_now: advances the clock past the
  last record with no new observation; asserts the published timestamp is now
  (a fresh price, not a stale window) and the value reflects the extrapolated
  tail.
- amm_twap_publish_price_noop_with_fewer_than_two_observations: PublishPrice
  leaves the price account untouched when there is nothing to average.
2026-06-24 22:29:02 +02:00
r4bbit
53e563f8e3 feat(amm): create TWAP oracle price account on behalf of the pool
Add a CreateOraclePriceAccount instruction mirroring CreatePriceObservations:
anyone can register the consumer-facing OraclePriceAccount for a pool feed, and
the AMM authorizes the pool as the price source via its pool PDA seed through a
single chained call to the configured TWAP oracle program.
2026-06-24 21:52:59 +02:00
r4bbit
9d5eea2b41 test(twap): cover RecordTick end-to-end and add zkVM cycle benchmark
Add the first end-to-end coverage of the oracle's RecordTick path, which
previously existed only as native unit tests:

- amm_twap_observations_accumulate_across_swaps_and_yield_time_weighted_average:
  drives swaps + RecordTick across simulated time, then checks the cumulative
  accumulator and the consulted time-weighted average.
- amm_twap_record_tick_sampling_guard_skips_calls_below_min_interval: exercises
  the min-interval sampling guard through the real instruction path.

Running RecordTick through the zkVM surfaced that committing the oracle-owned
~100 KiB observations account costs ~50.9M cycles — over the 2^25 (~33.5M)
public-execution limit — so the instruction aborted on chain. Reduce
OBSERVATIONS_CAPACITY 6396 -> 2048 (~16.8M cycles, ~half the limit); window
coverage is unchanged, only sampling resolution.

Add programs/benchmark, a standalone crate (excluded from the workspace so CI
and the Makefile skip it) that runs the guest ELF through the RISC Zero
executor and reports the per-instruction cycle split, reproducing the on-chain
pass/fail at the limit. Its cost-vs-capacity sweep still spans to 6396, guarding
against bumping capacity back into the over-budget range.
2026-06-23 16:30:18 +02:00
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
r4bbit
03345db803 refactor(pda): use descriptive string seeds for PDA derivation
Replace the hardcoded numeric byte-stream seeds ([0; 32], [1; 32], ...)
used for domain separation in PDA derivation with descriptive byte-string
constants, mirroring the AMM config account's existing b"CONFIG" seed.

  amm:         [0; 32] -> b"LIQUIDITY_TOKEN"
               [1; 32] -> b"LP_LOCK_HOLDING"
  stablecoin:  [0; 32] -> b"POSITION"
               [1; 32] -> b"POSITION_VAULT"
  twap_oracle: [2; 32] -> b"PRICE_OBSERVATIONS"
               [3; 32] -> b"ORACLE_PRICE_ACCOUNT"
               [4; 32] -> b"CURRENT_TICK_ACCOUNT"

Since the seeds are now variable-length, each compute_*_pda_seed function
builds its hash input with a Vec and extend_from_slice instead of a
fixed-size buffer with offset writes.

Closes #146
2026-06-22 14:12:13 +02:00
r4bbit
0e2c5f9329 feat(amm): refresh TWAP current tick on every reserve-mutating instruction
Make swap_exact_input, swap_exact_output, add_liquidity, remove_liquidity,
and sync_reserves keep the pool's TWAP current tick in sync with its
reserves. Each now takes the current-tick and clock accounts, reads the
TWAP program ID from the config account, validates the clock account and
the current-tick PDA, and after computing the post-op pool chains an
UpdateCurrentTick to the oracle carrying the post-op spot price, with the
pool passed as the authorized price source via its pool PDA seed.

sync_reserves additionally now takes the config account so it can resolve
the TWAP program ID and gate on initialization, consistent with the other
instructions.

The invariant current_tick == tick(reserves) therefore holds after every
operation. Proportional add/remove preserve the price, so the tick is
unchanged for them, but the refresh still runs and lands on the correct
value.
2026-06-22 12:45:55 +02:00
r4bbit
b997ca678e feat(amm): bootstrap pool TWAP current-tick account at pool creation
Extend new_definition to also create the pool's TWAP current-tick account
via a chained CreateCurrentTickAccount, so a pool and its price feed are
born together. The opening tick is derived on-chain from the pool's own
reserves (reserve_b / reserve_a as Q64.64), not caller-supplied, so it
cannot be forged. The pool is passed in its post-claim state and authorized
as the price source via its pool PDA seed.

Add spot_price_q64_64 to amm_core (not the oracle): the reserves -> price
mapping is the price source's concern; the oracle only converts price to a
tick.
2026-06-22 11:02:22 +02:00
r4bbit
4e4338945d feat(amm): create TWAP price observations on behalf of the pool
Add a `CreatePriceObservations` instruction that registers a TWAP
price-observations account for a pool over a time window, via a chained
call to the configured TWAP oracle program. The pool acts as the price
source: the AMM authorizes it with its pool PDA seed so the oracle ties
the feed to that pool.

The feed's initial tick is read from the pool's authoritative
`CurrentTickAccount` (validated against its pool-derived PDA) rather than
being supplied by the caller, so the feed cannot be seeded at a forged
price — mirroring what `RecordTick` does. The clock is verified to be the
canonical 1-block LEZ clock, and creation is rejected if the observations
account already exists.

To support the chained call, `AmmConfig` and the `Initialize` instruction
are extended with a `twap_oracle_program_id` that the instruction reads.
2026-06-22 09:47:45 +02:00
r4bbit
1d9e3dcb49 feat(amm): add admin authority and UpdateConfig instruction
Add an admin authority to the AMM config so configuration can be changed
after initialization. AmmConfig gains an `authority` field, set by
Initialize, and a new UpdateConfig instruction lets that admin change
config values.

UpdateConfig is access-controlled: the authority account must equal the
stored config.authority and be passed authorized (signed). Both fields are
optional — token_program_id updates the chained-call token program, and
new_authority transfers admin control to a different account. Without this
gate any caller could repoint the AMM at a malicious token program.
2026-06-19 16:09:51 +02:00
r4bbit
3624ea1451 feat(amm): add Initialize instruction with config-gated chained calls
Introduce a singleton AMM configuration account, a PDA derived from the
constant "CONFIG" seed, created once via a new `Initialize` instruction.
The config stores the Token Program ID the AMM issues every chained call
to, replacing the previous behavior of trusting the program owner of a
caller-supplied holding.

The config account's existence is the Program's initialization gate: the
chained-call instructions (new_definition, add_liquidity, remove_liquidity,
swap_exact_input, swap_exact_output) now take the config as their first
account, validate it against `compute_config_pda(self_program_id)`, and
read the Token Program ID from it on demand — rejecting calls until the
Program is initialized. Vaults and user holdings are asserted to match the
configured Token Program. sync_reserves is left ungated, as it cannot act
on a pool that could not have existed before initialization.

- amm_core: AmmConfig type, compute_config_pda/_seed, Initialize variant
- amm: initialize.rs + config threading through chained-call instructions
- guest: initialize instruction; config + self_program_id on gated calls
- tests: config fixtures, init-gate unit tests, end-to-end Initialize VM test
2026-06-18 16:11:28 +02:00
r4bbit
e8fe634a2c feat(twap-oracle): implement RecordTick instruction
Add RecordTick — a permissionless instruction that reads the current tick
from a CurrentTickAccount and advances a PriceObservations ring buffer.

Authorization is implicit: both PDAs are verified against price_source_id,
so the tick can only have been written by whoever controls that price source.
A sampling guard silently no-ops if less than `window_duration /
OBSERVATIONS_CAPACITY`` ms have elapsed, allowing keepers to call blindly on
every block. Tick-delta truncation clamps the per-observation delta to
`MAX_TICK_DELTA (9 116)` before advancing tick_cumulative, with
last_recorded_tick tracking the untruncated position for the next delta.

Also switches ObservationEntry.tick_cumulative to use elapsed milliseconds
rather than seconds.

Closes #116
2026-06-17 14:46:30 +02:00
r4bbit
3285d5787e feat(twap-oracle): implement CreateCurrentTickAccount and UpdateCurrentTick
Add CurrentTickAccount — an oracle-owned PDA (one per price source) that holds
the latest raw tick written by the price source and a timestamp. The price source
calls UpdateCurrentTick after each price-changing operation; anyone can then call
RecordTick (upcoming) to advance the PriceObservations accumulator without
requiring the price source to be present. PDA is derived from price_source_id
only (no window) since a single current tick serves all time windows.

Add price_to_tick(price: u128) -> i32 to twap_oracle_core: isqrt(price << 128)
-> sqrtPriceX96 -> get_tick_at_sqrt_ratio. The sqrtPriceX96 is clamped to
>= MIN_SQRT_RATIO so a zero/dust price maps to MIN_TICK rather than erroring.

Add a pure-integer integer_sqrt(U256) (bit-by-bit, no floating point): ruint's
root is gated behind its std feature and seeds with f64, neither available in
the guest. Uses wrapping_shr for the digit loop (checked_shr rejects the
intended lossy shifts).

Pull in uniswap_v3_math (for get_tick_at_sqrt_ratio) and alloy-primitives
(U256), with ruint pinned to =1.17.0 — 1.18 raised its MSRV to rustc 1.90,
above the risc0 guest toolchain's 1.88.
2026-06-16 16:51:26 +02:00
r4bbit
7461c9552b feat(twap-oracle): implement CreateOraclePriceAccount instruction
Adds the CreateOraclePriceAccount instruction to the TWAP oracle program.
The instruction initialises a canonical OraclePriceAccount PDA for a given
price source and time window, seeding it with a non-zero initial price and
the current block timestamp so the account is immediately valid to consumers.

- PDA mirrors PriceObservations: derived from (oracle_program_id,
  price_source_id, window_duration) with a distinct seed constant, so each
  (source, window) pair maps to a distinct oracle price account that cannot
  collide with its corresponding observations account.

- source_id is not a parameter: it is always set to price_source.account_id.
  Accepting it as a free parameter would allow callers to register a price
  account that claims to represent a source it does not control. Deriving it
  from the authorized price source account closes that vector entirely.

- Authorization follows the same model as CreatePriceObservations:
  is_authorized = true on the price source proves the caller controls it; the
  PDA check ensures the supplied oracle price account address is the one
  derived from that specific source and window.

- The initial timestamp is read from the canonical 1-block LEZ clock
  (CLOCK_01_PROGRAM_ACCOUNT_ID), never from a caller-supplied value. The clock
  account_id is asserted, so a caller cannot substitute an account they
  control to forge the seeding timestamp.

- A zero price or zero timestamp is rejected at creation. Both are the
  "no valid price" sentinel consumers treat as unset, so an account must never
  be created in that state; the instruction asserts a non-zero initial_price
  and a non-zero clock timestamp.

- initial_price is a Q64.64 fixed-point value (real price = initial_price /
  2^64), matching the oracle price representation. The non-zero check rejects
  the sentinel but cannot validate scale — supplying a correctly-scaled value
  is the caller's responsibility.

Closes #129
2026-06-16 09:52:46 +02:00
r4bbit
3ce998c37c fix(twap_oracle): validate clock account
Ensures user controlled clock account is validated against constraints.
2026-06-09 14:41:03 +02:00
r4bbit
fe9d919299 feat(twap-oracle): implement CreatePriceObservations instruction
Adds the CreatePriceObservations instruction to the TWAP oracle program.
The instruction initialises a PriceObservations PDA for a given price
source account and time window, writing the initial tick and timestamp
as the first entry.

Key design decisions:

- Per-window accounts: each (price_source, window_duration) pair maps to
  a distinct PriceObservations PDA. The window duration is baked into the
  PDA seed so a single price source can support multiple TWAP windows
  (24h, 7d, 30d) at independent sampling rates without sharing a buffer.

- window_duration not stored on struct: it is implicit in the PDA address.
  Any reader that located the account already knows the window duration
  used to derive it. Storing it would be redundant.

- Authorization is implicit: the PriceObservations PDA is derived from
  the price source account ID, so is_authorized = true on the price source
  proves the caller controls it without a redundant authority field.

- Impersonation is prevented by the PDA check: passing a controlled price
  source with a victim's observations account ID fails immediately because
  the computed PDA (from the attacker's source) does not match.

Closes #126
2026-06-09 13:23:46 +02:00
r4bbit
3622016e6c refactor: move programs into programs and UIs into apps
This refactors the repository structure as it has grown over time.
2026-05-26 14:05:52 +02:00