Skip to content

Commit c534a5a

Browse files
authored
Implemented stake account checkpoints creation and fix tests (#204)
* WIP: implemented stake account checkpoints creation * Update delegation logic to use owner address instead of checkpoints address * Update CastVote with new checkpoints * Remove mockClockTime and freeze from GlobalConfig * Fix tests * Apply yarn format * Fix stake_account_checkpoints_last_index update in the delegate instruction. Update tests * Fix TransferVesting. Update tests * Fix tests * Fix castVote. Add tests * Return deleted emitted events DelegateVotesChanged
1 parent eea09cc commit c534a5a

21 files changed

+1930
-416
lines changed

solana/app/StakeConnection.ts

+143-47
Large diffs are not rendered by default.

solana/app/checkpoints.ts

+4
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ export class CheckpointAccount {
8181

8282
return `${this.checkpointData.toString()}\nCheckpoints:\n${checkpointsStr || "No valid checkpoints available."}`;
8383
}
84+
85+
getCheckpointCount(): number {
86+
return this.checkpoints.length;
87+
}
8488
}
8589

8690
export async function readCheckpoints(

solana/app/constants.ts

+3
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,6 @@ export const hubProposalMetadata = new Uint8Array([
1616

1717
/// Wormhole Hub Chain ID
1818
export const hubChainId = 10002;
19+
20+
export const CHECKPOINTS_ACCOUNT_LIMIT = 654998;
21+
export const TEST_CHECKPOINTS_ACCOUNT_LIMIT = 10;

solana/app/deploy/1_init_staking.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Wallet, AnchorProvider, Program } from "@coral-xyz/anchor";
22
import { Connection } from "@solana/web3.js";
33
import { DEPLOYER_AUTHORITY_KEYPAIR, WORMHOLE_TOKEN, RPC_NODE } from "./devnet";
4-
import { STAKING_ADDRESS } from "../constants";
4+
import { CHECKPOINTS_ACCOUNT_LIMIT, STAKING_ADDRESS } from "../constants";
55
import { Staking } from "../../target/types/staking";
66
import fs from "fs";
77

@@ -18,10 +18,10 @@ async function main() {
1818

1919
const globalConfig = {
2020
bump: 255,
21-
freeze: false,
2221
governanceAuthority: DEPLOYER_AUTHORITY_KEYPAIR.publicKey,
2322
whTokenMint: WORMHOLE_TOKEN,
2423
vestingAdmin: DEPLOYER_AUTHORITY_KEYPAIR.publicKey,
24+
maxCheckpointsAccountLimit: CHECKPOINTS_ACCOUNT_LIMIT,
2525
};
2626
await program.methods.initConfig(globalConfig).rpc();
2727
}

solana/app/deploy/5_withdraw.ts

-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ async function main() {
3838
.withdrawTokens(new BN(1))
3939
.accounts({
4040
currentDelegateStakeAccountCheckpoints: stakeAccountCheckpointsAddress,
41-
stakeAccountCheckpoints: stakeAccountCheckpointsAddress,
4241
destination: toAccount,
4342
})
4443
.rpc({ skipPreflight: DEBUG });

solana/app/deploy/test_vesting.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ async function main() {
220220
.then(confirm);
221221

222222
let stakeAccountCheckpointsAddress =
223-
await vesterStakeConnection.delegate_with_vest(
223+
await vesterStakeConnection.delegateWithVest(
224224
vester.publicKey,
225225
WHTokenBalance.fromString("0"),
226226
true,

solana/programs/staking/Cargo.toml

+1-2
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@ no-entrypoint = []
1313
no-idl = []
1414
no-log-ix-name = []
1515
cpi = ["no-entrypoint"]
16-
mock-clock = []
1716
wasm = ["no-entrypoint", "js-sys", "bincode"]
1817
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build", "wormhole-anchor-sdk/idl-build"]
19-
default = ["mock-clock","testnet", "idl-build"]
18+
default = ["testnet", "idl-build"]
2019
localnet = ["wormhole-solana-consts/localnet"]
2120
testnet = ["wormhole-solana-consts/testnet", "wormhole-anchor-sdk/solana-devnet"]
2221
mainnet = ["wormhole-solana-consts/mainnet", "wormhole-anchor-sdk/mainnet"]

solana/programs/staking/src/context.rs

+63-23
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ pub struct InitConfig<'info> {
5353
}
5454

5555
#[derive(Accounts)]
56+
#[instruction(delegatee: Pubkey, current_delegate_stake_account_owner: Pubkey)]
5657
pub struct Delegate<'info> {
5758
// Native payer:
5859
#[account(address = stake_account_metadata.owner)]
@@ -64,41 +65,38 @@ pub struct Delegate<'info> {
6465
AccountLoader<'info, checkpoints::CheckpointData>,
6566
#[account(
6667
mut,
67-
seeds = [STAKE_ACCOUNT_METADATA_SEED.as_bytes(), current_delegate_stake_account_checkpoints.key().as_ref()],
68+
seeds = [STAKE_ACCOUNT_METADATA_SEED.as_bytes(), current_delegate_stake_account_owner.as_ref()],
6869
bump = current_delegate_stake_account_metadata.metadata_bump
6970
)]
7071
pub current_delegate_stake_account_metadata:
7172
Box<Account<'info, stake_account::StakeAccountMetadata>>,
72-
7373
// Delegatee stake accounts:
7474
#[account(mut)]
7575
pub delegatee_stake_account_checkpoints: AccountLoader<'info, checkpoints::CheckpointData>,
7676
#[account(
7777
mut,
78-
seeds = [STAKE_ACCOUNT_METADATA_SEED.as_bytes(), delegatee_stake_account_checkpoints.key().as_ref()],
78+
seeds = [STAKE_ACCOUNT_METADATA_SEED.as_bytes(), delegatee.as_ref()],
7979
bump = delegatee_stake_account_metadata.metadata_bump
8080
)]
8181
pub delegatee_stake_account_metadata: Box<Account<'info, stake_account::StakeAccountMetadata>>,
8282

8383
// User stake account:
84-
#[account(mut)]
85-
pub stake_account_checkpoints: AccountLoader<'info, checkpoints::CheckpointData>,
8684
#[account(
8785
mut,
88-
seeds = [STAKE_ACCOUNT_METADATA_SEED.as_bytes(), stake_account_checkpoints.key().as_ref()],
86+
seeds = [STAKE_ACCOUNT_METADATA_SEED.as_bytes(), payer.key().as_ref()],
8987
bump = stake_account_metadata.metadata_bump,
90-
constraint = stake_account_metadata.delegate == current_delegate_stake_account_checkpoints.key()
88+
constraint = stake_account_metadata.delegate == current_delegate_stake_account_owner
9189
@ ErrorCode::InvalidCurrentDelegate
9290
)]
9391
pub stake_account_metadata: Box<Account<'info, stake_account::StakeAccountMetadata>>,
9492
/// CHECK : This AccountInfo is safe because it's a checked PDA
95-
#[account(seeds = [AUTHORITY_SEED.as_bytes(), stake_account_checkpoints.key().as_ref()], bump)]
93+
#[account(seeds = [AUTHORITY_SEED.as_bytes(), payer.key().as_ref()], bump)]
9694
pub custody_authority: AccountInfo<'info>,
9795
#[account(
9896
mut,
9997
seeds = [
10098
CUSTODY_SEED.as_bytes(),
101-
stake_account_checkpoints.key().as_ref()
99+
payer.key().as_ref()
102100
],
103101
bump,
104102
token::mint = mint,
@@ -120,7 +118,11 @@ pub struct Delegate<'info> {
120118
}
121119

