Skip to content

Commit ac7519a

Browse files
committed
feat: add admin withdraw ix for vault token a,b
1 parent 4a9c484 commit ac7519a

File tree

5 files changed

+238
-6
lines changed

5 files changed

+238
-6
lines changed

programs/drip/src/actions/admin.rs

+43-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::errors::DripError;
22
use crate::instruction_accounts::{
3-
ClosePositionAccountAccounts, InitializeVaultAccountsBumps, WithdrawAAccounts,
3+
AdminWithdrawAccounts, ClosePositionAccountAccounts, InitializeVaultAccountsBumps,
4+
WithdrawAAccounts,
45
};
56
use crate::interactions::executor::CpiExecutor;
67
use crate::interactions::transfer_token::TransferToken;
@@ -31,6 +32,9 @@ pub enum Admin<'a, 'info> {
3132
WithdrawA {
3233
accounts: &'a mut WithdrawAAccounts<'info>,
3334
},
35+
AdminWithdraw {
36+
accounts: &'a mut AdminWithdrawAccounts<'info>,
37+
},
3438
ClosePositionAccount {
3539
accounts: &'a mut ClosePositionAccountAccounts<'info>,
3640
},
@@ -127,6 +131,32 @@ impl<'a, 'info> Validatable for Admin<'a, 'info> {
127131
DripError::VaultTokenAAccountIsEmpty
128132
);
129133
}
134+
Admin::AdminWithdraw { accounts } => {
135+
validate!(
136+
accounts.admin.key() == accounts.vault_proto_config.admin,
137+
DripError::SignerIsNotAdmin
138+
);
139+
140+
validate!(
141+
accounts.vault_proto_config.key() == accounts.vault.proto_config,
142+
DripError::InvalidVaultProtoConfigReference
143+
);
144+
145+
validate!(
146+
accounts.vault_token_account.owner.key() == accounts.vault.key(),
147+
DripError::IncorrectVaultTokenAccount
148+
);
149+
150+
validate!(
151+
accounts.vault.drip_amount == 0,
152+
DripError::CannotWithdrawAWithNonZeroDripAmount
153+
);
154+
155+
validate!(
156+
accounts.vault_token_account.amount > 0,
157+
DripError::VaultTokenAAccountIsEmpty
158+
);
159+
}
130160
Admin::ClosePositionAccount { accounts } => {
131161
validate!(
132162
accounts.admin.key() == accounts.vault_proto_config.admin,
@@ -191,6 +221,18 @@ impl<'a, 'info> Executable for Admin<'a, 'info> {
191221
let signer: &Vault = &accounts.vault;
192222
cpi_executor.execute_all(vec![&Some(&transfer_a_to_admin)], signer)?;
193223
}
224+
Admin::AdminWithdraw { accounts } => {
225+
let withdrawal_amount = accounts.vault_token_account.amount;
226+
let transfer = TransferToken::new(
227+
&accounts.token_program,
228+
&accounts.vault_token_account,
229+
&accounts.destination_token_account,
230+
&accounts.vault.to_account_info(),
231+
withdrawal_amount,
232+
);
233+
let signer: &Vault = &accounts.vault;
234+
cpi_executor.execute_all(vec![&Some(&transfer)], signer)?;
235+
}
194236
Admin::ClosePositionAccount { accounts } => {
195237
accounts
196238
.position

programs/drip/src/instruction_accounts/admin.rs

+19
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,25 @@ pub struct WithdrawAAccounts<'info> {
105105
pub token_program: Program<'info, Token>,
106106
}
107107

108+
#[derive(Accounts)]
109+
pub struct AdminWithdrawAccounts<'info> {
110+
pub admin: Signer<'info>,
111+
112+
pub vault_proto_config: Account<'info, VaultProtoConfig>,
113+
114+
pub vault: Account<'info, Vault>,
115+
116+
// mut needed because we are changing state
117+
#[account(mut)]
118+
pub vault_token_account: Account<'info, TokenAccount>,
119+
120+
// mut needed because we are changing state
121+
#[account(mut)]
122+
pub destination_token_account: Account<'info, TokenAccount>,
123+
124+
pub token_program: Program<'info, Token>,
125+
}
126+
108127
#[derive(Accounts)]
109128
pub struct ClosePositionAccountAccounts<'info> {
110129
pub admin: Signer<'info>,

programs/drip/src/lib.rs

+9
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,21 @@ pub mod drip {
108108
})
109109
}
110110

