Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 61 additions & 0 deletions artifacts/amm-idl.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,51 @@
{
"name": "token_program_id",
"type": "program_id"
},
{
"name": "twap_oracle_program_id",
"type": "program_id"
}
]
},
{
"name": "create_price_observations",
"accounts": [
{
"name": "config",
"writable": false,
"signer": false,
"init": false
},
{
"name": "pool",
"writable": false,
"signer": false,
"init": false
},
{
"name": "current_tick_account",
"writable": false,
"signer": false,
"init": false
},
{
"name": "price_observations",
"writable": false,
"signer": false,
"init": false
},
{
"name": "clock",
"writable": false,
"signer": false,
"init": false
}
],
"args": [
{
"name": "window_duration",
"type": "u64"
}
]
},
Expand Down Expand Up @@ -75,6 +120,18 @@
"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": [
Expand Down Expand Up @@ -434,6 +491,10 @@
{
"name": "token_program_id",
"type": "program_id"
},
{
"name": "twap_oracle_program_id",
"type": "program_id"
}
]
}
Expand Down
2 changes: 2 additions & 0 deletions programs/amm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ workspace = true

[dependencies]
nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc3", features = ["host"] }
clock_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc3" }
amm_core = { path = "core" }
token_core = { path = "../token/core" }
twap_oracle_core = { path = "../twap_oracle/core" }
5 changes: 5 additions & 0 deletions programs/amm/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ token_core = { path = "../../token/core" }
borsh = { version = "1.5", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
risc0-zkvm = { version = "=3.0.5", default-features = false }
alloy-primitives = { version = "1", default-features = false }
# Pin ruint (transitive via alloy-primitives) below 1.18, which raised its MSRV to rustc 1.90.
# The risc0 guest toolchain ships rustc 1.88, so 1.18+ fails the guest build. 1.17.0 (MSRV 1.85)
# is the newest compatible release. Remove this pin once the risc0 toolchain advances past 1.90.
ruint = { version = "=1.17.0", default-features = false }
110 changes: 106 additions & 4 deletions programs/amm/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,45 @@ pub enum Instruction {
/// Initializes the AMM Program by creating its singleton configuration account.
///
/// The configuration account is a PDA derived from the constant `"CONFIG"` seed
/// (`compute_config_pda(self_program_id)`). It stores the Token Program ID that the AMM
/// uses for every chained call. The Program must be initialized via this instruction before
/// any pool can be created or interacted with — the other instructions read the Token
/// Program ID from this account and reject calls when it does not yet exist.
/// (`compute_config_pda(self_program_id)`). It stores the program IDs the AMM issues chained
/// calls to (the Token Program and the TWAP oracle program). The Program must be initialized
/// via this instruction before any pool can be created or interacted with — the other
/// instructions read these program IDs from this account and reject calls when it does not
/// yet exist.
///
/// Required accounts:
/// - AMM Config Account, uninitialized, derived as `compute_config_pda(self_program_id)`
Initialize {
/// Program ID of the Token Program the AMM will issue chained calls to.
token_program_id: ProgramId,
/// Program ID of the TWAP oracle program the AMM will issue chained calls to.
twap_oracle_program_id: ProgramId,
},

/// Creates a TWAP price-observations account for a pool over a time window, on behalf of the
/// AMM, via a chained call to the configured TWAP oracle program.
///
/// The pool acts as the price source: the AMM authorizes it (via its pool PDA seed) so the
/// oracle ties the observations account to this pool. The feed's initial tick is read from the
/// pool's [`CurrentTickAccount`](twap_oracle_core::CurrentTickAccount) — the authoritative
/// tick the AMM previously wrote — rather than being supplied by the caller, so the feed
/// cannot be seeded at a forged price. Rejects if the observations account already exists.
/// The clock must be the canonical 1-block LEZ clock.
///
/// Required accounts:
/// - AMM Config Account (initialized)
/// - AMM Pool (initialized; acts as the price source)
/// - Current Tick Account, the pool's initialized TWAP PDA derived as
/// `compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id)`; supplies the
/// initial tick
/// - Price Observations Account, uninitialized TWAP PDA derived as
/// `compute_price_observations_pda(twap_oracle_program_id, pool.account_id,
/// window_duration)`
/// - Clock Account (the canonical 1-block LEZ clock)
CreatePriceObservations {
/// Duration of the TWAP window this feed serves, in milliseconds. Part of the
/// observations PDA seed, so each window gets a distinct account.
window_duration: u64,
},

/// Initializes a new Pool (or re-initializes an existing zero-supply Pool).
Expand Down Expand Up @@ -172,6 +201,35 @@ pub fn assert_supported_fee_tier(fees: u128) {
);
}

/// Computes a `Q64.64` spot price (`reserve_quote` per `reserve_base`) from raw pool reserves.
///
/// This is the constant-product AMM's spot price (`reserve_quote / reserve_base`) expressed as a
/// `Q64.64` fixed-point value: `(reserve_quote / reserve_base) * 2^64`. It is computed in 256-bit
/// precision and saturates at `u128::MAX` if the ratio exceeds the representable range. The TWAP
/// oracle consumes exactly this representation (it converts the `Q64.64` price to a tick), so the
/// AMM owns the reserves → price mapping and the oracle stays agnostic to how the price is formed.
///
/// # Panics
/// Panics if `reserve_base` is zero.
#[must_use]
pub fn spot_price_q64_64(reserve_base: u128, reserve_quote: u128) -> u128 {
use alloy_primitives::U256;

assert!(
reserve_base != 0,
"spot_price_q64_64: reserve_base must be non-zero"
);

let numerator = U256::from(reserve_quote)
.checked_shl(64)
.expect("reserve_quote < 2^128, so reserve_quote << 64 fits in U256");
let price = numerator
.checked_div(U256::from(reserve_base))
.expect("reserve_base is non-zero after the assertion above");

u128::try_from(price).unwrap_or(u128::MAX)
}

impl TryFrom<&Data> for PoolDefinition {
type Error = std::io::Error;

Expand Down Expand Up @@ -204,6 +262,8 @@ impl From<&PoolDefinition> for Data {
pub struct AmmConfig {
/// Program ID of the Token Program the AMM issues chained calls to.
pub token_program_id: ProgramId,
/// Program ID of the TWAP oracle program the AMM issues chained calls to.
pub twap_oracle_program_id: ProgramId,
}

impl TryFrom<&Data> for AmmConfig {
Expand Down Expand Up @@ -380,3 +440,45 @@ pub fn read_vault_fungible_balances(

(vault_a_balance, vault_b_balance)
}

#[cfg(test)]
mod tests {
use super::*;

/// `1.0` in Q64.64 is `2^64`.
const ONE_Q64_64: u128 = 1u128 << 64;

#[test]
fn equal_reserves_map_to_unit_price() {
assert_eq!(spot_price_q64_64(1_000, 1_000), ONE_Q64_64);
}

#[test]
fn spot_price_reflects_reserve_ratio() {
// reserve_quote / reserve_base = 2.0 -> 2 * 2^64.
assert_eq!(spot_price_q64_64(1_000, 2_000), ONE_Q64_64 * 2);
// reserve_quote / reserve_base = 0.5 -> 2^64 / 2.
assert_eq!(spot_price_q64_64(2_000, 1_000), ONE_Q64_64 / 2);
}

#[test]
fn spot_price_saturates_instead_of_overflowing() {
// A huge quote-to-base ratio would exceed u128 in Q64.64; it must saturate, not panic.
assert_eq!(spot_price_q64_64(1, u128::MAX), u128::MAX);
}

#[test]
fn spot_price_handles_large_reserves_without_intermediate_overflow() {
// reserve_quote >= 2^64 would overflow a naive `reserve_quote << 64` in u128; the U256
// intermediate keeps it exact. Ratio here is 4.0.
let base = 1u128 << 64;
let quote = 1u128 << 66;
assert_eq!(spot_price_q64_64(base, quote), ONE_Q64_64 * 4);
}

#[test]
#[should_panic(expected = "reserve_base must be non-zero")]
fn zero_reserve_base_panics() {
let _ = spot_price_q64_64(0, 1_000);
}
}
Loading