diff --git a/idl/drip.ts b/idl/drip.ts index ccab5c9..493b22a 100644 --- a/idl/drip.ts +++ b/idl/drip.ts @@ -839,6 +839,150 @@ export type Drip = { } ], "args": [] + }, + { + "name": "adminWithdraw", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "vaultProtoConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "vault", + "isMut": false, + "isSigner": false + }, + { + "name": "vaultTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "destinationTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "closeVaultPeriod", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "solDestination", + "isMut": true, + "isSigner": false + } + ] + }, + { + "name": "vaultProtoConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "vault", + "isMut": false, + "isSigner": false + }, + { + "name": "vaultPeriod", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "closeVault", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "solDestination", + "isMut": true, + "isSigner": false + } + ] + }, + { + "name": "vaultProtoConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "vault", + "isMut": true, + "isSigner": false + }, + { + "name": "vaultTokenAAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "vaultTokenBAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "closeVaultProtoConfig", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "solDestination", + "isMut": true, + "isSigner": false + } + ] + }, + { + "name": "vaultProtoConfig", + "isMut": true, + "isSigner": false + } + ], + "args": [] } ], "accounts": [ @@ -1286,6 +1430,21 @@ export type Drip = { "code": 6028, "name": "InvalidSolDestination", "msg": "Invalid sol_destination" + }, + { + "code": 6029, + "name": "VaultPeriodDarNotEmpty", + "msg": "Vault Period DAR is not 0" + }, + { + "code": 6030, + "name": "VaultDripAmountNotZero", + "msg": "Vault drip_amount is not 0" + }, + { + "code": 6031, + "name": "WithdrawADeprecated", + "msg": "Withdraw_a is deprecated, use admin_withdraw instead" } ] }; @@ -2131,6 +2290,150 @@ export const IDL: Drip = { } ], "args": [] + }, + { + "name": "adminWithdraw", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "vaultProtoConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "vault", + "isMut": false, + "isSigner": false + }, + { + "name": "vaultTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "destinationTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "closeVaultPeriod", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "solDestination", + "isMut": true, + "isSigner": false + } + ] + }, + { + "name": "vaultProtoConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "vault", + "isMut": false, + "isSigner": false + }, + { + "name": "vaultPeriod", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "closeVault", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "solDestination", + "isMut": true, + "isSigner": false + } + ] + }, + { + "name": "vaultProtoConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "vault", + "isMut": true, + "isSigner": false + }, + { + "name": "vaultTokenAAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "vaultTokenBAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "closeVaultProtoConfig", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "solDestination", + "isMut": true, + "isSigner": false + } + ] + }, + { + "name": "vaultProtoConfig", + "isMut": true, + "isSigner": false + } + ], + "args": [] } ], "accounts": [ @@ -2578,6 +2881,21 @@ export const IDL: Drip = { "code": 6028, "name": "InvalidSolDestination", "msg": "Invalid sol_destination" + }, + { + "code": 6029, + "name": "VaultPeriodDarNotEmpty", + "msg": "Vault Period DAR is not 0" + }, + { + "code": 6030, + "name": "VaultDripAmountNotZero", + "msg": "Vault drip_amount is not 0" + }, + { + "code": 6031, + "name": "WithdrawADeprecated", + "msg": "Withdraw_a is deprecated, use admin_withdraw instead" } ] }; \ No newline at end of file diff --git a/idl/idl.json b/idl/idl.json index 606339d..d3373d8 100644 --- a/idl/idl.json +++ b/idl/idl.json @@ -839,6 +839,150 @@ } ], "args": [] + }, + { + "name": "adminWithdraw", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "vaultProtoConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "vault", + "isMut": false, + "isSigner": false + }, + { + "name": "vaultTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "destinationTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "closeVaultPeriod", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "solDestination", + "isMut": true, + "isSigner": false + } + ] + }, + { + "name": "vaultProtoConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "vault", + "isMut": false, + "isSigner": false + }, + { + "name": "vaultPeriod", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "closeVault", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "solDestination", + "isMut": true, + "isSigner": false + } + ] + }, + { + "name": "vaultProtoConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "vault", + "isMut": true, + "isSigner": false + }, + { + "name": "vaultTokenAAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "vaultTokenBAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "closeVaultProtoConfig", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "solDestination", + "isMut": true, + "isSigner": false + } + ] + }, + { + "name": "vaultProtoConfig", + "isMut": true, + "isSigner": false + } + ], + "args": [] } ], "accounts": [ @@ -1286,6 +1430,21 @@ "code": 6028, "name": "InvalidSolDestination", "msg": "Invalid sol_destination" + }, + { + "code": 6029, + "name": "VaultPeriodDarNotEmpty", + "msg": "Vault Period DAR is not 0" + }, + { + "code": 6030, + "name": "VaultDripAmountNotZero", + "msg": "Vault drip_amount is not 0" + }, + { + "code": 6031, + "name": "WithdrawADeprecated", + "msg": "Withdraw_a is deprecated, use admin_withdraw instead" } ] } \ No newline at end of file diff --git a/programs/drip/src/actions/admin.rs b/programs/drip/src/actions/admin.rs index 90a3cf5..6e9d688 100644 --- a/programs/drip/src/actions/admin.rs +++ b/programs/drip/src/actions/admin.rs @@ -1,5 +1,9 @@ use crate::errors::DripError; -use crate::instruction_accounts::{InitializeVaultAccountsBumps, WithdrawAAccounts}; +use crate::instruction_accounts::{ + CloseVaultAccounts, CloseVaultPeriodAccounts, CloseVaultProtoConfigAccounts, + InitializeVaultAccountsBumps, WithdrawAAccounts, WithdrawAccounts, +}; +use crate::interactions::close_account::CloseAccount; use crate::interactions::executor::CpiExecutor; use crate::interactions::transfer_token::TransferToken; use crate::state::{ @@ -29,6 +33,18 @@ pub enum Admin<'a, 'info> { WithdrawA { accounts: &'a mut WithdrawAAccounts<'info>, }, + Withdraw { + accounts: &'a mut WithdrawAccounts<'info>, + }, + CloseVaultPeriod { + accounts: &'a mut CloseVaultPeriodAccounts<'info>, + }, + CloseVault { + accounts: &'a mut CloseVaultAccounts<'info>, + }, + CloseVaultProtoConfig { + accounts: &'a mut CloseVaultProtoConfigAccounts<'info>, + }, } impl<'a, 'info> Validatable for Admin<'a, 'info> { @@ -91,7 +107,10 @@ impl<'a, 'info> Validatable for Admin<'a, 'info> { DripError::InvalidNumSwaps ); } - Admin::WithdrawA { accounts } => { + Admin::WithdrawA { .. } => { + return Err(DripError::WithdrawADeprecated.into()); + } + Admin::Withdraw { accounts } => { validate!( accounts.admin.key() == accounts.vault_proto_config.admin, DripError::SignerIsNotAdmin @@ -101,25 +120,43 @@ impl<'a, 'info> Validatable for Admin<'a, 'info> { accounts.vault_proto_config.key() == accounts.vault.proto_config, DripError::InvalidVaultProtoConfigReference ); - + } + Admin::CloseVaultPeriod { accounts } => { validate!( - accounts.vault_token_a_account.key() == accounts.vault.token_a_account, - DripError::IncorrectVaultTokenAccount + accounts.vault_proto_config.admin == accounts.common.admin.key(), + DripError::InvalidVaultProtoConfigReference ); - validate!( - accounts.admin_token_a_account.owner == accounts.admin.key(), - DripError::InvalidOwner + accounts.vault_period.dar.eq(&0), + DripError::VaultPeriodDarNotEmpty ); - validate!( - accounts.vault.drip_amount == 0, - DripError::CannotWithdrawAWithNonZeroDripAmount + accounts.vault_proto_config.key() == accounts.vault.proto_config.key(), + DripError::InvalidVaultReference ); - validate!( - accounts.vault_token_a_account.amount > 0, - DripError::VaultTokenAAccountIsEmpty + accounts.vault.drip_amount.eq(&0), + DripError::VaultDripAmountNotZero + ); + } + Admin::CloseVault { accounts } => { + validate!( + accounts.vault.proto_config.key() == accounts.vault_proto_config.key(), + DripError::InvalidVaultProtoConfigReference + ); + validate!( + accounts.vault_proto_config.admin == accounts.common.admin.key(), + DripError::InvalidVaultProtoConfigReference + ); + validate!( + accounts.vault.drip_amount.eq(&0), + DripError::VaultDripAmountNotZero + ); + } + Admin::CloseVaultProtoConfig { accounts } => { + validate!( + accounts.vault_proto_config.admin == accounts.common.admin.key(), + DripError::InvalidVaultProtoConfigReference ); } } @@ -154,19 +191,62 @@ impl<'a, 'info> Executable for Admin<'a, 'info> { .vault .set_whitelisted_swaps(params.whitelisted_swaps); } - Admin::WithdrawA { accounts } => { - let withdrawable_amount_a = accounts.vault_token_a_account.amount; + Admin::Withdraw { accounts } => { + let withdrawable_amount = accounts.vault_token_account.amount; - let transfer_a_to_admin = TransferToken::new( + let transfer_token_from_vault = TransferToken::new( + &accounts.token_program, + &accounts.vault_token_account, + &accounts.destination_token_account, + &accounts.vault.to_account_info(), + withdrawable_amount, + ); + + let signer: &Vault = &accounts.vault; + cpi_executor.execute_all(vec![&Some(&transfer_token_from_vault)], signer)?; + } + Admin::CloseVaultPeriod { accounts } => { + accounts + .vault_period + .close(accounts.common.sol_destination.to_account_info())?; + } + Admin::CloseVault { accounts } => { + /* STATE UPDATES (EFFECTS) */ + let close_vault_token_a_account = CloseAccount::new( &accounts.token_program, &accounts.vault_token_a_account, - &accounts.admin_token_a_account, + &accounts.common.sol_destination, &accounts.vault.to_account_info(), - withdrawable_amount_a, ); + let close_vault_token_b_account = CloseAccount::new( + &accounts.token_program, + &accounts.vault_token_b_account, + &accounts.common.sol_destination, + &accounts.vault.to_account_info(), + ); + + // /* MANUAL CPI (INTERACTIONS) */ let signer: &Vault = &accounts.vault; - cpi_executor.execute_all(vec![&Some(&transfer_a_to_admin)], signer)?; + cpi_executor.execute_all( + vec![ + &Some(&close_vault_token_a_account), + &Some(&close_vault_token_b_account), + ], + signer, + )?; + + accounts + .vault + .close(accounts.common.sol_destination.to_account_info())?; + } + Admin::CloseVaultProtoConfig { accounts } => { + accounts + .vault_proto_config + .close(accounts.common.sol_destination.to_account_info())?; + } + Admin::WithdrawA { .. } => { + return Err(DripError::WithdrawADeprecated.into()); } } diff --git a/programs/drip/src/errors/mod.rs b/programs/drip/src/errors/mod.rs index 832cb68..acb832e 100644 --- a/programs/drip/src/errors/mod.rs +++ b/programs/drip/src/errors/mod.rs @@ -61,4 +61,10 @@ pub enum DripError { VaultTokenAAccountIsEmpty, #[msg("Invalid sol_destination")] InvalidSolDestination, + #[msg("Vault Period DAR is not 0")] + VaultPeriodDarNotEmpty, + #[msg("Vault drip_amount is not 0")] + VaultDripAmountNotZero, + #[msg("Withdraw_a is deprecated, use admin_withdraw instead")] + WithdrawADeprecated, } diff --git a/programs/drip/src/instruction_accounts/admin.rs b/programs/drip/src/instruction_accounts/admin.rs index 176eacd..3480f9e 100644 --- a/programs/drip/src/instruction_accounts/admin.rs +++ b/programs/drip/src/instruction_accounts/admin.rs @@ -1,4 +1,4 @@ -use crate::state::{Vault, VaultProtoConfig}; +use crate::state::{Vault, VaultPeriod, VaultProtoConfig}; use anchor_lang::prelude::*; use anchor_spl::associated_token::AssociatedToken; use anchor_spl::token::{Mint, Token, TokenAccount}; @@ -104,3 +104,71 @@ pub struct WithdrawAAccounts<'info> { pub token_program: Program<'info, Token>, } + +#[derive(Accounts)] +pub struct WithdrawAccounts<'info> { + #[account(mut)] + pub admin: Signer<'info>, + + pub vault_proto_config: Account<'info, VaultProtoConfig>, + + pub vault: Account<'info, Vault>, + + // mut needed because we are changing state + #[account(mut)] + pub vault_token_account: Account<'info, TokenAccount>, + + // mut needed because we are changing state + #[account(mut)] + pub destination_token_account: Account<'info, TokenAccount>, + + pub token_program: Program<'info, Token>, +} + +#[derive(Accounts)] +pub struct CloseCommonAccounts<'info> { + pub admin: Signer<'info>, + + #[account(mut)] + /// CHECK: We don't care what this account is + pub sol_destination: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct CloseVaultPeriodAccounts<'info> { + pub common: CloseCommonAccounts<'info>, + + pub vault_proto_config: Account<'info, VaultProtoConfig>, + + pub vault: Account<'info, Vault>, + + #[account(mut)] + pub vault_period: Account<'info, VaultPeriod>, +} + +#[derive(Accounts)] +pub struct CloseVaultAccounts<'info> { + pub common: CloseCommonAccounts<'info>, + + pub vault_proto_config: Account<'info, VaultProtoConfig>, + + #[account(mut)] + pub vault: Account<'info, Vault>, + + /* TOKEN ACCOUNTS */ + #[account(mut)] + pub vault_token_a_account: Account<'info, TokenAccount>, + + #[account(mut)] + pub vault_token_b_account: Account<'info, TokenAccount>, + + pub token_program: Program<'info, Token>, +} + +#[derive(Accounts)] +pub struct CloseVaultProtoConfigAccounts<'info> { + pub common: CloseCommonAccounts<'info>, + + #[account(mut)] + pub vault_proto_config: Account<'info, VaultProtoConfig>, +} diff --git a/programs/drip/src/interactions/close_account.rs b/programs/drip/src/interactions/close_account.rs index 3337dfe..a349398 100644 --- a/programs/drip/src/interactions/close_account.rs +++ b/programs/drip/src/interactions/close_account.rs @@ -1,5 +1,6 @@ use std::fmt; +use crate::sign; use crate::state::traits::{CPI, PDA}; use anchor_lang::prelude::*; use anchor_lang::solana_program::program::invoke_signed; @@ -45,7 +46,7 @@ impl<'info> fmt::Debug for CloseAccount<'info> { } impl<'info> CPI for CloseAccount<'info> { - fn execute(&self, _: &dyn PDA) -> Result<()> { + fn execute(&self, signer: &dyn PDA) -> Result<()> { invoke_signed( &close_account( self.token_program.key, @@ -60,7 +61,7 @@ impl<'info> CPI for CloseAccount<'info> { self.destination.to_account_info(), self.authority.to_account_info(), ], - &[], + &[sign!(signer)], )?; Ok(()) } diff --git a/programs/drip/src/lib.rs b/programs/drip/src/lib.rs index ffe703d..be9125f 100644 --- a/programs/drip/src/lib.rs +++ b/programs/drip/src/lib.rs @@ -113,6 +113,30 @@ pub mod drip { accounts: ctx.accounts, }) } + + pub fn admin_withdraw(ctx: Context) -> Result<()> { + handle_action(Admin::Withdraw { + accounts: ctx.accounts, + }) + } + + pub fn close_vault_period(ctx: Context) -> Result<()> { + handle_action(Admin::CloseVaultPeriod { + accounts: ctx.accounts, + }) + } + + pub fn close_vault(ctx: Context) -> Result<()> { + handle_action(Admin::CloseVault { + accounts: ctx.accounts, + }) + } + + pub fn close_vault_proto_config(ctx: Context) -> Result<()> { + handle_action(Admin::CloseVaultProtoConfig { + accounts: ctx.accounts, + }) + } } fn handle_action(action: impl Validatable + Executable) -> Result<()> { diff --git a/tests/integration-tests/batch1/close.ts b/tests/integration-tests/batch1/close.ts new file mode 100644 index 0000000..c47019c --- /dev/null +++ b/tests/integration-tests/batch1/close.ts @@ -0,0 +1,313 @@ +import "should"; +import { SolUtil } from "../../utils/sol.util"; +import { TokenUtil } from "../../utils/token.util"; +import { + amount, + Denom, + findAssociatedTokenAddress, + generatePair, + generatePairs, +} from "../../utils/common.util"; +import { + closePositionWrapper, + deployVault, + deployVaultPeriod, + deployVaultProtoConfig, + depositToVault, + sleep, +} from "../../utils/setup.util"; +import { Keypair, PublicKey } from "@solana/web3.js"; +import { AccountUtil } from "../../utils/account.util"; +import { initLog } from "../../utils/log.util"; +import { VaultUtil } from "../../utils/vault.util"; +import { TestUtil } from "../../utils/config.util"; + +describe("#close", testCloseActions); + +function testCloseActions() { + initLog(); + + let payerKeypair: Keypair; + let vaultAdminKeypair: Keypair; + + let vaultProtoConfig: PublicKey; + let vault: PublicKey; + let vaultPeriods: PublicKey[]; + let vaultTokenAAccount: PublicKey; + let vaultTokenBAccount: PublicKey; + + let closePosition: ReturnType; + + beforeEach(async () => { + // https://discord.com/channels/889577356681945098/889702325231427584/910244405443715092 + // sleep to progress to the next block + await sleep(500); + const user = generatePair(); + const tokenOwnerKeypair = generatePair(); + [payerKeypair, vaultAdminKeypair] = generatePairs(4); + await Promise.all([ + SolUtil.fundAccount(user.publicKey, SolUtil.solToLamports(0.1)), + SolUtil.fundAccount(payerKeypair.publicKey, SolUtil.solToLamports(0.1)), + SolUtil.fundAccount( + tokenOwnerKeypair.publicKey, + SolUtil.solToLamports(0.1), + ), + SolUtil.fundAccount( + vaultAdminKeypair.publicKey, + SolUtil.solToLamports(0.1), + ), + ]); + + const tokenA = await TokenUtil.createMint( + tokenOwnerKeypair.publicKey, + null, + 6, + payerKeypair, + ); + + const tokenB = await TokenUtil.createMint( + tokenOwnerKeypair.publicKey, + null, + 6, + payerKeypair, + ); + + vaultProtoConfig = await deployVaultProtoConfig( + 1, + 5, + 5, + 0, + vaultAdminKeypair.publicKey, + ); + + const vaultTreasuryTokenBAccount = await TokenUtil.createTokenAccount( + tokenB, + payerKeypair.publicKey, + payerKeypair, + ); + + vault = ( + await deployVault( + tokenA.address, + tokenB.address, + vaultTreasuryTokenBAccount, + vaultProtoConfig, + undefined, + vaultAdminKeypair, + ) + ).publicKey; + + [vaultTokenAAccount, vaultTokenBAccount] = await Promise.all([ + findAssociatedTokenAddress(vault, tokenA.address), + findAssociatedTokenAddress(vault, tokenB.address), + ]); + + vaultPeriods = ( + await Promise.all( + [...Array(6).keys()].map((i) => + deployVaultPeriod( + vaultProtoConfig, + vault, + tokenA.address, + tokenB.address, + i, + ), + ), + ) + ).map((pda) => pda.publicKey); + + const userTokenAAccount = await TokenUtil.getOrCreateAssociatedTokenAccount( + tokenA, + user.publicKey, + user, + ); + const mintAmount = TokenUtil.scaleAmount(amount(2, Denom.Thousand), tokenA); + await TokenUtil.mintTo({ + payer: user, + token: tokenA, + mintAuthority: tokenOwnerKeypair, + recipient: userTokenAAccount, + amount: mintAmount, + }); + + const userTokenBAccount = await TokenUtil.getOrCreateAssociatedTokenAccount( + tokenB, + user.publicKey, + user, + ); + + const depositAmount = TokenUtil.scaleAmount( + amount(1, Denom.Thousand), + tokenA, + ); + const [userPositionNFTMint, userPositionAccount, userPositionNFTAccount] = + await depositToVault( + user, + tokenA, + depositAmount, + BigInt(4), + vault, + vaultPeriods[4], + userTokenAAccount, + vaultTreasuryTokenBAccount, + ); + + closePosition = closePositionWrapper( + user, + vault, + vaultProtoConfig, + userPositionAccount, + vaultTokenAAccount, + vaultTokenBAccount, + vaultTreasuryTokenBAccount, + userTokenAAccount, + userTokenBAccount, + userPositionNFTAccount, + userPositionNFTMint, + ); + }); + + describe("#closeVaultPeriod", () => { + it("should not be able to close vault period if signer is not admin", async () => { + await VaultUtil.closeVaultPeriod( + payerKeypair, + vault, + vaultProtoConfig, + vaultPeriods[0], + vaultAdminKeypair.publicKey, + ).should.be.rejectedWith(/0x177a/); + }); + + it("should not be able to close vault period if vault still has drip amount", async () => { + await VaultUtil.closeVaultPeriod( + vaultAdminKeypair, + vault, + vaultProtoConfig, + vaultPeriods[0], + vaultAdminKeypair.publicKey, + ).should.be.rejectedWith(/0x178e/); + }); + + it("should be able to close all vault periods after closing position", async () => { + const solDestBefore = await TestUtil.provider.connection.getAccountInfo( + vaultAdminKeypair.publicKey, + ); + const [i, j, k] = [0, 0, 4]; + await closePosition(vaultPeriods[i], vaultPeriods[j], vaultPeriods[k]); + for (let i = 0; i < vaultPeriods.length; i++) { + await VaultUtil.closeVaultPeriod( + vaultAdminKeypair, + vault, + vaultProtoConfig, + vaultPeriods[i], + vaultAdminKeypair.publicKey, + ); + await AccountUtil.fetchVaultPeriodAccount( + vaultPeriods[i], + ).should.be.rejectedWith(/Account does not exist or has no data/); + + const solDestAfter = await TestUtil.provider.connection.getAccountInfo( + vaultAdminKeypair.publicKey, + ); + (solDestAfter.lamports > solDestBefore.lamports).should.be.true(); + } + }); + }); + + describe("#closeVault", () => { + it("should not be able to close vault if signer is not the vault admin", async () => { + await VaultUtil.closeVault( + payerKeypair, + vault, + vaultProtoConfig, + vaultTokenAAccount, + vaultTokenBAccount, + vaultAdminKeypair.publicKey, + ).should.be.rejectedWith(/0x177a/); + }); + + it("should not be able to close vault if drip_amount is not 0", async () => { + const solDestBefore = await TestUtil.provider.connection.getAccountInfo( + vaultAdminKeypair.publicKey, + ); + await VaultUtil.closeVault( + vaultAdminKeypair, + vault, + vaultProtoConfig, + vaultTokenAAccount, + vaultTokenBAccount, + vaultAdminKeypair.publicKey, + ).should.be.rejectedWith(/0x178e/); + + const solDestAfter = await TestUtil.provider.connection.getAccountInfo( + vaultAdminKeypair.publicKey, + ); + (solDestAfter.lamports === solDestBefore.lamports).should.be.true(); + }); + + it("should be able to close vault", async () => { + const solDestBefore = await TestUtil.provider.connection.getAccountInfo( + vaultAdminKeypair.publicKey, + ); + + const [i, j, k] = [0, 0, 4]; + await closePosition(vaultPeriods[i], vaultPeriods[j], vaultPeriods[k]); + await VaultUtil.closeVault( + vaultAdminKeypair, + vault, + vaultProtoConfig, + vaultTokenAAccount, + vaultTokenBAccount, + vaultAdminKeypair.publicKey, + ); + + await AccountUtil.fetchVaultAccount(vault).should.be.rejectedWith( + /Account does not exist or has no data/, + ); + ( + (await TestUtil.provider.connection.getAccountInfo( + vaultTokenAAccount, + )) === null + ).should.be.true(); + ( + (await TestUtil.provider.connection.getAccountInfo( + vaultTokenBAccount, + )) === null + ).should.be.true(); + const solDestAfter = await TestUtil.provider.connection.getAccountInfo( + vaultAdminKeypair.publicKey, + ); + (solDestAfter.lamports > solDestBefore.lamports).should.be.true(); + }); + }); + + describe("#closeVaultProtoConfig", () => { + it("should not be able to close vault proto config if signer is not the vault admin", async () => { + await VaultUtil.closeVaultProtoConfig( + payerKeypair, + vaultProtoConfig, + vaultAdminKeypair.publicKey, + ).should.be.rejectedWith(/0x177a/); + }); + + it("should be able to close vault proto config", async () => { + const solDestBefore = await TestUtil.provider.connection.getAccountInfo( + vaultAdminKeypair.publicKey, + ); + const [i, j, k] = [0, 0, 4]; + await closePosition(vaultPeriods[i], vaultPeriods[j], vaultPeriods[k]); + await VaultUtil.closeVaultProtoConfig( + vaultAdminKeypair, + vaultProtoConfig, + vaultAdminKeypair.publicKey, + ); + await AccountUtil.fetchVaultProtoConfigAccount( + vaultProtoConfig, + ).should.be.rejectedWith(/Account does not exist or has no data/); + const solDestAfter = await TestUtil.provider.connection.getAccountInfo( + vaultAdminKeypair.publicKey, + ); + (solDestAfter.lamports > solDestBefore.lamports).should.be.true(); + }); + }); +} diff --git a/tests/integration-tests/batch4/withdrawA.ts b/tests/integration-tests/batch4/adminWithdraw.ts similarity index 57% rename from tests/integration-tests/batch4/withdrawA.ts rename to tests/integration-tests/batch4/adminWithdraw.ts index b5b8b1c..fb0a929 100644 --- a/tests/integration-tests/batch4/withdrawA.ts +++ b/tests/integration-tests/batch4/adminWithdraw.ts @@ -1,7 +1,7 @@ import "should"; import { initLog } from "../../utils/log.util"; import { before } from "mocha"; -import { Mint, TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { Mint } from "@solana/spl-token"; import { TokenUtil } from "../../utils/token.util"; import { Keypair, PublicKey } from "@solana/web3.js"; import { TestUtil } from "../../utils/config.util"; @@ -10,10 +10,11 @@ import { findAssociatedTokenAddress } from "../../utils/common.util"; import { VaultUtil } from "../../utils/vault.util"; import { SolUtil } from "../../utils/sol.util"; -describe("#withdrawA", () => { +describe("#adminWithdraw", () => { initLog(); let tokensAuthority: Keypair; + let vaultAdminKeypair: Keypair; let tokenA: Mint, tokenB: Mint; let vaultProtoConfig: PublicKey; let vault: PublicKey; @@ -21,11 +22,15 @@ describe("#withdrawA", () => { before(async () => { tokensAuthority = Keypair.generate(); - + vaultAdminKeypair = Keypair.generate(); await SolUtil.fundAccount( tokensAuthority.publicKey, SolUtil.solToLamports(0.1), ); + await SolUtil.fundAccount( + vaultAdminKeypair.publicKey, + SolUtil.solToLamports(0.1), + ); [tokenA, tokenB] = await TokenUtil.createMints( [tokensAuthority.publicKey, tokensAuthority.publicKey], @@ -52,6 +57,8 @@ describe("#withdrawA", () => { tokenB.address, vaultTreasuryTokenBAccount, vaultProtoConfig, + undefined, + vaultAdminKeypair, ); vault = vaultPDA.publicKey; @@ -67,24 +74,32 @@ describe("#withdrawA", () => { recipient: vaultTokenAAccount, amount: BigInt(1_000_000_000), }); + await TokenUtil.mintTo({ + payer: tokensAuthority, + token: tokenB, + mintAuthority: tokensAuthority, + recipient: vaultTokenBAccount, + amount: BigInt(1_000_000_000), + }); }); - it("allows admin to withdraw funds to admin's token A account", async () => { + it("allows admin to withdraw token A", async () => { const adminTokenAccount = await TokenUtil.getOrCreateAssociatedTokenAccount( tokenA, - TestUtil.provider.publicKey, + vaultAdminKeypair.publicKey, tokensAuthority, ); const adminTokenABalanceBefore = await TokenUtil.getTokenAccount(adminTokenAccount); adminTokenABalanceBefore.amount.toString().should.equal("0"); - await VaultUtil.withdrawA( + + await VaultUtil.adminWithdraw( + vaultAdminKeypair, vault, vaultTokenAAccount, adminTokenAccount, vaultProtoConfig, - TOKEN_PROGRAM_ID, ); const adminTokenABalanceAfter = @@ -92,32 +107,55 @@ describe("#withdrawA", () => { adminTokenABalanceAfter.amount.toString().should.equal("1000000000"); }); - it("does not allow admin to withdraw funds to admin's token A account", async () => { - const admin = Keypair.generate(); - await SolUtil.fundAccount(admin.publicKey, SolUtil.solToLamports(0.1)); - + it("allows admin to withdraw token B", async () => { const adminTokenAccount = await TokenUtil.getOrCreateAssociatedTokenAccount( - tokenA, - admin.publicKey, - admin, + tokenB, + vaultAdminKeypair.publicKey, + tokensAuthority, ); - const adminTokenABalanceBefore = + const adminTokenBBalanceBefore = await TokenUtil.getTokenAccount(adminTokenAccount); + adminTokenBBalanceBefore.amount.toString().should.equal("0"); - adminTokenABalanceBefore.amount.toString().should.equal("0"); - - await VaultUtil.withdrawA( + await VaultUtil.adminWithdraw( + vaultAdminKeypair, vault, - vaultTokenAAccount, + vaultTokenBAccount, adminTokenAccount, vaultProtoConfig, - TOKEN_PROGRAM_ID, - admin, - ).should.be.rejectedWith(/0x1785/); + ); - const adminTokenABalanceAfter = + const adminTokenBBalanceAfter = await TokenUtil.getTokenAccount(adminTokenAccount); - adminTokenABalanceAfter.amount.toString().should.equal("0"); + adminTokenBBalanceAfter.amount.toString().should.equal("1000000000"); }); + + // it("does not allow admin to withdraw funds to admin's token A account", async () => { + // const admin = Keypair.generate(); + // await SolUtil.fundAccount(admin.publicKey, SolUtil.solToLamports(0.1)); + // + // const adminTokenAccount = await TokenUtil.getOrCreateAssociatedTokenAccount( + // tokenA, + // admin.publicKey, + // admin, + // ); + // + // const adminTokenABalanceBefore = + // await TokenUtil.getTokenAccount(adminTokenAccount); + // + // adminTokenABalanceBefore.amount.toString().should.equal("0"); + // + // await VaultUtil.adminWithdraw( + // vaultAdminKeypair, + // vault, + // vaultTokenAAccount, + // adminTokenAccount, + // vaultProtoConfig, + // ).should.be.rejectedWith(/0x1785/); + // + // const adminTokenABalanceAfter = + // await TokenUtil.getTokenAccount(adminTokenAccount); + // adminTokenABalanceAfter.amount.toString().should.equal("0"); + // }); }); diff --git a/tests/utils/vault.util.ts b/tests/utils/vault.util.ts index 93e4ba3..69e30be 100644 --- a/tests/utils/vault.util.ts +++ b/tests/utils/vault.util.ts @@ -468,42 +468,119 @@ export class VaultUtil extends TestUtil { .transaction(); return this.provider.sendAndConfirm(tx, [withdrawer]); } + static async adminWithdraw( + admin: Keypair | Signer, + vault: PublicKey, + vaultTokenAccount: PublicKey, + destinationTokenAccount: PublicKey, + vaultProtoConfig: PublicKey, + ): Promise { + const tx = await ProgramUtil.dripProgram.methods + .adminWithdraw() + .accounts({ + admin: admin.publicKey, + vault, + vaultTokenAccount, + destinationTokenAccount, + vaultProtoConfig, + tokenProgram: ProgramUtil.tokenProgram, + }) + .transaction(); + return this.provider.connection.sendTransaction(tx, [admin]); + } static async withdrawA( + admin: Keypair | Signer, vault: PublicKey, vaultTokenAAccount: PublicKey, adminTokenAAccount: PublicKey, vaultProtoConfig: PublicKey, - tokenProgram: PublicKey, - admin?: Keypair | Signer, ): Promise { const tx = await ProgramUtil.dripProgram.methods .withdrawA() .accounts({ - admin: - admin?.publicKey.toBase58() ?? this.provider.publicKey.toBase58(), - vault: vault.toBase58(), - vaultTokenAAccount: vaultTokenAAccount.toBase58(), - adminTokenAAccount: adminTokenAAccount.toBase58(), - vaultProtoConfig: vaultProtoConfig.toBase58(), - tokenProgram: tokenProgram.toBase58(), + admin: admin.publicKey, + vault, + vaultTokenAAccount, + adminTokenAAccount, + vaultProtoConfig, + tokenProgram: ProgramUtil.tokenProgram, }) .transaction(); + return this.provider.connection.sendTransaction(tx, [admin]); + } - if (admin) { - return this.provider.connection.sendTransaction(tx, [admin]); - } + static async closeVaultPeriod( + admin: Keypair | Signer, + vault: PublicKey, + vaultProtoConfig: PublicKey, + vaultPeriod: PublicKey, + solDestination: PublicKey, + ): Promise { + const tx = await ProgramUtil.dripProgram.methods + .closeVaultPeriod() + .accounts({ + common: { + admin: admin.publicKey, + solDestination: solDestination, + }, + vaultProtoConfig: vaultProtoConfig.toBase58(), + vault: vault.toBase58(), + vaultPeriod: vaultPeriod.toBase58(), + }) + .transaction(); + return this.provider.sendAndConfirm(tx, [admin]); + } - return this.provider.sendAndConfirm(tx); + static async closeVault( + admin: Keypair | Signer, + vault: PublicKey, + vaultProtoConfig: PublicKey, + vaultTokenAAccount: PublicKey, + vaultTokenBAccount: PublicKey, + solDestination: PublicKey, + ): Promise { + const tx = await ProgramUtil.dripProgram.methods + .closeVault() + .accounts({ + common: { + admin: admin.publicKey, + solDestination: solDestination, + }, + vaultTokenAAccount, + vaultTokenBAccount, + vaultProtoConfig, + vault, + tokenProgram: ProgramUtil.tokenProgram, + }) + .transaction(); + return this.provider.sendAndConfirm(tx, [admin]); } + static async closeVaultProtoConfig( + admin: Keypair | Signer, + vaultProtoConfig: PublicKey, + solDestination: PublicKey, + ): Promise { + const tx = await ProgramUtil.dripProgram.methods + .closeVaultProtoConfig() + .accounts({ + common: { + admin: admin.publicKey, + solDestination: solDestination, + }, + vaultProtoConfig: vaultProtoConfig.toBase58(), + }) + .transaction(); + return this.provider.sendAndConfirm(tx, [admin]); + } /* - Deploy Vault Proto Config if not provided - Create token accounts - Deploy Vault - Deposit into vault - Create Vault Periods - */ + Deploy Vault Proto Config if not provided + Create token accounts + Deploy Vault + Deposit into vault + Create Vault Periods + */ static async deployVault({ tokenA, tokenB, diff --git a/yarn.lock b/yarn.lock index e8134fa..055236a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1359,11 +1359,11 @@ __metadata: linkType: hard "@types/node@npm:*": - version: 20.11.17 - resolution: "@types/node@npm:20.11.17" + version: 20.11.19 + resolution: "@types/node@npm:20.11.19" dependencies: undici-types: ~5.26.4 - checksum: 59c0dde187120adc97da30063c86511664b24b50fe777abfe1f557c217d0a0b84a68aaab5ef8ac44f5c2986b3f9cd605a15fa6e4f31195e594da96bbe9617c20 + checksum: 259d16643ba611ade617a8212e594a3ac014727457507389bbf7213971346ab052d870f1e6e2df0afd0876ecd7874f578bccb130be01e069263cfc7136ddc0c1 languageName: node linkType: hard @@ -2245,14 +2245,13 @@ __metadata: linkType: hard "define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.2": - version: 1.1.3 - resolution: "define-data-property@npm:1.1.3" + version: 1.1.4 + resolution: "define-data-property@npm:1.1.4" dependencies: + es-define-property: ^1.0.0 es-errors: ^1.3.0 - get-intrinsic: ^1.2.4 gopd: ^1.0.1 - has-property-descriptors: ^1.0.1 - checksum: 1e12cca443152ed309a9aa1bd4303e033906e0fa36ab4161d043a10859a0ca8bc9802d8c18e67ccc7102d31eba6d0026e6ec81f49eb75da3be4b51bb8e746584 + checksum: 8068ee6cab694d409ac25936eb861eea704b7763f7f342adbdfe337fc27c78d7ae0eff2364b2917b58c508d723c7a074326d068eef2e45c4edcd85cf94d0313b languageName: node linkType: hard @@ -2310,9 +2309,9 @@ __metadata: linkType: hard "diff@npm:^5.1.0": - version: 5.1.0 - resolution: "diff@npm:5.1.0" - checksum: c7bf0df7c9bfbe1cf8a678fd1b2137c4fb11be117a67bc18a0e03ae75105e8533dbfb1cda6b46beb3586ef5aed22143ef9d70713977d5fb1f9114e21455fba90 + version: 5.2.0 + resolution: "diff@npm:5.2.0" + checksum: 12b63ca9c36c72bafa3effa77121f0581b4015df18bc16bac1f8e263597735649f1a173c26f7eba17fb4162b073fee61788abe49610e6c70a2641fe1895443fd languageName: node linkType: hard @@ -2854,22 +2853,22 @@ __metadata: linkType: hard "http-proxy-agent@npm:^7.0.0": - version: 7.0.0 - resolution: "http-proxy-agent@npm:7.0.0" + version: 7.0.2 + resolution: "http-proxy-agent@npm:7.0.2" dependencies: agent-base: ^7.1.0 debug: ^4.3.4 - checksum: 48d4fac997917e15f45094852b63b62a46d0c8a4f0b9c6c23ca26d27b8df8d178bed88389e604745e748bd9a01f5023e25093722777f0593c3f052009ff438b6 + checksum: 670858c8f8f3146db5889e1fa117630910101db601fff7d5a8aa637da0abedf68c899f03d3451cac2f83bcc4c3d2dabf339b3aa00ff8080571cceb02c3ce02f3 languageName: node linkType: hard "https-proxy-agent@npm:^7.0.1": - version: 7.0.2 - resolution: "https-proxy-agent@npm:7.0.2" + version: 7.0.4 + resolution: "https-proxy-agent@npm:7.0.4" dependencies: agent-base: ^7.0.2 debug: 4 - checksum: 088969a0dd476ea7a0ed0a2cf1283013682b08f874c3bc6696c83fa061d2c157d29ef0ad3eb70a2046010bb7665573b2388d10fdcb3e410a66995e5248444292 + checksum: daaab857a967a2519ddc724f91edbbd388d766ff141b9025b629f92b9408fc83cee8a27e11a907aede392938e9c398e240d643e178408a59e4073539cde8cfe9 languageName: node linkType: hard @@ -2970,10 +2969,13 @@ __metadata: languageName: node linkType: hard -"ip@npm:^2.0.0": - version: 2.0.0 - resolution: "ip@npm:2.0.0" - checksum: cfcfac6b873b701996d71ec82a7dd27ba92450afdb421e356f44044ed688df04567344c36cbacea7d01b1c39a4c732dc012570ebe9bebfb06f27314bca625349 +"ip-address@npm:^9.0.5": + version: 9.0.5 + resolution: "ip-address@npm:9.0.5" + dependencies: + jsbn: 1.1.0 + sprintf-js: ^1.1.3 + checksum: aa15f12cfd0ef5e38349744e3654bae649a34c3b10c77a674a167e99925d1549486c5b14730eebce9fea26f6db9d5e42097b00aa4f9f612e68c79121c71652dc languageName: node linkType: hard @@ -3186,6 +3188,13 @@ __metadata: languageName: node linkType: hard +"jsbn@npm:1.1.0": + version: 1.1.0 + resolution: "jsbn@npm:1.1.0" + checksum: 944f924f2bd67ad533b3850eee47603eed0f6ae425fd1ee8c760f477e8c34a05f144c1bd4f5a5dd1963141dc79a2c55f89ccc5ab77d039e7077f3ad196b64965 + languageName: node + linkType: hard + "json-bigint@npm:^1.0.0": version: 1.0.0 resolution: "json-bigint@npm:1.0.0" @@ -4367,12 +4376,12 @@ __metadata: linkType: hard "socks@npm:^2.7.1": - version: 2.7.1 - resolution: "socks@npm:2.7.1" + version: 2.8.0 + resolution: "socks@npm:2.8.0" dependencies: - ip: ^2.0.0 + ip-address: ^9.0.5 smart-buffer: ^4.2.0 - checksum: 259d9e3e8e1c9809a7f5c32238c3d4d2a36b39b83851d0f573bfde5f21c4b1288417ce1af06af1452569cd1eb0841169afd4998f0e04ba04656f6b7f0e46d748 + checksum: b245081650c5fc112f0e10d2ee3976f5665d2191b9f86b181edd3c875d53d84a94bc173752d5be2651a450e3ef799fe7ec405dba3165890c08d9ac0b4ec1a487 languageName: node linkType: hard @@ -4393,6 +4402,13 @@ __metadata: languageName: node linkType: hard +"sprintf-js@npm:^1.1.3": + version: 1.1.3 + resolution: "sprintf-js@npm:1.1.3" + checksum: a3fdac7b49643875b70864a9d9b469d87a40dfeaf5d34d9d0c5b1cda5fd7d065531fcb43c76357d62254c57184a7b151954156563a4d6a747015cfb41021cad0 + languageName: node + linkType: hard + "ssri@npm:^10.0.0": version: 10.0.5 resolution: "ssri@npm:10.0.5"