Skip to content
Merged
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
59 changes: 59 additions & 0 deletions artifacts/amm-idl.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,32 @@
"version": "0.1.0",
"name": "amm",
"instructions": [
{
"name": "initialize",
"accounts": [
{
"name": "config",
"writable": false,
"signer": false,
"init": false
}
],
"args": [
{
"name": "token_program_id",
"type": "program_id"
}
]
},
{
"name": "new_definition",
"accounts": [
{
"name": "config",
"writable": false,
"signer": false,
"init": false
},
{
"name": "pool",
"writable": false,
Expand Down Expand Up @@ -76,6 +99,12 @@
{
"name": "add_liquidity",
"accounts": [
{
"name": "config",
"writable": false,
"signer": false,
"init": false
},
{
"name": "pool",
"writable": false,
Expand Down Expand Up @@ -141,6 +170,12 @@
{
"name": "remove_liquidity",
"accounts": [
{
"name": "config",
"writable": false,
"signer": false,
"init": false
},
{
"name": "pool",
"writable": false,
Expand Down Expand Up @@ -206,6 +241,12 @@
{
"name": "swap_exact_input",
"accounts": [
{
"name": "config",
"writable": false,
"signer": false,
"init": false
},
{
"name": "pool",
"writable": false,
Expand Down Expand Up @@ -259,6 +300,12 @@
{
"name": "swap_exact_output",
"accounts": [
{
"name": "config",
"writable": false,
"signer": false,
"init": false
},
{
"name": "pool",
"writable": false,
Expand Down Expand Up @@ -379,6 +426,18 @@
]
}
},
{
"name": "AmmConfig",
"type": {
"kind": "struct",
"fields": [
{
"name": "token_program_id",
"type": "program_id"
}
]
}
},
{
"name": "TokenDefinition",
"type": {
Expand Down
70 changes: 70 additions & 0 deletions programs/amm/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@ const LP_LOCK_HOLDING_PDA_SEED: [u8; 32] = [1; 32];
/// AMM Program Instruction.
#[derive(Serialize, Deserialize)]
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.
///
/// 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,
},

/// Initializes a new Pool (or re-initializes an existing zero-supply Pool).
///
/// On initialization, `MINIMUM_LIQUIDITY` LP tokens are permanently locked
Expand Down Expand Up @@ -177,6 +192,61 @@ impl From<&PoolDefinition> for Data {
}
}

/// Singleton configuration account for the AMM Program.
///
/// Stored at the PDA derived from the constant `"CONFIG"` seed
/// (`compute_config_pda(amm_program_id)`). Created once via the `Initialize` instruction; its
/// existence is the Program's "initialized" flag. Every chained-call instruction reads
/// `token_program_id` from here instead of trusting the program owner of a caller-supplied
/// account.
#[account_type]
#[derive(Clone, Default, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct AmmConfig {
/// Program ID of the Token Program the AMM issues chained calls to.
pub token_program_id: ProgramId,
}

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

fn try_from(data: &Data) -> Result<Self, Self::Error> {
AmmConfig::try_from_slice(data.as_ref())
}
}

impl From<&AmmConfig> for Data {
fn from(config: &AmmConfig) -> Self {
let mut data = Vec::with_capacity(std::mem::size_of_val(config));

BorshSerialize::serialize(config, &mut data).expect("Serialization to Vec should not fail");

Data::try_from(data).expect("AMM config encoded data should fit into Data")
}
}

// Stable seed marker for the singleton config PDA. The literal `"CONFIG"` bytes are hashed into
// the 32-byte seed; this must stay unchanged for address compatibility.
const CONFIG_PDA_SEED: &[u8] = b"CONFIG";

/// Derives the [`AccountId`] of the AMM Program's singleton config PDA.
#[must_use]
pub fn compute_config_pda(amm_program_id: ProgramId) -> AccountId {
AccountId::for_public_pda(&amm_program_id, &compute_config_pda_seed())
}

/// Derives the [`PdaSeed`] of the AMM Program's singleton config PDA from the `"CONFIG"` bytes.
#[must_use]
pub fn compute_config_pda_seed() -> PdaSeed {
use risc0_zkvm::sha::{Impl, Sha256};

PdaSeed::new(
Impl::hash_bytes(CONFIG_PDA_SEED)
.as_bytes()
.try_into()
.expect("Hash output must be exactly 32 bytes long"),
)
}

pub fn compute_pool_pda(
amm_program_id: ProgramId,
definition_token_a_id: AccountId,
Expand Down
36 changes: 35 additions & 1 deletion programs/amm/methods/guest/src/bin/amm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use spel_framework::prelude::*;
use spel_framework::context::ProgramContext;
use nssa_core::{
account::{AccountId, AccountWithMetadata},
program::ProgramId,
};

#[cfg(not(test))]
Expand All @@ -19,15 +20,31 @@ mod amm {
)]
use super::*;

/// Initializes the AMM Program by creating its singleton config account.
///
/// Expected accounts:
/// 1. `config` — uninitialized config PDA derived from `compute_config_pda(self_program_id)`.
#[instruction]
pub fn initialize(
ctx: ProgramContext,
config: AccountWithMetadata,
token_program_id: ProgramId,
) -> SpelResult {
let post_states =
amm_program::initialize::initialize(config, token_program_id, ctx.self_program_id);
Ok(spel_framework::SpelOutput::execute(post_states, vec![]))
}

/// Initializes a new Pool (or re-initializes an existing zero-supply Pool).
/// A fresh user LP holding must be explicitly authorized by the caller.
#[expect(
clippy::too_many_arguments,
reason = "instruction interface requires explicit pool, vault, mint, lock, and user accounts"
reason = "instruction interface requires explicit config, pool, vault, mint, lock, and user accounts"
)]
#[instruction]
pub fn new_definition(
ctx: ProgramContext,
config: AccountWithMetadata,
pool: AccountWithMetadata,
vault_a: AccountWithMetadata,
vault_b: AccountWithMetadata,
Expand All @@ -42,6 +59,7 @@ mod amm {
deadline: u64,
) -> SpelResult {
let (post_states, chained_calls) = amm_program::new_definition::new_definition(
config,
pool,
vault_a,
vault_b,
Expand All @@ -66,6 +84,8 @@ mod amm {
)]
#[instruction]
pub fn add_liquidity(
ctx: ProgramContext,
config: AccountWithMetadata,
pool: AccountWithMetadata,
vault_a: AccountWithMetadata,
vault_b: AccountWithMetadata,
Expand All @@ -79,6 +99,7 @@ mod amm {
deadline: u64,
) -> SpelResult {
let (post_states, chained_calls) = amm_program::add::add_liquidity(
config,
pool,
vault_a,
vault_b,
Expand All @@ -89,6 +110,7 @@ mod amm {
NonZeroU128::new(min_amount_liquidity).expect("min_amount_liquidity must be nonzero"),
max_amount_to_add_token_a,
max_amount_to_add_token_b,
ctx.self_program_id,
);
Ok(spel_framework::SpelOutput::execute(post_states, chained_calls)
.with_timestamp_validity_window(..deadline))
Expand All @@ -101,6 +123,8 @@ mod amm {
)]
#[instruction]
pub fn remove_liquidity(
ctx: ProgramContext,
config: AccountWithMetadata,
pool: AccountWithMetadata,
vault_a: AccountWithMetadata,
vault_b: AccountWithMetadata,
Expand All @@ -114,6 +138,7 @@ mod amm {
deadline: u64,
) -> SpelResult {
let (post_states, chained_calls) = amm_program::remove::remove_liquidity(
config,
pool,
vault_a,
vault_b,
Expand All @@ -125,6 +150,7 @@ mod amm {
.expect("remove_liquidity_amount must be nonzero"),
min_amount_to_remove_token_a,
min_amount_to_remove_token_b,
ctx.self_program_id,
);
Ok(spel_framework::SpelOutput::execute(post_states, chained_calls)
.with_timestamp_validity_window(..deadline))
Expand All @@ -137,6 +163,8 @@ mod amm {
)]
#[instruction]
pub fn swap_exact_input(
ctx: ProgramContext,
config: AccountWithMetadata,
pool: AccountWithMetadata,
vault_a: AccountWithMetadata,
vault_b: AccountWithMetadata,
Expand All @@ -148,6 +176,7 @@ mod amm {
deadline: u64,
) -> SpelResult {
let (post_states, chained_calls) = amm_program::swap::swap_exact_input(
config,
pool,
vault_a,
vault_b,
Expand All @@ -156,6 +185,7 @@ mod amm {
swap_amount_in,
min_amount_out,
token_definition_id_in,
ctx.self_program_id,
);
Ok(spel_framework::SpelOutput::execute(post_states, chained_calls)
.with_timestamp_validity_window(..deadline))
Expand All @@ -168,6 +198,8 @@ mod amm {
)]
#[instruction]
pub fn swap_exact_output(
ctx: ProgramContext,
config: AccountWithMetadata,
pool: AccountWithMetadata,
vault_a: AccountWithMetadata,
vault_b: AccountWithMetadata,
Expand All @@ -179,6 +211,7 @@ mod amm {
deadline: u64,
) -> SpelResult {
let (post_states, chained_calls) = amm_program::swap::swap_exact_output(
config,
pool,
vault_a,
vault_b,
Expand All @@ -187,6 +220,7 @@ mod amm {
exact_amount_out,
max_amount_in,
token_definition_id_in,
ctx.self_program_id,
);
Ok(spel_framework::SpelOutput::execute(post_states, chained_calls)
.with_timestamp_validity_window(..deadline))
Expand Down
Loading
Loading