[AMM] feat: add sync reserves and recover surplus functionality#12
[AMM] feat: add sync reserves and recover surplus functionality#123esmit wants to merge 6 commits intologos-blockchain:mainfrom
Conversation
- Introduced `SyncReserves` instruction to synchronize pool reserves with current vault balances. - Added `RecoverSurplus` instruction to recover surplus balances not backed by reserves. - Implemented utility functions for reading vault balances and fungible holdings. - Updated liquidity management to ensure minimum liquidity is maintained during operations. - Enhanced tests to cover new functionalities and edge cases for surplus recovery and reserve synchronization.
There was a problem hiding this comment.
Pull request overview
This PR migrates AMM “safety” functionality into lez-programs by adding explicit reserve reconciliation and surplus recovery flows, plus a minimum-liquidity LP lock to prevent pools from being fully drained to zero supply.
Changes:
- Adds
sync_reservesandrecover_surplus { mode }instructions (core enum + guest entrypoint + IDL), plus shared vault parsing helpers. - Introduces
MINIMUM_LIQUIDITY = 1and an LP lock holding PDA; updatesnew_definitionandremove_liquidityto enforce the permanent minimum-liquidity lock. - Extends unit/integration tests to cover donation syncing, surplus recovery, and minimum-liquidity behavior.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| integration_tests/tests/amm.rs | Adjusts expected initial user LP to account for permanently locked MINIMUM_LIQUIDITY. |
| amm/src/vault_utils.rs | Adds shared helpers to parse fungible token holdings / vault balances with contextual panic messages. |
| amm/src/sync.rs | Implements sync_reserves to update pool reserves from current vault balances. |
| amm/src/recover.rs | Implements recover_surplus to transfer only vault_balance - reserve (with under-collateralization checks). |
| amm/src/new_definition.rs | Mints locked LP first, then mints user LP; adds minimum-liquidity lock PDA usage and related checks. |
| amm/src/remove.rs | Prevents burning locked minimum liquidity and adds explicit “amount exceeds user balance” assertion. |
| amm/src/add.rs | Refactors vault holding parsing to shared vault_utils helpers. |
| amm/src/swap.rs | Refactors vault holding parsing to shared vault_utils helpers. |
| amm/src/lib.rs | Wires in new modules (recover, sync) and shared vault_utils. |
| amm/methods/guest/src/bin/amm.rs | Exposes sync_reserves / recover_surplus as #[instruction] guest entrypoints. |
| amm/core/src/lib.rs | Adds new instruction variants/types, MINIMUM_LIQUIDITY, LP lock PDA derivation, and PDA domain separator constants. |
| amm/src/tests.rs | Updates/extends unit tests for min-liquidity lock, sync, and surplus recovery scenarios. |
| amm/amm-idl.json | Regenerates IDL to include the new instructions and args. |
Comments suppressed due to low confidence (1)
amm/src/new_definition.rs:79
new_definitioncurrently allows initializing any pool withactive == false, even if it already has nonzeroliquidity_pool_supplyand/or the LPTokenDefinitionalready has a nonzerototal_supply. In that state, the instruction will mintMINIMUM_LIQUIDITY + user_lpagain, inflating LP token supply and desynchronizing it frompool_post_definition.liquidity_pool_supply. Recommend adding an explicit guard when!is_new_pool(e.g., requirepool_account_data.liquidity_pool_supply == 0and the existing LP token definitiontotal_supply == 0) before proceeding with the re-initialization mint path.
let is_new_pool = pool.account == Account::default();
let pool_account_data = if is_new_pool {
PoolDefinition::default()
} else {
PoolDefinition::try_from(&pool.account.data)
.expect("AMM program expects a valid Pool account")
};
assert!(
!pool_account_data.active,
"Cannot initialize an active Pool Definition"
);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… and add panic test for vault A definition mismatch
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 13 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| InactiveOrZeroSupplyOnly, | ||
| } | ||
|
|
||
| pub const MINIMUM_LIQUIDITY: u128 = 1; |
There was a problem hiding this comment.
Let's increase this to 1000 to make this stronger (similar to how it's done in Uniswap)
| /// | ||
| /// This transfers only balances above the tracked reserves, so pool reserves remain | ||
| /// unchanged and no follow-up `SyncReserves` call is required. | ||
| RecoverSurplus { mode: RecoverSurplusMode }, |
There was a problem hiding this comment.
I think if we have SyncReserves, there's little point in having RecoverSurplus. Recovering funds can easily be prevent by syncing reserves.
| @@ -0,0 +1,29 @@ | |||
| use nssa_core::account::{AccountId, AccountWithMetadata}; | |||
There was a problem hiding this comment.
Can we move this stuff into core/src (maybe just in a utils.rs file?
Let's keep the convention to only have program function files in /src
|
|
||
| // These separators are part of the PDA derivation scheme and must stay stable for compatibility. | ||
| const LIQUIDITY_TOKEN_PDA_DOMAIN_SEPARATOR: [u8; 32] = [0; 32]; | ||
| const LP_LOCK_HOLDING_PDA_DOMAIN_SEPARATOR: [u8; 32] = [1; 32]; |
There was a problem hiding this comment.
These are actually seeds so let's rename them to
| const LP_LOCK_HOLDING_PDA_DOMAIN_SEPARATOR: [u8; 32] = [1; 32]; | |
| const LP_LOCK_HOLDING_PDA_SEED: [u8; 32] = [1; 32]; |
Maybe, we can also consider changing the seeds to something like:
const LIQUIDITY_TOKEN_PDA_SEED: &[u8] = b"amm:liquidity_token";
const LP_LOCK_HOLDING_PDA_SEED: &[u8] = b"amm:lp_lock_holding";
Let's explore best practices here.
| panic!( | ||
| "Remove liquidity: AMM Program expects a valid Fungible Token Holding Account for liquidity token" | ||
| ); | ||
| }; |
There was a problem hiding this comment.
Shouldn't this also use the new utilities in vault_utils.rs?
| assert!( | ||
| remove_liquidity_amount <= user_lp_balance, | ||
| "Cannot remove more liquidity than owned" | ||
| ); |
There was a problem hiding this comment.
This check is not needed because this will fail in Transfer if the user tries to transfer more than what they have.
| assert!( | ||
| pool_def_data.liquidity_pool_supply > MINIMUM_LIQUIDITY, | ||
| "Pool only contains locked liquidity" | ||
| ); |
There was a problem hiding this comment.
This check seems unnecessary as well.
If lp pool supply is <= MINIMUM_SUPPLY, that means there are no additional LP tokens. So the user won't have any balance to burn in the first place
| assert!( | ||
| remove_liquidity_amount <= pool_def_data.liquidity_pool_supply - MINIMUM_LIQUIDITY, | ||
| "Cannot remove locked minimum liquidity" | ||
| ); |
There was a problem hiding this comment.
This as well. I don't think there's a scenario where this will ever be true.
|
|
||
| #[derive(Clone, Copy, Serialize, Deserialize)] | ||
| pub enum RecoverSurplusMode { | ||
| InactiveOrZeroSupplyOnly, |
There was a problem hiding this comment.
This is already cancelling itself out with MINIMUM_LIQUIDITY.
When a pool is initialized, it will have a supply (MINIMUM_LIQUIDITY) which also makes it active.
So with this , surplus can never be recovered.
I created #25 to make this more clear.
In any case, either we remove this mode, or we drop recover surplus entirely.
🎯 Purpose
Fixes #6.
Migrate the AMM safety work from
logos-execution-zonePR 371 intolez-programs, adapted to this repository's current structure and guest program model.⚙️ What Changed
SyncReservesandRecoverSurplus { InactiveOrZeroSupplyOnly }toamm_core, wire them throughamm/methods/guest/src/bin/amm.rs, and regenerateamm/amm-idl.json.MINIMUM_LIQUIDITY, stable LP lock PDA helpers/domain separators, and mint locked LP before user LP innew_definition.remove_liquidityso it rejects invalid LP holdings, over-burns, and attempts to remove the permanently locked liquidity.vault_utilsinadd,swap,sync, andrecover, with explicit under-collateralization and token-definition mismatch checks.integration_tests/tests/amm.rsso pool initialization expects user LP to equalsqrt(a * b) - MINIMUM_LIQUIDITY.🔎 Migration Notes
logos-execution-zonePR 371, but adapts it tospel_framework#[instruction]guests and the per-programmethods/guestlayout used in this repo.recover_surplusnow transfers onlyvault_balance - reserve, so reserves remain unchanged and no follow-upsync_reservescall is required after recovery.mainis scoped to AMM program/core/guest/IDL files plusintegration_tests/tests/amm.rs.🔎 Review Focus
new_definitionnow mints locked LP first and uses the post-lock LP definition state for the user mint.sync_reservesandrecover_surplusboth fail if a vault is under-collateralized instead of silently saturating.recover_surplusis restricted to inactive or zero-supply pools and validates vault/recipient token definitions before transferring.🧪 How to Test
RISC0_SKIP_BUILD=1 cargo +1.94.0 clippy --workspace --all-targets -- -D warningsRISC0_DEV_MODE=1 cargo +1.94.0 test -p amm_programRISC0_DEV_MODE=1 cargo +1.94.0 test -p integration_tests --test ammspel-cli generate-idl amm/methods/guest/src/bin/amm.rs > amm/amm-idl.json🔗 Dependencies
None.
🔜 Future Work
sync_reservesandrecover_surplusin external tooling or clients that invoke AMM instructions.📋 PR Completion Checklist
lez-programs