122120
#[derive(Accounts)]
123-
#[instruction(proposal_id: [u8; 32])]
121+
#[instruction(proposal_id: [u8; 32],
122+
_against_votes: u64,
123+
_for_votes: u64,
124+
_abstain_votes: u64,
125+
checkpoint_index: u8)]
124126
pub struct CastVote<'info> {
125127
#[account(mut)]
126128
pub owner: Signer<'info>,
@@ -132,19 +134,27 @@ pub struct CastVote<'info> {
132134
)]
133135
pub proposal: Account<'info, proposal::ProposalData>,
134136

135-
#[account(mut, has_one = owner)]
137+
#[account(
138+
mut,
139+
has_one = owner,
140+
seeds = [CHECKPOINT_DATA_SEED.as_bytes(), owner.key().as_ref(), checkpoint_index.to_le_bytes().as_ref()],
141+
bump
142+
)]
136143
pub voter_checkpoints: AccountLoader<'info, checkpoints::CheckpointData>,
137144

138145
#[account(
139146
init_if_needed,
140147
payer = owner,
141148
space = proposal_voters_weight_cast::ProposalVotersWeightCast::LEN,
142-
seeds = [b"proposal_voters_weight_cast", proposal.key().as_ref(), voter_checkpoints.key().as_ref()],
149+
seeds = [b"proposal_voters_weight_cast", proposal.key().as_ref(), owner.key().as_ref()],
143150
bump
144151
)]
145152
pub proposal_voters_weight_cast:
146153
Account<'info, proposal_voters_weight_cast::ProposalVotersWeightCast>,
147154

155+
#[account(seeds = [CONFIG_SEED.as_bytes()], bump = config.bump)]
156+
pub config: Box<Account<'info, global_config::GlobalConfig>>,
157+
148158
pub system_program: Program<'info, System>,
149159
}
150160

