81 Commits

Author SHA1 Message Date
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
Andrea Franz
222c01e7d6 docs(stablecoin): remove minimum time between fee acrruals 2026-06-19 10:54:43 +02:00
Andrea Franz
5298c43dad docs(stablecoin): add refresh_globals and use milliseconds everywhere 2026-06-19 10:54:43 +02:00
Andrea Franz
b6c53e096d docs(stablecoin): add more examples and debt accrual terminology 2026-06-19 10:54:43 +02:00
Andrea Franz
78f61f43e2 docs(stablecoin): fix rate adjustment math 2026-06-19 10:54:43 +02:00
Andrea Franz
b3369d2a3d docs(stablecoin): improve after comments 2026-06-19 10:54:43 +02:00
Andrea Franz
63311157c8 docs(stablecoin): fix sections references 2026-06-19 10:54:43 +02:00
Andrea Franz
7c62668731 docs(stablecoin): add stablecoin design docs 2026-06-19 10:54:43 +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
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
r4bbit
7b1696f98e docs: update README and CLAUDE.md to reflect current state
- Rename amm-ui to amm in the Apps table
- Add "Running Apps" section with Nix flake setup and amm example
- Remove hardcoded Rust toolchain version; point to rust-toolchain.toml
- Replace raw cargo/clippy invocations with make targets (clippy, test, fmt, idl)
- Expand program listings in overview and workspace structure to cover
  all five programs: token, amm, ata, stablecoin, twap_oracle
