11 Commits

Author SHA1 Message Date
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
b0ac30039b fix(idl-gen): sort types array for deterministic output
The IDL `types` array was emitted in HashMap iteration order by
spel-framework-core, which is non-deterministic across processes. Two
independent regenerations of the same source could therefore disagree on
type ordering, producing different bytes.

This is what makes the check-idl CI job flaky: a PR's committed IDL is
generated locally with one ordering, but CI regenerates with a different
ordering and the diff fails — including PRs that were green when posted
then breaking main after merge.

Sort the top-level `types` array by name before serializing so output is
byte-stable regardless of where idl-gen runs. Enable serde_json's
`preserve_order` feature so the Value round-trip preserves struct-field
key order (otherwise all keys would alphabetize and churn every artifact).

Only top-level `types` was unstable; variants and fields already follow
source order. Committed artifacts are unchanged — they happened to already
be in sorted order.
2026-06-16 16:21:07 +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
Andrea Franz
cdb53a4d0c feat(stablecoin): implement repay_debt (#93) 2026-05-26 10:31:52 +02:00
Andrea Franz
eb7f44a98a feat(stablecoin): implement withdraw_collateral
closes #92
2026-05-26 10:31:52 +02:00
Ricardo Guilherme Schmidt
291149b114 chore(twap_oracle): scaffold twap_oracle program
Closes #114
2026-05-25 20:04:33 +02:00
Ricardo Guilherme Schmidt
0b078b2dde fix(stablecoin): address open position review feedback 2026-05-13 12:23:20 +02:00
Ricardo Guilherme Schmidt
f4f7b45bd4 feat(stablecoin): implement open_position
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.

- Add `Position` struct, `OpenPosition` instruction variant, and
  `compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
  in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
  `new_definition` patterns: authorization and uninitialized-state asserts, PDA
  verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
  IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
  non-collision in 11 new unit tests.
2026-05-13 12:23:20 +02:00
Andrea Franz
4178406fda feat(stablecoin): initial stablecoin scaffold
closes #86
2026-05-11 16:53:04 +02:00