Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[solana] Add frozen vester check in transfer_vesting #263

Merged
merged 2 commits into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions solana/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion solana/programs/staking/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ anchor-lang = { version = "0.30.1", git = "https://github.com/coral-xyz/anchor/"
] }
anchor-spl = { version = "0.30.1", git = "https://github.com/coral-xyz/anchor/", rev = "06527e57c3e59683c36eb0a5c69ee669100b42e5"}
anchor-attribute-error= { version = "0.30.1", git = "https://github.com/coral-xyz/anchor/", rev = "06527e57c3e59683c36eb0a5c69ee669100b42e5"}

spl-token = { version = "4.0", features = ["no-entrypoint"] }
wasm-bindgen = "0.2.93"
js-sys = { version = "0.3.70", optional = true }
bincode = { version = "1.3.3", optional = true }
Expand Down
6 changes: 6 additions & 0 deletions solana/programs/staking/src/contexts/transfer_vesting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::{error::ErrorCode, error::VestingError};
use anchor_lang::prelude::*;
use anchor_spl::associated_token::AssociatedToken;
use anchor_spl::token::{Mint, Token, TokenAccount};
use spl_token::state::AccountState;

#[event_cpi]
#[derive(Accounts)]
Expand Down Expand Up @@ -115,6 +116,11 @@ impl<'info> crate::contexts::TransferVesting<'info> {
new_stake_account_metadata: None,
};

// Check if vester_ta is frozen
if self.vester_ta.state == AccountState::Frozen {
return err!(VestingError::FrozenVesterAccount);
}

// Self transfers are not allowed
if self.new_vester_ta.owner.key() == self.vester_ta.owner.key() {
return err!(VestingError::TransferVestToMyself);
Expand Down
2 changes: 2 additions & 0 deletions solana/programs/staking/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ pub enum VestingError {
StakeAccountDelegatesMismatch,
#[msg("Stake account delegation loop detected")]
StakeAccountDelegationLoop,
#[msg("Cannot transfer vesting from a frozen token account")]
FrozenVesterAccount,
}

#[error_code]
Expand Down
3 changes: 1 addition & 2 deletions solana/tests/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ describe("config", async () => {
const whMintAccount = new Keypair();
const whMintAuthority = new Keypair();
const randomUser = new Keypair();
const zeroPubkey = new PublicKey(0);

const vestingAdminKeypair = new Keypair();
const vestingAdmin = vestingAdminKeypair.publicKey;
Expand All @@ -68,7 +67,7 @@ describe("config", async () => {
program.provider,
whMintAccount,
whMintAuthority.publicKey,
null,
whMintAuthority.publicKey,
WH_TOKEN_DECIMALS,
);

Expand Down
2 changes: 1 addition & 1 deletion solana/tests/utils/before.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ export async function standardSetup(
provider,
whMintAccount,
whMintAuthority.publicKey,
null,
whMintAuthority.publicKey,
WH_TOKEN_DECIMALS,
);

Expand Down
74 changes: 74 additions & 0 deletions solana/tests/vesting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import {
ASSOCIATED_TOKEN_PROGRAM_ID,
createAssociatedTokenAccountIdempotentInstruction,
createAssociatedTokenAccountInstruction,
createFreezeAccountInstruction,
createInitializeMintInstruction,
createMintToInstruction,
createThawAccountInstruction,
createTransferCheckedInstruction,
getAccount,
getAssociatedTokenAddressSync,
Expand Down Expand Up @@ -3675,6 +3677,78 @@ describe("vesting", () => {
}
});

it("should fail to transfer vest if vester_ta is frozen", async () => {
let freezeTx = new Transaction();
freezeTx.add(
createFreezeAccountInstruction(
vester3Ta,
whMintAccount.publicKey,
whMintAuthority.publicKey,
)
);
await stakeConnection.provider.sendAndConfirm(freezeTx, [whMintAuthority]);

let vester3TaAccount = await getAccount(stakeConnection.provider.connection, vester3Ta);
assert.equal(vester3TaAccount.isFrozen, true, "vester3Ta should be frozen");

let stakeAccountMetadataAddress = await vester3StakeConnection.getStakeMetadataAddress(
vester3.publicKey
);
let newVester3StakeAccountMetadataAddress =
await newVester3StakeConnection.getStakeMetadataAddress(
newVester3.publicKey,
);

let newVester3Vest = PublicKey.findProgramAddressSync(
[
Buffer.from(wasm.Constants.VEST_SEED()),
config.toBuffer(),
newVester3Ta.toBuffer(),
FEW_LATER.toBuffer("le", 8),
],
stakeConnection.program.programId
)[0];

try {
await vesterStakeConnection.program.methods
.transferVesting()
.accounts({
...accounts,
vester: vester3.publicKey,
vest: vest3NowForTransfer,
vestingBalance: vesting3Balance,
delegateStakeAccountCheckpoints: null,
delegateStakeAccountMetadata: null,
stakeAccountMetadata: stakeAccountMetadataAddress,
newStakeAccountMetadata: newVester3StakeAccountMetadataAddress,
vesterTa: vester3Ta,
newVesterTa: newVester3Ta,
newVest: newVester3Vest,
newVestingBalance: newVesting3Balance,
globalConfig: vester3StakeConnection.configAddress,
})
.signers([vester3])
.rpc()
.then(confirm);

assert.fail("Expected error was not thrown");
} catch (e) {
assert(
(e as AnchorError).error?.errorCode?.code === "FrozenVesterAccount",
);
}

let thawTx = new Transaction();
thawTx.add(
createThawAccountInstruction(
vester3Ta,
whMintAccount.publicKey,
whMintAuthority.publicKey,
)
);
await stakeConnection.provider.sendAndConfirm(thawTx, [whMintAuthority]);
});

it("should fail to transfer vest if the sender hasn't delegated, but the recipient has", async () => {});

it("should successfully transfer a vest when both the sender and recipient haven't delegated", async () => {});
Expand Down