111+
/*
112+
DEPRECATED: USE admin_withdraw
113+
*/
111114
pub fn withdraw_a(ctx: Context<WithdrawAAccounts>) -> Result<()> {
112115
handle_action(Admin::WithdrawA {
113116
accounts: ctx.accounts,
114117
})
115118
}
116119

120+
pub fn admin_withdraw(ctx: Context<AdminWithdrawAccounts>) -> Result<()> {
121+
handle_action(Admin::AdminWithdraw {
122+
accounts: ctx.accounts,
123+
})
124+
}
125+
117126
pub fn admin_close_position_account(ctx: Context<ClosePositionAccountAccounts>) -> Result<()> {
118127
handle_action(Admin::ClosePositionAccount {
119128
accounts: ctx.accounts,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import "should";
2+
import { initLog } from "../../utils/log.util";
3+
import { before } from "mocha";
4+
import { Mint } from "@solana/spl-token";
5+
import { TokenUtil } from "../../utils/token.util";
6+
import { Keypair, PublicKey } from "@solana/web3.js";
7+
import { TestUtil } from "../../utils/config.util";
8+
import { deployVault, deployVaultProtoConfig } from "../../utils/setup.util";
9+
import { findAssociatedTokenAddress } from "../../utils/common.util";
10+
import { VaultUtil } from "../../utils/vault.util";
11+
import { SolUtil } from "../../utils/sol.util";
12+
13+
describe("#adminWithdraw", () => {
14+
initLog();
15+
16+
let tokensAuthority: Keypair;
17+
let vaultAdmin: Keypair;
18+
let tokenA: Mint, tokenB: Mint;
19+
let vaultProtoConfig: PublicKey;
20+
let vault: PublicKey;
21+
let vaultTokenAAccount: PublicKey, vaultTokenBAccount: PublicKey;
22+
23+
before(async () => {
24+
tokensAuthority = Keypair.generate();
25+
vaultAdmin = Keypair.generate();
26+
27+
await SolUtil.fundAccount(
28+
tokensAuthority.publicKey,
29+
SolUtil.solToLamports(0.1),
30+
);
31+
await SolUtil.fundAccount(vaultAdmin.publicKey, SolUtil.solToLamports(0.1));
32+
33+
[tokenA, tokenB] = await TokenUtil.createMints(
34+
[tokensAuthority.publicKey, tokensAuthority.publicKey],
35+
[6, 9],
36+
);
37+
});
38+
39+
beforeEach(async () => {
40+
vaultProtoConfig = await deployVaultProtoConfig(
41+
1,
42+
5,
43+
5,
44+
0,
45+
vaultAdmin.publicKey,
46+
);
47+
const vaultTreasuryTokenBAccount = await TokenUtil.createTokenAccount(
48+
tokenB,
49+
TestUtil.provider.publicKey,
50+
tokensAuthority,
51+
);
52+
53+
const vaultPDA = await deployVault(
54+
tokenA.address,
55+
tokenB.address,
56+
vaultTreasuryTokenBAccount,
57+
vaultProtoConfig,
58+
undefined,
59+
vaultAdmin,
60+
);
61+
62+
vault = vaultPDA.publicKey;
63+
vaultTokenAAccount = await findAssociatedTokenAddress(
64+
vaultPDA.publicKey,
65+
tokenA.address,
66+
);
67+
vaultTokenBAccount = await findAssociatedTokenAddress(
68+
vaultPDA.publicKey,
69+
tokenB.address,
70+
);
71+
await TokenUtil.mintTo({
72+
payer: tokensAuthority,
73+
token: tokenA,
74+
mintAuthority: tokensAuthority,
75+
recipient: vaultTokenAAccount,
76+
amount: BigInt(1_000_000_000),
77+
});
78+
await TokenUtil.mintTo({
79+
payer: tokensAuthority,
80+
token: tokenB,
81+
mintAuthority: tokensAuthority,
82+
recipient: vaultTokenBAccount,
83+
amount: BigInt(1_000_000_000),
84+
});
85+
});
86+
87+
it("allows admin to withdraw funds to admin's token B account", async () => {
88+
const adminTokenAccount = await TokenUtil.getOrCreateAssociatedTokenAccount(
89+
tokenB,
90+
vaultAdmin.publicKey,
91+
tokensAuthority,
92+
);
93+
const adminTokenBBalanceBefore =
94+
await TokenUtil.getTokenAccount(adminTokenAccount);
95+
adminTokenBBalanceBefore.amount.toString().should.equal("0");
96+
await VaultUtil.adminWithdraw(
97+
vault,
98+
vaultTokenBAccount,
99+
adminTokenAccount,
100+
vaultProtoConfig,
101+
vaultAdmin,
102+
);
103+
const adminTokenBBalanceAfter =
104+
await TokenUtil.getTokenAccount(adminTokenAccount);
105+
adminTokenBBalanceAfter.amount.toString().should.equal("1000000000");
106+
});
107+
108+
it("allows admin to withdraw funds to admin's token A account", async () => {
109+
const adminTokenAccount = await TokenUtil.getOrCreateAssociatedTokenAccount(
110+
tokenA,
111+
vaultAdmin.publicKey,
112+
tokensAuthority,
113+
);
114+
const adminTokenABalanceBefore =
115+
await TokenUtil.getTokenAccount(adminTokenAccount);
116+
adminTokenABalanceBefore.amount.toString().should.equal("0");
117+
await VaultUtil.adminWithdraw(
118+
vault,
119+
vaultTokenAAccount,
120+
adminTokenAccount,
121+
vaultProtoConfig,
122+
vaultAdmin,
123+
);
124+
const adminTokenABalanceAfter =
125+
await TokenUtil.getTokenAccount(adminTokenAccount);
126+
adminTokenABalanceAfter.amount.toString().should.equal("1000000000");
127+
});
128+
129+
it("does not allow non-admin to withdraw", async () => {
130+
const adminTokenAccount = await TokenUtil.getOrCreateAssociatedTokenAccount(
131+
tokenB,
132+
vaultAdmin.publicKey,
133+
tokensAuthority,
134+
);
135+
await VaultUtil.adminWithdraw(
136+
vault,
137+
vaultTokenBAccount,
138+
adminTokenAccount,
139+
vaultProtoConfig,
140+
tokensAuthority,
141+
).should.be.rejectedWith(/0x1785/);
142+
});
143+
});

tests/utils/vault.util.ts

+24-5
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
Granularity,
1717
PDA,
1818
} from "./common.util";
19-
import { BN } from "@coral-xyz/anchor";
19+
import { BN, Program } from "@coral-xyz/anchor";
2020
import { TokenUtil } from "./token.util";
2121
import { deployVaultPeriod, depositToVault } from "./setup.util";
2222
import { SolUtil } from "./sol.util";
@@ -192,9 +192,7 @@ export class VaultUtil extends TestUtil {
192192
.transaction();
193193
if (admin) {
194194
const blockhash = await TestUtil.provider.connection.getLatestBlockhash();
195-
const txId = await TestUtil.provider.connection.sendTransaction(tx, [
196-
admin,
197-
]);
195+
const txId = await TestUtil.provider.sendAndConfirm(tx, [admin]);
198196
await TestUtil.provider.connection.confirmTransaction(
199197
{
200198
signature: txId,
@@ -469,6 +467,27 @@ export class VaultUtil extends TestUtil {
469467
return this.provider.sendAndConfirm(tx, [withdrawer]);
470468
}
471469

470+
static async adminWithdraw(
471+
vault: PublicKey,
472+
vaultTokenAccount: PublicKey,
473+
destinationTokenAccount: PublicKey,
474+
vaultProtoConfig: PublicKey,
475+
admin: Keypair | Signer,
476+
): Promise<TransactionSignature> {
477+
const tx = await ProgramUtil.dripProgram.methods
478+
.adminWithdraw()
479+
.accounts({
480+
admin: admin.publicKey,
481+
vaultProtoConfig,
482+
vault,
483+
vaultTokenAccount,
484+
destinationTokenAccount,
485+
tokenProgram: ProgramUtil.tokenProgram.programId,
486+
})
487+
.transaction();
488+
return this.provider.sendAndConfirm(tx, [admin]);
489+
}
490+
472491
static async withdrawA(
473492
vault: PublicKey,
474493
vaultTokenAAccount: PublicKey,
@@ -491,7 +510,7 @@ export class VaultUtil extends TestUtil {
491510
.transaction();
492511

493512
if (admin) {
494-
return this.provider.connection.sendTransaction(tx, [admin]);
513+
return this.provider.sendAndConfirm(tx, [admin]);
495514
}
496515

497516
return this.provider.sendAndConfirm(tx);

0 commit comments

Comments
 (0)