Skip to content

[AMM] feat: add sync reserves and recover surplus functionality#12

Open
3esmit wants to merge 6 commits intologos-blockchain:mainfrom
3esmit:fix/amm-surplus-sync-recover-min-lp
Open

[AMM] feat: add sync reserves and recover surplus functionality#12
3esmit wants to merge 6 commits intologos-blockchain:mainfrom
3esmit:fix/amm-surplus-sync-recover-min-lp

Conversation

@3esmit
Copy link
Copy Markdown
Collaborator

@3esmit 3esmit commented Mar 31, 2026

🎯 Purpose

Fixes #6.
Migrate the AMM safety work from logos-execution-zone PR 371 into lez-programs, adapted to this repository's current structure and guest program model.

  • Adds explicit reserve sync and surplus recovery flows so direct vault donations no longer skew pricing silently.
  • Adds permanent minimum-liquidity locking so LP supply cannot be fully drained to zero.
  • Updates the guest entrypoint, checked-in IDL, and AMM tests to match the new behavior.

⚙️ What Changed

  • Add SyncReserves and RecoverSurplus { InactiveOrZeroSupplyOnly } to amm_core, wire them through amm/methods/guest/src/bin/amm.rs, and regenerate amm/amm-idl.json.
  • Add MINIMUM_LIQUIDITY, stable LP lock PDA helpers/domain separators, and mint locked LP before user LP in new_definition.
  • Tighten remove_liquidity so it rejects invalid LP holdings, over-burns, and attempts to remove the permanently locked liquidity.
  • Read live vault balances through shared vault_utils in add, swap, sync, and recover, with explicit under-collateralization and token-definition mismatch checks.
  • Extend AMM unit coverage for donation sync, surplus recovery, under-collateralized vaults, LP lock behavior, and reinitialization paths.
  • Update integration_tests/tests/amm.rs so pool initialization expects user LP to equal sqrt(a * b) - MINIMUM_LIQUIDITY.

🔎 Migration Notes

  • Ports the intent of logos-execution-zone PR 371, but adapts it to spel_framework #[instruction] guests and the per-program methods/guest layout used in this repo.
  • recover_surplus now transfers only vault_balance - reserve, so reserves remain unchanged and no follow-up sync_reserves call is required after recovery.
  • The final diff from main is scoped to AMM program/core/guest/IDL files plus integration_tests/tests/amm.rs.

🔎 Review Focus

  • new_definition now mints locked LP first and uses the post-lock LP definition state for the user mint.
  • sync_reserves and recover_surplus both fail if a vault is under-collateralized instead of silently saturating.
  • recover_surplus is restricted to inactive or zero-supply pools and validates vault/recipient token definitions before transferring.

🧪 How to Test

  1. RISC0_SKIP_BUILD=1 cargo +1.94.0 clippy --workspace --all-targets -- -D warnings
  2. RISC0_DEV_MODE=1 cargo +1.94.0 test -p amm_program
  3. RISC0_DEV_MODE=1 cargo +1.94.0 test -p integration_tests --test amm
  4. spel-cli generate-idl amm/methods/guest/src/bin/amm.rs > amm/amm-idl.json

🔗 Dependencies

None.

🔜 Future Work

  • Add caller-facing support for sync_reserves and recover_surplus in external tooling or clients that invoke AMM instructions.

📋 PR Completion Checklist

  • Port donation-handling and minimum-liquidity protections into lez-programs
  • Wire the new AMM instructions through the guest and checked-in IDL
  • Add or update AMM unit and integration coverage
  • Keep the final diff scoped to AMM behavior plus AMM integration artifacts

- 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.
Copilot AI review requested due to automatic review settings March 31, 2026 15:05
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_reserves and recover_surplus { mode } instructions (core enum + guest entrypoint + IDL), plus shared vault parsing helpers.
  • Introduces MINIMUM_LIQUIDITY = 1 and an LP lock holding PDA; updates new_definition and remove_liquidity to 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_definition currently allows initializing any pool with active == false, even if it already has nonzero liquidity_pool_supply and/or the LP TokenDefinition already has a nonzero total_supply. In that state, the instruction will mint MINIMUM_LIQUIDITY + user_lp again, inflating LP token supply and desynchronizing it from pool_post_definition.liquidity_pool_supply. Recommend adding an explicit guard when !is_new_pool (e.g., require pool_account_data.liquidity_pool_supply == 0 and the existing LP token definition total_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.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@3esmit 3esmit changed the title [AMM] add sync reserves and recover surplus functionality [AMM] feat: add sync reserves and recover surplus functionality Mar 31, 2026
InactiveOrZeroSupplyOnly,
}

pub const MINIMUM_LIQUIDITY: u128 = 1;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 },
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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];
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are actually seeds so let's rename them to

Suggested change
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"
);
};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"
);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"
);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"
);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement dead shares to avoid pool drain and first depositor attacks

3 participants