From 6e913382d5533fe79f1d27b898694c2d9b877ff3 Mon Sep 17 00:00:00 2001 From: DevRozaDev <158298065+DevRozaDev@users.noreply.github.com> Date: Tue, 21 Jan 2025 10:08:32 +0100 Subject: [PATCH 1/4] remove defunct pool entrypoint + sdk + tests --- package-lock.json | 2 +- package.json | 3 +- programs/invariant/src/instructions/mod.rs | 2 + .../src/instructions/remove_defunct_pool.rs | 137 +++++++++++ programs/invariant/src/interfaces/mod.rs | 4 +- .../src/interfaces/take_ref_tokens.rs | 2 +- programs/invariant/src/lib.rs | 5 + sdk/src/idl/invariant.ts | 132 +++++++++++ sdk/src/market.ts | 44 ++++ sdk/src/utils.ts | 2 +- tests/remove-pool.spec.ts | 224 ++++++++++++++++++ 11 files changed, 551 insertions(+), 6 deletions(-) create mode 100644 programs/invariant/src/instructions/remove_defunct_pool.rs create mode 100644 tests/remove-pool.spec.ts diff --git a/package-lock.json b/package-lock.json index 96b73a73..35c0e71e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4526,7 +4526,7 @@ }, "sdk": { "name": "@invariant-labs/sdk", - "version": "0.9.68", + "version": "0.9.70", "dev": true, "dependencies": { "@project-serum/anchor": "0.21.0", diff --git a/package.json b/package.json index 238076e0..4f2074db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "scripts": { - "test:invariant-all": "npm run test:swap && npm run test:multiple-swap && npm run test:cross && npm run test:cross-both-side && npm run test:liquidity-gap && npm run test:reversed && npm run test:position && npm run test:math && npm run test:withdraw && npm run test:position-list && npm run test:claim && npm run test:protocol-fee && npm run test:target && npm run test:slippage && npm run test:position-slippage && npm run test:fee-tier && npm run test:simulate-swap && npm run test:simulate-claim-amount && npm run test:oracle && npm run test:limits && npm run test:big-swap && npm run test:compare && npm run test:tickmap && npm run test:change-fee-receiver && npm run test:random && npm run test:change-protocol-fee && npm run test:whole-liquidity && npm run test:cu && npm run test:referral-default && npm run test:referral-all && npm run test:referral-none && npm run test:referral-jupiter && npm run test:max-tick-cross", + "test:invariant-all": "npm run test:swap && npm run test:multiple-swap && npm run test:cross && npm run test:cross-both-side && npm run test:liquidity-gap && npm run test:reversed && npm run test:position && npm run test:math && npm run test:withdraw && npm run test:position-list && npm run test:claim && npm run test:protocol-fee && npm run test:target && npm run test:slippage && npm run test:position-slippage && npm run test:fee-tier && npm run test:simulate-swap && npm run test:simulate-claim-amount && npm run test:oracle && npm run test:limits && npm run test:big-swap && npm run test:compare && npm run test:tickmap && npm run test:change-fee-receiver && npm run test:remove-pool && npm run test:random && npm run test:change-protocol-fee && npm run test:whole-liquidity && npm run test:cu && npm run test:referral-default && npm run test:referral-all && npm run test:referral-none && npm run test:referral-jupiter && npm run test:max-tick-cross", "test:staker-all": "npm run test:create && npm run test:stake && npm run test:withdraw-staker && npm run test:multicall && npm run test:position-change && npm run test:math-staker && npm run test:close-stake", "test:all": "npm run test:invariant-all && npm run test:staker-all", "test:swap": "anchor test --skip-build tests/swap.spec.ts", @@ -10,6 +10,7 @@ "test:referral-none": "anchor test tests/referral-swap-none.spec.ts -- --features \"none\"", "test:referral-jupiter": "anchor test tests/referral-swap-none.spec.ts -- --features \"jupiter\"", "test:range": "anchor test --skip-build tests/liquidity-range.spec.ts", + "test:remove-pool": "anchor test --skip-build tests/remove-pool.spec.ts", "test:cross-both-side": "anchor test --skip-build tests/cross-both-side.spec.ts", "test:liquidity-gap": "anchor test --skip-build tests/liquidity-gap.spec.ts", "test:simulate-swap": "anchor test --skip-build tests/simulate-swap.spec.ts", diff --git a/programs/invariant/src/instructions/mod.rs b/programs/invariant/src/instructions/mod.rs index d3fc05b2..32ca98e0 100644 --- a/programs/invariant/src/instructions/mod.rs +++ b/programs/invariant/src/instructions/mod.rs @@ -8,6 +8,7 @@ pub mod create_position_list; pub mod create_state; pub mod create_tick; pub mod initialize_oracle; +pub mod remove_defunct_pool; pub mod remove_position; pub mod swap; pub mod transfer_position_ownership; @@ -24,6 +25,7 @@ pub use create_position_list::*; pub use create_state::*; pub use create_tick::*; pub use initialize_oracle::*; +pub use remove_defunct_pool::*; pub use remove_position::*; pub use swap::*; pub use transfer_position_ownership::*; diff --git a/programs/invariant/src/instructions/remove_defunct_pool.rs b/programs/invariant/src/instructions/remove_defunct_pool.rs new file mode 100644 index 00000000..4704556c --- /dev/null +++ b/programs/invariant/src/instructions/remove_defunct_pool.rs @@ -0,0 +1,137 @@ +use crate::get_signer; +use crate::interfaces; +use crate::interfaces::SendTokens; +use crate::structs::pool::Pool; +use crate::structs::tickmap::Tickmap; +use crate::structs::State; +use crate::ErrorCode::*; +use crate::SEED; +use anchor_lang::prelude::*; +use anchor_spl::token; +use anchor_spl::token::CloseAccount; +use anchor_spl::token::Token; +use anchor_spl::token::Transfer; +use anchor_spl::token::{Mint, TokenAccount}; + +#[derive(Accounts)] +pub struct RemoveDefunctPool<'info> { + #[account(seeds = [b"statev1".as_ref()], bump = state.load()?.bump)] + pub state: AccountLoader<'info, State>, + #[account(mut, + close = admin, + seeds = [b"poolv1", token_x.key().as_ref(), token_y.key().as_ref(), &pool.load()?.fee.v.to_le_bytes(), &pool.load()?.tick_spacing.to_le_bytes()], + bump = pool.load()?.bump + )] + pub pool: AccountLoader<'info, Pool>, + #[account(mut, + close = admin, + address = pool.load()?.tickmap @ InvalidTickmap + )] + pub tickmap: AccountLoader<'info, Tickmap>, + pub token_x: Account<'info, Mint>, + pub token_y: Account<'info, Mint>, + #[account(mut, + constraint = account_x.mint == token_x.key() @ InvalidMint, + constraint = &account_x.owner == admin.key @ InvalidOwner, + )] + pub account_x: Box>, + #[account(mut, + constraint = account_y.mint == token_y.key() @ InvalidMint, + constraint = &account_y.owner == admin.key @ InvalidOwner + )] + pub account_y: Box>, + #[account(mut, + constraint = reserve_x.mint == token_x.key() @ InvalidMint, + constraint = &reserve_x.owner == program_authority.key @ InvalidOwner, + address = pool.load()?.token_x_reserve @ InvalidTokenAccount + )] + pub reserve_x: Box>, + #[account(mut, + constraint = reserve_y.mint == token_y.key() @ InvalidMint, + constraint = &reserve_y.owner == program_authority.key @ InvalidOwner, + address = pool.load()?.token_y_reserve @ InvalidTokenAccount + )] + pub reserve_y: Box>, + #[account(address = state.load()?.admin @ InvalidAdmin)] + pub admin: Signer<'info>, + #[account(constraint = &state.load()?.authority == program_authority.key @ InvalidAuthority)] + pub program_authority: AccountInfo<'info>, + pub token_program: Program<'info, Token>, +} + +impl<'info> interfaces::send_tokens::SendTokens<'info> for RemoveDefunctPool<'info> { + fn send_x(&self) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> { + CpiContext::new( + self.token_program.to_account_info(), + Transfer { + from: self.reserve_x.to_account_info(), + to: self.account_x.to_account_info(), + authority: self.program_authority.to_account_info(), + }, + ) + } + + fn send_y(&self) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> { + CpiContext::new( + self.token_program.to_account_info(), + Transfer { + from: self.reserve_y.to_account_info(), + to: self.account_y.to_account_info(), + authority: self.program_authority.to_account_info(), + }, + ) + } +} + +impl<'info> RemoveDefunctPool<'info> { + fn close_reserve_x(&self) -> CpiContext<'_, '_, '_, 'info, CloseAccount<'info>> { + return CpiContext::new( + self.token_program.to_account_info(), + CloseAccount { + account: self.reserve_x.to_account_info(), + destination: self.admin.to_account_info(), + authority: self.program_authority.to_account_info(), + }, + ); + } + + fn close_reserve_y(&self) -> CpiContext<'_, '_, '_, 'info, CloseAccount<'info>> { + return CpiContext::new( + self.token_program.to_account_info(), + CloseAccount { + account: self.reserve_y.to_account_info(), + destination: self.admin.to_account_info(), + authority: self.program_authority.to_account_info(), + }, + ); + } + + fn validate_tickmap(&self) -> ProgramResult { + let tickmap = self.tickmap.load()?; + for tick in tickmap.bitmap.iter() { + if *tick != 0 { + return Err(InvalidTickmap.into()); + } + } + Ok(()) + } + + pub fn handler(&self) -> ProgramResult { + msg!("INVARIANT: REMOVE DEFUNCT POOL"); + + // contraint: all ticks are empty + self.validate_tickmap()?; + + let signer: &[&[&[u8]]] = get_signer!(self.state.load()?.nonce); + + let amount_x = self.reserve_x.amount; + let amount_y = self.reserve_y.amount; + + token::transfer(self.send_x().with_signer(signer), amount_x)?; + token::transfer(self.send_y().with_signer(signer), amount_y)?; + token::close_account(self.close_reserve_x().with_signer(signer))?; + token::close_account(self.close_reserve_y().with_signer(signer))?; + + Ok(()) + } +} diff --git a/programs/invariant/src/interfaces/mod.rs b/programs/invariant/src/interfaces/mod.rs index 793e2a2d..18d8a6a4 100644 --- a/programs/invariant/src/interfaces/mod.rs +++ b/programs/invariant/src/interfaces/mod.rs @@ -1,7 +1,7 @@ pub mod send_tokens; -pub mod take_tokens; pub mod take_ref_tokens; +pub mod take_tokens; pub use send_tokens::*; -pub use take_tokens::*; pub use take_ref_tokens::*; +pub use take_tokens::*; diff --git a/programs/invariant/src/interfaces/take_ref_tokens.rs b/programs/invariant/src/interfaces/take_ref_tokens.rs index 309dbd14..178d4157 100644 --- a/programs/invariant/src/interfaces/take_ref_tokens.rs +++ b/programs/invariant/src/interfaces/take_ref_tokens.rs @@ -1,6 +1,6 @@ use anchor_lang::prelude::*; use anchor_spl::token::Transfer; pub trait TakeRefTokens<'info> { - fn take_ref_x(&self, to: AccountInfo<'info>) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>>; + fn take_ref_x(&self, to: AccountInfo<'info>) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>>; fn take_ref_y(&self, to: AccountInfo<'info>) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>>; } diff --git a/programs/invariant/src/lib.rs b/programs/invariant/src/lib.rs index 6dc99c43..fe7cba23 100644 --- a/programs/invariant/src/lib.rs +++ b/programs/invariant/src/lib.rs @@ -140,6 +140,11 @@ pub mod invariant { pub fn change_fee_receiver(ctx: Context) -> ProgramResult { ctx.accounts.handler() } + + #[access_control(admin(&ctx.accounts.state, &ctx.accounts.admin))] + pub fn remove_defunct_pool(ctx: Context) -> ProgramResult { + ctx.accounts.handler() + } } fn admin(state_loader: &AccountLoader, signer: &AccountInfo) -> Result<()> { diff --git a/sdk/src/idl/invariant.ts b/sdk/src/idl/invariant.ts index ef7f64fa..94c650af 100644 --- a/sdk/src/idl/invariant.ts +++ b/sdk/src/idl/invariant.ts @@ -935,6 +935,72 @@ export type Invariant = { } ], "args": [] + }, + { + "name": "removeDefunctPool", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "pool", + "isMut": true, + "isSigner": false + }, + { + "name": "tickmap", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenX", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenY", + "isMut": false, + "isSigner": false + }, + { + "name": "accountX", + "isMut": true, + "isSigner": false + }, + { + "name": "accountY", + "isMut": true, + "isSigner": false + }, + { + "name": "reserveX", + "isMut": true, + "isSigner": false + }, + { + "name": "reserveY", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "programAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] } ], "accounts": [ @@ -2463,6 +2529,72 @@ export const IDL: Invariant = { } ], "args": [] + }, + { + "name": "removeDefunctPool", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "pool", + "isMut": true, + "isSigner": false + }, + { + "name": "tickmap", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenX", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenY", + "isMut": false, + "isSigner": false + }, + { + "name": "accountX", + "isMut": true, + "isSigner": false + }, + { + "name": "accountY", + "isMut": true, + "isSigner": false + }, + { + "name": "reserveX", + "isMut": true, + "isSigner": false + }, + { + "name": "reserveY", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "programAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] } ], "accounts": [ diff --git a/sdk/src/market.ts b/sdk/src/market.ts index 3b5b103f..9d85faec 100644 --- a/sdk/src/market.ts +++ b/sdk/src/market.ts @@ -1350,6 +1350,43 @@ export class Market { await signAndSend(tx, [signer], this.connection) } + async removeDefunctPoolInstruction(removeDefunctPool: RemoveDefunctPool) { + const { pair, accountX, accountY } = removeDefunctPool + const adminPubkey = removeDefunctPool.admin ?? this.wallet.publicKey + const { address: stateAddress } = await this.getStateAddress() + const poolAddress = await pair.getAddress(this.program.programId) + const pool = await this.getPool(pair) + + return this.program.instruction.removeDefunctPool({ + accounts: { + state: stateAddress, + pool: poolAddress, + tickmap: pool.tickmap, + tokenX: pair.tokenX, + tokenY: pair.tokenY, + accountX, + accountY, + reserveX: pool.tokenXReserve, + reserveY: pool.tokenYReserve, + admin: adminPubkey, + programAuthority: this.programAuthority, + tokenProgram: TOKEN_PROGRAM_ID + } + }) + } + + async removeDefunctPoolTransaction(removeDefunctPool: RemoveDefunctPool) { + const ix = await this.removeDefunctPoolInstruction(removeDefunctPool) + removeDefunctPool + return new Transaction().add(ix) + } + + async removeDefunctPool(removeDefunctPool: RemoveDefunctPool, signer: Keypair) { + const tx = await this.removeDefunctPoolTransaction(removeDefunctPool) + + await signAndSend(tx, [signer], this.connection) + } + async getWholeLiquidity(pair: Pair) { const poolPublicKey = await pair.getAddress(this.program.programId) const positions: Position[] = ( @@ -1672,6 +1709,13 @@ export interface ChangeFeeReceiver { feeReceiver: PublicKey } +export interface RemoveDefunctPool { + pair: Pair + accountX: PublicKey + accountY: PublicKey + admin?: PublicKey +} + export interface PositionInitData { lowerTick: number upperTick: number diff --git a/sdk/src/utils.ts b/sdk/src/utils.ts index 03447297..5901b5d8 100644 --- a/sdk/src/utils.ts +++ b/sdk/src/utils.ts @@ -210,7 +210,7 @@ export async function assertThrowsAsync(fn: Promise, word?: string) { err = e.toString() } if (word) { - const regex = new RegExp(`${word}$`) + const regex = new RegExp(`${word}`) if (!regex.test(err)) { console.log(err) throw new Error('Invalid Error message') diff --git a/tests/remove-pool.spec.ts b/tests/remove-pool.spec.ts new file mode 100644 index 00000000..abaec8e2 --- /dev/null +++ b/tests/remove-pool.spec.ts @@ -0,0 +1,224 @@ +import * as anchor from '@project-serum/anchor' +import { Keypair } from '@solana/web3.js' +import { assert } from 'chai' +import { + Market, + Pair, + calculatePriceSqrt, + LIQUIDITY_DENOMINATOR, + Network +} from '@invariant-labs/sdk' +import { Provider, BN } from '@project-serum/anchor' +import { Token, u64, TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { createToken, initMarket } from './testUtils' +import { fromFee, assertThrowsAsync, tou64 } from '@invariant-labs/sdk/src/utils' +import { FeeTier } from '@invariant-labs/sdk/lib/market' + +describe('remove pool', () => { + const provider = Provider.local() + const connection = provider.connection + // @ts-expect-error + const wallet = provider.wallet.payer as Keypair + const mintAuthority = Keypair.generate() + const positionOwner = Keypair.generate() + const admin = Keypair.generate() + const feeTier: FeeTier = { fee: fromFee(new BN(20)), tickSpacing: 4 } + let market: Market + let pair: Pair + let tokenX: Token + let tokenY: Token + let initTick: number + let xOwnerAmount: u64 + let yOwnerAmount: u64 + before(async () => { + market = await Market.build( + Network.LOCAL, + provider.wallet, + connection, + anchor.workspace.Invariant.programId + ) + + // Request airdrops + await Promise.all([ + connection.requestAirdrop(wallet.publicKey, 1e9), + connection.requestAirdrop(mintAuthority.publicKey, 1e9), + connection.requestAirdrop(admin.publicKey, 1e9), + connection.requestAirdrop(positionOwner.publicKey, 1e9) + ]) + // Create pair + const tokens = await Promise.all([ + createToken(connection, wallet, mintAuthority), + createToken(connection, wallet, mintAuthority) + ]) + pair = new Pair(tokens[0].publicKey, tokens[1].publicKey, feeTier) + tokenX = new Token(connection, pair.tokenX, TOKEN_PROGRAM_ID, wallet) + tokenY = new Token(connection, pair.tokenY, TOKEN_PROGRAM_ID, wallet) + }) + + it('#init()', async () => { + initTick = -23028 + await initMarket(market, [pair], admin, initTick) + }) + + it('#removeDefunctPool()', async () => { + const adminTokenXAccount = await tokenX.createAccount(admin.publicKey) + const adminTokenYAccount = await tokenY.createAccount(admin.publicKey) + + const poolState = await market.getPool(pair) + + await market.removeDefunctPool( + { pair, admin: admin.publicKey, accountX: adminTokenXAccount, accountY: adminTokenYAccount }, + admin + ) + await assertThrowsAsync(market.getPool(pair), 'Error: Account does not exist') + await assertThrowsAsync( + market.program.account.tickmap.fetch(poolState.tickmap), + 'Error: Account does not exist' + ) + + await assertThrowsAsync( + tokenX.getAccountInfo(poolState.tokenXReserve), + 'Error: Failed to find account' + ) + await assertThrowsAsync( + tokenY.getAccountInfo(poolState.tokenYReserve), + 'Error: Failed to find account' + ) + }) + + it('#removeDefunctPool() cannot as not admin', async () => { + await market.createPool({ + pair, + payer: admin, + initTick + }) + + const positionOwnerTokenXAccount = await tokenX.createAccount(positionOwner.publicKey) + const positionOwnerTokenYAccount = await tokenY.createAccount(positionOwner.publicKey) + + await assertThrowsAsync( + market.removeDefunctPool( + { + pair, + admin: positionOwner.publicKey, + accountX: positionOwnerTokenXAccount, + accountY: positionOwnerTokenYAccount + }, + positionOwner + ), + 'custom program error: 0x1787' + ) + }) + + it('#removeDefunctPool() cannot with existing position', async () => { + await market.createPositionList(positionOwner.publicKey, positionOwner) + + // checks position list + const positionList = await market.getPositionList(positionOwner.publicKey) + assert.equal(positionList.head, 0) + + const lowerTick = -22980 + const upperTick = 0 + + await market.createTick( + { + pair, + index: lowerTick, + payer: admin.publicKey + }, + admin + ) + + await market.createTick( + { + pair, + index: upperTick, + payer: admin.publicKey + }, + admin + ) + + const userTokenXAccount = await tokenX.createAccount(positionOwner.publicKey) + const userTokenYAccount = await tokenY.createAccount(positionOwner.publicKey) + + xOwnerAmount = tou64(1e10) + yOwnerAmount = tou64(1e10) + + await tokenX.mintTo(userTokenXAccount, mintAuthority.publicKey, [mintAuthority], xOwnerAmount) + await tokenY.mintTo(userTokenYAccount, mintAuthority.publicKey, [mintAuthority], yOwnerAmount) + + const liquidityDelta = { v: LIQUIDITY_DENOMINATOR.muln(10_000) } + const positionIndex = 0 + await market.initPosition( + { + pair, + owner: positionOwner.publicKey, + userTokenX: userTokenXAccount, + userTokenY: userTokenYAccount, + lowerTick, + upperTick, + liquidityDelta, + knownPrice: calculatePriceSqrt(initTick), + slippage: { v: new BN(0) } + }, + positionOwner + ) + await market.getPosition(positionOwner.publicKey, positionIndex) + + const adminTokenXAccount = await tokenX.createAccount(admin.publicKey) + const adminTokenYAccount = await tokenY.createAccount(admin.publicKey) + + await assertThrowsAsync( + market.removeDefunctPool( + { + pair, + admin: admin.publicKey, + accountX: adminTokenXAccount, + accountY: adminTokenYAccount + }, + admin + ), + 'custom program error: 0x178b' + ) + }) + + it('#removeDefunctPool() after removing all positions', async () => { + const userTokenXAccount = await tokenX.createAccount(positionOwner.publicKey) + const userTokenYAccount = await tokenY.createAccount(positionOwner.publicKey) + + await market.removePosition( + { + pair, + index: 0, + owner: positionOwner.publicKey, + userTokenX: userTokenXAccount, + userTokenY: userTokenYAccount + }, + positionOwner + ) + + const adminTokenXAccount = await tokenX.createAccount(admin.publicKey) + const adminTokenYAccount = await tokenY.createAccount(admin.publicKey) + + const poolState = await market.getPool(pair) + + await market.removeDefunctPool( + { pair, admin: admin.publicKey, accountX: adminTokenXAccount, accountY: adminTokenYAccount }, + admin + ) + await assertThrowsAsync(market.getPool(pair), 'Error: Account does not exist') + await assertThrowsAsync( + market.program.account.tickmap.fetch(poolState.tickmap), + 'Error: Account does not exist' + ) + + await assertThrowsAsync( + tokenX.getAccountInfo(poolState.tokenXReserve), + 'Error: Failed to find account' + ) + await assertThrowsAsync( + tokenY.getAccountInfo(poolState.tokenYReserve), + 'Error: Failed to find account' + ) + }) +}) From c94761c48e41b6fe2d33fe70beac2aa4e49c7471 Mon Sep 17 00:00:00 2001 From: DevRozaDev <158298065+DevRozaDev@users.noreply.github.com> Date: Tue, 21 Jan 2025 13:22:59 +0100 Subject: [PATCH 2/4] remove_defunct_pool: move accounts to ATAs initialized if needed --- programs/invariant/Cargo.toml | 2 +- .../src/instructions/remove_defunct_pool.rs | 18 +++++++---- sdk/src/idl/invariant.ts | 30 ++++++++++++++++++ sdk/src/market.ts | 15 ++++++--- sdk/src/token.ts | 11 +++++++ tests/remove-pool.spec.ts | 31 +++---------------- 6 files changed, 68 insertions(+), 39 deletions(-) create mode 100644 sdk/src/token.ts diff --git a/programs/invariant/Cargo.toml b/programs/invariant/Cargo.toml index 2f14df07..17159059 100644 --- a/programs/invariant/Cargo.toml +++ b/programs/invariant/Cargo.toml @@ -21,7 +21,7 @@ all = [] [dependencies] decimal = { path = "decimal" } -anchor-lang = "0.21.0" +anchor-lang = { version = "0.21.0", features = ['init-if-needed'] } anchor-spl = "0.21.0" integer-sqrt = "0.1.5" uint = "0.9.1" diff --git a/programs/invariant/src/instructions/remove_defunct_pool.rs b/programs/invariant/src/instructions/remove_defunct_pool.rs index 4704556c..b88d8014 100644 --- a/programs/invariant/src/instructions/remove_defunct_pool.rs +++ b/programs/invariant/src/instructions/remove_defunct_pool.rs @@ -7,6 +7,7 @@ use crate::structs::State; use crate::ErrorCode::*; use crate::SEED; use anchor_lang::prelude::*; +use anchor_spl::associated_token::AssociatedToken; use anchor_spl::token; use anchor_spl::token::CloseAccount; use anchor_spl::token::Token; @@ -30,14 +31,16 @@ pub struct RemoveDefunctPool<'info> { pub tickmap: AccountLoader<'info, Tickmap>, pub token_x: Account<'info, Mint>, pub token_y: Account<'info, Mint>, - #[account(mut, - constraint = account_x.mint == token_x.key() @ InvalidMint, - constraint = &account_x.owner == admin.key @ InvalidOwner, + #[account(init_if_needed, + payer = admin, + associated_token::mint = token_x, + associated_token::authority = admin )] pub account_x: Box>, - #[account(mut, - constraint = account_y.mint == token_y.key() @ InvalidMint, - constraint = &account_y.owner == admin.key @ InvalidOwner + #[account(init_if_needed, + payer = admin, + associated_token::mint = token_y, + associated_token::authority = admin )] pub account_y: Box>, #[account(mut, @@ -57,6 +60,9 @@ pub struct RemoveDefunctPool<'info> { #[account(constraint = &state.load()?.authority == program_authority.key @ InvalidAuthority)] pub program_authority: AccountInfo<'info>, pub token_program: Program<'info, Token>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, } impl<'info> interfaces::send_tokens::SendTokens<'info> for RemoveDefunctPool<'info> { diff --git a/sdk/src/idl/invariant.ts b/sdk/src/idl/invariant.ts index 94c650af..918da754 100644 --- a/sdk/src/idl/invariant.ts +++ b/sdk/src/idl/invariant.ts @@ -998,6 +998,21 @@ export type Invariant = { "name": "tokenProgram", "isMut": false, "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false } ], "args": [] @@ -2592,6 +2607,21 @@ export const IDL: Invariant = { "name": "tokenProgram", "isMut": false, "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false } ], "args": [] diff --git a/sdk/src/market.ts b/sdk/src/market.ts index 9d85faec..d8e7a85e 100644 --- a/sdk/src/market.ts +++ b/sdk/src/market.ts @@ -1,5 +1,5 @@ import { BN, Program, utils, Provider } from '@project-serum/anchor' -import { Token, TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { ASSOCIATED_TOKEN_PROGRAM_ID, Token, TOKEN_PROGRAM_ID } from '@solana/spl-token' import { ComputeBudgetProgram, Connection, @@ -39,6 +39,7 @@ import { Invariant, IDL } from './idl/invariant' import { DENOMINATOR, IWallet, Pair, signAndSend } from '.' import { getMarketAddress, Network } from './network' import { bs58 } from '@project-serum/anchor/dist/cjs/utils/bytes' +import { getAssociatedTokenAddress } from './token' const POSITION_SEED = 'positionv1' const TICK_SEED = 'tickv1' @@ -1351,12 +1352,15 @@ export class Market { } async removeDefunctPoolInstruction(removeDefunctPool: RemoveDefunctPool) { - const { pair, accountX, accountY } = removeDefunctPool + const { pair } = removeDefunctPool const adminPubkey = removeDefunctPool.admin ?? this.wallet.publicKey const { address: stateAddress } = await this.getStateAddress() const poolAddress = await pair.getAddress(this.program.programId) const pool = await this.getPool(pair) + const accountX = getAssociatedTokenAddress(adminPubkey, pair.tokenX) + const accountY = getAssociatedTokenAddress(adminPubkey, pair.tokenY) + return this.program.instruction.removeDefunctPool({ accounts: { state: stateAddress, @@ -1370,7 +1374,10 @@ export class Market { reserveY: pool.tokenYReserve, admin: adminPubkey, programAuthority: this.programAuthority, - tokenProgram: TOKEN_PROGRAM_ID + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId } }) } @@ -1711,8 +1718,6 @@ export interface ChangeFeeReceiver { export interface RemoveDefunctPool { pair: Pair - accountX: PublicKey - accountY: PublicKey admin?: PublicKey } diff --git a/sdk/src/token.ts b/sdk/src/token.ts new file mode 100644 index 00000000..1c2c9d48 --- /dev/null +++ b/sdk/src/token.ts @@ -0,0 +1,11 @@ +import { TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { PublicKey } from '@solana/web3.js' + +// function only available in higher versions of spl-token than current 0.1.8 +export function getAssociatedTokenAddress(owner: PublicKey, token: PublicKey) { + const [address] = PublicKey.findProgramAddressSync( + [owner.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), token.toBuffer()], + ASSOCIATED_TOKEN_PROGRAM_ID + ) + return address +} diff --git a/tests/remove-pool.spec.ts b/tests/remove-pool.spec.ts index abaec8e2..df394bd6 100644 --- a/tests/remove-pool.spec.ts +++ b/tests/remove-pool.spec.ts @@ -61,15 +61,9 @@ describe('remove pool', () => { }) it('#removeDefunctPool()', async () => { - const adminTokenXAccount = await tokenX.createAccount(admin.publicKey) - const adminTokenYAccount = await tokenY.createAccount(admin.publicKey) - const poolState = await market.getPool(pair) - await market.removeDefunctPool( - { pair, admin: admin.publicKey, accountX: adminTokenXAccount, accountY: adminTokenYAccount }, - admin - ) + await market.removeDefunctPool({ pair, admin: admin.publicKey }, admin) await assertThrowsAsync(market.getPool(pair), 'Error: Account does not exist') await assertThrowsAsync( market.program.account.tickmap.fetch(poolState.tickmap), @@ -93,16 +87,11 @@ describe('remove pool', () => { initTick }) - const positionOwnerTokenXAccount = await tokenX.createAccount(positionOwner.publicKey) - const positionOwnerTokenYAccount = await tokenY.createAccount(positionOwner.publicKey) - await assertThrowsAsync( market.removeDefunctPool( { pair, - admin: positionOwner.publicKey, - accountX: positionOwnerTokenXAccount, - accountY: positionOwnerTokenYAccount + admin: positionOwner.publicKey }, positionOwner ), @@ -165,16 +154,11 @@ describe('remove pool', () => { ) await market.getPosition(positionOwner.publicKey, positionIndex) - const adminTokenXAccount = await tokenX.createAccount(admin.publicKey) - const adminTokenYAccount = await tokenY.createAccount(admin.publicKey) - await assertThrowsAsync( market.removeDefunctPool( { pair, - admin: admin.publicKey, - accountX: adminTokenXAccount, - accountY: adminTokenYAccount + admin: admin.publicKey }, admin ), @@ -196,16 +180,9 @@ describe('remove pool', () => { }, positionOwner ) - - const adminTokenXAccount = await tokenX.createAccount(admin.publicKey) - const adminTokenYAccount = await tokenY.createAccount(admin.publicKey) - const poolState = await market.getPool(pair) - await market.removeDefunctPool( - { pair, admin: admin.publicKey, accountX: adminTokenXAccount, accountY: adminTokenYAccount }, - admin - ) + await market.removeDefunctPool({ pair, admin: admin.publicKey }, admin) await assertThrowsAsync(market.getPool(pair), 'Error: Account does not exist') await assertThrowsAsync( market.program.account.tickmap.fetch(poolState.tickmap), From 50419e94c0360a81377222c57b7471f960cfb8de Mon Sep 17 00:00:00 2001 From: DevRozaDev <158298065+DevRozaDev@users.noreply.github.com> Date: Tue, 21 Jan 2025 13:56:22 +0100 Subject: [PATCH 3/4] script to find possible defunct pools --- scripts/find-defunct-pools.ts | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 scripts/find-defunct-pools.ts diff --git a/scripts/find-defunct-pools.ts b/scripts/find-defunct-pools.ts new file mode 100644 index 00000000..3124a821 --- /dev/null +++ b/scripts/find-defunct-pools.ts @@ -0,0 +1,53 @@ +import { Provider } from '@project-serum/anchor' +import { Network } from '@invariant-labs/sdk/src/network' +import { Market } from '@invariant-labs/sdk/src' +import { Decimal, PoolStructure } from '@invariant-labs/sdk/lib/market' +import { DECIMAL } from '@invariant-labs/sdk/lib/utils' +import { DENOMINATOR } from '@invariant-labs/sdk' + +require('dotenv').config() +console.log(process.cwd()) + +const provider = Provider.local( + 'https://mainnet.helius-rpc.com/?api-key=ef843b40-9876-4a02-a181-a1e6d3e61b4c' +) + +const connection = provider.connection + +const main = async () => { + const market = await Market.build(Network.MAIN, provider.wallet, connection) + + const allPools = await market.getAllPools() + const allTickmaps = Object.fromEntries( + (await market.program.account.tickmap.all()).map(x => [x.publicKey, x.account.bitmap]) + ) + + const emptyPools: PoolStructure[] = [] + + for (const poolState of allPools) { + const tickmap: number[] = allTickmaps[poolState.tickmap.toString()] + + const empty = !tickmap.some((tick: number) => tick !== 0) + if (empty) { + emptyPools.push(poolState) + console.log( + poolState.tokenX.toString(), + poolState.tokenY.toString(), + formatFee(poolState.fee), + poolState.tickSpacing.toString() + ) + } + } + + console.log(emptyPools.length) + console.log(allPools.length) +} + +main() + +export const formatFee = (fee: Decimal) => { + const feeB = BigInt(fee.v.toString()) + const feeDenominator = BigInt(DENOMINATOR) + let afterDot = (feeB % feeDenominator).toString() + return (feeB / feeDenominator).toString() + '.' + '0'.repeat(DECIMAL - afterDot.length) + afterDot +} From 1dfbd616d06f8662745a6c0f677a3641d7a22b8e Mon Sep 17 00:00:00 2001 From: DevRozaDev <158298065+DevRozaDev@users.noreply.github.com> Date: Wed, 22 Jan 2025 15:54:50 +0100 Subject: [PATCH 4/4] remove-defunct-pool: add a test where pool is not initialized by an admin, add the tests to CI --- .github/workflows/test.yml | 1 + sdk/src/market.ts | 2 +- tests/remove-pool.spec.ts | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 81385845..48b8b547 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,6 +39,7 @@ jobs: 'npm run test:change-protocol-fee', 'npm run test:tickmap', 'npm run test:change-fee-receiver', + 'npm run test:remove-pool', 'npm run test:whole-liquidity', 'npm run test:position-change', 'npm run test:protocol-fee', diff --git a/sdk/src/market.ts b/sdk/src/market.ts index d8e7a85e..fac466ae 100644 --- a/sdk/src/market.ts +++ b/sdk/src/market.ts @@ -1384,7 +1384,7 @@ export class Market { async removeDefunctPoolTransaction(removeDefunctPool: RemoveDefunctPool) { const ix = await this.removeDefunctPoolInstruction(removeDefunctPool) - removeDefunctPool + return new Transaction().add(ix) } diff --git a/tests/remove-pool.spec.ts b/tests/remove-pool.spec.ts index df394bd6..fe266c0c 100644 --- a/tests/remove-pool.spec.ts +++ b/tests/remove-pool.spec.ts @@ -80,6 +80,22 @@ describe('remove pool', () => { ) }) + it('#removeDefunctPool() initialized by another signer', async () => { + await market.createPool({ + pair, + payer: positionOwner, + initTick + }) + + await market.removeDefunctPool( + { + pair, + admin: admin.publicKey + }, + admin + ) + }) + it('#removeDefunctPool() cannot as not admin', async () => { await market.createPool({ pair,