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
lez-programs
Essential programs for the Logos Execution Zone (LEZ) — a zkVM-based execution environment built on RISC Zero. Programs run inside the RISC Zero zkVM (riscv32im-risc0-zkvm-elf target) and interact with the LEZ runtime via the nssa_core library.
Programs
| Program | Description |
|---|---|
| token | Fungible and non-fungible token program — create definitions, mint/burn tokens, transfer, initialize accounts, print NFTs |
| amm | Constant-product AMM — add/remove liquidity and swap via chained calls to the token program |
| ata | Associated Token Account program — derives and initializes deterministic token holding accounts for a given owner and token definition |
| stablecoin | Collateral-backed position program — open collateral positions as a foundation for stablecoin debt issuance |
| twap_oracle | TWAP oracle — provides canonical on-chain price accounts consumed by other programs (e.g. stablecoin) |
Apps
| App | Description |
|---|---|
| amm | QML-based UI for interacting with the AMM program |
Running Apps
Apps live under apps/ and are standalone UI applications. Each app has its own README.md with full details.
Apps use Nix flakes. Enable flakes if you haven't already:
mkdir -p ~/.config/nix && echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf
Example (apps/amm)
cd apps/amm
# Run the app
nix run .
# Update pinned dependencies
nix flake update
Prerequisites
-
Rust — install via rustup. The pinned toolchain version is set in
rust-toolchain.toml. -
RISC Zero toolchain — required to build guest ZK binaries:
cargo install cargo-risczero cargo risczero install -
SPEL toolchain — provides
spelandwalletCLI tools. Install from logos-co/spel. -
LEZ — provides
walletCLI. Install from logos-blockchain/logos-execution-zone
Build & Test
# Lint the entire workspace (skips expensive guest ZK builds)
make clippy
# Format check
make fmt
# Run unit tests for all programs (no zkVM, no ZK proof generation)
RISC0_DEV_MODE=1 cargo test -p token_program -p amm_program -p ata_program -p stablecoin_program -p twap_oracle_program
# Run integration tests (dev mode skips ZK proof generation)
RISC0_DEV_MODE=1 cargo test -p integration_tests
# Run all tests
make test
Integration tests live in programs/integration_tests/tests/ and cover token, amm, and ata programs end-to-end through the zkVM using RISC0_DEV_MODE=1 to skip proof generation. Each test file corresponds to a program:
programs/integration_tests/tests/token.rsprograms/integration_tests/tests/amm.rsprograms/integration_tests/tests/ata.rs
stablecoin and twap_oracle are tested via their own unit tests (cargo test -p stablecoin_program -p twap_oracle_program).
Compile Guest Binaries
The guest binaries are compiled to the riscv32im-risc0-zkvm-elf target. This requires the RISC Zero toolchain.
cargo risczero build --manifest-path <PROGRAM>/methods/guest/Cargo.toml
Binaries are output to:
<PROGRAM>/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/<PROGRAM>.bin
Deployment
# Deploy a program binary to the sequencer
wallet deploy-program <path-to-binary>
# Example
wallet deploy-program programs/token/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/token.bin
wallet deploy-program programs/amm/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/amm.bin
wallet deploy-program programs/ata/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/ata.bin
wallet deploy-program programs/stablecoin/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/stablecoin.bin
wallet deploy-program programs/twap_oracle/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/twap_oracle.bin
To inspect the ProgramId of a built binary:
spel inspect <path-to-binary>
Interacting with Programs via spel
Generate an IDL
The IDL describes the program's instructions and can be used to interact with a deployed program.
Using the idl-gen crate (no external toolchain required — this is what CI uses):
make idl
Using the spel CLI (requires the SPEL toolchain):
spel generate-idl programs/token/methods/guest/src/bin/token.rs > artifacts/token-idl.json
spel generate-idl programs/amm/methods/guest/src/bin/amm.rs > artifacts/amm-idl.json
spel generate-idl programs/ata/methods/guest/src/bin/ata.rs > artifacts/ata-idl.json
spel generate-idl programs/stablecoin/methods/guest/src/bin/stablecoin.rs > artifacts/stablecoin-idl.json
spel generate-idl programs/twap_oracle/methods/guest/src/bin/twap_oracle.rs > artifacts/twap_oracle-idl.json
Generated IDL files are committed under artifacts/. CI will fail if a program's IDL is missing or out of date.
Invoke Instructions
Use spel --idl <IDL> <INSTRUCTION> [ARGS...] to call a deployed program instruction:
spel --idl artifacts/token-idl.json <instruction> [args...]
spel --idl artifacts/amm-idl.json <instruction> [args...]
spel --idl artifacts/ata-idl.json <instruction> [args...]
spel --idl artifacts/stablecoin-idl.json <instruction> [args...]
spel --idl artifacts/twap_oracle-idl.json <instruction> [args...]