mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-05-18 15:09:51 +00:00
feat(amm): add SyncReserves instruction
Adds a new `SyncReserves` instruction that updates a pool's recorded reserves to match the actual vault balances. This allows the pool to absorb donations (direct token transfers to vaults) without breaking the invariant — only upward adjustments are permitted; vaults may not be under-collateralized relative to reserves. Vault reading helpers (`read_fungible_holding`, `read_vault_fungible_balances`) are implemented in `amm_core` so they can be shared across instructions without crossing crate boundaries.
This commit is contained in:
parent
fddd6e15bd
commit
e61cd594b5
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -46,6 +46,7 @@ dependencies = [
|
||||
"nssa_core",
|
||||
"risc0-zkvm",
|
||||
"serde",
|
||||
"token_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@ -239,6 +239,30 @@
|
||||
"type": "account_id"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sync_reserves",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "pool",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "vault_a",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "vault_b",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
}
|
||||
],
|
||||
"instruction_type": "amm_core::Instruction"
|
||||
|
||||
@ -5,6 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", rev = "ffcbc15972adbf557939bf3e2852af276422631b", features = ["host"] }
|
||||
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 }
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use nssa_core::{
|
||||
account::{AccountId, Data},
|
||||
account::{AccountId, AccountWithMetadata, Data},
|
||||
program::{PdaSeed, ProgramId},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -83,6 +83,14 @@ pub enum Instruction {
|
||||
min_amount_out: u128,
|
||||
token_definition_id_in: AccountId,
|
||||
},
|
||||
|
||||
/// Sync pool reserves with current vault balances.
|
||||
///
|
||||
/// Required accounts:
|
||||
/// - AMM Pool (initialized, active)
|
||||
/// - Vault Holding Account for Token A (initialized)
|
||||
/// - Vault Holding Account for Token B (initialized)
|
||||
SyncReserves,
|
||||
}
|
||||
|
||||
pub const MINIMUM_LIQUIDITY: u128 = 1_000;
|
||||
@ -230,3 +238,31 @@ pub fn compute_lp_lock_holding_pda_seed(pool_id: AccountId) -> PdaSeed {
|
||||
.expect("Hash output must be exactly 32 bytes long"),
|
||||
)
|
||||
}
|
||||
|
||||
fn read_fungible_holding(account: &AccountWithMetadata, context: &str) -> (AccountId, u128) {
|
||||
let token_holding = token_core::TokenHolding::try_from(&account.account.data)
|
||||
.unwrap_or_else(|_| panic!("{context}: AMM Program expects a valid Token Holding Account"));
|
||||
|
||||
let token_core::TokenHolding::Fungible {
|
||||
definition_id,
|
||||
balance,
|
||||
} = token_holding
|
||||
else {
|
||||
panic!("{context}: AMM Program expects a valid Fungible Token Holding Account");
|
||||
};
|
||||
|
||||
(definition_id, balance)
|
||||
}
|
||||
|
||||
pub fn read_vault_fungible_balances(
|
||||
context: &str,
|
||||
vault_a: &AccountWithMetadata,
|
||||
vault_b: &AccountWithMetadata,
|
||||
) -> (u128, u128) {
|
||||
let vault_a_context = format!("{context}: Vault A");
|
||||
let vault_b_context = format!("{context}: Vault B");
|
||||
let (_, vault_a_balance) = read_fungible_holding(vault_a, &vault_a_context);
|
||||
let (_, vault_b_balance) = read_fungible_holding(vault_b, &vault_b_context);
|
||||
|
||||
(vault_a_balance, vault_b_balance)
|
||||
}
|
||||
|
||||
1
amm/methods/guest/Cargo.lock
generated
1
amm/methods/guest/Cargo.lock
generated
@ -51,6 +51,7 @@ dependencies = [
|
||||
"nssa_core",
|
||||
"risc0-zkvm",
|
||||
"serde",
|
||||
"token_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@ -129,4 +129,16 @@ mod amm {
|
||||
);
|
||||
Ok(SpelOutput::with_chained_calls(post_states, chained_calls))
|
||||
}
|
||||
|
||||
/// Sync pool reserves with current vault balances.
|
||||
#[instruction]
|
||||
pub fn sync_reserves(
|
||||
pool: AccountWithMetadata,
|
||||
vault_a: AccountWithMetadata,
|
||||
vault_b: AccountWithMetadata,
|
||||
) -> SpelResult {
|
||||
let (post_states, chained_calls) =
|
||||
amm_program::sync::sync_reserves(pool, vault_a, vault_b);
|
||||
Ok(SpelOutput::with_chained_calls(post_states, chained_calls))
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,5 +6,6 @@ pub mod add;
|
||||
pub mod new_definition;
|
||||
pub mod remove;
|
||||
pub mod swap;
|
||||
pub mod sync;
|
||||
|
||||
mod tests;
|
||||
|
||||
52
amm/src/sync.rs
Normal file
52
amm/src/sync.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use amm_core::{read_vault_fungible_balances, PoolDefinition};
|
||||
use nssa_core::{
|
||||
account::{AccountWithMetadata, Data},
|
||||
program::{AccountPostState, ChainedCall},
|
||||
};
|
||||
|
||||
pub fn sync_reserves(
|
||||
pool: AccountWithMetadata,
|
||||
vault_a: AccountWithMetadata,
|
||||
vault_b: AccountWithMetadata,
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
let pool_def_data = PoolDefinition::try_from(&pool.account.data)
|
||||
.expect("Sync reserves: AMM Program expects a valid Pool Definition Account");
|
||||
|
||||
assert!(pool_def_data.active, "Pool is inactive");
|
||||
assert_eq!(
|
||||
vault_a.account_id, pool_def_data.vault_a_id,
|
||||
"Vault A was not provided"
|
||||
);
|
||||
assert_eq!(
|
||||
vault_b.account_id, pool_def_data.vault_b_id,
|
||||
"Vault B was not provided"
|
||||
);
|
||||
|
||||
let (vault_a_balance, vault_b_balance) =
|
||||
read_vault_fungible_balances("Sync reserves", &vault_a, &vault_b);
|
||||
assert!(
|
||||
vault_a_balance >= pool_def_data.reserve_a,
|
||||
"Sync reserves: vault A balance is less than its reserve"
|
||||
);
|
||||
assert!(
|
||||
vault_b_balance >= pool_def_data.reserve_b,
|
||||
"Sync reserves: vault B balance is less than its reserve"
|
||||
);
|
||||
|
||||
let mut pool_post = pool.account.clone();
|
||||
let pool_post_definition = PoolDefinition {
|
||||
reserve_a: vault_a_balance,
|
||||
reserve_b: vault_b_balance,
|
||||
..pool_def_data
|
||||
};
|
||||
pool_post.data = Data::from(&pool_post_definition);
|
||||
|
||||
(
|
||||
vec![
|
||||
AccountPostState::new(pool_post),
|
||||
AccountPostState::new(vault_a.account.clone()),
|
||||
AccountPostState::new(vault_b.account.clone()),
|
||||
],
|
||||
Vec::new(),
|
||||
)
|
||||
}
|
||||
112
amm/src/tests.rs
112
amm/src/tests.rs
@ -14,6 +14,7 @@ use token_core::{TokenDefinition, TokenHolding};
|
||||
|
||||
use crate::{
|
||||
add::add_liquidity, new_definition::new_definition, remove::remove_liquidity, swap::swap,
|
||||
sync::sync_reserves,
|
||||
};
|
||||
|
||||
const TOKEN_PROGRAM_ID: ProgramId = [15; 8];
|
||||
@ -2236,3 +2237,114 @@ fn test_minimum_liquidity_lock_and_remove_all_user_lp() {
|
||||
assert!(pool_after_remove.reserve_b > 0);
|
||||
assert!(pool_after_remove.active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_reserves_with_donation() {
|
||||
let pool = AccountWithMetadataForTests::pool_definition_init();
|
||||
let donation_a = 111u128;
|
||||
|
||||
let mut donated_vault_a = AccountWithMetadataForTests::vault_a_init();
|
||||
donated_vault_a.account.data = Data::from(&TokenHolding::Fungible {
|
||||
definition_id: IdForTests::token_a_definition_id(),
|
||||
balance: BalanceForTests::vault_a_reserve_init() + donation_a,
|
||||
});
|
||||
|
||||
let pool_pre = PoolDefinition::try_from(&pool.account.data).unwrap();
|
||||
assert_eq!(pool_pre.reserve_a, BalanceForTests::vault_a_reserve_init());
|
||||
|
||||
let (post_states, chained_calls) = sync_reserves(
|
||||
pool,
|
||||
donated_vault_a,
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
);
|
||||
assert!(chained_calls.is_empty());
|
||||
|
||||
let pool_post = PoolDefinition::try_from(&post_states[0].account().data).unwrap();
|
||||
assert_eq!(
|
||||
pool_post.reserve_a,
|
||||
BalanceForTests::vault_a_reserve_init() + donation_a
|
||||
);
|
||||
assert_eq!(pool_post.reserve_b, BalanceForTests::vault_b_reserve_init());
|
||||
}
|
||||
|
||||
#[should_panic(expected = "Sync reserves: vault A balance is less than its reserve")]
|
||||
#[test]
|
||||
fn test_sync_reserves_panics_when_vault_a_under_collateralized() {
|
||||
let _ = sync_reserves(
|
||||
AccountWithMetadataForTests::pool_definition_init(),
|
||||
AccountWithMetadataForTests::vault_a_init_low(),
|
||||
AccountWithMetadataForTests::vault_b_init(),
|
||||
);
|
||||
}
|
||||
|
||||
#[should_panic(expected = "Sync reserves: vault B balance is less than its reserve")]
|
||||
#[test]
|
||||
fn test_sync_reserves_panics_when_vault_b_under_collateralized() {
|
||||
let _ = sync_reserves(
|
||||
AccountWithMetadataForTests::pool_definition_init(),
|
||||
AccountWithMetadataForTests::vault_a_init(),
|
||||
AccountWithMetadataForTests::vault_b_init_low(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_donation_then_add_liquidity_sync_mitigates_mispricing() {
|
||||
let donation_a = 100u128;
|
||||
|
||||
let mut donated_vault_a = AccountWithMetadataForTests::vault_a_init();
|
||||
donated_vault_a.account.data = Data::from(&TokenHolding::Fungible {
|
||||
definition_id: IdForTests::token_a_definition_id(),
|
||||
balance: BalanceForTests::vault_a_reserve_init() + donation_a,
|
||||
});
|
||||
let donated_vault_b = AccountWithMetadataForTests::vault_b_init();
|
||||
|
||||
let (post_unsynced, _) = add_liquidity(
|
||||
AccountWithMetadataForTests::pool_definition_init(),
|
||||
donated_vault_a.clone(),
|
||||
donated_vault_b.clone(),
|
||||
AccountWithMetadataForTests::pool_lp_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
NonZero::new(1).unwrap(),
|
||||
100,
|
||||
50,
|
||||
);
|
||||
let unsynced_pool_post = PoolDefinition::try_from(&post_unsynced[0].account().data).unwrap();
|
||||
let unsynced_delta_lp =
|
||||
unsynced_pool_post.liquidity_pool_supply - BalanceForTests::lp_supply_init();
|
||||
|
||||
let donated_vault_a_for_synced_add = donated_vault_a.clone();
|
||||
let donated_vault_b_for_synced_add = donated_vault_b.clone();
|
||||
|
||||
let (sync_post, _) = sync_reserves(
|
||||
AccountWithMetadataForTests::pool_definition_init(),
|
||||
donated_vault_a,
|
||||
donated_vault_b,
|
||||
);
|
||||
let synced_pool = AccountWithMetadata {
|
||||
account: sync_post[0].account().clone(),
|
||||
is_authorized: true,
|
||||
account_id: IdForTests::pool_definition_id(),
|
||||
};
|
||||
|
||||
let (post_synced, _) = add_liquidity(
|
||||
synced_pool,
|
||||
donated_vault_a_for_synced_add,
|
||||
donated_vault_b_for_synced_add,
|
||||
AccountWithMetadataForTests::pool_lp_init(),
|
||||
AccountWithMetadataForTests::user_holding_a(),
|
||||
AccountWithMetadataForTests::user_holding_b(),
|
||||
AccountWithMetadataForTests::user_holding_lp_init(),
|
||||
NonZero::new(1).unwrap(),
|
||||
100,
|
||||
50,
|
||||
);
|
||||
let synced_pool_post = PoolDefinition::try_from(&post_synced[0].account().data).unwrap();
|
||||
let synced_delta_lp = synced_pool_post.liquidity_pool_supply
|
||||
- PoolDefinition::try_from(&sync_post[0].account().data)
|
||||
.unwrap()
|
||||
.liquidity_pool_supply;
|
||||
|
||||
assert!(synced_delta_lp < unsynced_delta_lp);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user