@@ -385,16 +395,22 @@ pub struct CreateStakeAccount<'info> {
385395
// Stake program accounts:
386396
#[account(
387397
init,
388-
seeds = [CHECKPOINT_DATA_SEED.as_bytes(), payer.key().as_ref()],
398+
seeds = [CHECKPOINT_DATA_SEED.as_bytes(), payer.key().as_ref(), 0u8.to_le_bytes().as_ref()],
389399
bump,
390400
payer = payer,
391401
space = checkpoints::CheckpointData::LEN,
392402
)]
393403
pub stake_account_checkpoints: AccountLoader<'info, checkpoints::CheckpointData>,
394-
#[account(init, payer = payer, space = stake_account::StakeAccountMetadata::LEN, seeds = [STAKE_ACCOUNT_METADATA_SEED.as_bytes(), stake_account_checkpoints.key().as_ref()], bump)]
404+
#[account(
405+
init,
406+
payer = payer,
407+
space = stake_account::StakeAccountMetadata::LEN,
408+
seeds = [STAKE_ACCOUNT_METADATA_SEED.as_bytes(), payer.key().as_ref()],
409+
bump
410+
)]
395411
pub stake_account_metadata: Box<Account<'info, stake_account::StakeAccountMetadata>>,
396412
/// CHECK : This AccountInfo is safe because it's a checked PDA
397-
#[account(seeds = [AUTHORITY_SEED.as_bytes(), stake_account_checkpoints.key().as_ref()], bump)]
413+
#[account(seeds = [AUTHORITY_SEED.as_bytes(), payer.key().as_ref()], bump)]
398414
pub custody_authority: AccountInfo<'info>,
399415
#[account(seeds = [CONFIG_SEED.as_bytes()], bump = config.bump)]
400416
pub config: Box<Account<'info, global_config::GlobalConfig>>,
@@ -405,7 +421,7 @@ pub struct CreateStakeAccount<'info> {
405421
init,
406422
seeds = [
407423
CUSTODY_SEED.as_bytes(),
408-
stake_account_checkpoints.key().as_ref()
424+
payer.key().as_ref()
409425
],
410426
bump,
411427
payer = payer,
@@ -418,11 +434,35 @@ pub struct CreateStakeAccount<'info> {
418434
pub token_program: Program<'info, Token>,
419435
pub system_program: Program<'info, System>,
420436
}
437+
#[derive(Accounts)]
438+
pub struct CreateCheckpoints<'info> {
439+
// Native payer:
440+
#[account(mut)]
441+
pub payer: Signer<'info>,
442+
#[account(mut)]
443+
pub stake_account_metadata: Box<Account<'info, stake_account::StakeAccountMetadata>>,
444+
// Stake program accounts:
445+
#[account(mut)]
446+
pub stake_account_checkpoints: AccountLoader<'info, checkpoints::CheckpointData>,
447+
// Stake program accounts:
448+
#[account(
449+
init,
450+
seeds = [CHECKPOINT_DATA_SEED.as_bytes(), payer.key().as_ref(), stake_account_metadata.stake_account_checkpoints_last_index.to_le_bytes().as_ref()],
451+
bump,
452+
payer = payer,
453+
space = checkpoints::CheckpointData::LEN,
454+
)]
455+
pub new_stake_account_checkpoints: AccountLoader<'info, checkpoints::CheckpointData>,
456+
// Primitive accounts :
457+
pub system_program: Program<'info, System>,
458+
}
421459

422460
#[derive(Accounts)]
461+
#[instruction(amount: u64, current_delegate_stake_account_metadata_owner: Pubkey, stake_account_metadata_owner: Pubkey
462+
)]
423463
pub struct WithdrawTokens<'info> {
424464
// Native payer:
425-
#[account( address = stake_account_metadata.owner)]
465+
#[account(mut, address = stake_account_metadata.owner)]
426466
pub payer: Signer<'info>,
427467