- Add integration_tests and apps/amm to the workspace structure diagram
2026-05-26 16:42:18 +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
Andrea Franz
cdb53a4d0c feat(stablecoin): implement repay_debt (#93) 2026-05-26 10:31:52 +02:00
Andrea Franz
1ae2b325ff test(stablecoin): move chained-transfer coverage to integration tests 2026-05-26 10:31:52 +02:00
Andrea Franz
7da110a616 fix(stablecoin): leave position_post.program_owner default so the runtime sets it through Claim::Pda 2026-05-26 10:31:52 +02:00
Ricardo Guilherme Schmidt
a0a1e08dfb test(stablecoin): cover invalid withdraw transfer pre-states 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
r4bbit
f4f61be322 chore(Makefile): add idl command to Makefile 2026-05-25 20:57:59 +02:00
Ricardo Guilherme Schmidt
291149b114 chore(twap_oracle): scaffold twap_oracle program
Closes #114
2026-05-25 20:04:33 +02:00
copilot-swe-agent[bot]
9efef64b54 Fix CI regex to use PCRE2 mode for non-greedy quantifier support
Agent-Logs-Url: https://github.com/logos-blockchain/lez-programs/sessions/65c1e83e-b757-457f-bd78-2073df6b2c1a

Co-authored-by: 0x-r4bbit <445106+0x-r4bbit@users.noreply.github.com>
2026-05-22 09:46:53 +02:00
r4bbit
cfa4bb1e36 chore: include stablecoin program in clippy task 2026-05-22 09:46:53 +02:00
r4bbit
20a947137c refactor: rename rust-toolchain file
Fixes #13
2026-05-22 09:46:53 +02:00
r4bbit
6fd87766c2 chore: adjust linting rules and introduce Makefile
This relaxes some of the rules introduced in the previous commit.
To simplify testing, building and formatting locally and on CI, we also
introduce a dedicated makefile
2026-05-22 09:46:53 +02:00
Ricardo Guilherme Schmidt
49d7f91ee5 chore(lint): add staged lint baseline 2026-05-22 09:46:53 +02:00
r4bbit
035f593f5e chore: update spel to v0.3.0 2026-05-19 08:40:35 +02:00
Ricardo Guilherme Schmidt
5229855d57 fix(ata)!: namespace accounts by token program
ATA accounts are now namespaced by token program, so callers must
explicitly pass the token_program_id when invoking ATA::Transfer.

BREAKING CHANGE: `Instruction::Transfer`, `Instruction::Burn`, `Instruction::Create` now requires a
`token_program_id` field. Any existing call site that omits it will
fail to compile.

Closes #83
2026-05-18 12:53:07 +02:00
r4bbit
29b4c01739 fix(integration_tests) remove no longer needed program ID 2026-05-18 12:22:06 +02:00
Ricardo Guilherme Schmidt
f8cbcc6956 fix(ata): lock down ATA::Transfer recipient contract
Enforce at the ATA layer that the recipient token holding is already
initialized, owned by the same token program as the sender ATA, decodes
to a valid `TokenHolding`, and points at the same token definition as
the sender. Align the core instruction doc and guest wrapper doc with
that contract, and cover the boundary with unit tests (default,
foreign-owned, malformed, mismatched-definition recipients, plus the
missing-owner-auth and happy paths) and end-to-end integration tests
(default and mismatched-definition recipients).

Without this, the downstream `token::Transfer` default-recipient
`Claim::Authorized` path was reachable through ATA, so integrators had
to reverse-engineer recipient semantics from token/runtime internals.
2026-05-13 13:32:09 +02:00
Ricardo Guilherme Schmidt
0b078b2dde fix(stablecoin): address open position review feedback 2026-05-13 12:23:20 +02:00
Ricardo Guilherme Schmidt
d6082d0c81 docs(stablecoin): improve documentation for OpenPosition instruction 2026-05-13 12:23:20 +02:00
Ricardo Guilherme Schmidt
e63d09f793 docs(stablecoin): list all five OpenPosition accounts and qualify size_of_val
The `Instruction::OpenPosition` doc claimed four required accounts but the
handler and IDL take five — the collateral token definition was missing.
Update the list to match the actual contract.

Also fully qualify `std::mem::size_of_val` in `From<&Position> for Data`
so the call no longer relies on Rust 1.80+ prelude additions for the
2021 edition.
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
22b41bdb3d chore(amm-ui): reorganize liquidity page components 2026-05-12 16:11:24 +02:00
Andrea Franz
9375129c9e chore(amm-ui): add layout and reorganize swap UI files 2026-05-12 16:11:24 +02:00
Ricardo Guilherme Schmidt
8005c74e26 feat(token): verify definition ownership via self_program_id in initialize and mint
Pass `ctx.self_program_id` from `ProgramContext` into `initialize_account`
and `mint`, which now assert that the token definition account is owned by
the token program. This prevents callers from supplying a foreign-owned
account as the definition.

See https://github.com/logos-co/spel/issues/172
2026-05-12 16:10:40 +02:00
r4bbit
ceb8a4b597 chore: update spel
This updates the spel dependency, which introduces a breaking change.
To make reviewing changes easier from other changes, this update comes
in a separate commit.
2026-05-12 11:47:35 +02:00
r4bbit
f4a0aaf8d0 feat: make use of spel's [#account_type] directive
This annotates custom account data as [#account_type], to allow for deserializing abritrary LEZ account data.

Closes #49
2026-05-12 10:04:22 +02:00
r4bbit
e7a69f619f chore: update LEZ to v0.2.0-rc3 2026-05-12 08:46:36 +02:00
Andrea Franz
4178406fda feat(stablecoin): initial stablecoin scaffold
closes #86
2026-05-11 16:53:04 +02:00
Andrea Franz
6eed55d7e4 chore(amm-ui): add SlippageToleranceControl 2026-05-11 10:15:52 +02:00
Andrea Franz
476087a36b chore(amm-ui): swap form activates exact input or output based on the fields updated
closes #56
2026-05-11 10:15:52 +02:00
Andrea Franz
3df3c3d7c4 feat(amm/ui): show swap summary under the swap card 2026-05-11 10:15:52 +02:00
r4bbit
c8a192e377 chore(amm): validate fee tier in sync_reserves
All other entry functions validate the pools fee tier, except for this
function. This is likely because it doesn't make use of the fees.

To make the code consistent (and auditing easier), we're now validating
the fees in `sync_reserves` the same way.
2026-05-08 12:58:01 +02:00
r4bbit
0d532a8fd3 chore(amm): add defensive check for lp token solvency
This check is added to fulfill the program invariant that no more tokens
than owned can be burned. This was not a bug before, because the `token`
program will revert on `Transfer::Burn` when one tries to burn more
tokens than available.

So this change is merely for making the invariant explicit.
2026-05-08 12:43:22 +02:00