428468
// Current delegate stake account:
@@ -431,7 +471,7 @@ pub struct WithdrawTokens<'info> {
431471
AccountLoader<'info, checkpoints::CheckpointData>,
432472
#[account(
433473
mut,
434-
seeds = [STAKE_ACCOUNT_METADATA_SEED.as_bytes(), current_delegate_stake_account_checkpoints.key().as_ref()],
474+
seeds = [STAKE_ACCOUNT_METADATA_SEED.as_bytes(), current_delegate_stake_account_metadata_owner.as_ref()],
435475
bump = current_delegate_stake_account_metadata.metadata_bump
436476
)]
437477
pub current_delegate_stake_account_metadata:
@@ -441,23 +481,23 @@ pub struct WithdrawTokens<'info> {
441481
#[account(mut)]
442482
pub destination: Account<'info, TokenAccount>,
443483
// Stake program accounts:
444-
pub stake_account_checkpoints: AccountLoader<'info, checkpoints::CheckpointData>,
445484
#[account(
446485
mut,
447-
seeds = [STAKE_ACCOUNT_METADATA_SEED.as_bytes(), stake_account_checkpoints.key().as_ref()],
486+
seeds = [STAKE_ACCOUNT_METADATA_SEED.as_bytes(), stake_account_metadata_owner.as_ref()],
448487
bump = stake_account_metadata.metadata_bump,
449-
constraint = stake_account_metadata.delegate == current_delegate_stake_account_checkpoints.key()
488+
constraint = stake_account_metadata.delegate == current_delegate_stake_account_metadata_owner
450489
@ ErrorCode::InvalidCurrentDelegate
451490
)]
452491
pub stake_account_metadata: Box<Account<'info, stake_account::StakeAccountMetadata>>,
453492
#[account(
454493
mut,
455-
seeds = [CUSTODY_SEED.as_bytes(), stake_account_checkpoints.key().as_ref()],
494+
seeds = [CUSTODY_SEED.as_bytes(), payer.key().as_ref()],
456495
bump = stake_account_metadata.custody_bump,
457496
)]
458497
pub stake_account_custody: Account<'info, TokenAccount>,
459498
/// CHECK : This AccountInfo is safe because it's a checked PDA
460-
#[account(seeds = [AUTHORITY_SEED.as_bytes(), stake_account_checkpoints.key().as_ref()], bump = stake_account_metadata.authority_bump)]
499+
#[account(seeds = [AUTHORITY_SEED.as_bytes(), payer.key().as_ref()], bump = stake_account_metadata.authority_bump
500+
)]
461501
pub custody_authority: AccountInfo<'info>,
462502
#[account(seeds = [CONFIG_SEED.as_bytes()], bump = config.bump)]
463503
pub config: Account<'info, global_config::GlobalConfig>,

solana/programs/staking/src/contexts/claim_vesting.rs

+26-9
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ use anchor_spl::token_interface::{
55
transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked,
66
};
77
use std::convert::TryInto;
8-
9-
use crate::error::VestingError;
8+
use crate::{
9+
error::{ErrorCode, VestingError},
10+
state::{Vesting, VestingBalance, VestingConfig},
11+
};
1012
use crate::state::checkpoints::{push_checkpoint, CheckpointData, Operation};
1113
use crate::state::global_config::GlobalConfig;
1214
use crate::state::stake_account::StakeAccountMetadata;
13-
use crate::state::{Vesting, VestingBalance, VestingConfig};
1415

1516
#[derive(Accounts)]
1617
pub struct ClaimVesting<'info> {
@@ -79,6 +80,21 @@ impl<'info> ClaimVesting<'info> {
7980
&mut self.stake_account_metadata,
8081
&mut self.stake_account_checkpoints,
8182
) {
83+
// Check if stake account checkpoints is out of bounds
84+
let loaded_checkpoints = stake_account_checkpoints.load()?;
85+
require!(
86+
loaded_checkpoints.next_index
87+
< self.global_config.max_checkpoints_account_limit.into(),
88+
ErrorCode::TooManyCheckpoints,
89+
);
90+
91+
// Verify that the actual address matches the expected one
92+
require!(
93+
stake_account_metadata.delegate.key() == loaded_checkpoints.owner,
94+
VestingError::InvalidStakeAccountCheckpoints
95+
);
96+
drop(loaded_checkpoints);
97+
8298
require!(
8399
self.config.mint == self.global_config.wh_token_mint,
84100
// This error can never happen here, because for the condition above
@@ -89,12 +105,6 @@ impl<'info> ClaimVesting<'info> {
89105
VestingError::InvalidVestingMint
90106
);
91107

92-
// Verify that the actual address matches the expected one
93-
require!(
94-
stake_account_metadata.delegate.key() == stake_account_checkpoints.key(),
95-
VestingError::InvalidStakeAccountCheckpoints
96-
);
97-
98108
// Additional checks to ensure the owner matches
99109
require!(
100110
stake_account_metadata.owner == self.vesting_balance.vester,
@@ -122,6 +132,13 @@ impl<'info> ClaimVesting<'info> {
122132
&self.vester.to_account_info(),
123133
&self.system_program.to_account_info(),
124134
)?;
135+
136+
let loaded_checkpoints = stake_account_checkpoints.load()?;
137+
if loaded_checkpoints.next_index
138+
>= self.global_config.max_checkpoints_account_limit.into()
139+
{
140+
stake_account_metadata.stake_account_checkpoints_last_index += 1;
141+
}
125142
} else {
126143
return err!(VestingError::ErrorOfStakeAccountParsing);
127144
}

0 commit comments

Comments
 (0)