From c0473d7afc7c27b94af4a54e395a477e473fcce2 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Fri, 13 Oct 2023 17:30:31 +0100 Subject: [PATCH 01/51] Draft --- staking/programs/staking/src/lib.rs | 4 ++++ staking/target/types/staking.ts | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index ba40e52f..a45b62ca 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -413,6 +413,10 @@ pub mod staking { let epoch_of_snapshot: u64; voter_record.weight_action = Some(action); + if get_current_epoch(&config)? < ctx.accounts.stake_account_metadata.creation_epoch { + return Err(error!(ErrorCode::NoRemainingAccount)); + } + match action { VoterWeightAction::CastVote => { let proposal_account: &AccountInfo = ctx diff --git a/staking/target/types/staking.ts b/staking/target/types/staking.ts index 7aa0f227..7079938b 100644 --- a/staking/target/types/staking.ts +++ b/staking/target/types/staking.ts @@ -2485,10 +2485,15 @@ export const IDL: Staking = { "type": "u8" }, { +<<<<<<< HEAD "name": "transferEpoch", "type": { "option": "u64" } +======= + "name": "creationEpoch", + "type": "u64" +>>>>>>> 7902b94 (Draft) } ] } From ae7eddd5b48c3e98bd4ae78526fc6a5f10b4b885 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 16 Oct 2023 14:12:09 +0100 Subject: [PATCH 02/51] Add authority --- staking/programs/staking/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index a45b62ca..6dab55c8 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -414,7 +414,7 @@ pub mod staking { voter_record.weight_action = Some(action); if get_current_epoch(&config)? < ctx.accounts.stake_account_metadata.creation_epoch { - return Err(error!(ErrorCode::NoRemainingAccount)); + return Err(error!(ErrorCode::VoteCreationEpoch)); } match action { From f238967d0c6ccab7ef5c758a11ea220679d204bf Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 16 Oct 2023 14:13:57 +0100 Subject: [PATCH 03/51] Clippy --- staking/programs/staking/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index 6dab55c8..2e7d3667 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -413,7 +413,7 @@ pub mod staking { let epoch_of_snapshot: u64; voter_record.weight_action = Some(action); - if get_current_epoch(&config)? < ctx.accounts.stake_account_metadata.creation_epoch { + if get_current_epoch(config)? < ctx.accounts.stake_account_metadata.creation_epoch { return Err(error!(ErrorCode::VoteCreationEpoch)); } From a2fbe05da4e6c2cb4b5221f6ab099ccb2b3a0f2a Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 16 Oct 2023 14:45:11 +0100 Subject: [PATCH 04/51] Checkpoint --- staking/target/types/staking.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/staking/target/types/staking.ts b/staking/target/types/staking.ts index 7079938b..7b926e4c 100644 --- a/staking/target/types/staking.ts +++ b/staking/target/types/staking.ts @@ -2988,8 +2988,13 @@ export const IDL: Staking = { }, { "code": 6028, +<<<<<<< HEAD "name": "VoteDuringTransferEpoch", "msg": "Can't vote during an account's transfer epoch" +======= + "name": "VoteCreationEpoch", + "msg": "Can't vote on the creation epoch" +>>>>>>> dc00407 (Checkpoint) }, { "code": 6029, From 3b688e805a56454a19e538019bad3a193cf72731 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 16 Oct 2023 15:02:04 +0100 Subject: [PATCH 05/51] Do it --- staking/programs/staking/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index 2e7d3667..41af562f 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -413,8 +413,10 @@ pub mod staking { let epoch_of_snapshot: u64; voter_record.weight_action = Some(action); - if get_current_epoch(config)? < ctx.accounts.stake_account_metadata.creation_epoch { - return Err(error!(ErrorCode::VoteCreationEpoch)); + if let Some(transfer_epoch) = ctx.accounts.stake_account_metadata.transfer_epoch { + if get_current_epoch(config)? < transfer_epoch { + return Err(error!(ErrorCode::VoteCreationEpoch)); + } } match action { From 49c2bc7e76526f59a987e94b9c5dc304027fb4b8 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 16 Oct 2023 15:03:22 +0100 Subject: [PATCH 06/51] Rename error --- staking/programs/staking/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index 41af562f..07aca3c5 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -415,7 +415,7 @@ pub mod staking { if let Some(transfer_epoch) = ctx.accounts.stake_account_metadata.transfer_epoch { if get_current_epoch(config)? < transfer_epoch { - return Err(error!(ErrorCode::VoteCreationEpoch)); + return Err(error!(ErrorCode::VoteDuringTransferEpoch)); } } From 76b1edcef7e766caf47c563680fc0d36341581e0 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 16 Oct 2023 15:22:30 +0100 Subject: [PATCH 07/51] Update stuff --- staking/target/types/staking.ts | 10 +++++----- staking/tests/api_test.ts | 4 ++-- staking/tests/clock_api_test.ts | 4 ++-- staking/tests/create_product.ts | 5 ++++- staking/tests/max_pos.ts | 2 +- staking/tests/position_lifecycle.ts | 2 +- staking/tests/staking.ts | 2 +- staking/tests/unlock_api_test.ts | 2 +- staking/tests/vesting_test.ts | 2 +- 9 files changed, 18 insertions(+), 15 deletions(-) diff --git a/staking/target/types/staking.ts b/staking/target/types/staking.ts index 7b926e4c..044788bc 100644 --- a/staking/target/types/staking.ts +++ b/staking/target/types/staking.ts @@ -2485,15 +2485,10 @@ export const IDL: Staking = { "type": "u8" }, { -<<<<<<< HEAD "name": "transferEpoch", "type": { "option": "u64" } -======= - "name": "creationEpoch", - "type": "u64" ->>>>>>> 7902b94 (Draft) } ] } @@ -2988,6 +2983,7 @@ export const IDL: Staking = { }, { "code": 6028, +<<<<<<< HEAD <<<<<<< HEAD "name": "VoteDuringTransferEpoch", "msg": "Can't vote during an account's transfer epoch" @@ -2995,6 +2991,10 @@ export const IDL: Staking = { "name": "VoteCreationEpoch", "msg": "Can't vote on the creation epoch" >>>>>>> dc00407 (Checkpoint) +======= + "name": "VoteDuringTransferEpoch", + "msg": "Can't vote during an account's transfer epoch" +>>>>>>> f92af94 (Update stuff) }, { "code": 6029, diff --git a/staking/tests/api_test.ts b/staking/tests/api_test.ts index a8b5fa3a..66815167 100644 --- a/staking/tests/api_test.ts +++ b/staking/tests/api_test.ts @@ -1,4 +1,4 @@ -import { Keypair } from "@solana/web3.js"; +import { Keypair, PublicKey } from "@solana/web3.js"; import assert from "assert"; import { StakeConnection } from "../app/StakeConnection"; import { @@ -39,7 +39,7 @@ describe("api", async () => { config, pythMintAccount, pythMintAuthority, - makeDefaultConfig(pythMintAccount.publicKey), + makeDefaultConfig(pythMintAccount.publicKey, PublicKey.unique()), PythBalance.fromString("1000") )); diff --git a/staking/tests/clock_api_test.ts b/staking/tests/clock_api_test.ts index 042beb8b..b5e6bda2 100644 --- a/staking/tests/clock_api_test.ts +++ b/staking/tests/clock_api_test.ts @@ -8,7 +8,7 @@ import { standardSetup, } from "./utils/before"; import path from "path"; -import { Keypair } from "@solana/web3.js"; +import { Keypair, PublicKey } from "@solana/web3.js"; import { StakeConnection } from "../app"; import assert from "assert"; import { BN } from "@project-serum/anchor"; @@ -33,7 +33,7 @@ describe("clock_api", async () => { config, pythMintAccount, pythMintAuthority, - makeDefaultConfig(pythMintAccount.publicKey) + makeDefaultConfig(pythMintAccount.publicKey, PublicKey.unique()) )); }); diff --git a/staking/tests/create_product.ts b/staking/tests/create_product.ts index 04409216..585ce7a0 100644 --- a/staking/tests/create_product.ts +++ b/staking/tests/create_product.ts @@ -35,7 +35,10 @@ describe("create_product", async () => { before(async () => { const config = readAnchorConfig(ANCHOR_CONFIG_PATH); - let globalConfig = makeDefaultConfig(pythMintAccount.publicKey); + let globalConfig = makeDefaultConfig( + pythMintAccount.publicKey, + PublicKey.unique() + ); ({ controller, stakeConnection } = await standardSetup( portNumber, diff --git a/staking/tests/max_pos.ts b/staking/tests/max_pos.ts index db0c420e..766678eb 100644 --- a/staking/tests/max_pos.ts +++ b/staking/tests/max_pos.ts @@ -56,7 +56,7 @@ describe("fills a stake account with positions", async () => { config, pythMintAccount, pythMintAuthority, - makeDefaultConfig(pythMintAccount.publicKey) + makeDefaultConfig(pythMintAccount.publicKey, PublicKey.unique()) )); program = stakeConnection.program; provider = stakeConnection.provider; diff --git a/staking/tests/position_lifecycle.ts b/staking/tests/position_lifecycle.ts index 02826c85..268c3966 100644 --- a/staking/tests/position_lifecycle.ts +++ b/staking/tests/position_lifecycle.ts @@ -54,7 +54,7 @@ describe("position_lifecycle", async () => { config, pythMintAccount, pythMintAuthority, - makeDefaultConfig(pythMintAccount.publicKey) + makeDefaultConfig(pythMintAccount.publicKey, PublicKey.unique()) )); program = stakeConnection.program; owner = stakeConnection.provider.wallet.publicKey; diff --git a/staking/tests/staking.ts b/staking/tests/staking.ts index 3fca8603..6c119140 100644 --- a/staking/tests/staking.ts +++ b/staking/tests/staking.ts @@ -67,7 +67,7 @@ describe("staking", async () => { config, pythMintAccount, pythMintAuthority, - makeDefaultConfig(pythMintAccount.publicKey) + makeDefaultConfig(pythMintAccount.publicKey, PublicKey.unique()) )); program = stakeConnection.program; provider = stakeConnection.provider; diff --git a/staking/tests/unlock_api_test.ts b/staking/tests/unlock_api_test.ts index b6cac64f..708ecf18 100644 --- a/staking/tests/unlock_api_test.ts +++ b/staking/tests/unlock_api_test.ts @@ -34,7 +34,7 @@ describe("unlock_api", async () => { config, pythMintAccount, pythMintAuthority, - makeDefaultConfig(pythMintAccount.publicKey) + makeDefaultConfig(pythMintAccount.publicKey, PublicKey.unique()) )); EPOCH_DURATION = stakeConnection.config.epochDuration; diff --git a/staking/tests/vesting_test.ts b/staking/tests/vesting_test.ts index 23035f4b..60f2343a 100644 --- a/staking/tests/vesting_test.ts +++ b/staking/tests/vesting_test.ts @@ -47,7 +47,7 @@ describe("vesting", async () => { config, pythMintAccount, pythMintAuthority, - makeDefaultConfig(pythMintAccount.publicKey) + makeDefaultConfig(pythMintAccount.publicKey, PublicKey.unique()) )); EPOCH_DURATION = stakeConnection.config.epochDuration; From 46424303adbfd5206ed67f43731739c5843b5ceb Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 16 Oct 2023 15:42:40 +0100 Subject: [PATCH 08/51] Use epoch_of_snapshot --- staking/programs/staking/src/lib.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index 07aca3c5..ba40e52f 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -413,12 +413,6 @@ pub mod staking { let epoch_of_snapshot: u64; voter_record.weight_action = Some(action); - if let Some(transfer_epoch) = ctx.accounts.stake_account_metadata.transfer_epoch { - if get_current_epoch(config)? < transfer_epoch { - return Err(error!(ErrorCode::VoteDuringTransferEpoch)); - } - } - match action { VoterWeightAction::CastVote => { let proposal_account: &AccountInfo = ctx From aacc7dcae241303c90b3d1689f0e75ba265a19bb Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 16 Oct 2023 17:12:17 +0100 Subject: [PATCH 09/51] Checkpoint --- staking/app/StakeConnection.ts | 36 + staking/package.json | 2 +- staking/programs/staking/src/context.rs | 90 ++ staking/programs/staking/src/lib.rs | 26 + staking/programs/staking/src/state/mod.rs | 1 + .../staking/src/state/split_request.rs | 17 + staking/target/idl/staking.json | 311 ++++ staking/target/types/staking.ts | 1302 ++++++++++++----- staking/tests/split_vesting_account.ts | 272 ++++ staking/tests/utils/before.ts | 5 +- 10 files changed, 1719 insertions(+), 343 deletions(-) create mode 100644 staking/programs/staking/src/state/split_request.rs create mode 100644 staking/tests/split_vesting_account.ts diff --git a/staking/app/StakeConnection.ts b/staking/app/StakeConnection.ts index 9c44f3b3..48b680b7 100644 --- a/staking/app/StakeConnection.ts +++ b/staking/app/StakeConnection.ts @@ -810,6 +810,42 @@ export class StakeConnection { }) .rpc(); } + + public async requestSplit( + stakeAccount: StakeAccount, + amount: PythBalance, + recipient: PublicKey + ) { + await this.program.methods + .createSplitRequest(amount.toBN(), recipient) + .accounts({ + stakeAccountPositions: stakeAccount.address, + }) + .rpc(); + } + + public async confirmSplit(stakeAccount: StakeAccount) { + const newStakeAccountKeypair = new Keypair(); + + const instructions = []; + instructions.push( + await this.program.account.positionData.createInstruction( + newStakeAccountKeypair, + wasm.Constants.POSITIONS_ACCOUNT_SIZE() + ) + ); + + await this.program.methods + .acceptSplitRequest() + .accounts({ + currentStakeAccountPositions: stakeAccount.address, + newStakeAccountPositions: newStakeAccountKeypair.publicKey, + mint: this.config.pythTokenMint, + }) + .signers([newStakeAccountKeypair]) + .preInstructions(instructions) + .rpc(); + } } export interface BalanceSummary { withdrawable: PythBalance; diff --git a/staking/package.json b/staking/package.json index a18d30b6..4b0a76db 100644 --- a/staking/package.json +++ b/staking/package.json @@ -30,7 +30,7 @@ "wasm-pack": "^0.10.2" }, "scripts": { - "test": "npm run build_wasm && anchor build -- --features mock-clock && npm run dump_governance && ts-mocha --parallel -p ./tsconfig.json -t 1000000 tests/*.ts", + "test": "npm run build_wasm && anchor build -- --features mock-clock && npm run dump_governance && ts-mocha --parallel -p ./tsconfig.json -t 1000000 tests/split_vesting_account.ts", "build": "npm run build_wasm && tsc -p tsconfig.api.json", "build_wasm": "./scripts/build_wasm.sh", "localnet": "anchor build && npm run dump_governance && ts-node ./app/scripts/localnet.ts", diff --git a/staking/programs/staking/src/context.rs b/staking/programs/staking/src/context.rs index 3c41baa7..0f37d0d1 100644 --- a/staking/programs/staking/src/context.rs +++ b/staking/programs/staking/src/context.rs @@ -19,6 +19,7 @@ pub const TARGET_SEED: &str = "target"; pub const MAX_VOTER_RECORD_SEED: &str = "max_voter"; pub const VOTING_TARGET_SEED: &str = "voting"; pub const DATA_TARGET_SEED: &str = "staking"; +pub const SPLIT_REQUEST: &str = "split_request"; impl positions::Target { pub fn get_seed(&self) -> Vec { @@ -278,6 +279,95 @@ pub struct CreateTarget<'info> { pub system_program: Program<'info, System>, } +#[derive(Accounts)] +#[instruction(amount : u64, recipient : Pubkey)] +pub struct CreateSplitRequest<'info> { + // Native payer: + #[account(mut, address = stake_account_metadata.owner)] + pub payer: Signer<'info>, + // Stake program accounts: + pub stake_account_positions: AccountLoader<'info, positions::PositionData>, + #[account(seeds = [STAKE_ACCOUNT_METADATA_SEED.as_bytes(), stake_account_positions.key().as_ref()], bump = stake_account_metadata.metadata_bump)] + pub stake_account_metadata: Account<'info, stake_account::StakeAccountMetadataV2>, + #[account(init_if_needed, payer = payer, space=split_request::SplitRequest::LEN , seeds = [SPLIT_REQUEST.as_bytes(), stake_account_positions.key().as_ref()], bump)] + pub stake_account_split_request: Account<'info, split_request::SplitRequest>, + #[account(seeds = [CONFIG_SEED.as_bytes()], bump = config.bump)] + pub config: Account<'info, global_config::GlobalConfig>, + + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct AcceptSplitRequest<'info> { + // Native payer: + #[account(mut, address = config.pda_authority)] + pub payer: Signer<'info>, + // Current stake accounts: + #[account(mut)] + pub current_stake_account_positions: AccountLoader<'info, positions::PositionData>, + #[account(mut, seeds = [STAKE_ACCOUNT_METADATA_SEED.as_bytes(), current_stake_account_positions.key().as_ref()], bump = current_stake_account_metadata.metadata_bump)] + pub current_stake_account_metadata: Account<'info, stake_account::StakeAccountMetadataV2>, + #[account(seeds = [SPLIT_REQUEST.as_bytes(), current_stake_account_positions.key().as_ref()], bump)] + pub current_stake_account_split_request: Account<'info, split_request::SplitRequest>, + #[account( + mut, + seeds = [CUSTODY_SEED.as_bytes(), current_stake_account_positions.key().as_ref()], + bump = current_stake_account_metadata.custody_bump, + )] + pub current_stake_account_custody: Account<'info, TokenAccount>, + /// CHECK : This AccountInfo is safe because it's a checked PDA + #[account(seeds = [AUTHORITY_SEED.as_bytes(), current_stake_account_positions.key().as_ref()], bump = current_stake_account_metadata.authority_bump)] + pub current_custody_authority: AccountInfo<'info>, + #[account(seeds = [CONFIG_SEED.as_bytes()], bump = config.bump)] + pub config: Account<'info, global_config::GlobalConfig>, + + // New stake accounts : + pub new_stake_account_positions: AccountLoader<'info, positions::PositionData>, + #[account(init, payer = payer, space = stake_account::StakeAccountMetadataV2::LEN, seeds = [STAKE_ACCOUNT_METADATA_SEED.as_bytes(), new_stake_account_positions.key().as_ref()], bump)] + pub new_stake_account_metadata: Box>, + #[account( + init, + seeds = [CUSTODY_SEED.as_bytes(), new_stake_account_positions.key().as_ref()], + bump, + payer = payer, + token::mint = mint, + token::authority = new_custody_authority, + )] + pub new_stake_account_custody: Account<'info, TokenAccount>, + /// CHECK : This AccountInfo is safe because it's a checked PDA + #[account(seeds = [AUTHORITY_SEED.as_bytes(), new_stake_account_positions.key().as_ref()], bump)] + pub new_custody_authority: AccountInfo<'info>, + #[account( + init, + payer = payer, + space = voter_weight_record::VoterWeightRecord::LEN, + seeds = [VOTER_RECORD_SEED.as_bytes(), new_stake_account_positions.key().as_ref()], + bump)] + pub new_voter_record: Account<'info, voter_weight_record::VoterWeightRecord>, + // Other accounts needed + #[account(address = config.pyth_token_mint)] + pub mint: Account<'info, Mint>, + pub rent: Sysvar<'info, Rent>, + pub token_program: Program<'info, Token>, + pub system_program: Program<'info, System>, +} + +impl<'a, 'b, 'c, 'info> From<&AcceptSplitRequest<'info>> + for CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> +{ + fn from( + accounts: &AcceptSplitRequest<'info>, + ) -> CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> { + let cpi_accounts = Transfer { + from: accounts.current_stake_account_custody.to_account_info(), + to: accounts.new_stake_account_custody.to_account_info(), + authority: accounts.current_custody_authority.to_account_info(), + }; + let cpi_program = accounts.token_program.to_account_info(); + CpiContext::new(cpi_program, cpi_accounts) + } +} + // Anchor's parser doesn't understand cfg(feature), so the IDL gets messed // up if we try to use it here. We can just keep the definition the same. #[derive(Accounts)] diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index ba40e52f..b4db6f79 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -534,4 +534,30 @@ pub mod staking { Err(error!(ErrorCode::DebuggingOnly)) } } + + pub fn create_split_request( + ctx: Context, + amount: u64, + recipient: Pubkey, + ) -> Result<()> { + ctx.accounts.stake_account_split_request.amount = amount; + ctx.accounts.stake_account_split_request.recipient = recipient; + Ok(()) + } + + + pub fn accept_split_request(ctx: Context) -> Result<()> { + let split_request = &ctx.accounts.current_stake_account_split_request; + + // Transfer tokens + transfer( + CpiContext::from(&*ctx.accounts).with_signer(&[&[ + AUTHORITY_SEED.as_bytes(), + ctx.accounts.current_stake_account_positions.key().as_ref(), + &[ctx.accounts.current_stake_account_metadata.authority_bump], + ]]), + split_request.amount, + )?; + Ok(()) + } } diff --git a/staking/programs/staking/src/state/mod.rs b/staking/programs/staking/src/state/mod.rs index caf06de6..6e310945 100644 --- a/staking/programs/staking/src/state/mod.rs +++ b/staking/programs/staking/src/state/mod.rs @@ -1,6 +1,7 @@ pub mod global_config; pub mod max_voter_weight_record; pub mod positions; +pub mod split_request; pub mod stake_account; pub mod target; pub mod vesting; diff --git a/staking/programs/staking/src/state/split_request.rs b/staking/programs/staking/src/state/split_request.rs new file mode 100644 index 00000000..15e72304 --- /dev/null +++ b/staking/programs/staking/src/state/split_request.rs @@ -0,0 +1,17 @@ +use { + anchor_lang::prelude::*, + borsh::BorshSchema, +}; + +#[account] +#[derive(Default, BorshSchema)] +pub struct SplitRequest { + pub amount: u64, + pub recipient: Pubkey, +} + +impl SplitRequest { + pub const LEN: usize = 8 // Discriminant + + 8 // Amount + + 32; // Recipient +} diff --git a/staking/target/idl/staking.json b/staking/target/idl/staking.json index 1c4bd776..c35faa53 100644 --- a/staking/target/idl/staking.json +++ b/staking/target/idl/staking.json @@ -791,6 +791,301 @@ "type": "i64" } ] + }, + { + "name": "createSplitRequest", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "stakeAccountPositions", + "isMut": false, + "isSigner": false + }, + { + "name": "stakeAccountMetadata", + "isMut": false, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "stake_metadata" + }, + { + "kind": "account", + "type": "publicKey", + "path": "stake_account_positions" + } + ] + } + }, + { + "name": "stakeAccountSplitRequest", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "split_request" + }, + { + "kind": "account", + "type": "publicKey", + "path": "stake_account_positions" + } + ] + } + }, + { + "name": "config", + "isMut": false, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "config" + } + ] + } + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "recipient", + "type": "publicKey" + } + ] + }, + { + "name": "acceptSplitRequest", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "currentStakeAccountPositions", + "isMut": true, + "isSigner": false + }, + { + "name": "currentStakeAccountMetadata", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "stake_metadata" + }, + { + "kind": "account", + "type": "publicKey", + "path": "current_stake_account_positions" + } + ] + } + }, + { + "name": "currentStakeAccountSplitRequest", + "isMut": false, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "split_request" + }, + { + "kind": "account", + "type": "publicKey", + "path": "current_stake_account_positions" + } + ] + } + }, + { + "name": "currentStakeAccountCustody", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "custody" + }, + { + "kind": "account", + "type": "publicKey", + "path": "current_stake_account_positions" + } + ] + } + }, + { + "name": "currentCustodyAuthority", + "isMut": false, + "isSigner": false, + "docs": [ + "CHECK : This AccountInfo is safe because it's a checked PDA" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "authority" + }, + { + "kind": "account", + "type": "publicKey", + "path": "current_stake_account_positions" + } + ] + } + }, + { + "name": "config", + "isMut": false, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "config" + } + ] + } + }, + { + "name": "newStakeAccountPositions", + "isMut": false, + "isSigner": false + }, + { + "name": "newStakeAccountMetadata", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "stake_metadata" + }, + { + "kind": "account", + "type": "publicKey", + "path": "new_stake_account_positions" + } + ] + } + }, + { + "name": "newStakeAccountCustody", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "custody" + }, + { + "kind": "account", + "type": "publicKey", + "path": "new_stake_account_positions" + } + ] + } + }, + { + "name": "newCustodyAuthority", + "isMut": false, + "isSigner": false, + "docs": [ + "CHECK : This AccountInfo is safe because it's a checked PDA" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "authority" + }, + { + "kind": "account", + "type": "publicKey", + "path": "new_stake_account_positions" + } + ] + } + }, + { + "name": "newVoterRecord", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "voter_weight" + }, + { + "kind": "account", + "type": "publicKey", + "path": "new_stake_account_positions" + } + ] + } + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] } ], "accounts": [ @@ -946,6 +1241,22 @@ ] } }, + { + "name": "SplitRequest", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "recipient", + "type": "publicKey" + } + ] + } + }, { "name": "StakeAccountMetadataV2", "docs": [ diff --git a/staking/target/types/staking.ts b/staking/target/types/staking.ts index 044788bc..a3b55072 100644 --- a/staking/target/types/staking.ts +++ b/staking/target/types/staking.ts @@ -791,6 +791,301 @@ export type Staking = { "type": "i64" } ] + }, + { + "name": "createSplitRequest", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "stakeAccountPositions", + "isMut": false, + "isSigner": false + }, + { + "name": "stakeAccountMetadata", + "isMut": false, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "stake_metadata" + }, + { + "kind": "account", + "type": "publicKey", + "path": "stake_account_positions" + } + ] + } + }, + { + "name": "stakeAccountSplitRequest", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "split_request" + }, + { + "kind": "account", + "type": "publicKey", + "path": "stake_account_positions" + } + ] + } + }, + { + "name": "config", + "isMut": false, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "config" + } + ] + } + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "recipient", + "type": "publicKey" + } + ] + }, + { + "name": "acceptSplitRequest", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "currentStakeAccountPositions", + "isMut": true, + "isSigner": false + }, + { + "name": "currentStakeAccountMetadata", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "stake_metadata" + }, + { + "kind": "account", + "type": "publicKey", + "path": "current_stake_account_positions" + } + ] + } + }, + { + "name": "currentStakeAccountSplitRequest", + "isMut": false, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "split_request" + }, + { + "kind": "account", + "type": "publicKey", + "path": "current_stake_account_positions" + } + ] + } + }, + { + "name": "currentStakeAccountCustody", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "custody" + }, + { + "kind": "account", + "type": "publicKey", + "path": "current_stake_account_positions" + } + ] + } + }, + { + "name": "currentCustodyAuthority", + "isMut": false, + "isSigner": false, + "docs": [ + "CHECK : This AccountInfo is safe because it's a checked PDA" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "authority" + }, + { + "kind": "account", + "type": "publicKey", + "path": "current_stake_account_positions" + } + ] + } + }, + { + "name": "config", + "isMut": false, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "config" + } + ] + } + }, + { + "name": "newStakeAccountPositions", + "isMut": false, + "isSigner": false + }, + { + "name": "newStakeAccountMetadata", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "stake_metadata" + }, + { + "kind": "account", + "type": "publicKey", + "path": "new_stake_account_positions" + } + ] + } + }, + { + "name": "newStakeAccountCustody", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "custody" + }, + { + "kind": "account", + "type": "publicKey", + "path": "new_stake_account_positions" + } + ] + } + }, + { + "name": "newCustodyAuthority", + "isMut": false, + "isSigner": false, + "docs": [ + "CHECK : This AccountInfo is safe because it's a checked PDA" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "authority" + }, + { + "kind": "account", + "type": "publicKey", + "path": "new_stake_account_positions" + } + ] + } + }, + { + "name": "newVoterRecord", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "voter_weight" + }, + { + "kind": "account", + "type": "publicKey", + "path": "new_stake_account_positions" + } + ] + } + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] } ], "accounts": [ @@ -946,6 +1241,22 @@ export type Staking = { ] } }, + { + "name": "splitRequest", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "recipient", + "type": "publicKey" + } + ] + } + }, { "name": "stakeAccountMetadataV2", "docs": [ @@ -1489,29 +1800,356 @@ export type Staking = { "msg": "Can't vote during an account's transfer epoch" }, { - "code": 6029, - "name": "Other", - "msg": "Other" - } - ] -}; - -export const IDL: Staking = { - "version": "1.0.0", - "name": "staking", - "instructions": [ - { - "name": "initConfig", + "code": 6029, + "name": "Other", + "msg": "Other" + } + ] +}; + +export const IDL: Staking = { + "version": "1.0.0", + "name": "staking", + "instructions": [ + { + "name": "initConfig", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "configAccount", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "config" + } + ] + } + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "globalConfig", + "type": { + "defined": "GlobalConfig" + } + } + ] + }, + { + "name": "updateGovernanceAuthority", + "accounts": [ + { + "name": "governanceSigner", + "isMut": false, + "isSigner": true + }, + { + "name": "config", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "config" + } + ] + } + } + ], + "args": [ + { + "name": "newAuthority", + "type": "publicKey" + } + ] + }, + { + "name": "updateFreeze", + "accounts": [ + { + "name": "governanceSigner", + "isMut": false, + "isSigner": true + }, + { + "name": "config", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "config" + } + ] + } + } + ], + "args": [ + { + "name": "freeze", + "type": "bool" + } + ] + }, + { + "name": "updateTokenListTime", + "accounts": [ + { + "name": "governanceSigner", + "isMut": false, + "isSigner": true + }, + { + "name": "config", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "config" + } + ] + } + } + ], + "args": [ + { + "name": "tokenListTime", + "type": { + "option": "i64" + } + } + ] + }, + { + "name": "createStakeAccount", + "docs": [ + "Trustless instruction that creates a stake account for a user", + "The main account i.e. the position accounts needs to be initialized outside of the program", + "otherwise we run into stack limits" + ], + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "stakeAccountPositions", + "isMut": true, + "isSigner": false + }, + { + "name": "stakeAccountMetadata", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "stake_metadata" + }, + { + "kind": "account", + "type": "publicKey", + "path": "stake_account_positions" + } + ] + } + }, + { + "name": "stakeAccountCustody", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "custody" + }, + { + "kind": "account", + "type": "publicKey", + "path": "stake_account_positions" + } + ] + } + }, + { + "name": "custodyAuthority", + "isMut": false, + "isSigner": false, + "docs": [ + "CHECK : This AccountInfo is safe because it's a checked PDA" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "authority" + }, + { + "kind": "account", + "type": "publicKey", + "path": "stake_account_positions" + } + ] + } + }, + { + "name": "voterRecord", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "voter_weight" + }, + { + "kind": "account", + "type": "publicKey", + "path": "stake_account_positions" + } + ] + } + }, + { + "name": "config", + "isMut": false, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "config" + } + ] + } + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "owner", + "type": "publicKey" + }, + { + "name": "lock", + "type": { + "defined": "VestingSchedule" + } + } + ] + }, + { + "name": "createPosition", + "docs": [ + "Creates a position", + "Looks for the first available place in the array, fails if array is full", + "Computes risk and fails if new positions exceed risk limit" + ], "accounts": [ { "name": "payer", - "isMut": true, + "isMut": false, "isSigner": true }, { - "name": "configAccount", + "name": "stakeAccountPositions", + "isMut": true, + "isSigner": false + }, + { + "name": "stakeAccountMetadata", "isMut": true, "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "stake_metadata" + }, + { + "kind": "account", + "type": "publicKey", + "path": "stake_account_positions" + } + ] + } + }, + { + "name": "stakeAccountCustody", + "isMut": false, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "custody" + }, + { + "kind": "account", + "type": "publicKey", + "path": "stake_account_positions" + } + ] + } + }, + { + "name": "config", + "isMut": false, + "isSigner": false, "pda": { "seeds": [ { @@ -1523,65 +2161,151 @@ export const IDL: Staking = { } }, { - "name": "rent", + "name": "targetAccount", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "targetWithParameters", + "type": { + "defined": "TargetWithParameters" + } + }, + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "closePosition", + "accounts": [ + { + "name": "payer", "isMut": false, + "isSigner": true + }, + { + "name": "stakeAccountPositions", + "isMut": true, "isSigner": false }, { - "name": "systemProgram", + "name": "stakeAccountMetadata", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "stake_metadata" + }, + { + "kind": "account", + "type": "publicKey", + "path": "stake_account_positions" + } + ] + } + }, + { + "name": "stakeAccountCustody", + "isMut": false, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "custody" + }, + { + "kind": "account", + "type": "publicKey", + "path": "stake_account_positions" + } + ] + } + }, + { + "name": "config", "isMut": false, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "config" + } + ] + } + }, + { + "name": "targetAccount", + "isMut": true, "isSigner": false } ], "args": [ { - "name": "globalConfig", + "name": "index", + "type": "u8" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "targetWithParameters", "type": { - "defined": "GlobalConfig" + "defined": "TargetWithParameters" } } ] }, { - "name": "updateGovernanceAuthority", + "name": "withdrawStake", "accounts": [ { - "name": "governanceSigner", + "name": "payer", "isMut": false, "isSigner": true }, { - "name": "config", + "name": "destination", "isMut": true, + "isSigner": false + }, + { + "name": "stakeAccountPositions", + "isMut": false, + "isSigner": false + }, + { + "name": "stakeAccountMetadata", + "isMut": false, "isSigner": false, "pda": { "seeds": [ { "kind": "const", "type": "string", - "value": "config" + "value": "stake_metadata" + }, + { + "kind": "account", + "type": "publicKey", + "path": "stake_account_positions" } ] } - } - ], - "args": [ - { - "name": "newAuthority", - "type": "publicKey" - } - ] - }, - { - "name": "updateFreeze", - "accounts": [ - { - "name": "governanceSigner", - "isMut": false, - "isSigner": true }, { - "name": "config", + "name": "stakeAccountCustody", "isMut": true, "isSigner": false, "pda": { @@ -1589,30 +2313,41 @@ export const IDL: Staking = { { "kind": "const", "type": "string", - "value": "config" + "value": "custody" + }, + { + "kind": "account", + "type": "publicKey", + "path": "stake_account_positions" } ] } - } - ], - "args": [ - { - "name": "freeze", - "type": "bool" - } - ] - }, - { - "name": "updateTokenListTime", - "accounts": [ + }, { - "name": "governanceSigner", + "name": "custodyAuthority", "isMut": false, - "isSigner": true + "isSigner": false, + "docs": [ + "CHECK : This AccountInfo is safe because it's a checked PDA" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "authority" + }, + { + "kind": "account", + "type": "publicKey", + "path": "stake_account_positions" + } + ] + } }, { "name": "config", - "isMut": true, + "isMut": false, "isSigner": false, "pda": { "seeds": [ @@ -1623,38 +2358,36 @@ export const IDL: Staking = { } ] } + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false } ], "args": [ { - "name": "tokenListTime", - "type": { - "option": "i64" - } + "name": "amount", + "type": "u64" } ] }, { - "name": "createStakeAccount", - "docs": [ - "Trustless instruction that creates a stake account for a user", - "The main account i.e. the position accounts needs to be initialized outside of the program", - "otherwise we run into stack limits" - ], + "name": "updateVoterWeight", "accounts": [ { "name": "payer", - "isMut": true, + "isMut": false, "isSigner": true }, { "name": "stakeAccountPositions", - "isMut": true, + "isMut": false, "isSigner": false }, { "name": "stakeAccountMetadata", - "isMut": true, + "isMut": false, "isSigner": false, "pda": { "seeds": [ @@ -1673,7 +2406,7 @@ export const IDL: Staking = { }, { "name": "stakeAccountCustody", - "isMut": true, + "isMut": false, "isSigner": false, "pda": { "seeds": [ @@ -1691,18 +2424,15 @@ export const IDL: Staking = { } }, { - "name": "custodyAuthority", - "isMut": false, + "name": "voterRecord", + "isMut": true, "isSigner": false, - "docs": [ - "CHECK : This AccountInfo is safe because it's a checked PDA" - ], "pda": { "seeds": [ { "kind": "const", "type": "string", - "value": "authority" + "value": "voter_weight" }, { "kind": "account", @@ -1713,111 +2443,107 @@ export const IDL: Staking = { } }, { - "name": "voterRecord", - "isMut": true, + "name": "config", + "isMut": false, "isSigner": false, "pda": { "seeds": [ { "kind": "const", "type": "string", - "value": "voter_weight" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" + "value": "config" } ] } }, { - "name": "config", - "isMut": false, + "name": "governanceTarget", + "isMut": true, "isSigner": false, "pda": { "seeds": [ { "kind": "const", "type": "string", - "value": "config" + "value": "target" + }, + { + "kind": "const", + "type": "string", + "value": "voting" } ] } - }, - { - "name": "mint", - "isMut": false, - "isSigner": false - }, - { - "name": "rent", - "isMut": false, - "isSigner": false - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false } ], "args": [ { - "name": "owner", - "type": "publicKey" - }, - { - "name": "lock", + "name": "action", "type": { - "defined": "VestingSchedule" + "defined": "VoterWeightAction" } } ] }, { - "name": "createPosition", - "docs": [ - "Creates a position", - "Looks for the first available place in the array, fails if array is full", - "Computes risk and fails if new positions exceed risk limit" - ], + "name": "updateMaxVoterWeight", "accounts": [ { "name": "payer", - "isMut": false, + "isMut": true, "isSigner": true }, { - "name": "stakeAccountPositions", + "name": "maxVoterRecord", "isMut": true, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "max_voter" + } + ] + } }, { - "name": "stakeAccountMetadata", - "isMut": true, + "name": "config", + "isMut": false, "isSigner": false, "pda": { "seeds": [ { "kind": "const", "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" + "value": "config" } ] } }, { - "name": "stakeAccountCustody", + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "createTarget", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "governanceSigner", + "isMut": false, + "isSigner": true + }, + { + "name": "config", "isMut": false, "isSigner": false, "pda": { @@ -1825,19 +2551,37 @@ export const IDL: Staking = { { "kind": "const", "type": "string", - "value": "custody" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" + "value": "config" } ] } }, { - "name": "config", + "name": "targetAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "target", + "type": { + "defined": "Target" + } + } + ] + }, + { + "name": "advanceClock", + "accounts": [ + { + "name": "config", + "isMut": true, "isSigner": false, "pda": { "seeds": [ @@ -1848,42 +2592,31 @@ export const IDL: Staking = { } ] } - }, - { - "name": "targetAccount", - "isMut": true, - "isSigner": false } ], "args": [ { - "name": "targetWithParameters", - "type": { - "defined": "TargetWithParameters" - } - }, - { - "name": "amount", - "type": "u64" + "name": "seconds", + "type": "i64" } ] }, { - "name": "closePosition", + "name": "createSplitRequest", "accounts": [ { "name": "payer", - "isMut": false, + "isMut": true, "isSigner": true }, { "name": "stakeAccountPositions", - "isMut": true, + "isMut": false, "isSigner": false }, { "name": "stakeAccountMetadata", - "isMut": true, + "isMut": false, "isSigner": false, "pda": { "seeds": [ @@ -1901,15 +2634,15 @@ export const IDL: Staking = { } }, { - "name": "stakeAccountCustody", - "isMut": false, + "name": "stakeAccountSplitRequest", + "isMut": true, "isSigner": false, "pda": { "seeds": [ { "kind": "const", "type": "string", - "value": "custody" + "value": "split_request" }, { "kind": "account", @@ -1934,48 +2667,56 @@ export const IDL: Staking = { } }, { - "name": "targetAccount", - "isMut": true, + "name": "systemProgram", + "isMut": false, "isSigner": false } ], "args": [ - { - "name": "index", - "type": "u8" - }, { "name": "amount", "type": "u64" }, { - "name": "targetWithParameters", - "type": { - "defined": "TargetWithParameters" - } + "name": "recipient", + "type": "publicKey" } ] }, { - "name": "withdrawStake", + "name": "acceptSplitRequest", "accounts": [ { "name": "payer", - "isMut": false, + "isMut": true, "isSigner": true }, { - "name": "destination", + "name": "currentStakeAccountPositions", "isMut": true, "isSigner": false }, { - "name": "stakeAccountPositions", - "isMut": false, - "isSigner": false + "name": "currentStakeAccountMetadata", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "stake_metadata" + }, + { + "kind": "account", + "type": "publicKey", + "path": "current_stake_account_positions" + } + ] + } }, { - "name": "stakeAccountMetadata", + "name": "currentStakeAccountSplitRequest", "isMut": false, "isSigner": false, "pda": { @@ -1983,18 +2724,18 @@ export const IDL: Staking = { { "kind": "const", "type": "string", - "value": "stake_metadata" + "value": "split_request" }, { "kind": "account", "type": "publicKey", - "path": "stake_account_positions" + "path": "current_stake_account_positions" } ] } }, { - "name": "stakeAccountCustody", + "name": "currentStakeAccountCustody", "isMut": true, "isSigner": false, "pda": { @@ -2007,13 +2748,13 @@ export const IDL: Staking = { { "kind": "account", "type": "publicKey", - "path": "stake_account_positions" + "path": "current_stake_account_positions" } ] } }, { - "name": "custodyAuthority", + "name": "currentCustodyAuthority", "isMut": false, "isSigner": false, "docs": [ @@ -2029,7 +2770,7 @@ export const IDL: Staking = { { "kind": "account", "type": "publicKey", - "path": "stake_account_positions" + "path": "current_stake_account_positions" } ] } @@ -2049,34 +2790,13 @@ export const IDL: Staking = { } }, { - "name": "tokenProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "amount", - "type": "u64" - } - ] - }, - { - "name": "updateVoterWeight", - "accounts": [ - { - "name": "payer", - "isMut": false, - "isSigner": true - }, - { - "name": "stakeAccountPositions", + "name": "newStakeAccountPositions", "isMut": false, "isSigner": false }, { - "name": "stakeAccountMetadata", - "isMut": false, + "name": "newStakeAccountMetadata", + "isMut": true, "isSigner": false, "pda": { "seeds": [ @@ -2088,14 +2808,14 @@ export const IDL: Staking = { { "kind": "account", "type": "publicKey", - "path": "stake_account_positions" + "path": "new_stake_account_positions" } ] } }, { - "name": "stakeAccountCustody", - "isMut": false, + "name": "newStakeAccountCustody", + "isMut": true, "isSigner": false, "pda": { "seeds": [ @@ -2107,46 +2827,35 @@ export const IDL: Staking = { { "kind": "account", "type": "publicKey", - "path": "stake_account_positions" + "path": "new_stake_account_positions" } ] } }, { - "name": "voterRecord", - "isMut": true, + "name": "newCustodyAuthority", + "isMut": false, "isSigner": false, + "docs": [ + "CHECK : This AccountInfo is safe because it's a checked PDA" + ], "pda": { "seeds": [ { "kind": "const", "type": "string", - "value": "voter_weight" + "value": "authority" }, { "kind": "account", "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" + "path": "new_stake_account_positions" } ] } }, { - "name": "governanceTarget", + "name": "newVoterRecord", "isMut": true, "isSigner": false, "pda": { @@ -2154,100 +2863,29 @@ export const IDL: Staking = { { "kind": "const", "type": "string", - "value": "target" + "value": "voter_weight" }, { - "kind": "const", - "type": "string", - "value": "voting" - } - ] - } - } - ], - "args": [ - { - "name": "action", - "type": { - "defined": "VoterWeightAction" - } - } - ] - }, - { - "name": "updateMaxVoterWeight", - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "maxVoterRecord", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "max_voter" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" + "kind": "account", + "type": "publicKey", + "path": "new_stake_account_positions" } ] } }, { - "name": "systemProgram", + "name": "mint", "isMut": false, "isSigner": false - } - ], - "args": [] - }, - { - "name": "createTarget", - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true }, { - "name": "governanceSigner", + "name": "rent", "isMut": false, - "isSigner": true + "isSigner": false }, { - "name": "config", + "name": "tokenProgram", "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "targetAccount", - "isMut": true, "isSigner": false }, { @@ -2256,39 +2894,7 @@ export const IDL: Staking = { "isSigner": false } ], - "args": [ - { - "name": "target", - "type": { - "defined": "Target" - } - } - ] - }, - { - "name": "advanceClock", - "accounts": [ - { - "name": "config", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - } - ], - "args": [ - { - "name": "seconds", - "type": "i64" - } - ] + "args": [] } ], "accounts": [ @@ -2444,6 +3050,22 @@ export const IDL: Staking = { ] } }, + { + "name": "splitRequest", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "recipient", + "type": "publicKey" + } + ] + } + }, { "name": "stakeAccountMetadataV2", "docs": [ diff --git a/staking/tests/split_vesting_account.ts b/staking/tests/split_vesting_account.ts new file mode 100644 index 00000000..15363e69 --- /dev/null +++ b/staking/tests/split_vesting_account.ts @@ -0,0 +1,272 @@ +import { + ANCHOR_CONFIG_PATH, + CustomAbortController, + getPortNumber, + makeDefaultConfig, + readAnchorConfig, + requestPythAirdrop, + standardSetup, +} from "./utils/before"; +import path from "path"; +import { Keypair, PublicKey, Transaction } from "@solana/web3.js"; +import { StakeConnection, PythBalance, VestingAccountState } from "../app"; +import { BN, Wallet } from "@project-serum/anchor"; +import { assertBalanceMatches, loadAndUnlock } from "./utils/api_utils"; +import assert from "assert"; + +const ONE_MONTH = new BN(3600 * 24 * 30.5); +const portNumber = getPortNumber(path.basename(__filename)); + +describe("split vesting account", async () => { + const pythMintAccount = new Keypair(); + const pythMintAuthority = new Keypair(); + let EPOCH_DURATION: BN; + + let stakeConnection: StakeConnection; + let controller: CustomAbortController; + + let owner: PublicKey; + + let pdaAuthority = new Keypair(); + let pdaConnection: StakeConnection; + + let sam = new Keypair(); + let samConnection: StakeConnection; + + let alice = new Keypair(); + let aliceConnection: StakeConnection; + + before(async () => { + const config = readAnchorConfig(ANCHOR_CONFIG_PATH); + ({ controller, stakeConnection } = await standardSetup( + portNumber, + config, + pythMintAccount, + pythMintAuthority, + makeDefaultConfig(pythMintAccount.publicKey, PublicKey.unique()) + )); + + EPOCH_DURATION = stakeConnection.config.epochDuration; + owner = stakeConnection.provider.wallet.publicKey; + + samConnection = await StakeConnection.createStakeConnection( + stakeConnection.provider.connection, + new Wallet(sam), + stakeConnection.program.programId + ); + + pdaConnection = await StakeConnection.createStakeConnection( + stakeConnection.provider.connection, + new Wallet(pdaAuthority), + stakeConnection.program.programId + ); + }); + + it("create a vesting account", async () => { + await samConnection.provider.connection.requestAirdrop( + sam.publicKey, + 1_000_000_000_000 + ); + await requestPythAirdrop( + sam.publicKey, + pythMintAccount.publicKey, + pythMintAuthority, + PythBalance.fromString("200"), + samConnection.provider.connection + ); + + const transaction = new Transaction(); + + const stakeAccountKeypair = await samConnection.withCreateAccount( + transaction.instructions, + sam.publicKey, + { + periodicVesting: { + initialBalance: PythBalance.fromString("100").toBN(), + startDate: await stakeConnection.getTime(), + periodDuration: ONE_MONTH, + numPeriods: new BN(72), + }, + } + ); + + transaction.instructions.push( + await samConnection.buildTransferInstruction( + stakeAccountKeypair.publicKey, + PythBalance.fromString("100").toBN() + ) + ); + + await samConnection.provider.sendAndConfirm( + transaction, + [stakeAccountKeypair], + { skipPreflight: true } + ); + + let stakeAccount = await samConnection.getMainAccount(sam.publicKey); + assert( + VestingAccountState.UnvestedTokensFullyUnlocked == + stakeAccount.getVestingAccountState(await samConnection.getTime()) + ); + await assertBalanceMatches( + samConnection, + sam.publicKey, + { + unvested: { + unlocked: PythBalance.fromString("100"), + }, + }, + await samConnection.getTime() + ); + }); + + it("request split", async () => { + await pdaConnection.provider.connection.requestAirdrop( + pdaAuthority.publicKey, + 1_000_000_000_000 + ); + + let stakeAccount = await samConnection.getMainAccount(sam.publicKey); + await samConnection.requestSplit( + stakeAccount, + PythBalance.fromString("50"), + alice.publicKey + ); + + await pdaConnection.confirmSplit(stakeAccount); + }); + + // it("one month minus 1 later", async () => { + // await samConnection.program.methods + // .advanceClock(ONE_MONTH.sub(EPOCH_DURATION)) + // .rpc(); + + // await assertBalanceMatches( + // samConnection, + // sam.publicKey, + // { + // unvested: { + // locked: PythBalance.fromString("100"), + // }, + // }, + // await samConnection.getTime() + // ); + + // let samStakeAccount = await samConnection.getMainAccount(sam.publicKey); + + // assert( + // VestingAccountState.UnvestedTokensFullyLocked == + // samStakeAccount.getVestingAccountState(await samConnection.getTime()) + // ); + + // await samConnection.depositAndLockTokens( + // samStakeAccount, + // PythBalance.fromString("1") + // ); + + // samStakeAccount = await samConnection.getMainAccount(sam.publicKey); + // assert( + // VestingAccountState.UnvestedTokensFullyLocked == + // samStakeAccount.getVestingAccountState(await samConnection.getTime()) + // ); + // await assertBalanceMatches( + // samConnection, + // sam.publicKey, + // { + // unvested: { + // locked: PythBalance.fromString("100"), + // }, + // locked: { locking: PythBalance.fromString("1") }, + // }, + // await samConnection.getTime() + // ); + // }); + + // it("one month later", async () => { + // await samConnection.program.methods.advanceClock(EPOCH_DURATION).rpc(); + + // let samStakeAccount = await samConnection.getMainAccount(sam.publicKey); + // assert( + // VestingAccountState.UnvestedTokensFullyLocked == + // samStakeAccount.getVestingAccountState(await samConnection.getTime()) + // ); + + // await assertBalanceMatches( + // samConnection, + // sam.publicKey, + // { + // unvested: { + // locked: PythBalance.fromString("98.611112"), + // }, + // locked: { locked: PythBalance.fromString("2.388888") }, + // }, + // await samConnection.getTime() + // ); + + // await samConnection.depositAndLockTokens( + // samStakeAccount, + // PythBalance.fromString("1") + // ); + + // samStakeAccount = await samConnection.getMainAccount(sam.publicKey); + + // assert( + // VestingAccountState.UnvestedTokensFullyLocked == + // samStakeAccount.getVestingAccountState(await samConnection.getTime()) + // ); + + // await assertBalanceMatches( + // samConnection, + // sam.publicKey, + // { + // unvested: { + // locked: PythBalance.fromString("98.611112"), + // }, + // locked: { + // locked: PythBalance.fromString("2.388888"), + // locking: PythBalance.fromString("1"), + // }, + // }, + // await samConnection.getTime() + // ); + + // await loadAndUnlock( + // samConnection, + // sam.publicKey, + // PythBalance.fromString("1") + // ); + + // samStakeAccount = await samConnection.getMainAccount(sam.publicKey); + + // assert( + // VestingAccountState.UnvestedTokensFullyLocked == + // samStakeAccount.getVestingAccountState(await samConnection.getTime()) + // ); + + // await assertBalanceMatches( + // samConnection, + // sam.publicKey, + // { + // unvested: { + // locked: PythBalance.fromString("98.611112"), + // }, + // locked: { + // preunlocking: PythBalance.fromString("1"), + // locked: PythBalance.fromString("1.388888"), + // locking: PythBalance.fromString("1"), + // }, + // }, + // await samConnection.getTime() + // ); + + // assert( + // samStakeAccount + // .getNetExcessGovernanceAtVesting(await samConnection.getTime()) + // .eq(PythBalance.fromString("3.777777").toBN()) + // ); + // }); + + after(async () => { + controller.abort(); + }); +}); diff --git a/staking/tests/utils/before.ts b/staking/tests/utils/before.ts index 7ba6f168..a87843b9 100644 --- a/staking/tests/utils/before.ts +++ b/staking/tests/utils/before.ts @@ -324,7 +324,8 @@ export async function initConfig( export function makeDefaultConfig( pythMint: PublicKey, - governanceProgram: PublicKey = PublicKey.unique() + governanceProgram: PublicKey = PublicKey.unique(), + pdaAuthority: PublicKey = PublicKey.unique() ): GlobalConfig { return { governanceAuthority: null, @@ -337,7 +338,7 @@ export function makeDefaultConfig( bump: 0, pythTokenListTime: null, governanceProgram, - pdaAuthority: PublicKey.unique(), + pdaAuthority, }; } From 1631311c0242a04f373ea2fa4bcbcab87fcf6739 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 16 Oct 2023 17:25:20 +0100 Subject: [PATCH 10/51] Scaffolding --- staking/app/StakeConnection.ts | 6 +++--- staking/programs/staking/src/context.rs | 17 ++++++++--------- staking/programs/staking/src/lib.rs | 14 ++++++++------ staking/target/idl/staking.json | 6 +++--- staking/target/types/staking.ts | 12 ++++++------ staking/tests/split_vesting_account.ts | 8 ++++++-- 6 files changed, 34 insertions(+), 29 deletions(-) diff --git a/staking/app/StakeConnection.ts b/staking/app/StakeConnection.ts index 48b680b7..4aae4da8 100644 --- a/staking/app/StakeConnection.ts +++ b/staking/app/StakeConnection.ts @@ -817,14 +817,14 @@ export class StakeConnection { recipient: PublicKey ) { await this.program.methods - .createSplitRequest(amount.toBN(), recipient) + .requestSplit(amount.toBN(), recipient) .accounts({ stakeAccountPositions: stakeAccount.address, }) .rpc(); } - public async confirmSplit(stakeAccount: StakeAccount) { + public async acceptSplit(stakeAccount: StakeAccount) { const newStakeAccountKeypair = new Keypair(); const instructions = []; @@ -836,7 +836,7 @@ export class StakeConnection { ); await this.program.methods - .acceptSplitRequest() + .acceptSplit() .accounts({ currentStakeAccountPositions: stakeAccount.address, newStakeAccountPositions: newStakeAccountKeypair.publicKey, diff --git a/staking/programs/staking/src/context.rs b/staking/programs/staking/src/context.rs index 0f37d0d1..e8ec332e 100644 --- a/staking/programs/staking/src/context.rs +++ b/staking/programs/staking/src/context.rs @@ -281,7 +281,7 @@ pub struct CreateTarget<'info> { #[derive(Accounts)] #[instruction(amount : u64, recipient : Pubkey)] -pub struct CreateSplitRequest<'info> { +pub struct RequestSplit<'info> { // Native payer: #[account(mut, address = stake_account_metadata.owner)] pub payer: Signer<'info>, @@ -298,7 +298,7 @@ pub struct CreateSplitRequest<'info> { } #[derive(Accounts)] -pub struct AcceptSplitRequest<'info> { +pub struct AcceptSplit<'info> { // Native payer: #[account(mut, address = config.pda_authority)] pub payer: Signer<'info>, @@ -306,9 +306,9 @@ pub struct AcceptSplitRequest<'info> { #[account(mut)] pub current_stake_account_positions: AccountLoader<'info, positions::PositionData>, #[account(mut, seeds = [STAKE_ACCOUNT_METADATA_SEED.as_bytes(), current_stake_account_positions.key().as_ref()], bump = current_stake_account_metadata.metadata_bump)] - pub current_stake_account_metadata: Account<'info, stake_account::StakeAccountMetadataV2>, + pub current_stake_account_metadata: Box>, #[account(seeds = [SPLIT_REQUEST.as_bytes(), current_stake_account_positions.key().as_ref()], bump)] - pub current_stake_account_split_request: Account<'info, split_request::SplitRequest>, + pub current_stake_account_split_request: Box>, #[account( mut, seeds = [CUSTODY_SEED.as_bytes(), current_stake_account_positions.key().as_ref()], @@ -322,6 +322,7 @@ pub struct AcceptSplitRequest<'info> { pub config: Account<'info, global_config::GlobalConfig>, // New stake accounts : + #[account(zero)] pub new_stake_account_positions: AccountLoader<'info, positions::PositionData>, #[account(init, payer = payer, space = stake_account::StakeAccountMetadataV2::LEN, seeds = [STAKE_ACCOUNT_METADATA_SEED.as_bytes(), new_stake_account_positions.key().as_ref()], bump)] pub new_stake_account_metadata: Box>, @@ -343,7 +344,7 @@ pub struct AcceptSplitRequest<'info> { space = voter_weight_record::VoterWeightRecord::LEN, seeds = [VOTER_RECORD_SEED.as_bytes(), new_stake_account_positions.key().as_ref()], bump)] - pub new_voter_record: Account<'info, voter_weight_record::VoterWeightRecord>, + pub new_voter_record: Box>, // Other accounts needed #[account(address = config.pyth_token_mint)] pub mint: Account<'info, Mint>, @@ -352,12 +353,10 @@ pub struct AcceptSplitRequest<'info> { pub system_program: Program<'info, System>, } -impl<'a, 'b, 'c, 'info> From<&AcceptSplitRequest<'info>> +impl<'a, 'b, 'c, 'info> From<&AcceptSplit<'info>> for CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> { - fn from( - accounts: &AcceptSplitRequest<'info>, - ) -> CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> { + fn from(accounts: &AcceptSplit<'info>) -> CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> { let cpi_accounts = Transfer { from: accounts.current_stake_account_custody.to_account_info(), to: accounts.new_stake_account_custody.to_account_info(), diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index b4db6f79..0047d6a9 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -535,20 +535,20 @@ pub mod staking { } } - pub fn create_split_request( - ctx: Context, - amount: u64, - recipient: Pubkey, - ) -> Result<()> { + pub fn request_split(ctx: Context, amount: u64, recipient: Pubkey) -> Result<()> { ctx.accounts.stake_account_split_request.amount = amount; ctx.accounts.stake_account_split_request.recipient = recipient; Ok(()) } - pub fn accept_split_request(ctx: Context) -> Result<()> { + pub fn accept_split(ctx: Context) -> Result<()> { let split_request = &ctx.accounts.current_stake_account_split_request; + // Split vesting schedule between both accounts + + // Transfer stake positions to the new account + // Transfer tokens transfer( CpiContext::from(&*ctx.accounts).with_signer(&[&[ @@ -559,5 +559,7 @@ pub mod staking { split_request.amount, )?; Ok(()) + + // Check both accounts are valid after the transfer } } diff --git a/staking/target/idl/staking.json b/staking/target/idl/staking.json index c35faa53..da2ce2eb 100644 --- a/staking/target/idl/staking.json +++ b/staking/target/idl/staking.json @@ -793,7 +793,7 @@ ] }, { - "name": "createSplitRequest", + "name": "requestSplit", "accounts": [ { "name": "payer", @@ -875,7 +875,7 @@ ] }, { - "name": "acceptSplitRequest", + "name": "acceptSplit", "accounts": [ { "name": "payer", @@ -982,7 +982,7 @@ }, { "name": "newStakeAccountPositions", - "isMut": false, + "isMut": true, "isSigner": false }, { diff --git a/staking/target/types/staking.ts b/staking/target/types/staking.ts index a3b55072..caf30579 100644 --- a/staking/target/types/staking.ts +++ b/staking/target/types/staking.ts @@ -793,7 +793,7 @@ export type Staking = { ] }, { - "name": "createSplitRequest", + "name": "requestSplit", "accounts": [ { "name": "payer", @@ -875,7 +875,7 @@ export type Staking = { ] }, { - "name": "acceptSplitRequest", + "name": "acceptSplit", "accounts": [ { "name": "payer", @@ -982,7 +982,7 @@ export type Staking = { }, { "name": "newStakeAccountPositions", - "isMut": false, + "isMut": true, "isSigner": false }, { @@ -2602,7 +2602,7 @@ export const IDL: Staking = { ] }, { - "name": "createSplitRequest", + "name": "requestSplit", "accounts": [ { "name": "payer", @@ -2684,7 +2684,7 @@ export const IDL: Staking = { ] }, { - "name": "acceptSplitRequest", + "name": "acceptSplit", "accounts": [ { "name": "payer", @@ -2791,7 +2791,7 @@ export const IDL: Staking = { }, { "name": "newStakeAccountPositions", - "isMut": false, + "isMut": true, "isSigner": false }, { diff --git a/staking/tests/split_vesting_account.ts b/staking/tests/split_vesting_account.ts index 15363e69..ce33ad9b 100644 --- a/staking/tests/split_vesting_account.ts +++ b/staking/tests/split_vesting_account.ts @@ -43,7 +43,11 @@ describe("split vesting account", async () => { config, pythMintAccount, pythMintAuthority, - makeDefaultConfig(pythMintAccount.publicKey, PublicKey.unique()) + makeDefaultConfig( + pythMintAccount.publicKey, + PublicKey.unique(), + pdaAuthority.publicKey + ) )); EPOCH_DURATION = stakeConnection.config.epochDuration; @@ -133,7 +137,7 @@ describe("split vesting account", async () => { alice.publicKey ); - await pdaConnection.confirmSplit(stakeAccount); + await pdaConnection.acceptSplit(stakeAccount); }); // it("one month minus 1 later", async () => { From b5fb19132ca359462f4e7dd4a32038198e45f958 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 16 Oct 2023 17:54:11 +0100 Subject: [PATCH 11/51] Checkpoint --- staking/programs/staking/src/context.rs | 21 ++++++++++++--------- staking/programs/staking/src/lib.rs | 24 ++++++++++++++---------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/staking/programs/staking/src/context.rs b/staking/programs/staking/src/context.rs index e8ec332e..3461e9d9 100644 --- a/staking/programs/staking/src/context.rs +++ b/staking/programs/staking/src/context.rs @@ -293,8 +293,8 @@ pub struct RequestSplit<'info> { pub stake_account_split_request: Account<'info, split_request::SplitRequest>, #[account(seeds = [CONFIG_SEED.as_bytes()], bump = config.bump)] pub config: Account<'info, global_config::GlobalConfig>, - - pub system_program: Program<'info, System>, + // Primitive accounts : + pub system_program: Program<'info, System>, } #[derive(Accounts)] @@ -318,8 +318,6 @@ pub struct AcceptSplit<'info> { /// CHECK : This AccountInfo is safe because it's a checked PDA #[account(seeds = [AUTHORITY_SEED.as_bytes(), current_stake_account_positions.key().as_ref()], bump = current_stake_account_metadata.authority_bump)] pub current_custody_authority: AccountInfo<'info>, - #[account(seeds = [CONFIG_SEED.as_bytes()], bump = config.bump)] - pub config: Account<'info, global_config::GlobalConfig>, // New stake accounts : #[account(zero)] @@ -345,12 +343,17 @@ pub struct AcceptSplit<'info> { seeds = [VOTER_RECORD_SEED.as_bytes(), new_stake_account_positions.key().as_ref()], bump)] pub new_voter_record: Box>, - // Other accounts needed + + #[account(seeds = [CONFIG_SEED.as_bytes()], bump = config.bump)] + pub config: Account<'info, global_config::GlobalConfig>, + + // Pyth token mint: #[account(address = config.pyth_token_mint)] - pub mint: Account<'info, Mint>, - pub rent: Sysvar<'info, Rent>, - pub token_program: Program<'info, Token>, - pub system_program: Program<'info, System>, + pub mint: Account<'info, Mint>, + // Primitive accounts : + pub rent: Sysvar<'info, Rent>, + pub token_program: Program<'info, Token>, + pub system_program: Program<'info, System>, } impl<'a, 'b, 'c, 'info> From<&AcceptSplit<'info>> diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index 0047d6a9..95e452cd 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -543,21 +543,25 @@ pub mod staking { pub fn accept_split(ctx: Context) -> Result<()> { - let split_request = &ctx.accounts.current_stake_account_split_request; - // Split vesting schedule between both accounts // Transfer stake positions to the new account // Transfer tokens - transfer( - CpiContext::from(&*ctx.accounts).with_signer(&[&[ - AUTHORITY_SEED.as_bytes(), - ctx.accounts.current_stake_account_positions.key().as_ref(), - &[ctx.accounts.current_stake_account_metadata.authority_bump], - ]]), - split_request.amount, - )?; + { + let split_request = &ctx.accounts.current_stake_account_split_request; + transfer( + CpiContext::from(&*ctx.accounts).with_signer(&[&[ + AUTHORITY_SEED.as_bytes(), + ctx.accounts.current_stake_account_positions.key().as_ref(), + &[ctx.accounts.current_stake_account_metadata.authority_bump], + ]]), + split_request.amount, + )?; + } + { + ctx.accounts.current_stake_account_split_request.amount = 0; + } Ok(()) // Check both accounts are valid after the transfer From 6752e22bed1243fcd2c892843d8a8f2e79f78f0e Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 16 Oct 2023 18:00:04 +0100 Subject: [PATCH 12/51] Cleanup --- staking/tests/staking.ts | 2 +- staking/tests/unlock_api_test.ts | 2 +- staking/tests/vesting_test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/staking/tests/staking.ts b/staking/tests/staking.ts index 6c119140..3fca8603 100644 --- a/staking/tests/staking.ts +++ b/staking/tests/staking.ts @@ -67,7 +67,7 @@ describe("staking", async () => { config, pythMintAccount, pythMintAuthority, - makeDefaultConfig(pythMintAccount.publicKey, PublicKey.unique()) + makeDefaultConfig(pythMintAccount.publicKey) )); program = stakeConnection.program; provider = stakeConnection.provider; diff --git a/staking/tests/unlock_api_test.ts b/staking/tests/unlock_api_test.ts index 708ecf18..b6cac64f 100644 --- a/staking/tests/unlock_api_test.ts +++ b/staking/tests/unlock_api_test.ts @@ -34,7 +34,7 @@ describe("unlock_api", async () => { config, pythMintAccount, pythMintAuthority, - makeDefaultConfig(pythMintAccount.publicKey, PublicKey.unique()) + makeDefaultConfig(pythMintAccount.publicKey) )); EPOCH_DURATION = stakeConnection.config.epochDuration; diff --git a/staking/tests/vesting_test.ts b/staking/tests/vesting_test.ts index 60f2343a..23035f4b 100644 --- a/staking/tests/vesting_test.ts +++ b/staking/tests/vesting_test.ts @@ -47,7 +47,7 @@ describe("vesting", async () => { config, pythMintAccount, pythMintAuthority, - makeDefaultConfig(pythMintAccount.publicKey, PublicKey.unique()) + makeDefaultConfig(pythMintAccount.publicKey) )); EPOCH_DURATION = stakeConnection.config.epochDuration; From e2a0d58e66087b5a701269a87b0cbc55a2ebffd3 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 16 Oct 2023 18:01:18 +0100 Subject: [PATCH 13/51] Cleanup test --- staking/tests/split_vesting_account.ts | 133 +------------------------ 1 file changed, 1 insertion(+), 132 deletions(-) diff --git a/staking/tests/split_vesting_account.ts b/staking/tests/split_vesting_account.ts index ce33ad9b..be907a08 100644 --- a/staking/tests/split_vesting_account.ts +++ b/staking/tests/split_vesting_account.ts @@ -11,7 +11,7 @@ import path from "path"; import { Keypair, PublicKey, Transaction } from "@solana/web3.js"; import { StakeConnection, PythBalance, VestingAccountState } from "../app"; import { BN, Wallet } from "@project-serum/anchor"; -import { assertBalanceMatches, loadAndUnlock } from "./utils/api_utils"; +import { assertBalanceMatches } from "./utils/api_utils"; import assert from "assert"; const ONE_MONTH = new BN(3600 * 24 * 30.5); @@ -34,7 +34,6 @@ describe("split vesting account", async () => { let samConnection: StakeConnection; let alice = new Keypair(); - let aliceConnection: StakeConnection; before(async () => { const config = readAnchorConfig(ANCHOR_CONFIG_PATH); @@ -140,136 +139,6 @@ describe("split vesting account", async () => { await pdaConnection.acceptSplit(stakeAccount); }); - // it("one month minus 1 later", async () => { - // await samConnection.program.methods - // .advanceClock(ONE_MONTH.sub(EPOCH_DURATION)) - // .rpc(); - - // await assertBalanceMatches( - // samConnection, - // sam.publicKey, - // { - // unvested: { - // locked: PythBalance.fromString("100"), - // }, - // }, - // await samConnection.getTime() - // ); - - // let samStakeAccount = await samConnection.getMainAccount(sam.publicKey); - - // assert( - // VestingAccountState.UnvestedTokensFullyLocked == - // samStakeAccount.getVestingAccountState(await samConnection.getTime()) - // ); - - // await samConnection.depositAndLockTokens( - // samStakeAccount, - // PythBalance.fromString("1") - // ); - - // samStakeAccount = await samConnection.getMainAccount(sam.publicKey); - // assert( - // VestingAccountState.UnvestedTokensFullyLocked == - // samStakeAccount.getVestingAccountState(await samConnection.getTime()) - // ); - // await assertBalanceMatches( - // samConnection, - // sam.publicKey, - // { - // unvested: { - // locked: PythBalance.fromString("100"), - // }, - // locked: { locking: PythBalance.fromString("1") }, - // }, - // await samConnection.getTime() - // ); - // }); - - // it("one month later", async () => { - // await samConnection.program.methods.advanceClock(EPOCH_DURATION).rpc(); - - // let samStakeAccount = await samConnection.getMainAccount(sam.publicKey); - // assert( - // VestingAccountState.UnvestedTokensFullyLocked == - // samStakeAccount.getVestingAccountState(await samConnection.getTime()) - // ); - - // await assertBalanceMatches( - // samConnection, - // sam.publicKey, - // { - // unvested: { - // locked: PythBalance.fromString("98.611112"), - // }, - // locked: { locked: PythBalance.fromString("2.388888") }, - // }, - // await samConnection.getTime() - // ); - - // await samConnection.depositAndLockTokens( - // samStakeAccount, - // PythBalance.fromString("1") - // ); - - // samStakeAccount = await samConnection.getMainAccount(sam.publicKey); - - // assert( - // VestingAccountState.UnvestedTokensFullyLocked == - // samStakeAccount.getVestingAccountState(await samConnection.getTime()) - // ); - - // await assertBalanceMatches( - // samConnection, - // sam.publicKey, - // { - // unvested: { - // locked: PythBalance.fromString("98.611112"), - // }, - // locked: { - // locked: PythBalance.fromString("2.388888"), - // locking: PythBalance.fromString("1"), - // }, - // }, - // await samConnection.getTime() - // ); - - // await loadAndUnlock( - // samConnection, - // sam.publicKey, - // PythBalance.fromString("1") - // ); - - // samStakeAccount = await samConnection.getMainAccount(sam.publicKey); - - // assert( - // VestingAccountState.UnvestedTokensFullyLocked == - // samStakeAccount.getVestingAccountState(await samConnection.getTime()) - // ); - - // await assertBalanceMatches( - // samConnection, - // sam.publicKey, - // { - // unvested: { - // locked: PythBalance.fromString("98.611112"), - // }, - // locked: { - // preunlocking: PythBalance.fromString("1"), - // locked: PythBalance.fromString("1.388888"), - // locking: PythBalance.fromString("1"), - // }, - // }, - // await samConnection.getTime() - // ); - - // assert( - // samStakeAccount - // .getNetExcessGovernanceAtVesting(await samConnection.getTime()) - // .eq(PythBalance.fromString("3.777777").toBN()) - // ); - // }); - after(async () => { controller.abort(); }); From f1f455e69c460669bedf6a5aad8e0179b9981fbd Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 16 Oct 2023 18:02:26 +0100 Subject: [PATCH 14/51] Another round --- staking/tests/max_pos.ts | 2 +- staking/tests/position_lifecycle.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/staking/tests/max_pos.ts b/staking/tests/max_pos.ts index 766678eb..db0c420e 100644 --- a/staking/tests/max_pos.ts +++ b/staking/tests/max_pos.ts @@ -56,7 +56,7 @@ describe("fills a stake account with positions", async () => { config, pythMintAccount, pythMintAuthority, - makeDefaultConfig(pythMintAccount.publicKey, PublicKey.unique()) + makeDefaultConfig(pythMintAccount.publicKey) )); program = stakeConnection.program; provider = stakeConnection.provider; diff --git a/staking/tests/position_lifecycle.ts b/staking/tests/position_lifecycle.ts index 268c3966..02826c85 100644 --- a/staking/tests/position_lifecycle.ts +++ b/staking/tests/position_lifecycle.ts @@ -54,7 +54,7 @@ describe("position_lifecycle", async () => { config, pythMintAccount, pythMintAuthority, - makeDefaultConfig(pythMintAccount.publicKey, PublicKey.unique()) + makeDefaultConfig(pythMintAccount.publicKey) )); program = stakeConnection.program; owner = stakeConnection.provider.wallet.publicKey; From feb94bf74b021ded4cd315f7c4d7754885158abc Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 16 Oct 2023 18:03:03 +0100 Subject: [PATCH 15/51] Cleanup --- staking/tests/api_test.ts | 4 ++-- staking/tests/clock_api_test.ts | 4 ++-- staking/tests/create_product.ts | 5 +---- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/staking/tests/api_test.ts b/staking/tests/api_test.ts index 66815167..a8b5fa3a 100644 --- a/staking/tests/api_test.ts +++ b/staking/tests/api_test.ts @@ -1,4 +1,4 @@ -import { Keypair, PublicKey } from "@solana/web3.js"; +import { Keypair } from "@solana/web3.js"; import assert from "assert"; import { StakeConnection } from "../app/StakeConnection"; import { @@ -39,7 +39,7 @@ describe("api", async () => { config, pythMintAccount, pythMintAuthority, - makeDefaultConfig(pythMintAccount.publicKey, PublicKey.unique()), + makeDefaultConfig(pythMintAccount.publicKey), PythBalance.fromString("1000") )); diff --git a/staking/tests/clock_api_test.ts b/staking/tests/clock_api_test.ts index b5e6bda2..042beb8b 100644 --- a/staking/tests/clock_api_test.ts +++ b/staking/tests/clock_api_test.ts @@ -8,7 +8,7 @@ import { standardSetup, } from "./utils/before"; import path from "path"; -import { Keypair, PublicKey } from "@solana/web3.js"; +import { Keypair } from "@solana/web3.js"; import { StakeConnection } from "../app"; import assert from "assert"; import { BN } from "@project-serum/anchor"; @@ -33,7 +33,7 @@ describe("clock_api", async () => { config, pythMintAccount, pythMintAuthority, - makeDefaultConfig(pythMintAccount.publicKey, PublicKey.unique()) + makeDefaultConfig(pythMintAccount.publicKey) )); }); diff --git a/staking/tests/create_product.ts b/staking/tests/create_product.ts index 585ce7a0..04409216 100644 --- a/staking/tests/create_product.ts +++ b/staking/tests/create_product.ts @@ -35,10 +35,7 @@ describe("create_product", async () => { before(async () => { const config = readAnchorConfig(ANCHOR_CONFIG_PATH); - let globalConfig = makeDefaultConfig( - pythMintAccount.publicKey, - PublicKey.unique() - ); + let globalConfig = makeDefaultConfig(pythMintAccount.publicKey); ({ controller, stakeConnection } = await standardSetup( portNumber, From 229db4df4b2301cc549b50ae84f7b06b93ad58bf Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 16 Oct 2023 18:04:09 +0100 Subject: [PATCH 16/51] Restore all tests --- staking/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/staking/package.json b/staking/package.json index 4b0a76db..a18d30b6 100644 --- a/staking/package.json +++ b/staking/package.json @@ -30,7 +30,7 @@ "wasm-pack": "^0.10.2" }, "scripts": { - "test": "npm run build_wasm && anchor build -- --features mock-clock && npm run dump_governance && ts-mocha --parallel -p ./tsconfig.json -t 1000000 tests/split_vesting_account.ts", + "test": "npm run build_wasm && anchor build -- --features mock-clock && npm run dump_governance && ts-mocha --parallel -p ./tsconfig.json -t 1000000 tests/*.ts", "build": "npm run build_wasm && tsc -p tsconfig.api.json", "build_wasm": "./scripts/build_wasm.sh", "localnet": "anchor build && npm run dump_governance && ts-node ./app/scripts/localnet.ts", From 25229a8f1f7bd60d1121554d7ef684a7b0f9ea25 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 16 Oct 2023 18:05:26 +0100 Subject: [PATCH 17/51] add todos --- staking/programs/staking/src/lib.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index 95e452cd..1d7d99e1 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -543,9 +543,9 @@ pub mod staking { pub fn accept_split(ctx: Context) -> Result<()> { - // Split vesting schedule between both accounts + // TODO : Split vesting schedule between both accounts - // Transfer stake positions to the new account + // TODO : Transfer stake positions to the new account if need // Transfer tokens { @@ -559,11 +559,13 @@ pub mod staking { split_request.amount, )?; } + + // Delete current request { ctx.accounts.current_stake_account_split_request.amount = 0; } Ok(()) - // Check both accounts are valid after the transfer + // TODO Check both accounts are valid after the transfer } } From cdca77dee0217b898f77bd438cfc2f526a6b1354 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 16 Oct 2023 18:12:44 +0100 Subject: [PATCH 18/51] Cleanup idls --- staking/target/idl/staking.json | 28 +++++++------- staking/target/types/staking.ts | 66 ++++++++++++++------------------- 2 files changed, 42 insertions(+), 52 deletions(-) diff --git a/staking/target/idl/staking.json b/staking/target/idl/staking.json index da2ce2eb..2315cf2f 100644 --- a/staking/target/idl/staking.json +++ b/staking/target/idl/staking.json @@ -966,20 +966,6 @@ ] } }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, { "name": "newStakeAccountPositions", "isMut": true, @@ -1064,6 +1050,20 @@ ] } }, + { + "name": "config", + "isMut": false, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "config" + } + ] + } + }, { "name": "mint", "isMut": false, diff --git a/staking/target/types/staking.ts b/staking/target/types/staking.ts index caf30579..548d77e7 100644 --- a/staking/target/types/staking.ts +++ b/staking/target/types/staking.ts @@ -966,20 +966,6 @@ export type Staking = { ] } }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, { "name": "newStakeAccountPositions", "isMut": true, @@ -1064,6 +1050,20 @@ export type Staking = { ] } }, + { + "name": "config", + "isMut": false, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "config" + } + ] + } + }, { "name": "mint", "isMut": false, @@ -2775,20 +2775,6 @@ export const IDL: Staking = { ] } }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, { "name": "newStakeAccountPositions", "isMut": true, @@ -2873,6 +2859,20 @@ export const IDL: Staking = { ] } }, + { + "name": "config", + "isMut": false, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "config" + } + ] + } + }, { "name": "mint", "isMut": false, @@ -3605,18 +3605,8 @@ export const IDL: Staking = { }, { "code": 6028, -<<<<<<< HEAD -<<<<<<< HEAD - "name": "VoteDuringTransferEpoch", - "msg": "Can't vote during an account's transfer epoch" -======= - "name": "VoteCreationEpoch", - "msg": "Can't vote on the creation epoch" ->>>>>>> dc00407 (Checkpoint) -======= "name": "VoteDuringTransferEpoch", "msg": "Can't vote during an account's transfer epoch" ->>>>>>> f92af94 (Update stuff) }, { "code": 6029, From bf609fb8925e2e18cc9250da21c1b6fd6a2001ce Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 17 Oct 2023 14:58:17 +0100 Subject: [PATCH 19/51] Throw error since it's not implemented --- staking/programs/staking/src/lib.rs | 6 +++--- staking/tests/split_vesting_account.ts | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index 1d7d99e1..ce75b3fb 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -547,6 +547,8 @@ pub mod staking { // TODO : Transfer stake positions to the new account if need + // TODO Check both accounts are valid after the transfer + // Transfer tokens { let split_request = &ctx.accounts.current_stake_account_split_request; @@ -564,8 +566,6 @@ pub mod staking { { ctx.accounts.current_stake_account_split_request.amount = 0; } - Ok(()) - - // TODO Check both accounts are valid after the transfer + err!(ErrorCode::NotImplemented) } } diff --git a/staking/tests/split_vesting_account.ts b/staking/tests/split_vesting_account.ts index be907a08..4b15343d 100644 --- a/staking/tests/split_vesting_account.ts +++ b/staking/tests/split_vesting_account.ts @@ -136,7 +136,10 @@ describe("split vesting account", async () => { alice.publicKey ); - await pdaConnection.acceptSplit(stakeAccount); + try { + await pdaConnection.acceptSplit(stakeAccount); + throw Error("This should've failed"); + } catch {} }); after(async () => { From 4f4016984a7ad4bfe313225ff15fb0883b7d15d9 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 17 Oct 2023 15:11:09 +0100 Subject: [PATCH 20/51] Add some comments --- staking/programs/staking/src/context.rs | 26 ++++++++++++------------- staking/programs/staking/src/lib.rs | 21 ++++++++++++++++---- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/staking/programs/staking/src/context.rs b/staking/programs/staking/src/context.rs index 3461e9d9..a9c9569c 100644 --- a/staking/programs/staking/src/context.rs +++ b/staking/programs/staking/src/context.rs @@ -301,23 +301,23 @@ pub struct RequestSplit<'info> { pub struct AcceptSplit<'info> { // Native payer: #[account(mut, address = config.pda_authority)] - pub payer: Signer<'info>, + pub payer: Signer<'info>, // Current stake accounts: #[account(mut)] - pub current_stake_account_positions: AccountLoader<'info, positions::PositionData>, - #[account(mut, seeds = [STAKE_ACCOUNT_METADATA_SEED.as_bytes(), current_stake_account_positions.key().as_ref()], bump = current_stake_account_metadata.metadata_bump)] - pub current_stake_account_metadata: Box>, - #[account(seeds = [SPLIT_REQUEST.as_bytes(), current_stake_account_positions.key().as_ref()], bump)] - pub current_stake_account_split_request: Box>, + pub source_stake_account_positions: AccountLoader<'info, positions::PositionData>, + #[account(mut, seeds = [STAKE_ACCOUNT_METADATA_SEED.as_bytes(), source_stake_account_positions.key().as_ref()], bump = source_stake_account_metadata.metadata_bump)] + pub source_stake_account_metadata: Box>, + #[account(seeds = [SPLIT_REQUEST.as_bytes(), source_stake_account_positions.key().as_ref()], bump)] + pub source_stake_account_split_request: Box>, #[account( mut, - seeds = [CUSTODY_SEED.as_bytes(), current_stake_account_positions.key().as_ref()], - bump = current_stake_account_metadata.custody_bump, + seeds = [CUSTODY_SEED.as_bytes(), source_stake_account_positions.key().as_ref()], + bump = source_stake_account_metadata.custody_bump, )] - pub current_stake_account_custody: Account<'info, TokenAccount>, + pub source_stake_account_custody: Account<'info, TokenAccount>, /// CHECK : This AccountInfo is safe because it's a checked PDA - #[account(seeds = [AUTHORITY_SEED.as_bytes(), current_stake_account_positions.key().as_ref()], bump = current_stake_account_metadata.authority_bump)] - pub current_custody_authority: AccountInfo<'info>, + #[account(seeds = [AUTHORITY_SEED.as_bytes(), source_stake_account_positions.key().as_ref()], bump = source_stake_account_metadata.authority_bump)] + pub source_custody_authority: AccountInfo<'info>, // New stake accounts : #[account(zero)] @@ -361,9 +361,9 @@ impl<'a, 'b, 'c, 'info> From<&AcceptSplit<'info>> { fn from(accounts: &AcceptSplit<'info>) -> CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> { let cpi_accounts = Transfer { - from: accounts.current_stake_account_custody.to_account_info(), + from: accounts.source_stake_account_custody.to_account_info(), to: accounts.new_stake_account_custody.to_account_info(), - authority: accounts.current_custody_authority.to_account_info(), + authority: accounts.source_custody_authority.to_account_info(), }; let cpi_program = accounts.token_program.to_account_info(); CpiContext::new(cpi_program, cpi_accounts) diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index ce75b3fb..0ead012b 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -535,6 +535,13 @@ pub mod staking { } } + /** + * Any user of the staking program can request to split their account and + * give a part of it to another user. This is mostly useful to transfer unvested + * tokens. + * In the first step, the user requests a split by specifying the amount of tokens + * they want to give to the other user and the recipient's pubkey. + */ pub fn request_split(ctx: Context, amount: u64, recipient: Pubkey) -> Result<()> { ctx.accounts.stake_account_split_request.amount = amount; ctx.accounts.stake_account_split_request.recipient = recipient; @@ -542,6 +549,12 @@ pub mod staking { } + /** + * A split request can only be accepted by the pda_authority from + * the config account. If accepted `amount` tokens are transferred to the + * recipient and the split request is reset (by setting amount to 0). + * The recipient of a transfer can't vote during the epoch of the transfer. + */ pub fn accept_split(ctx: Context) -> Result<()> { // TODO : Split vesting schedule between both accounts @@ -551,12 +564,12 @@ pub mod staking { // Transfer tokens { - let split_request = &ctx.accounts.current_stake_account_split_request; + let split_request = &ctx.accounts.source_stake_account_split_request; transfer( CpiContext::from(&*ctx.accounts).with_signer(&[&[ AUTHORITY_SEED.as_bytes(), - ctx.accounts.current_stake_account_positions.key().as_ref(), - &[ctx.accounts.current_stake_account_metadata.authority_bump], + ctx.accounts.source_stake_account_positions.key().as_ref(), + &[ctx.accounts.source_stake_account_metadata.authority_bump], ]]), split_request.amount, )?; @@ -564,7 +577,7 @@ pub mod staking { // Delete current request { - ctx.accounts.current_stake_account_split_request.amount = 0; + ctx.accounts.source_stake_account_split_request.amount = 0; } err!(ErrorCode::NotImplemented) } From 65f9035f9009168d3cfa2a14fe8fcc012bf83d3c Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 17 Oct 2023 15:42:22 +0100 Subject: [PATCH 21/51] Box everything --- staking/programs/staking/src/context.rs | 8 ++--- staking/target/idl/staking.json | 24 ++++++++----- staking/target/types/staking.ts | 48 +++++++++++++++---------- 3 files changed, 49 insertions(+), 31 deletions(-) diff --git a/staking/programs/staking/src/context.rs b/staking/programs/staking/src/context.rs index a9c9569c..86161572 100644 --- a/staking/programs/staking/src/context.rs +++ b/staking/programs/staking/src/context.rs @@ -314,7 +314,7 @@ pub struct AcceptSplit<'info> { seeds = [CUSTODY_SEED.as_bytes(), source_stake_account_positions.key().as_ref()], bump = source_stake_account_metadata.custody_bump, )] - pub source_stake_account_custody: Account<'info, TokenAccount>, + pub source_stake_account_custody: Box>, /// CHECK : This AccountInfo is safe because it's a checked PDA #[account(seeds = [AUTHORITY_SEED.as_bytes(), source_stake_account_positions.key().as_ref()], bump = source_stake_account_metadata.authority_bump)] pub source_custody_authority: AccountInfo<'info>, @@ -332,7 +332,7 @@ pub struct AcceptSplit<'info> { token::mint = mint, token::authority = new_custody_authority, )] - pub new_stake_account_custody: Account<'info, TokenAccount>, + pub new_stake_account_custody: Box>, /// CHECK : This AccountInfo is safe because it's a checked PDA #[account(seeds = [AUTHORITY_SEED.as_bytes(), new_stake_account_positions.key().as_ref()], bump)] pub new_custody_authority: AccountInfo<'info>, @@ -345,11 +345,11 @@ pub struct AcceptSplit<'info> { pub new_voter_record: Box>, #[account(seeds = [CONFIG_SEED.as_bytes()], bump = config.bump)] - pub config: Account<'info, global_config::GlobalConfig>, + pub config: Box>, // Pyth token mint: #[account(address = config.pyth_token_mint)] - pub mint: Account<'info, Mint>, + pub mint: Box>, // Primitive accounts : pub rent: Sysvar<'info, Rent>, pub token_program: Program<'info, Token>, diff --git a/staking/target/idl/staking.json b/staking/target/idl/staking.json index 2315cf2f..cc8c3559 100644 --- a/staking/target/idl/staking.json +++ b/staking/target/idl/staking.json @@ -794,6 +794,9 @@ }, { "name": "requestSplit", + "docs": [ + "* Any user of the staking program can request to split their account and\n * give a part of it to another user. This is mostly useful to transfer unvested\n * tokens.\n * In the first step, the user requests a split by specifying the amount of tokens\n * they want to give to the other user and the recipient's pubkey." + ], "accounts": [ { "name": "payer", @@ -876,6 +879,9 @@ }, { "name": "acceptSplit", + "docs": [ + "* A split request can only be accepted by the pda_authority from\n * the config account. If accepted `amount` tokens are transferred to the\n * recipient and the split request is reset (by setting amount to 0).\n * The recipient of a transfer can't vote during the epoch of the transfer." + ], "accounts": [ { "name": "payer", @@ -883,12 +889,12 @@ "isSigner": true }, { - "name": "currentStakeAccountPositions", + "name": "sourceStakeAccountPositions", "isMut": true, "isSigner": false }, { - "name": "currentStakeAccountMetadata", + "name": "sourceStakeAccountMetadata", "isMut": true, "isSigner": false, "pda": { @@ -901,13 +907,13 @@ { "kind": "account", "type": "publicKey", - "path": "current_stake_account_positions" + "path": "source_stake_account_positions" } ] } }, { - "name": "currentStakeAccountSplitRequest", + "name": "sourceStakeAccountSplitRequest", "isMut": false, "isSigner": false, "pda": { @@ -920,13 +926,13 @@ { "kind": "account", "type": "publicKey", - "path": "current_stake_account_positions" + "path": "source_stake_account_positions" } ] } }, { - "name": "currentStakeAccountCustody", + "name": "sourceStakeAccountCustody", "isMut": true, "isSigner": false, "pda": { @@ -939,13 +945,13 @@ { "kind": "account", "type": "publicKey", - "path": "current_stake_account_positions" + "path": "source_stake_account_positions" } ] } }, { - "name": "currentCustodyAuthority", + "name": "sourceCustodyAuthority", "isMut": false, "isSigner": false, "docs": [ @@ -961,7 +967,7 @@ { "kind": "account", "type": "publicKey", - "path": "current_stake_account_positions" + "path": "source_stake_account_positions" } ] } diff --git a/staking/target/types/staking.ts b/staking/target/types/staking.ts index 548d77e7..78510f13 100644 --- a/staking/target/types/staking.ts +++ b/staking/target/types/staking.ts @@ -794,6 +794,9 @@ export type Staking = { }, { "name": "requestSplit", + "docs": [ + "* Any user of the staking program can request to split their account and\n * give a part of it to another user. This is mostly useful to transfer unvested\n * tokens.\n * In the first step, the user requests a split by specifying the amount of tokens\n * they want to give to the other user and the recipient's pubkey." + ], "accounts": [ { "name": "payer", @@ -876,6 +879,9 @@ export type Staking = { }, { "name": "acceptSplit", + "docs": [ + "* A split request can only be accepted by the pda_authority from\n * the config account. If accepted `amount` tokens are transferred to the\n * recipient and the split request is reset (by setting amount to 0).\n * The recipient of a transfer can't vote during the epoch of the transfer." + ], "accounts": [ { "name": "payer", @@ -883,12 +889,12 @@ export type Staking = { "isSigner": true }, { - "name": "currentStakeAccountPositions", + "name": "sourceStakeAccountPositions", "isMut": true, "isSigner": false }, { - "name": "currentStakeAccountMetadata", + "name": "sourceStakeAccountMetadata", "isMut": true, "isSigner": false, "pda": { @@ -901,13 +907,13 @@ export type Staking = { { "kind": "account", "type": "publicKey", - "path": "current_stake_account_positions" + "path": "source_stake_account_positions" } ] } }, { - "name": "currentStakeAccountSplitRequest", + "name": "sourceStakeAccountSplitRequest", "isMut": false, "isSigner": false, "pda": { @@ -920,13 +926,13 @@ export type Staking = { { "kind": "account", "type": "publicKey", - "path": "current_stake_account_positions" + "path": "source_stake_account_positions" } ] } }, { - "name": "currentStakeAccountCustody", + "name": "sourceStakeAccountCustody", "isMut": true, "isSigner": false, "pda": { @@ -939,13 +945,13 @@ export type Staking = { { "kind": "account", "type": "publicKey", - "path": "current_stake_account_positions" + "path": "source_stake_account_positions" } ] } }, { - "name": "currentCustodyAuthority", + "name": "sourceCustodyAuthority", "isMut": false, "isSigner": false, "docs": [ @@ -961,7 +967,7 @@ export type Staking = { { "kind": "account", "type": "publicKey", - "path": "current_stake_account_positions" + "path": "source_stake_account_positions" } ] } @@ -2603,6 +2609,9 @@ export const IDL: Staking = { }, { "name": "requestSplit", + "docs": [ + "* Any user of the staking program can request to split their account and\n * give a part of it to another user. This is mostly useful to transfer unvested\n * tokens.\n * In the first step, the user requests a split by specifying the amount of tokens\n * they want to give to the other user and the recipient's pubkey." + ], "accounts": [ { "name": "payer", @@ -2685,6 +2694,9 @@ export const IDL: Staking = { }, { "name": "acceptSplit", + "docs": [ + "* A split request can only be accepted by the pda_authority from\n * the config account. If accepted `amount` tokens are transferred to the\n * recipient and the split request is reset (by setting amount to 0).\n * The recipient of a transfer can't vote during the epoch of the transfer." + ], "accounts": [ { "name": "payer", @@ -2692,12 +2704,12 @@ export const IDL: Staking = { "isSigner": true }, { - "name": "currentStakeAccountPositions", + "name": "sourceStakeAccountPositions", "isMut": true, "isSigner": false }, { - "name": "currentStakeAccountMetadata", + "name": "sourceStakeAccountMetadata", "isMut": true, "isSigner": false, "pda": { @@ -2710,13 +2722,13 @@ export const IDL: Staking = { { "kind": "account", "type": "publicKey", - "path": "current_stake_account_positions" + "path": "source_stake_account_positions" } ] } }, { - "name": "currentStakeAccountSplitRequest", + "name": "sourceStakeAccountSplitRequest", "isMut": false, "isSigner": false, "pda": { @@ -2729,13 +2741,13 @@ export const IDL: Staking = { { "kind": "account", "type": "publicKey", - "path": "current_stake_account_positions" + "path": "source_stake_account_positions" } ] } }, { - "name": "currentStakeAccountCustody", + "name": "sourceStakeAccountCustody", "isMut": true, "isSigner": false, "pda": { @@ -2748,13 +2760,13 @@ export const IDL: Staking = { { "kind": "account", "type": "publicKey", - "path": "current_stake_account_positions" + "path": "source_stake_account_positions" } ] } }, { - "name": "currentCustodyAuthority", + "name": "sourceCustodyAuthority", "isMut": false, "isSigner": false, "docs": [ @@ -2770,7 +2782,7 @@ export const IDL: Staking = { { "kind": "account", "type": "publicKey", - "path": "current_stake_account_positions" + "path": "source_stake_account_positions" } ] } From 649c847c5e7142d81c7b8da9be3433424904e5d1 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 17 Oct 2023 15:47:27 +0100 Subject: [PATCH 22/51] Add soruce --- staking/app/StakeConnection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/staking/app/StakeConnection.ts b/staking/app/StakeConnection.ts index 4aae4da8..823a95d4 100644 --- a/staking/app/StakeConnection.ts +++ b/staking/app/StakeConnection.ts @@ -838,7 +838,7 @@ export class StakeConnection { await this.program.methods .acceptSplit() .accounts({ - currentStakeAccountPositions: stakeAccount.address, + sourceStakeAccountPositions: stakeAccount.address, newStakeAccountPositions: newStakeAccountKeypair.publicKey, mint: this.config.pythTokenMint, }) From 23ea4230b29c0af038cd190585be55ff275d1a22 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 17 Oct 2023 15:54:20 +0100 Subject: [PATCH 23/51] Add more comments --- staking/programs/staking/src/lib.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index 0ead012b..eb26c6e3 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -539,8 +539,8 @@ pub mod staking { * Any user of the staking program can request to split their account and * give a part of it to another user. This is mostly useful to transfer unvested * tokens. - * In the first step, the user requests a split by specifying the amount of tokens - * they want to give to the other user and the recipient's pubkey. + * In the first step, the user requests a split by specifying the `amount` of tokens + * they want to give to the other user and the `recipient`'s pubkey. */ pub fn request_split(ctx: Context, amount: u64, recipient: Pubkey) -> Result<()> { ctx.accounts.stake_account_split_request.amount = amount; @@ -550,9 +550,9 @@ pub mod staking { /** - * A split request can only be accepted by the pda_authority from - * the config account. If accepted `amount` tokens are transferred to the - * recipient and the split request is reset (by setting amount to 0). + * A split request can only be accepted by the `pda_authority`` from + * the config account. If accepted, `amount` tokens are transferred to a new stake account + * owned by the `recipient` and the split request is reset (by setting `amount` to 0). * The recipient of a transfer can't vote during the epoch of the transfer. */ pub fn accept_split(ctx: Context) -> Result<()> { From 413f606c3a0810bf720b91f8a375830445f8051a Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 17 Oct 2023 16:04:49 +0100 Subject: [PATCH 24/51] Add another comment --- staking/programs/staking/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index eb26c6e3..ccf8e881 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -537,8 +537,9 @@ pub mod staking { /** * Any user of the staking program can request to split their account and - * give a part of it to another user. This is mostly useful to transfer unvested - * tokens. + * give a part of it to another user. + * This is mostly useful to transfer unvested tokens. Each user can only have one active + * request at a time. * In the first step, the user requests a split by specifying the `amount` of tokens * they want to give to the other user and the `recipient`'s pubkey. */ From 89afd64df19ba064a5c000e552a8c1a4f760a962 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 17 Oct 2023 18:08:17 +0100 Subject: [PATCH 25/51] First implementation --- staking/programs/staking/src/error.rs | 8 +- staking/programs/staking/src/lib.rs | 150 +- .../programs/staking/src/state/positions.rs | 133 +- .../staking/src/state/stake_account.rs | 24 + staking/programs/staking/src/state/vesting.rs | 67 + .../staking/src/state/voter_weight_record.rs | 15 +- staking/programs/staking/src/utils/risk.rs | 1 - staking/target/idl/staking.json | 1814 -------- staking/target/types/staking.ts | 3629 ----------------- staking/tests/split_vesting_account.ts | 5 +- 10 files changed, 368 insertions(+), 5478 deletions(-) delete mode 100644 staking/target/idl/staking.json delete mode 100644 staking/target/types/staking.ts diff --git a/staking/programs/staking/src/error.rs b/staking/programs/staking/src/error.rs index f66c38ae..4b783e31 100644 --- a/staking/programs/staking/src/error.rs +++ b/staking/programs/staking/src/error.rs @@ -61,6 +61,12 @@ pub enum ErrorCode { PositionOutOfBounds, #[msg("Can't vote during an account's transfer epoch")] //6028 VoteDuringTransferEpoch, - #[msg("Other")] //6029 + #[msg("Can't split 0 tokens from an account")] // 6029 + SplitZeroTokens, + #[msg("Can't split more tokens than are in the account")] // 6030 + SplitTooManyTokens, + #[msg("Sanity check failed")] // 6031 + SanityCheckFailed, + #[msg("Other")] //6032 Other, } diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index ccf8e881..70b673eb 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -53,6 +53,10 @@ pub mod staking { /// Creates a global config for the program use super::*; + use { + crate::state::stake_account, + state::split_request, + }; pub fn init_config(ctx: Context, global_config: GlobalConfig) -> Result<()> { let config_account = &mut ctx.accounts.config_account; config_account.bump = *ctx.bumps.get("config_account").unwrap(); @@ -113,25 +117,24 @@ pub mod staking { let config = &ctx.accounts.config; config.check_frozen()?; - let stake_account_metadata = &mut ctx.accounts.stake_account_metadata; - stake_account_metadata.metadata_bump = *ctx.bumps.get("stake_account_metadata").unwrap(); - stake_account_metadata.custody_bump = *ctx.bumps.get("stake_account_custody").unwrap(); - stake_account_metadata.authority_bump = *ctx.bumps.get("custody_authority").unwrap(); - stake_account_metadata.voter_bump = *ctx.bumps.get("voter_record").unwrap(); - stake_account_metadata.owner = owner; - stake_account_metadata.next_index = 0; - - stake_account_metadata.lock = lock; - stake_account_metadata.transfer_epoch = None; + let stake_account_metadata: &mut Box< + Account<'_, state::stake_account::StakeAccountMetadataV2>, + > = &mut ctx.accounts.stake_account_metadata; + stake_account_metadata.initialize( + *ctx.bumps.get("stake_account_metadata").unwrap(), + *ctx.bumps.get("stake_account_custody").unwrap(), + *ctx.bumps.get("custody_authority").unwrap(), + *ctx.bumps.get("voter_record").unwrap(), + &owner, + None, + ); + stake_account_metadata.set_lock(lock); let stake_account_positions = &mut ctx.accounts.stake_account_positions.load_init()?; - stake_account_positions.owner = owner; + stake_account_positions.initialize(&owner); let voter_record = &mut ctx.accounts.voter_record; - - voter_record.realm = config.pyth_governance_realm; - voter_record.governing_token_mint = config.pyth_token_mint; - voter_record.governing_token_owner = owner; + voter_record.initialize(config, &owner); Ok(()) } @@ -557,15 +560,84 @@ pub mod staking { * The recipient of a transfer can't vote during the epoch of the transfer. */ pub fn accept_split(ctx: Context) -> Result<()> { - // TODO : Split vesting schedule between both accounts + let config = &ctx.accounts.config; + config.check_frozen()?; - // TODO : Transfer stake positions to the new account if need + let current_epoch = get_current_epoch(config)?; + + let split_request = &ctx.accounts.source_stake_account_split_request; + + // Initialize new accounts + let new_stake_account_metadata: &mut Box< + Account<'_, state::stake_account::StakeAccountMetadataV2>, + > = &mut ctx.accounts.new_stake_account_metadata; + new_stake_account_metadata.initialize( + *ctx.bumps.get("stake_account_metadata").unwrap(), + *ctx.bumps.get("stake_account_custody").unwrap(), + *ctx.bumps.get("custody_authority").unwrap(), + *ctx.bumps.get("voter_record").unwrap(), + &split_request.recipient, + None, + ); + + let new_stake_account_positions = + &mut ctx.accounts.new_stake_account_positions.load_init()?; + new_stake_account_positions.initialize(&split_request.recipient); + + let new_voter_record = &mut ctx.accounts.new_voter_record; + new_voter_record.initialize(config, &split_request.recipient); + + // Split off source account + let source_stake_account_custody = &ctx.accounts.source_stake_account_custody; + let source_stake_account_metadata = &mut ctx.accounts.source_stake_account_metadata; + let source_stake_account_positions = + &mut ctx.accounts.source_stake_account_positions.load_mut()?; + + // Pre-check + utils::risk::validate( + &source_stake_account_positions, + source_stake_account_custody.amount, + source_stake_account_metadata.lock.get_unvested_balance( + utils::clock::get_current_time(config), + config.pyth_token_list_time, + )?, + current_epoch, + config.unlocking_duration, + )?; + + require!(split_request.amount > 0, ErrorCode::SplitZeroTokens); + require!( + split_request.amount < source_stake_account_custody.amount, + ErrorCode::SplitTooManyTokens + ); + let remaining_amount = source_stake_account_custody + .amount + .saturating_sub(split_request.amount); + + // Split vesting account + let (source_vesting_account, new_vesting_account) = + source_stake_account_metadata.lock.split_vesting_schedule( + remaining_amount, + split_request.amount, + source_stake_account_custody.amount, + )?; + source_stake_account_metadata.set_lock(source_vesting_account); + new_stake_account_metadata.set_lock(new_vesting_account); + + // Split positions + source_stake_account_positions.split( + new_stake_account_positions, + &mut new_stake_account_metadata.next_index, + &mut source_stake_account_metadata.next_index, + remaining_amount, + split_request.amount, + source_stake_account_custody.amount, + current_epoch, + config.unlocking_duration, + )?; - // TODO Check both accounts are valid after the transfer - // Transfer tokens { - let split_request = &ctx.accounts.source_stake_account_split_request; transfer( CpiContext::from(&*ctx.accounts).with_signer(&[&[ AUTHORITY_SEED.as_bytes(), @@ -576,10 +648,42 @@ pub mod staking { )?; } + ctx.accounts.source_stake_account_custody.reload()?; + ctx.accounts.new_stake_account_custody.reload()?; + + + // Post-check + utils::risk::validate( + &source_stake_account_positions, + ctx.accounts.source_stake_account_custody.amount, + ctx.accounts + .source_stake_account_metadata + .lock + .get_unvested_balance( + utils::clock::get_current_time(config), + config.pyth_token_list_time, + )?, + current_epoch, + config.unlocking_duration, + )?; + + utils::risk::validate( + &new_stake_account_positions, + ctx.accounts.new_stake_account_custody.amount, + ctx.accounts + .new_stake_account_metadata + .lock + .get_unvested_balance( + utils::clock::get_current_time(config), + config.pyth_token_list_time, + )?, + current_epoch, + config.unlocking_duration, + )?; + // Delete current request - { - ctx.accounts.source_stake_account_split_request.amount = 0; - } + ctx.accounts.source_stake_account_split_request.amount = 0; + err!(ErrorCode::NotImplemented) } } diff --git a/staking/programs/staking/src/state/positions.rs b/staking/programs/staking/src/state/positions.rs index 0a4d22b8..3d9e8d70 100644 --- a/staking/programs/staking/src/state/positions.rs +++ b/staking/programs/staking/src/state/positions.rs @@ -1,4 +1,5 @@ use { + super::target, crate::error::ErrorCode, anchor_lang::{ prelude::{ @@ -10,9 +11,15 @@ use { wasm_bindgen, }, }, - std::fmt::{ - self, - Debug, + std::{ + convert::{ + TryFrom, + TryInto, + }, + fmt::{ + self, + Debug, + }, }, }; @@ -47,6 +54,10 @@ impl Default for PositionData { } } impl PositionData { + pub fn initialize(&mut self, owner: &Pubkey) { + self.owner = *owner; + } + /// Finds first index available for a new position, increments the internal counter pub fn reserve_new_index(&mut self, next_index: &mut u8) -> Result { let res = *next_index as usize; @@ -79,6 +90,122 @@ impl PositionData { .ok_or_else(|| error!(ErrorCode::PositionOutOfBounds))?, ) } + + pub fn get_target_exposure( + &self, + target: &Target, + current_epoch: u64, + unlocking_duration: u8, + ) -> Result { + let mut exposure: u64 = 0; + + for i in 0..MAX_POSITIONS { + if let Some(position) = self.read_position(i)? { + match position.get_current_position(current_epoch, unlocking_duration)? { + PositionState::LOCKED + | PositionState::PREUNLOCKING + | PositionState::UNLOCKING + | PositionState::LOCKING => { + if position.target_with_parameters.get_target() == *target { + exposure = exposure + .checked_add(position.amount) + .ok_or(error!(ErrorCode::GenericOverflow))? + }; + } + _ => {} + } + } + } + return Ok(exposure); + } + + pub fn split( + &mut self, + dest_position_data: &mut PositionData, + src_next_index: &mut u8, + dest_next_index: &mut u8, + remaining_amount: u64, + transferred_amount: u64, + total_amount: u64, + current_epoch: u64, + unlocking_duration: u8, + ) -> Result<()> { + require!( + transferred_amount + .checked_add(remaining_amount) + .ok_or(ErrorCode::Other)? + == total_amount, + ErrorCode::SanityCheckFailed + ); + let governance_exposure = + self.get_target_exposure(&Target::VOTING, current_epoch, unlocking_duration)?; + require!( + governance_exposure <= total_amount, + ErrorCode::SanityCheckFailed + ); + + if remaining_amount < governance_exposure { + // We need to transfer some positions over to the new account + let mut excess_governance_exposure = + governance_exposure.saturating_sub(remaining_amount); + + while (excess_governance_exposure > 0 && *src_next_index > 0) { + let index = TryInto::::try_into(*src_next_index - 1) + .map_err(|_| ErrorCode::GenericOverflow)?; + match self.read_position(index)? { + Some(position) => { + match position.get_current_position(current_epoch, unlocking_duration)? { + PositionState::UNLOCKED => self.make_none(index, src_next_index)?, + PositionState::LOCKING + | PositionState::LOCKED + | PositionState::PREUNLOCKING + | PositionState::UNLOCKING => { + if excess_governance_exposure < position.amount { + // We need to split the position + self.write_position( + index, + &Position { + amount: position + .amount + .saturating_sub(excess_governance_exposure), + ..position + }, + )?; + + let new_position = Position { + amount: excess_governance_exposure, + ..position + }; + + let new_index = + dest_position_data.reserve_new_index(dest_next_index)?; + dest_position_data.write_position(new_index, &new_position)?; + + excess_governance_exposure = 0; + } else { + // We need to transfer the whole position + let new_index = + dest_position_data.reserve_new_index(dest_next_index)?; + dest_position_data.write_position(new_index, &position)?; + + self.make_none(index, src_next_index)?; + excess_governance_exposure = + excess_governance_exposure.saturating_sub(position.amount); + } + } + } + } + None => { + // This should never happen + return Err(error!(ErrorCode::SanityCheckFailed)); + } + } + } + } + + + return Ok(()); + } } pub trait TryBorsh { diff --git a/staking/programs/staking/src/state/stake_account.rs b/staking/programs/staking/src/state/stake_account.rs index c3097043..ceeeb6bf 100644 --- a/staking/programs/staking/src/state/stake_account.rs +++ b/staking/programs/staking/src/state/stake_account.rs @@ -27,6 +27,30 @@ impl StakeAccountMetadataV2 { pub const LEN: usize = 87; } +impl StakeAccountMetadataV2 { + pub fn initialize( + &mut self, + metadata_bump: u8, + custody_bump: u8, + authority_bump: u8, + voter_record_bump: u8, + owner: &Pubkey, + transfer_epoch: Option, + ) { + self.metadata_bump = metadata_bump; + self.custody_bump = custody_bump; + self.authority_bump = authority_bump; + self.voter_bump = voter_record_bump; + self.owner = *owner; + self.next_index = 0; + self.transfer_epoch = transfer_epoch; + } + + pub fn set_lock(&mut self, lock: VestingSchedule) { + self.lock = lock; + } +} + #[cfg(test)] pub mod tests { use { diff --git a/staking/programs/staking/src/state/vesting.rs b/staking/programs/staking/src/state/vesting.rs index 06349e68..1be04488 100644 --- a/staking/programs/staking/src/state/vesting.rs +++ b/staking/programs/staking/src/state/vesting.rs @@ -235,6 +235,73 @@ impl VestingSchedule { amount, })) } + + pub fn split_vesting_schedule( + &self, + remaining_amount: u64, + transferred_amount: u64, + total_amount: u64, + ) -> Result<(VestingSchedule, VestingSchedule)> { + require!( + transferred_amount + .checked_add(remaining_amount) + .ok_or(ErrorCode::Other)? + == total_amount, + ErrorCode::SanityCheckFailed + ); + match self { + VestingSchedule::FullyVested => { + Ok((VestingSchedule::FullyVested, VestingSchedule::FullyVested)) + } + VestingSchedule::PeriodicVesting { + initial_balance, + start_date, + period_duration, + num_periods, + } => { + return Ok(( + VestingSchedule::PeriodicVesting { + initial_balance: ((remaining_amount as u128) * (*initial_balance as u128) + / (total_amount as u128)) + as u64, + start_date: *start_date, + period_duration: *period_duration, + num_periods: *num_periods, + }, + VestingSchedule::PeriodicVesting { + initial_balance: ((transferred_amount as u128) * (*initial_balance as u128) + / (total_amount as u128)) + as u64, + start_date: *start_date, + period_duration: *period_duration, + num_periods: *num_periods, + }, + )); + } + VestingSchedule::PeriodicVestingAfterListing { + initial_balance, + period_duration, + num_periods, + } => { + return Ok(( + VestingSchedule::PeriodicVestingAfterListing { + initial_balance: ((remaining_amount as u128) * (*initial_balance as u128) + / (total_amount as u128)) + as u64, + period_duration: *period_duration, + num_periods: *num_periods, + }, + VestingSchedule::PeriodicVestingAfterListing { + initial_balance: ((transferred_amount as u128) * (*initial_balance as u128) + / (total_amount as u128)) + as u64, + period_duration: *period_duration, + num_periods: *num_periods, + }, + )); + } + } + } } #[cfg(test)] diff --git a/staking/programs/staking/src/state/voter_weight_record.rs b/staking/programs/staking/src/state/voter_weight_record.rs index 09bf6142..cb8c0fc6 100644 --- a/staking/programs/staking/src/state/voter_weight_record.rs +++ b/staking/programs/staking/src/state/voter_weight_record.rs @@ -1,6 +1,9 @@ -use anchor_lang::prelude::{ - borsh::BorshSchema, - *, +use { + super::global_config::GlobalConfig, + anchor_lang::prelude::{ + borsh::BorshSchema, + *, + }, }; /// Copied this struct from https://github.com/solana-labs/solana-program-library/blob/master/governance/addin-api/src/voter_weight.rs @@ -62,6 +65,12 @@ pub struct VoterWeightRecord { impl VoterWeightRecord { pub const LEN: usize = 8 + 32 + 32 + 32 + 8 + 9 + 2 + 33 + 8; + + pub fn initialize(&mut self, config: &GlobalConfig, owner: &Pubkey) { + self.realm = config.pyth_governance_realm; + self.governing_token_mint = config.pyth_token_mint; + self.governing_token_owner = *owner; + } } /// The governance action VoterWeight is evaluated for #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy, BorshSchema)] diff --git a/staking/programs/staking/src/utils/risk.rs b/staking/programs/staking/src/utils/risk.rs index 0a0e9466..66c6c145 100644 --- a/staking/programs/staking/src/utils/risk.rs +++ b/staking/programs/staking/src/utils/risk.rs @@ -20,7 +20,6 @@ use { }, }; - /// Validates that a proposed set of positions meets all risk requirements /// stake_account_positions is untrusted, while everything else is trusted /// If it passes the risk check, it returns the max amount of vested balance diff --git a/staking/target/idl/staking.json b/staking/target/idl/staking.json deleted file mode 100644 index cc8c3559..00000000 --- a/staking/target/idl/staking.json +++ /dev/null @@ -1,1814 +0,0 @@ -{ - "version": "1.0.0", - "name": "staking", - "instructions": [ - { - "name": "initConfig", - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "configAccount", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "rent", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "globalConfig", - "type": { - "defined": "GlobalConfig" - } - } - ] - }, - { - "name": "updateGovernanceAuthority", - "accounts": [ - { - "name": "governanceSigner", - "isMut": false, - "isSigner": true - }, - { - "name": "config", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - } - ], - "args": [ - { - "name": "newAuthority", - "type": "publicKey" - } - ] - }, - { - "name": "updateFreeze", - "accounts": [ - { - "name": "governanceSigner", - "isMut": false, - "isSigner": true - }, - { - "name": "config", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - } - ], - "args": [ - { - "name": "freeze", - "type": "bool" - } - ] - }, - { - "name": "updateTokenListTime", - "accounts": [ - { - "name": "governanceSigner", - "isMut": false, - "isSigner": true - }, - { - "name": "config", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - } - ], - "args": [ - { - "name": "tokenListTime", - "type": { - "option": "i64" - } - } - ] - }, - { - "name": "createStakeAccount", - "docs": [ - "Trustless instruction that creates a stake account for a user", - "The main account i.e. the position accounts needs to be initialized outside of the program", - "otherwise we run into stack limits" - ], - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "stakeAccountPositions", - "isMut": true, - "isSigner": false - }, - { - "name": "stakeAccountMetadata", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "stakeAccountCustody", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "custody" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "custodyAuthority", - "isMut": false, - "isSigner": false, - "docs": [ - "CHECK : This AccountInfo is safe because it's a checked PDA" - ], - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "authority" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "voterRecord", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "voter_weight" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "mint", - "isMut": false, - "isSigner": false - }, - { - "name": "rent", - "isMut": false, - "isSigner": false - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "owner", - "type": "publicKey" - }, - { - "name": "lock", - "type": { - "defined": "VestingSchedule" - } - } - ] - }, - { - "name": "createPosition", - "docs": [ - "Creates a position", - "Looks for the first available place in the array, fails if array is full", - "Computes risk and fails if new positions exceed risk limit" - ], - "accounts": [ - { - "name": "payer", - "isMut": false, - "isSigner": true - }, - { - "name": "stakeAccountPositions", - "isMut": true, - "isSigner": false - }, - { - "name": "stakeAccountMetadata", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "stakeAccountCustody", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "custody" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "targetAccount", - "isMut": true, - "isSigner": false - } - ], - "args": [ - { - "name": "targetWithParameters", - "type": { - "defined": "TargetWithParameters" - } - }, - { - "name": "amount", - "type": "u64" - } - ] - }, - { - "name": "closePosition", - "accounts": [ - { - "name": "payer", - "isMut": false, - "isSigner": true - }, - { - "name": "stakeAccountPositions", - "isMut": true, - "isSigner": false - }, - { - "name": "stakeAccountMetadata", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "stakeAccountCustody", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "custody" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "targetAccount", - "isMut": true, - "isSigner": false - } - ], - "args": [ - { - "name": "index", - "type": "u8" - }, - { - "name": "amount", - "type": "u64" - }, - { - "name": "targetWithParameters", - "type": { - "defined": "TargetWithParameters" - } - } - ] - }, - { - "name": "withdrawStake", - "accounts": [ - { - "name": "payer", - "isMut": false, - "isSigner": true - }, - { - "name": "destination", - "isMut": true, - "isSigner": false - }, - { - "name": "stakeAccountPositions", - "isMut": false, - "isSigner": false - }, - { - "name": "stakeAccountMetadata", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "stakeAccountCustody", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "custody" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "custodyAuthority", - "isMut": false, - "isSigner": false, - "docs": [ - "CHECK : This AccountInfo is safe because it's a checked PDA" - ], - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "authority" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "amount", - "type": "u64" - } - ] - }, - { - "name": "updateVoterWeight", - "accounts": [ - { - "name": "payer", - "isMut": false, - "isSigner": true - }, - { - "name": "stakeAccountPositions", - "isMut": false, - "isSigner": false - }, - { - "name": "stakeAccountMetadata", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "stakeAccountCustody", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "custody" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "voterRecord", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "voter_weight" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "governanceTarget", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "target" - }, - { - "kind": "const", - "type": "string", - "value": "voting" - } - ] - } - } - ], - "args": [ - { - "name": "action", - "type": { - "defined": "VoterWeightAction" - } - } - ] - }, - { - "name": "updateMaxVoterWeight", - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "maxVoterRecord", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "max_voter" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, - { - "name": "createTarget", - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "governanceSigner", - "isMut": false, - "isSigner": true - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "targetAccount", - "isMut": true, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "target", - "type": { - "defined": "Target" - } - } - ] - }, - { - "name": "advanceClock", - "accounts": [ - { - "name": "config", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - } - ], - "args": [ - { - "name": "seconds", - "type": "i64" - } - ] - }, - { - "name": "requestSplit", - "docs": [ - "* Any user of the staking program can request to split their account and\n * give a part of it to another user. This is mostly useful to transfer unvested\n * tokens.\n * In the first step, the user requests a split by specifying the amount of tokens\n * they want to give to the other user and the recipient's pubkey." - ], - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "stakeAccountPositions", - "isMut": false, - "isSigner": false - }, - { - "name": "stakeAccountMetadata", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "stakeAccountSplitRequest", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "split_request" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "amount", - "type": "u64" - }, - { - "name": "recipient", - "type": "publicKey" - } - ] - }, - { - "name": "acceptSplit", - "docs": [ - "* A split request can only be accepted by the pda_authority from\n * the config account. If accepted `amount` tokens are transferred to the\n * recipient and the split request is reset (by setting amount to 0).\n * The recipient of a transfer can't vote during the epoch of the transfer." - ], - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "sourceStakeAccountPositions", - "isMut": true, - "isSigner": false - }, - { - "name": "sourceStakeAccountMetadata", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "source_stake_account_positions" - } - ] - } - }, - { - "name": "sourceStakeAccountSplitRequest", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "split_request" - }, - { - "kind": "account", - "type": "publicKey", - "path": "source_stake_account_positions" - } - ] - } - }, - { - "name": "sourceStakeAccountCustody", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "custody" - }, - { - "kind": "account", - "type": "publicKey", - "path": "source_stake_account_positions" - } - ] - } - }, - { - "name": "sourceCustodyAuthority", - "isMut": false, - "isSigner": false, - "docs": [ - "CHECK : This AccountInfo is safe because it's a checked PDA" - ], - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "authority" - }, - { - "kind": "account", - "type": "publicKey", - "path": "source_stake_account_positions" - } - ] - } - }, - { - "name": "newStakeAccountPositions", - "isMut": true, - "isSigner": false - }, - { - "name": "newStakeAccountMetadata", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "new_stake_account_positions" - } - ] - } - }, - { - "name": "newStakeAccountCustody", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "custody" - }, - { - "kind": "account", - "type": "publicKey", - "path": "new_stake_account_positions" - } - ] - } - }, - { - "name": "newCustodyAuthority", - "isMut": false, - "isSigner": false, - "docs": [ - "CHECK : This AccountInfo is safe because it's a checked PDA" - ], - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "authority" - }, - { - "kind": "account", - "type": "publicKey", - "path": "new_stake_account_positions" - } - ] - } - }, - { - "name": "newVoterRecord", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "voter_weight" - }, - { - "kind": "account", - "type": "publicKey", - "path": "new_stake_account_positions" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "mint", - "isMut": false, - "isSigner": false - }, - { - "name": "rent", - "isMut": false, - "isSigner": false - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [] - } - ], - "accounts": [ - { - "name": "GlobalConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "bump", - "type": "u8" - }, - { - "name": "governanceAuthority", - "type": "publicKey" - }, - { - "name": "pythTokenMint", - "type": "publicKey" - }, - { - "name": "pythGovernanceRealm", - "type": "publicKey" - }, - { - "name": "unlockingDuration", - "type": "u8" - }, - { - "name": "epochDuration", - "type": "u64" - }, - { - "name": "freeze", - "type": "bool" - }, - { - "name": "pdaAuthority", - "type": "publicKey" - }, - { - "name": "governanceProgram", - "type": "publicKey" - }, - { - "name": "pythTokenListTime", - "docs": [ - "Once the pyth token is listed, governance can update the config to set this value.", - "Once this value is set, vesting schedules that depend on the token list date can start", - "vesting." - ], - "type": { - "option": "i64" - } - }, - { - "name": "mockClockTime", - "type": "i64" - } - ] - } - }, - { - "name": "MaxVoterWeightRecord", - "docs": [ - "Copied this struct from https://github.com/solana-labs/solana-program-library/blob/master/governance/addin-api/src/max_voter_weight.rs" - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "realm", - "docs": [ - "The Realm the MaxVoterWeightRecord belongs to" - ], - "type": "publicKey" - }, - { - "name": "governingTokenMint", - "docs": [ - "Governing Token Mint the MaxVoterWeightRecord is associated with", - "Note: The addin can take deposits of any tokens and is not restricted to the community or", - "council tokens only" - ], - "type": "publicKey" - }, - { - "name": "maxVoterWeight", - "docs": [ - "Max voter weight", - "The max voter weight provided by the addin for the given realm and governing_token_mint" - ], - "type": "u64" - }, - { - "name": "maxVoterWeightExpiry", - "docs": [ - "The slot when the max voting weight expires", - "It should be set to None if the weight never expires", - "If the max vote weight decays with time, for example for time locked based weights, then", - "the expiry must be set As a pattern Revise instruction to update the max weight should", - "be invoked before governance instruction within the same transaction and the expiry set", - "to the current slot to provide up to date weight" - ], - "type": { - "option": "u64" - } - }, - { - "name": "reserved", - "docs": [ - "Reserved space for future versions" - ], - "type": { - "array": [ - "u8", - 8 - ] - } - } - ] - } - }, - { - "name": "PositionData", - "docs": [ - "An array that contains all of a user's positions i.e. where are the staking and who are they", - "staking to.", - "The invariant we preserve is : For i < next_index, positions[i] == Some", - "For i >= next_index, positions[i] == None" - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "owner", - "type": "publicKey" - }, - { - "name": "positions", - "type": { - "array": [ - { - "array": [ - "u8", - 200 - ] - }, - 20 - ] - } - } - ] - } - }, - { - "name": "SplitRequest", - "type": { - "kind": "struct", - "fields": [ - { - "name": "amount", - "type": "u64" - }, - { - "name": "recipient", - "type": "publicKey" - } - ] - } - }, - { - "name": "StakeAccountMetadataV2", - "docs": [ - "This is the metadata account for each staker", - "It is derived from the positions account with seeds \"stake_metadata\" and the positions account", - "pubkey It stores some PDA bumps, the owner of the account and the vesting schedule" - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "metadataBump", - "type": "u8" - }, - { - "name": "custodyBump", - "type": "u8" - }, - { - "name": "authorityBump", - "type": "u8" - }, - { - "name": "voterBump", - "type": "u8" - }, - { - "name": "owner", - "type": "publicKey" - }, - { - "name": "lock", - "type": { - "defined": "VestingSchedule" - } - }, - { - "name": "nextIndex", - "type": "u8" - }, - { - "name": "transferEpoch", - "type": { - "option": "u64" - } - } - ] - } - }, - { - "name": "TargetMetadata", - "docs": [ - "This represents a target that users can stake to", - "Currently we store the last time the target account was updated, the current locked balance", - "and the amount by which the locked balance will change in the next epoch" - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "bump", - "type": "u8" - }, - { - "name": "lastUpdateAt", - "type": "u64" - }, - { - "name": "prevEpochLocked", - "type": "u64" - }, - { - "name": "locked", - "type": "u64" - }, - { - "name": "deltaLocked", - "type": "i64" - } - ] - } - }, - { - "name": "VoterWeightRecord", - "docs": [ - "Copied this struct from https://github.com/solana-labs/solana-program-library/blob/master/governance/addin-api/src/voter_weight.rs", - "Anchor has a macro (vote_weight_record) that is supposed to generate this struct, but it doesn't", - "work because the error's macros are not updated for anchor 0.22.0.", - "Even if it did work, the type wouldn't show up in the IDL. SPL doesn't produce an API, which", - "means that means we'd need the equivalent of this code on the client side.", - "If Anchor fixes the macro, we might consider changing it" - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "realm", - "docs": [ - "VoterWeightRecord discriminator sha256(\"account:VoterWeightRecord\")[..8]", - "Note: The discriminator size must match the addin implementing program discriminator size", - "to ensure it's stored in the private space of the account data and it's unique", - "pub account_discriminator: [u8; 8],", - "The Realm the VoterWeightRecord belongs to" - ], - "type": "publicKey" - }, - { - "name": "governingTokenMint", - "docs": [ - "Governing Token Mint the VoterWeightRecord is associated with", - "Note: The addin can take deposits of any tokens and is not restricted to the community or", - "council tokens only" - ], - "type": "publicKey" - }, - { - "name": "governingTokenOwner", - "docs": [ - "The owner of the governing token and voter", - "This is the actual owner (voter) and corresponds to TokenOwnerRecord.governing_token_owner" - ], - "type": "publicKey" - }, - { - "name": "voterWeight", - "docs": [ - "Voter's weight", - "The weight of the voter provided by the addin for the given realm, governing_token_mint and", - "governing_token_owner (voter)" - ], - "type": "u64" - }, - { - "name": "voterWeightExpiry", - "docs": [ - "The slot when the voting weight expires", - "It should be set to None if the weight never expires", - "If the voter weight decays with time, for example for time locked based weights, then the", - "expiry must be set As a common pattern Revise instruction to update the weight should", - "be invoked before governance instruction within the same transaction and the expiry set", - "to the current slot to provide up to date weight" - ], - "type": { - "option": "u64" - } - }, - { - "name": "weightAction", - "docs": [ - "The governance action the voter's weight pertains to", - "It allows to provided voter's weight specific to the particular action the weight is", - "evaluated for When the action is provided then the governance program asserts the", - "executing action is the same as specified by the addin" - ], - "type": { - "option": { - "defined": "VoterWeightAction" - } - } - }, - { - "name": "weightActionTarget", - "docs": [ - "The target the voter's weight action pertains to", - "It allows to provided voter's weight specific to the target the weight is evaluated for", - "For example when addin supplies weight to vote on a particular proposal then it must", - "specify the proposal as the action target When the target is provided then the", - "governance program asserts the target is the same as specified by the addin" - ], - "type": { - "option": "publicKey" - } - }, - { - "name": "reserved", - "docs": [ - "Reserved space for future versions" - ], - "type": { - "array": [ - "u8", - 8 - ] - } - } - ] - } - } - ], - "types": [ - { - "name": "Position", - "docs": [ - "This represents a staking position, i.e. an amount that someone has staked to a particular", - "target. This is one of the core pieces of our staking design, and stores all", - "of the state related to a position The voting position is a position where the", - "target_with_parameters is VOTING" - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "amount", - "type": "u64" - }, - { - "name": "activationEpoch", - "type": "u64" - }, - { - "name": "unlockingStart", - "type": { - "option": "u64" - } - }, - { - "name": "targetWithParameters", - "type": { - "defined": "TargetWithParameters" - } - } - ] - } - }, - { - "name": "Target", - "type": { - "kind": "enum", - "variants": [ - { - "name": "VOTING" - }, - { - "name": "STAKING", - "fields": [ - { - "name": "product", - "type": "publicKey" - } - ] - } - ] - } - }, - { - "name": "TargetWithParameters", - "type": { - "kind": "enum", - "variants": [ - { - "name": "VOTING" - }, - { - "name": "STAKING", - "fields": [ - { - "name": "product", - "type": "publicKey" - }, - { - "name": "publisher", - "type": { - "defined": "Publisher" - } - } - ] - } - ] - } - }, - { - "name": "Publisher", - "type": { - "kind": "enum", - "variants": [ - { - "name": "DEFAULT" - }, - { - "name": "SOME", - "fields": [ - { - "name": "address", - "type": "publicKey" - } - ] - } - ] - } - }, - { - "name": "PositionState", - "docs": [ - "The core states that a position can be in" - ], - "type": { - "kind": "enum", - "variants": [ - { - "name": "UNLOCKED" - }, - { - "name": "LOCKING" - }, - { - "name": "LOCKED" - }, - { - "name": "PREUNLOCKING" - }, - { - "name": "UNLOCKING" - } - ] - } - }, - { - "name": "VestingSchedule", - "docs": [ - "Represents how a given initial balance vests over time", - "It is unit-less, but units must be consistent" - ], - "type": { - "kind": "enum", - "variants": [ - { - "name": "FullyVested" - }, - { - "name": "PeriodicVesting", - "fields": [ - { - "name": "initial_balance", - "type": "u64" - }, - { - "name": "start_date", - "type": "i64" - }, - { - "name": "period_duration", - "type": "u64" - }, - { - "name": "num_periods", - "type": "u64" - } - ] - }, - { - "name": "PeriodicVestingAfterListing", - "fields": [ - { - "name": "initial_balance", - "type": "u64" - }, - { - "name": "period_duration", - "type": "u64" - }, - { - "name": "num_periods", - "type": "u64" - } - ] - } - ] - } - }, - { - "name": "VoterWeightAction", - "docs": [ - "The governance action VoterWeight is evaluated for" - ], - "type": { - "kind": "enum", - "variants": [ - { - "name": "CastVote" - }, - { - "name": "CommentProposal" - }, - { - "name": "CreateGovernance" - }, - { - "name": "CreateProposal" - }, - { - "name": "SignOffProposal" - } - ] - } - } - ], - "errors": [ - { - "code": 6000, - "name": "TooMuchExposureToProduct", - "msg": "Too much exposure to product" - }, - { - "code": 6001, - "name": "TooMuchExposureToGovernance", - "msg": "Too much exposure to governance" - }, - { - "code": 6002, - "name": "TokensNotYetVested", - "msg": "Tokens not yet vested" - }, - { - "code": 6003, - "name": "RiskLimitExceeded", - "msg": "Risk limit exceeded" - }, - { - "code": 6004, - "name": "TooManyPositions", - "msg": "Number of position limit reached" - }, - { - "code": 6005, - "name": "PositionNotInUse", - "msg": "Position not in use" - }, - { - "code": 6006, - "name": "CreatePositionWithZero", - "msg": "New position needs to have positive balance" - }, - { - "code": 6007, - "name": "ClosePositionWithZero", - "msg": "Closing a position of 0 is not allowed" - }, - { - "code": 6008, - "name": "InvalidPosition", - "msg": "Invalid product/publisher pair" - }, - { - "code": 6009, - "name": "AmountBiggerThanPosition", - "msg": "Amount to unlock bigger than position" - }, - { - "code": 6010, - "name": "AlreadyUnlocking", - "msg": "Position already unlocking" - }, - { - "code": 6011, - "name": "ZeroEpochDuration", - "msg": "Epoch duration is 0" - }, - { - "code": 6012, - "name": "WithdrawToUnauthorizedAccount", - "msg": "Owner needs to own destination account" - }, - { - "code": 6013, - "name": "InsufficientWithdrawableBalance", - "msg": "Insufficient balance to cover the withdrawal" - }, - { - "code": 6014, - "name": "WrongTarget", - "msg": "Target in position doesn't match target in instruction data" - }, - { - "code": 6015, - "name": "GenericOverflow", - "msg": "An arithmetic operation unexpectedly overflowed" - }, - { - "code": 6016, - "name": "NegativeBalance", - "msg": "Locked balance must be positive" - }, - { - "code": 6017, - "name": "Frozen", - "msg": "Protocol is frozen" - }, - { - "code": 6018, - "name": "DebuggingOnly", - "msg": "Not allowed when not debugging" - }, - { - "code": 6019, - "name": "ProposalTooLong", - "msg": "Proposal too long" - }, - { - "code": 6020, - "name": "InvalidVotingEpoch", - "msg": "Voting epoch is either too old or hasn't started" - }, - { - "code": 6021, - "name": "ProposalNotActive", - "msg": "Voting hasn't started" - }, - { - "code": 6022, - "name": "NoRemainingAccount", - "msg": "Extra governance account required" - }, - { - "code": 6023, - "name": "Unauthorized", - "msg": "Unauthorized caller" - }, - { - "code": 6024, - "name": "AccountUpgradeFailed", - "msg": "Precondition to upgrade account violated" - }, - { - "code": 6025, - "name": "NotImplemented", - "msg": "Not implemented" - }, - { - "code": 6026, - "name": "PositionSerDe", - "msg": "Error deserializing position" - }, - { - "code": 6027, - "name": "PositionOutOfBounds", - "msg": "Position out of bounds" - }, - { - "code": 6028, - "name": "VoteDuringTransferEpoch", - "msg": "Can't vote during an account's transfer epoch" - }, - { - "code": 6029, - "name": "Other", - "msg": "Other" - } - ] -} \ No newline at end of file diff --git a/staking/target/types/staking.ts b/staking/target/types/staking.ts deleted file mode 100644 index 78510f13..00000000 --- a/staking/target/types/staking.ts +++ /dev/null @@ -1,3629 +0,0 @@ -export type Staking = { - "version": "1.0.0", - "name": "staking", - "instructions": [ - { - "name": "initConfig", - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "configAccount", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "rent", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "globalConfig", - "type": { - "defined": "GlobalConfig" - } - } - ] - }, - { - "name": "updateGovernanceAuthority", - "accounts": [ - { - "name": "governanceSigner", - "isMut": false, - "isSigner": true - }, - { - "name": "config", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - } - ], - "args": [ - { - "name": "newAuthority", - "type": "publicKey" - } - ] - }, - { - "name": "updateFreeze", - "accounts": [ - { - "name": "governanceSigner", - "isMut": false, - "isSigner": true - }, - { - "name": "config", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - } - ], - "args": [ - { - "name": "freeze", - "type": "bool" - } - ] - }, - { - "name": "updateTokenListTime", - "accounts": [ - { - "name": "governanceSigner", - "isMut": false, - "isSigner": true - }, - { - "name": "config", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - } - ], - "args": [ - { - "name": "tokenListTime", - "type": { - "option": "i64" - } - } - ] - }, - { - "name": "createStakeAccount", - "docs": [ - "Trustless instruction that creates a stake account for a user", - "The main account i.e. the position accounts needs to be initialized outside of the program", - "otherwise we run into stack limits" - ], - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "stakeAccountPositions", - "isMut": true, - "isSigner": false - }, - { - "name": "stakeAccountMetadata", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "stakeAccountCustody", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "custody" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "custodyAuthority", - "isMut": false, - "isSigner": false, - "docs": [ - "CHECK : This AccountInfo is safe because it's a checked PDA" - ], - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "authority" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "voterRecord", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "voter_weight" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "mint", - "isMut": false, - "isSigner": false - }, - { - "name": "rent", - "isMut": false, - "isSigner": false - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "owner", - "type": "publicKey" - }, - { - "name": "lock", - "type": { - "defined": "VestingSchedule" - } - } - ] - }, - { - "name": "createPosition", - "docs": [ - "Creates a position", - "Looks for the first available place in the array, fails if array is full", - "Computes risk and fails if new positions exceed risk limit" - ], - "accounts": [ - { - "name": "payer", - "isMut": false, - "isSigner": true - }, - { - "name": "stakeAccountPositions", - "isMut": true, - "isSigner": false - }, - { - "name": "stakeAccountMetadata", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "stakeAccountCustody", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "custody" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "targetAccount", - "isMut": true, - "isSigner": false - } - ], - "args": [ - { - "name": "targetWithParameters", - "type": { - "defined": "TargetWithParameters" - } - }, - { - "name": "amount", - "type": "u64" - } - ] - }, - { - "name": "closePosition", - "accounts": [ - { - "name": "payer", - "isMut": false, - "isSigner": true - }, - { - "name": "stakeAccountPositions", - "isMut": true, - "isSigner": false - }, - { - "name": "stakeAccountMetadata", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "stakeAccountCustody", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "custody" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "targetAccount", - "isMut": true, - "isSigner": false - } - ], - "args": [ - { - "name": "index", - "type": "u8" - }, - { - "name": "amount", - "type": "u64" - }, - { - "name": "targetWithParameters", - "type": { - "defined": "TargetWithParameters" - } - } - ] - }, - { - "name": "withdrawStake", - "accounts": [ - { - "name": "payer", - "isMut": false, - "isSigner": true - }, - { - "name": "destination", - "isMut": true, - "isSigner": false - }, - { - "name": "stakeAccountPositions", - "isMut": false, - "isSigner": false - }, - { - "name": "stakeAccountMetadata", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "stakeAccountCustody", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "custody" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "custodyAuthority", - "isMut": false, - "isSigner": false, - "docs": [ - "CHECK : This AccountInfo is safe because it's a checked PDA" - ], - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "authority" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "amount", - "type": "u64" - } - ] - }, - { - "name": "updateVoterWeight", - "accounts": [ - { - "name": "payer", - "isMut": false, - "isSigner": true - }, - { - "name": "stakeAccountPositions", - "isMut": false, - "isSigner": false - }, - { - "name": "stakeAccountMetadata", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "stakeAccountCustody", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "custody" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "voterRecord", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "voter_weight" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "governanceTarget", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "target" - }, - { - "kind": "const", - "type": "string", - "value": "voting" - } - ] - } - } - ], - "args": [ - { - "name": "action", - "type": { - "defined": "VoterWeightAction" - } - } - ] - }, - { - "name": "updateMaxVoterWeight", - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "maxVoterRecord", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "max_voter" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, - { - "name": "createTarget", - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "governanceSigner", - "isMut": false, - "isSigner": true - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "targetAccount", - "isMut": true, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "target", - "type": { - "defined": "Target" - } - } - ] - }, - { - "name": "advanceClock", - "accounts": [ - { - "name": "config", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - } - ], - "args": [ - { - "name": "seconds", - "type": "i64" - } - ] - }, - { - "name": "requestSplit", - "docs": [ - "* Any user of the staking program can request to split their account and\n * give a part of it to another user. This is mostly useful to transfer unvested\n * tokens.\n * In the first step, the user requests a split by specifying the amount of tokens\n * they want to give to the other user and the recipient's pubkey." - ], - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "stakeAccountPositions", - "isMut": false, - "isSigner": false - }, - { - "name": "stakeAccountMetadata", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "stakeAccountSplitRequest", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "split_request" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "amount", - "type": "u64" - }, - { - "name": "recipient", - "type": "publicKey" - } - ] - }, - { - "name": "acceptSplit", - "docs": [ - "* A split request can only be accepted by the pda_authority from\n * the config account. If accepted `amount` tokens are transferred to the\n * recipient and the split request is reset (by setting amount to 0).\n * The recipient of a transfer can't vote during the epoch of the transfer." - ], - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "sourceStakeAccountPositions", - "isMut": true, - "isSigner": false - }, - { - "name": "sourceStakeAccountMetadata", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "source_stake_account_positions" - } - ] - } - }, - { - "name": "sourceStakeAccountSplitRequest", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "split_request" - }, - { - "kind": "account", - "type": "publicKey", - "path": "source_stake_account_positions" - } - ] - } - }, - { - "name": "sourceStakeAccountCustody", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "custody" - }, - { - "kind": "account", - "type": "publicKey", - "path": "source_stake_account_positions" - } - ] - } - }, - { - "name": "sourceCustodyAuthority", - "isMut": false, - "isSigner": false, - "docs": [ - "CHECK : This AccountInfo is safe because it's a checked PDA" - ], - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "authority" - }, - { - "kind": "account", - "type": "publicKey", - "path": "source_stake_account_positions" - } - ] - } - }, - { - "name": "newStakeAccountPositions", - "isMut": true, - "isSigner": false - }, - { - "name": "newStakeAccountMetadata", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "new_stake_account_positions" - } - ] - } - }, - { - "name": "newStakeAccountCustody", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "custody" - }, - { - "kind": "account", - "type": "publicKey", - "path": "new_stake_account_positions" - } - ] - } - }, - { - "name": "newCustodyAuthority", - "isMut": false, - "isSigner": false, - "docs": [ - "CHECK : This AccountInfo is safe because it's a checked PDA" - ], - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "authority" - }, - { - "kind": "account", - "type": "publicKey", - "path": "new_stake_account_positions" - } - ] - } - }, - { - "name": "newVoterRecord", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "voter_weight" - }, - { - "kind": "account", - "type": "publicKey", - "path": "new_stake_account_positions" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "mint", - "isMut": false, - "isSigner": false - }, - { - "name": "rent", - "isMut": false, - "isSigner": false - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [] - } - ], - "accounts": [ - { - "name": "globalConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "bump", - "type": "u8" - }, - { - "name": "governanceAuthority", - "type": "publicKey" - }, - { - "name": "pythTokenMint", - "type": "publicKey" - }, - { - "name": "pythGovernanceRealm", - "type": "publicKey" - }, - { - "name": "unlockingDuration", - "type": "u8" - }, - { - "name": "epochDuration", - "type": "u64" - }, - { - "name": "freeze", - "type": "bool" - }, - { - "name": "pdaAuthority", - "type": "publicKey" - }, - { - "name": "governanceProgram", - "type": "publicKey" - }, - { - "name": "pythTokenListTime", - "docs": [ - "Once the pyth token is listed, governance can update the config to set this value.", - "Once this value is set, vesting schedules that depend on the token list date can start", - "vesting." - ], - "type": { - "option": "i64" - } - }, - { - "name": "mockClockTime", - "type": "i64" - } - ] - } - }, - { - "name": "maxVoterWeightRecord", - "docs": [ - "Copied this struct from https://github.com/solana-labs/solana-program-library/blob/master/governance/addin-api/src/max_voter_weight.rs" - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "realm", - "docs": [ - "The Realm the MaxVoterWeightRecord belongs to" - ], - "type": "publicKey" - }, - { - "name": "governingTokenMint", - "docs": [ - "Governing Token Mint the MaxVoterWeightRecord is associated with", - "Note: The addin can take deposits of any tokens and is not restricted to the community or", - "council tokens only" - ], - "type": "publicKey" - }, - { - "name": "maxVoterWeight", - "docs": [ - "Max voter weight", - "The max voter weight provided by the addin for the given realm and governing_token_mint" - ], - "type": "u64" - }, - { - "name": "maxVoterWeightExpiry", - "docs": [ - "The slot when the max voting weight expires", - "It should be set to None if the weight never expires", - "If the max vote weight decays with time, for example for time locked based weights, then", - "the expiry must be set As a pattern Revise instruction to update the max weight should", - "be invoked before governance instruction within the same transaction and the expiry set", - "to the current slot to provide up to date weight" - ], - "type": { - "option": "u64" - } - }, - { - "name": "reserved", - "docs": [ - "Reserved space for future versions" - ], - "type": { - "array": [ - "u8", - 8 - ] - } - } - ] - } - }, - { - "name": "positionData", - "docs": [ - "An array that contains all of a user's positions i.e. where are the staking and who are they", - "staking to.", - "The invariant we preserve is : For i < next_index, positions[i] == Some", - "For i >= next_index, positions[i] == None" - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "owner", - "type": "publicKey" - }, - { - "name": "positions", - "type": { - "array": [ - { - "array": [ - "u8", - 200 - ] - }, - 20 - ] - } - } - ] - } - }, - { - "name": "splitRequest", - "type": { - "kind": "struct", - "fields": [ - { - "name": "amount", - "type": "u64" - }, - { - "name": "recipient", - "type": "publicKey" - } - ] - } - }, - { - "name": "stakeAccountMetadataV2", - "docs": [ - "This is the metadata account for each staker", - "It is derived from the positions account with seeds \"stake_metadata\" and the positions account", - "pubkey It stores some PDA bumps, the owner of the account and the vesting schedule" - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "metadataBump", - "type": "u8" - }, - { - "name": "custodyBump", - "type": "u8" - }, - { - "name": "authorityBump", - "type": "u8" - }, - { - "name": "voterBump", - "type": "u8" - }, - { - "name": "owner", - "type": "publicKey" - }, - { - "name": "lock", - "type": { - "defined": "VestingSchedule" - } - }, - { - "name": "nextIndex", - "type": "u8" - }, - { - "name": "transferEpoch", - "type": { - "option": "u64" - } - } - ] - } - }, - { - "name": "targetMetadata", - "docs": [ - "This represents a target that users can stake to", - "Currently we store the last time the target account was updated, the current locked balance", - "and the amount by which the locked balance will change in the next epoch" - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "bump", - "type": "u8" - }, - { - "name": "lastUpdateAt", - "type": "u64" - }, - { - "name": "prevEpochLocked", - "type": "u64" - }, - { - "name": "locked", - "type": "u64" - }, - { - "name": "deltaLocked", - "type": "i64" - } - ] - } - }, - { - "name": "voterWeightRecord", - "docs": [ - "Copied this struct from https://github.com/solana-labs/solana-program-library/blob/master/governance/addin-api/src/voter_weight.rs", - "Anchor has a macro (vote_weight_record) that is supposed to generate this struct, but it doesn't", - "work because the error's macros are not updated for anchor 0.22.0.", - "Even if it did work, the type wouldn't show up in the IDL. SPL doesn't produce an API, which", - "means that means we'd need the equivalent of this code on the client side.", - "If Anchor fixes the macro, we might consider changing it" - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "realm", - "docs": [ - "VoterWeightRecord discriminator sha256(\"account:VoterWeightRecord\")[..8]", - "Note: The discriminator size must match the addin implementing program discriminator size", - "to ensure it's stored in the private space of the account data and it's unique", - "pub account_discriminator: [u8; 8],", - "The Realm the VoterWeightRecord belongs to" - ], - "type": "publicKey" - }, - { - "name": "governingTokenMint", - "docs": [ - "Governing Token Mint the VoterWeightRecord is associated with", - "Note: The addin can take deposits of any tokens and is not restricted to the community or", - "council tokens only" - ], - "type": "publicKey" - }, - { - "name": "governingTokenOwner", - "docs": [ - "The owner of the governing token and voter", - "This is the actual owner (voter) and corresponds to TokenOwnerRecord.governing_token_owner" - ], - "type": "publicKey" - }, - { - "name": "voterWeight", - "docs": [ - "Voter's weight", - "The weight of the voter provided by the addin for the given realm, governing_token_mint and", - "governing_token_owner (voter)" - ], - "type": "u64" - }, - { - "name": "voterWeightExpiry", - "docs": [ - "The slot when the voting weight expires", - "It should be set to None if the weight never expires", - "If the voter weight decays with time, for example for time locked based weights, then the", - "expiry must be set As a common pattern Revise instruction to update the weight should", - "be invoked before governance instruction within the same transaction and the expiry set", - "to the current slot to provide up to date weight" - ], - "type": { - "option": "u64" - } - }, - { - "name": "weightAction", - "docs": [ - "The governance action the voter's weight pertains to", - "It allows to provided voter's weight specific to the particular action the weight is", - "evaluated for When the action is provided then the governance program asserts the", - "executing action is the same as specified by the addin" - ], - "type": { - "option": { - "defined": "VoterWeightAction" - } - } - }, - { - "name": "weightActionTarget", - "docs": [ - "The target the voter's weight action pertains to", - "It allows to provided voter's weight specific to the target the weight is evaluated for", - "For example when addin supplies weight to vote on a particular proposal then it must", - "specify the proposal as the action target When the target is provided then the", - "governance program asserts the target is the same as specified by the addin" - ], - "type": { - "option": "publicKey" - } - }, - { - "name": "reserved", - "docs": [ - "Reserved space for future versions" - ], - "type": { - "array": [ - "u8", - 8 - ] - } - } - ] - } - } - ], - "types": [ - { - "name": "Position", - "docs": [ - "This represents a staking position, i.e. an amount that someone has staked to a particular", - "target. This is one of the core pieces of our staking design, and stores all", - "of the state related to a position The voting position is a position where the", - "target_with_parameters is VOTING" - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "amount", - "type": "u64" - }, - { - "name": "activationEpoch", - "type": "u64" - }, - { - "name": "unlockingStart", - "type": { - "option": "u64" - } - }, - { - "name": "targetWithParameters", - "type": { - "defined": "TargetWithParameters" - } - } - ] - } - }, - { - "name": "Target", - "type": { - "kind": "enum", - "variants": [ - { - "name": "VOTING" - }, - { - "name": "STAKING", - "fields": [ - { - "name": "product", - "type": "publicKey" - } - ] - } - ] - } - }, - { - "name": "TargetWithParameters", - "type": { - "kind": "enum", - "variants": [ - { - "name": "VOTING" - }, - { - "name": "STAKING", - "fields": [ - { - "name": "product", - "type": "publicKey" - }, - { - "name": "publisher", - "type": { - "defined": "Publisher" - } - } - ] - } - ] - } - }, - { - "name": "Publisher", - "type": { - "kind": "enum", - "variants": [ - { - "name": "DEFAULT" - }, - { - "name": "SOME", - "fields": [ - { - "name": "address", - "type": "publicKey" - } - ] - } - ] - } - }, - { - "name": "PositionState", - "docs": [ - "The core states that a position can be in" - ], - "type": { - "kind": "enum", - "variants": [ - { - "name": "UNLOCKED" - }, - { - "name": "LOCKING" - }, - { - "name": "LOCKED" - }, - { - "name": "PREUNLOCKING" - }, - { - "name": "UNLOCKING" - } - ] - } - }, - { - "name": "VestingSchedule", - "docs": [ - "Represents how a given initial balance vests over time", - "It is unit-less, but units must be consistent" - ], - "type": { - "kind": "enum", - "variants": [ - { - "name": "FullyVested" - }, - { - "name": "PeriodicVesting", - "fields": [ - { - "name": "initial_balance", - "type": "u64" - }, - { - "name": "start_date", - "type": "i64" - }, - { - "name": "period_duration", - "type": "u64" - }, - { - "name": "num_periods", - "type": "u64" - } - ] - }, - { - "name": "PeriodicVestingAfterListing", - "fields": [ - { - "name": "initial_balance", - "type": "u64" - }, - { - "name": "period_duration", - "type": "u64" - }, - { - "name": "num_periods", - "type": "u64" - } - ] - } - ] - } - }, - { - "name": "VoterWeightAction", - "docs": [ - "The governance action VoterWeight is evaluated for" - ], - "type": { - "kind": "enum", - "variants": [ - { - "name": "CastVote" - }, - { - "name": "CommentProposal" - }, - { - "name": "CreateGovernance" - }, - { - "name": "CreateProposal" - }, - { - "name": "SignOffProposal" - } - ] - } - } - ], - "errors": [ - { - "code": 6000, - "name": "TooMuchExposureToProduct", - "msg": "Too much exposure to product" - }, - { - "code": 6001, - "name": "TooMuchExposureToGovernance", - "msg": "Too much exposure to governance" - }, - { - "code": 6002, - "name": "TokensNotYetVested", - "msg": "Tokens not yet vested" - }, - { - "code": 6003, - "name": "RiskLimitExceeded", - "msg": "Risk limit exceeded" - }, - { - "code": 6004, - "name": "TooManyPositions", - "msg": "Number of position limit reached" - }, - { - "code": 6005, - "name": "PositionNotInUse", - "msg": "Position not in use" - }, - { - "code": 6006, - "name": "CreatePositionWithZero", - "msg": "New position needs to have positive balance" - }, - { - "code": 6007, - "name": "ClosePositionWithZero", - "msg": "Closing a position of 0 is not allowed" - }, - { - "code": 6008, - "name": "InvalidPosition", - "msg": "Invalid product/publisher pair" - }, - { - "code": 6009, - "name": "AmountBiggerThanPosition", - "msg": "Amount to unlock bigger than position" - }, - { - "code": 6010, - "name": "AlreadyUnlocking", - "msg": "Position already unlocking" - }, - { - "code": 6011, - "name": "ZeroEpochDuration", - "msg": "Epoch duration is 0" - }, - { - "code": 6012, - "name": "WithdrawToUnauthorizedAccount", - "msg": "Owner needs to own destination account" - }, - { - "code": 6013, - "name": "InsufficientWithdrawableBalance", - "msg": "Insufficient balance to cover the withdrawal" - }, - { - "code": 6014, - "name": "WrongTarget", - "msg": "Target in position doesn't match target in instruction data" - }, - { - "code": 6015, - "name": "GenericOverflow", - "msg": "An arithmetic operation unexpectedly overflowed" - }, - { - "code": 6016, - "name": "NegativeBalance", - "msg": "Locked balance must be positive" - }, - { - "code": 6017, - "name": "Frozen", - "msg": "Protocol is frozen" - }, - { - "code": 6018, - "name": "DebuggingOnly", - "msg": "Not allowed when not debugging" - }, - { - "code": 6019, - "name": "ProposalTooLong", - "msg": "Proposal too long" - }, - { - "code": 6020, - "name": "InvalidVotingEpoch", - "msg": "Voting epoch is either too old or hasn't started" - }, - { - "code": 6021, - "name": "ProposalNotActive", - "msg": "Voting hasn't started" - }, - { - "code": 6022, - "name": "NoRemainingAccount", - "msg": "Extra governance account required" - }, - { - "code": 6023, - "name": "Unauthorized", - "msg": "Unauthorized caller" - }, - { - "code": 6024, - "name": "AccountUpgradeFailed", - "msg": "Precondition to upgrade account violated" - }, - { - "code": 6025, - "name": "NotImplemented", - "msg": "Not implemented" - }, - { - "code": 6026, - "name": "PositionSerDe", - "msg": "Error deserializing position" - }, - { - "code": 6027, - "name": "PositionOutOfBounds", - "msg": "Position out of bounds" - }, - { - "code": 6028, - "name": "VoteDuringTransferEpoch", - "msg": "Can't vote during an account's transfer epoch" - }, - { - "code": 6029, - "name": "Other", - "msg": "Other" - } - ] -}; - -export const IDL: Staking = { - "version": "1.0.0", - "name": "staking", - "instructions": [ - { - "name": "initConfig", - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "configAccount", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "rent", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "globalConfig", - "type": { - "defined": "GlobalConfig" - } - } - ] - }, - { - "name": "updateGovernanceAuthority", - "accounts": [ - { - "name": "governanceSigner", - "isMut": false, - "isSigner": true - }, - { - "name": "config", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - } - ], - "args": [ - { - "name": "newAuthority", - "type": "publicKey" - } - ] - }, - { - "name": "updateFreeze", - "accounts": [ - { - "name": "governanceSigner", - "isMut": false, - "isSigner": true - }, - { - "name": "config", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - } - ], - "args": [ - { - "name": "freeze", - "type": "bool" - } - ] - }, - { - "name": "updateTokenListTime", - "accounts": [ - { - "name": "governanceSigner", - "isMut": false, - "isSigner": true - }, - { - "name": "config", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - } - ], - "args": [ - { - "name": "tokenListTime", - "type": { - "option": "i64" - } - } - ] - }, - { - "name": "createStakeAccount", - "docs": [ - "Trustless instruction that creates a stake account for a user", - "The main account i.e. the position accounts needs to be initialized outside of the program", - "otherwise we run into stack limits" - ], - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "stakeAccountPositions", - "isMut": true, - "isSigner": false - }, - { - "name": "stakeAccountMetadata", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "stakeAccountCustody", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "custody" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "custodyAuthority", - "isMut": false, - "isSigner": false, - "docs": [ - "CHECK : This AccountInfo is safe because it's a checked PDA" - ], - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "authority" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "voterRecord", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "voter_weight" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "mint", - "isMut": false, - "isSigner": false - }, - { - "name": "rent", - "isMut": false, - "isSigner": false - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "owner", - "type": "publicKey" - }, - { - "name": "lock", - "type": { - "defined": "VestingSchedule" - } - } - ] - }, - { - "name": "createPosition", - "docs": [ - "Creates a position", - "Looks for the first available place in the array, fails if array is full", - "Computes risk and fails if new positions exceed risk limit" - ], - "accounts": [ - { - "name": "payer", - "isMut": false, - "isSigner": true - }, - { - "name": "stakeAccountPositions", - "isMut": true, - "isSigner": false - }, - { - "name": "stakeAccountMetadata", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "stakeAccountCustody", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "custody" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "targetAccount", - "isMut": true, - "isSigner": false - } - ], - "args": [ - { - "name": "targetWithParameters", - "type": { - "defined": "TargetWithParameters" - } - }, - { - "name": "amount", - "type": "u64" - } - ] - }, - { - "name": "closePosition", - "accounts": [ - { - "name": "payer", - "isMut": false, - "isSigner": true - }, - { - "name": "stakeAccountPositions", - "isMut": true, - "isSigner": false - }, - { - "name": "stakeAccountMetadata", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "stakeAccountCustody", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "custody" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "targetAccount", - "isMut": true, - "isSigner": false - } - ], - "args": [ - { - "name": "index", - "type": "u8" - }, - { - "name": "amount", - "type": "u64" - }, - { - "name": "targetWithParameters", - "type": { - "defined": "TargetWithParameters" - } - } - ] - }, - { - "name": "withdrawStake", - "accounts": [ - { - "name": "payer", - "isMut": false, - "isSigner": true - }, - { - "name": "destination", - "isMut": true, - "isSigner": false - }, - { - "name": "stakeAccountPositions", - "isMut": false, - "isSigner": false - }, - { - "name": "stakeAccountMetadata", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "stakeAccountCustody", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "custody" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "custodyAuthority", - "isMut": false, - "isSigner": false, - "docs": [ - "CHECK : This AccountInfo is safe because it's a checked PDA" - ], - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "authority" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "amount", - "type": "u64" - } - ] - }, - { - "name": "updateVoterWeight", - "accounts": [ - { - "name": "payer", - "isMut": false, - "isSigner": true - }, - { - "name": "stakeAccountPositions", - "isMut": false, - "isSigner": false - }, - { - "name": "stakeAccountMetadata", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "stakeAccountCustody", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "custody" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "voterRecord", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "voter_weight" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "governanceTarget", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "target" - }, - { - "kind": "const", - "type": "string", - "value": "voting" - } - ] - } - } - ], - "args": [ - { - "name": "action", - "type": { - "defined": "VoterWeightAction" - } - } - ] - }, - { - "name": "updateMaxVoterWeight", - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "maxVoterRecord", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "max_voter" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, - { - "name": "createTarget", - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "governanceSigner", - "isMut": false, - "isSigner": true - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "targetAccount", - "isMut": true, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "target", - "type": { - "defined": "Target" - } - } - ] - }, - { - "name": "advanceClock", - "accounts": [ - { - "name": "config", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - } - ], - "args": [ - { - "name": "seconds", - "type": "i64" - } - ] - }, - { - "name": "requestSplit", - "docs": [ - "* Any user of the staking program can request to split their account and\n * give a part of it to another user. This is mostly useful to transfer unvested\n * tokens.\n * In the first step, the user requests a split by specifying the amount of tokens\n * they want to give to the other user and the recipient's pubkey." - ], - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "stakeAccountPositions", - "isMut": false, - "isSigner": false - }, - { - "name": "stakeAccountMetadata", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "stakeAccountSplitRequest", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "split_request" - }, - { - "kind": "account", - "type": "publicKey", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "amount", - "type": "u64" - }, - { - "name": "recipient", - "type": "publicKey" - } - ] - }, - { - "name": "acceptSplit", - "docs": [ - "* A split request can only be accepted by the pda_authority from\n * the config account. If accepted `amount` tokens are transferred to the\n * recipient and the split request is reset (by setting amount to 0).\n * The recipient of a transfer can't vote during the epoch of the transfer." - ], - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "sourceStakeAccountPositions", - "isMut": true, - "isSigner": false - }, - { - "name": "sourceStakeAccountMetadata", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "source_stake_account_positions" - } - ] - } - }, - { - "name": "sourceStakeAccountSplitRequest", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "split_request" - }, - { - "kind": "account", - "type": "publicKey", - "path": "source_stake_account_positions" - } - ] - } - }, - { - "name": "sourceStakeAccountCustody", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "custody" - }, - { - "kind": "account", - "type": "publicKey", - "path": "source_stake_account_positions" - } - ] - } - }, - { - "name": "sourceCustodyAuthority", - "isMut": false, - "isSigner": false, - "docs": [ - "CHECK : This AccountInfo is safe because it's a checked PDA" - ], - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "authority" - }, - { - "kind": "account", - "type": "publicKey", - "path": "source_stake_account_positions" - } - ] - } - }, - { - "name": "newStakeAccountPositions", - "isMut": true, - "isSigner": false - }, - { - "name": "newStakeAccountMetadata", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "stake_metadata" - }, - { - "kind": "account", - "type": "publicKey", - "path": "new_stake_account_positions" - } - ] - } - }, - { - "name": "newStakeAccountCustody", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "custody" - }, - { - "kind": "account", - "type": "publicKey", - "path": "new_stake_account_positions" - } - ] - } - }, - { - "name": "newCustodyAuthority", - "isMut": false, - "isSigner": false, - "docs": [ - "CHECK : This AccountInfo is safe because it's a checked PDA" - ], - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "authority" - }, - { - "kind": "account", - "type": "publicKey", - "path": "new_stake_account_positions" - } - ] - } - }, - { - "name": "newVoterRecord", - "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "voter_weight" - }, - { - "kind": "account", - "type": "publicKey", - "path": "new_stake_account_positions" - } - ] - } - }, - { - "name": "config", - "isMut": false, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "config" - } - ] - } - }, - { - "name": "mint", - "isMut": false, - "isSigner": false - }, - { - "name": "rent", - "isMut": false, - "isSigner": false - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [] - } - ], - "accounts": [ - { - "name": "globalConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "bump", - "type": "u8" - }, - { - "name": "governanceAuthority", - "type": "publicKey" - }, - { - "name": "pythTokenMint", - "type": "publicKey" - }, - { - "name": "pythGovernanceRealm", - "type": "publicKey" - }, - { - "name": "unlockingDuration", - "type": "u8" - }, - { - "name": "epochDuration", - "type": "u64" - }, - { - "name": "freeze", - "type": "bool" - }, - { - "name": "pdaAuthority", - "type": "publicKey" - }, - { - "name": "governanceProgram", - "type": "publicKey" - }, - { - "name": "pythTokenListTime", - "docs": [ - "Once the pyth token is listed, governance can update the config to set this value.", - "Once this value is set, vesting schedules that depend on the token list date can start", - "vesting." - ], - "type": { - "option": "i64" - } - }, - { - "name": "mockClockTime", - "type": "i64" - } - ] - } - }, - { - "name": "maxVoterWeightRecord", - "docs": [ - "Copied this struct from https://github.com/solana-labs/solana-program-library/blob/master/governance/addin-api/src/max_voter_weight.rs" - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "realm", - "docs": [ - "The Realm the MaxVoterWeightRecord belongs to" - ], - "type": "publicKey" - }, - { - "name": "governingTokenMint", - "docs": [ - "Governing Token Mint the MaxVoterWeightRecord is associated with", - "Note: The addin can take deposits of any tokens and is not restricted to the community or", - "council tokens only" - ], - "type": "publicKey" - }, - { - "name": "maxVoterWeight", - "docs": [ - "Max voter weight", - "The max voter weight provided by the addin for the given realm and governing_token_mint" - ], - "type": "u64" - }, - { - "name": "maxVoterWeightExpiry", - "docs": [ - "The slot when the max voting weight expires", - "It should be set to None if the weight never expires", - "If the max vote weight decays with time, for example for time locked based weights, then", - "the expiry must be set As a pattern Revise instruction to update the max weight should", - "be invoked before governance instruction within the same transaction and the expiry set", - "to the current slot to provide up to date weight" - ], - "type": { - "option": "u64" - } - }, - { - "name": "reserved", - "docs": [ - "Reserved space for future versions" - ], - "type": { - "array": [ - "u8", - 8 - ] - } - } - ] - } - }, - { - "name": "positionData", - "docs": [ - "An array that contains all of a user's positions i.e. where are the staking and who are they", - "staking to.", - "The invariant we preserve is : For i < next_index, positions[i] == Some", - "For i >= next_index, positions[i] == None" - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "owner", - "type": "publicKey" - }, - { - "name": "positions", - "type": { - "array": [ - { - "array": [ - "u8", - 200 - ] - }, - 20 - ] - } - } - ] - } - }, - { - "name": "splitRequest", - "type": { - "kind": "struct", - "fields": [ - { - "name": "amount", - "type": "u64" - }, - { - "name": "recipient", - "type": "publicKey" - } - ] - } - }, - { - "name": "stakeAccountMetadataV2", - "docs": [ - "This is the metadata account for each staker", - "It is derived from the positions account with seeds \"stake_metadata\" and the positions account", - "pubkey It stores some PDA bumps, the owner of the account and the vesting schedule" - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "metadataBump", - "type": "u8" - }, - { - "name": "custodyBump", - "type": "u8" - }, - { - "name": "authorityBump", - "type": "u8" - }, - { - "name": "voterBump", - "type": "u8" - }, - { - "name": "owner", - "type": "publicKey" - }, - { - "name": "lock", - "type": { - "defined": "VestingSchedule" - } - }, - { - "name": "nextIndex", - "type": "u8" - }, - { - "name": "transferEpoch", - "type": { - "option": "u64" - } - } - ] - } - }, - { - "name": "targetMetadata", - "docs": [ - "This represents a target that users can stake to", - "Currently we store the last time the target account was updated, the current locked balance", - "and the amount by which the locked balance will change in the next epoch" - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "bump", - "type": "u8" - }, - { - "name": "lastUpdateAt", - "type": "u64" - }, - { - "name": "prevEpochLocked", - "type": "u64" - }, - { - "name": "locked", - "type": "u64" - }, - { - "name": "deltaLocked", - "type": "i64" - } - ] - } - }, - { - "name": "voterWeightRecord", - "docs": [ - "Copied this struct from https://github.com/solana-labs/solana-program-library/blob/master/governance/addin-api/src/voter_weight.rs", - "Anchor has a macro (vote_weight_record) that is supposed to generate this struct, but it doesn't", - "work because the error's macros are not updated for anchor 0.22.0.", - "Even if it did work, the type wouldn't show up in the IDL. SPL doesn't produce an API, which", - "means that means we'd need the equivalent of this code on the client side.", - "If Anchor fixes the macro, we might consider changing it" - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "realm", - "docs": [ - "VoterWeightRecord discriminator sha256(\"account:VoterWeightRecord\")[..8]", - "Note: The discriminator size must match the addin implementing program discriminator size", - "to ensure it's stored in the private space of the account data and it's unique", - "pub account_discriminator: [u8; 8],", - "The Realm the VoterWeightRecord belongs to" - ], - "type": "publicKey" - }, - { - "name": "governingTokenMint", - "docs": [ - "Governing Token Mint the VoterWeightRecord is associated with", - "Note: The addin can take deposits of any tokens and is not restricted to the community or", - "council tokens only" - ], - "type": "publicKey" - }, - { - "name": "governingTokenOwner", - "docs": [ - "The owner of the governing token and voter", - "This is the actual owner (voter) and corresponds to TokenOwnerRecord.governing_token_owner" - ], - "type": "publicKey" - }, - { - "name": "voterWeight", - "docs": [ - "Voter's weight", - "The weight of the voter provided by the addin for the given realm, governing_token_mint and", - "governing_token_owner (voter)" - ], - "type": "u64" - }, - { - "name": "voterWeightExpiry", - "docs": [ - "The slot when the voting weight expires", - "It should be set to None if the weight never expires", - "If the voter weight decays with time, for example for time locked based weights, then the", - "expiry must be set As a common pattern Revise instruction to update the weight should", - "be invoked before governance instruction within the same transaction and the expiry set", - "to the current slot to provide up to date weight" - ], - "type": { - "option": "u64" - } - }, - { - "name": "weightAction", - "docs": [ - "The governance action the voter's weight pertains to", - "It allows to provided voter's weight specific to the particular action the weight is", - "evaluated for When the action is provided then the governance program asserts the", - "executing action is the same as specified by the addin" - ], - "type": { - "option": { - "defined": "VoterWeightAction" - } - } - }, - { - "name": "weightActionTarget", - "docs": [ - "The target the voter's weight action pertains to", - "It allows to provided voter's weight specific to the target the weight is evaluated for", - "For example when addin supplies weight to vote on a particular proposal then it must", - "specify the proposal as the action target When the target is provided then the", - "governance program asserts the target is the same as specified by the addin" - ], - "type": { - "option": "publicKey" - } - }, - { - "name": "reserved", - "docs": [ - "Reserved space for future versions" - ], - "type": { - "array": [ - "u8", - 8 - ] - } - } - ] - } - } - ], - "types": [ - { - "name": "Position", - "docs": [ - "This represents a staking position, i.e. an amount that someone has staked to a particular", - "target. This is one of the core pieces of our staking design, and stores all", - "of the state related to a position The voting position is a position where the", - "target_with_parameters is VOTING" - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "amount", - "type": "u64" - }, - { - "name": "activationEpoch", - "type": "u64" - }, - { - "name": "unlockingStart", - "type": { - "option": "u64" - } - }, - { - "name": "targetWithParameters", - "type": { - "defined": "TargetWithParameters" - } - } - ] - } - }, - { - "name": "Target", - "type": { - "kind": "enum", - "variants": [ - { - "name": "VOTING" - }, - { - "name": "STAKING", - "fields": [ - { - "name": "product", - "type": "publicKey" - } - ] - } - ] - } - }, - { - "name": "TargetWithParameters", - "type": { - "kind": "enum", - "variants": [ - { - "name": "VOTING" - }, - { - "name": "STAKING", - "fields": [ - { - "name": "product", - "type": "publicKey" - }, - { - "name": "publisher", - "type": { - "defined": "Publisher" - } - } - ] - } - ] - } - }, - { - "name": "Publisher", - "type": { - "kind": "enum", - "variants": [ - { - "name": "DEFAULT" - }, - { - "name": "SOME", - "fields": [ - { - "name": "address", - "type": "publicKey" - } - ] - } - ] - } - }, - { - "name": "PositionState", - "docs": [ - "The core states that a position can be in" - ], - "type": { - "kind": "enum", - "variants": [ - { - "name": "UNLOCKED" - }, - { - "name": "LOCKING" - }, - { - "name": "LOCKED" - }, - { - "name": "PREUNLOCKING" - }, - { - "name": "UNLOCKING" - } - ] - } - }, - { - "name": "VestingSchedule", - "docs": [ - "Represents how a given initial balance vests over time", - "It is unit-less, but units must be consistent" - ], - "type": { - "kind": "enum", - "variants": [ - { - "name": "FullyVested" - }, - { - "name": "PeriodicVesting", - "fields": [ - { - "name": "initial_balance", - "type": "u64" - }, - { - "name": "start_date", - "type": "i64" - }, - { - "name": "period_duration", - "type": "u64" - }, - { - "name": "num_periods", - "type": "u64" - } - ] - }, - { - "name": "PeriodicVestingAfterListing", - "fields": [ - { - "name": "initial_balance", - "type": "u64" - }, - { - "name": "period_duration", - "type": "u64" - }, - { - "name": "num_periods", - "type": "u64" - } - ] - } - ] - } - }, - { - "name": "VoterWeightAction", - "docs": [ - "The governance action VoterWeight is evaluated for" - ], - "type": { - "kind": "enum", - "variants": [ - { - "name": "CastVote" - }, - { - "name": "CommentProposal" - }, - { - "name": "CreateGovernance" - }, - { - "name": "CreateProposal" - }, - { - "name": "SignOffProposal" - } - ] - } - } - ], - "errors": [ - { - "code": 6000, - "name": "TooMuchExposureToProduct", - "msg": "Too much exposure to product" - }, - { - "code": 6001, - "name": "TooMuchExposureToGovernance", - "msg": "Too much exposure to governance" - }, - { - "code": 6002, - "name": "TokensNotYetVested", - "msg": "Tokens not yet vested" - }, - { - "code": 6003, - "name": "RiskLimitExceeded", - "msg": "Risk limit exceeded" - }, - { - "code": 6004, - "name": "TooManyPositions", - "msg": "Number of position limit reached" - }, - { - "code": 6005, - "name": "PositionNotInUse", - "msg": "Position not in use" - }, - { - "code": 6006, - "name": "CreatePositionWithZero", - "msg": "New position needs to have positive balance" - }, - { - "code": 6007, - "name": "ClosePositionWithZero", - "msg": "Closing a position of 0 is not allowed" - }, - { - "code": 6008, - "name": "InvalidPosition", - "msg": "Invalid product/publisher pair" - }, - { - "code": 6009, - "name": "AmountBiggerThanPosition", - "msg": "Amount to unlock bigger than position" - }, - { - "code": 6010, - "name": "AlreadyUnlocking", - "msg": "Position already unlocking" - }, - { - "code": 6011, - "name": "ZeroEpochDuration", - "msg": "Epoch duration is 0" - }, - { - "code": 6012, - "name": "WithdrawToUnauthorizedAccount", - "msg": "Owner needs to own destination account" - }, - { - "code": 6013, - "name": "InsufficientWithdrawableBalance", - "msg": "Insufficient balance to cover the withdrawal" - }, - { - "code": 6014, - "name": "WrongTarget", - "msg": "Target in position doesn't match target in instruction data" - }, - { - "code": 6015, - "name": "GenericOverflow", - "msg": "An arithmetic operation unexpectedly overflowed" - }, - { - "code": 6016, - "name": "NegativeBalance", - "msg": "Locked balance must be positive" - }, - { - "code": 6017, - "name": "Frozen", - "msg": "Protocol is frozen" - }, - { - "code": 6018, - "name": "DebuggingOnly", - "msg": "Not allowed when not debugging" - }, - { - "code": 6019, - "name": "ProposalTooLong", - "msg": "Proposal too long" - }, - { - "code": 6020, - "name": "InvalidVotingEpoch", - "msg": "Voting epoch is either too old or hasn't started" - }, - { - "code": 6021, - "name": "ProposalNotActive", - "msg": "Voting hasn't started" - }, - { - "code": 6022, - "name": "NoRemainingAccount", - "msg": "Extra governance account required" - }, - { - "code": 6023, - "name": "Unauthorized", - "msg": "Unauthorized caller" - }, - { - "code": 6024, - "name": "AccountUpgradeFailed", - "msg": "Precondition to upgrade account violated" - }, - { - "code": 6025, - "name": "NotImplemented", - "msg": "Not implemented" - }, - { - "code": 6026, - "name": "PositionSerDe", - "msg": "Error deserializing position" - }, - { - "code": 6027, - "name": "PositionOutOfBounds", - "msg": "Position out of bounds" - }, - { - "code": 6028, - "name": "VoteDuringTransferEpoch", - "msg": "Can't vote during an account's transfer epoch" - }, - { - "code": 6029, - "name": "Other", - "msg": "Other" - } - ] -}; diff --git a/staking/tests/split_vesting_account.ts b/staking/tests/split_vesting_account.ts index 4b15343d..be907a08 100644 --- a/staking/tests/split_vesting_account.ts +++ b/staking/tests/split_vesting_account.ts @@ -136,10 +136,7 @@ describe("split vesting account", async () => { alice.publicKey ); - try { - await pdaConnection.acceptSplit(stakeAccount); - throw Error("This should've failed"); - } catch {} + await pdaConnection.acceptSplit(stakeAccount); }); after(async () => { From 9718000c7cb7a872b0eab4b6feb6f6b9ef8f7233 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 17 Oct 2023 18:24:37 +0100 Subject: [PATCH 26/51] Delete current request --- staking/programs/staking/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index 70b673eb..823aeab8 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -684,6 +684,6 @@ pub mod staking { // Delete current request ctx.accounts.source_stake_account_split_request.amount = 0; - err!(ErrorCode::NotImplemented) + Ok(()) } } From a239ddf9883c558a221850ef99a69ff6537ab3dd Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 17 Oct 2023 18:48:06 +0100 Subject: [PATCH 27/51] add bumps --- staking/programs/staking/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index 823aeab8..f89b88fc 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -572,10 +572,10 @@ pub mod staking { Account<'_, state::stake_account::StakeAccountMetadataV2>, > = &mut ctx.accounts.new_stake_account_metadata; new_stake_account_metadata.initialize( - *ctx.bumps.get("stake_account_metadata").unwrap(), - *ctx.bumps.get("stake_account_custody").unwrap(), - *ctx.bumps.get("custody_authority").unwrap(), - *ctx.bumps.get("voter_record").unwrap(), + *ctx.bumps.get("new_stake_account_metadata").unwrap(), + *ctx.bumps.get("new_stake_account_custody").unwrap(), + *ctx.bumps.get("new_custody_authority").unwrap(), + *ctx.bumps.get("new_voter_record").unwrap(), &split_request.recipient, None, ); From de68014497cfdb8625898452b7a380dff6dd6806 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 17 Oct 2023 19:07:31 +0100 Subject: [PATCH 28/51] Fix bug --- staking/programs/staking/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index f89b88fc..c071a92f 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -627,8 +627,8 @@ pub mod staking { // Split positions source_stake_account_positions.split( new_stake_account_positions, - &mut new_stake_account_metadata.next_index, &mut source_stake_account_metadata.next_index, + &mut new_stake_account_metadata.next_index, remaining_amount, split_request.amount, source_stake_account_custody.amount, From 68219a93bfce540bdee5a65b71abf0116794729b Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 17 Oct 2023 19:09:46 +0100 Subject: [PATCH 29/51] Tests works --- staking/tests/split_vesting_account.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/staking/tests/split_vesting_account.ts b/staking/tests/split_vesting_account.ts index be907a08..eee80703 100644 --- a/staking/tests/split_vesting_account.ts +++ b/staking/tests/split_vesting_account.ts @@ -121,6 +121,8 @@ describe("split vesting account", async () => { }, await samConnection.getTime() ); + + await samConnection.lockAllUnvested(stakeAccount); }); it("request split", async () => { From 5dce63f6001135998fcfef7a890b8bc32411637a Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 17 Oct 2023 19:12:36 +0100 Subject: [PATCH 30/51] Update idls --- staking/target/idl/staking.json | 15 +++++++++++++++ staking/target/types/staking.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/staking/target/idl/staking.json b/staking/target/idl/staking.json index ca2078ff..223e068b 100644 --- a/staking/target/idl/staking.json +++ b/staking/target/idl/staking.json @@ -1807,6 +1807,21 @@ }, { "code": 6029, + "name": "SplitZeroTokens", + "msg": "Can't split 0 tokens from an account" + }, + { + "code": 6030, + "name": "SplitTooManyTokens", + "msg": "Can't split more tokens than are in the account" + }, + { + "code": 6031, + "name": "SanityCheckFailed", + "msg": "Sanity check failed" + }, + { + "code": 6032, "name": "Other", "msg": "Other" } diff --git a/staking/target/types/staking.ts b/staking/target/types/staking.ts index 96eff643..4dfb3a0d 100644 --- a/staking/target/types/staking.ts +++ b/staking/target/types/staking.ts @@ -1807,6 +1807,21 @@ export type Staking = { }, { "code": 6029, + "name": "SplitZeroTokens", + "msg": "Can't split 0 tokens from an account" + }, + { + "code": 6030, + "name": "SplitTooManyTokens", + "msg": "Can't split more tokens than are in the account" + }, + { + "code": 6031, + "name": "SanityCheckFailed", + "msg": "Sanity check failed" + }, + { + "code": 6032, "name": "Other", "msg": "Other" } @@ -3622,6 +3637,21 @@ export const IDL: Staking = { }, { "code": 6029, + "name": "SplitZeroTokens", + "msg": "Can't split 0 tokens from an account" + }, + { + "code": 6030, + "name": "SplitTooManyTokens", + "msg": "Can't split more tokens than are in the account" + }, + { + "code": 6031, + "name": "SanityCheckFailed", + "msg": "Sanity check failed" + }, + { + "code": 6032, "name": "Other", "msg": "Other" } From 90e954cfe62b1cdf6bc2ef7f81d0ef62df95f202 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 17 Oct 2023 19:20:14 +0100 Subject: [PATCH 31/51] Clippy --- staking/programs/staking/src/lib.rs | 11 ++- staking/programs/staking/src/state/vesting.rs | 68 ++++++++----------- 2 files changed, 34 insertions(+), 45 deletions(-) diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index c071a92f..2f101ae6 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -53,10 +53,7 @@ pub mod staking { /// Creates a global config for the program use super::*; - use { - crate::state::stake_account, - state::split_request, - }; + pub fn init_config(ctx: Context, global_config: GlobalConfig) -> Result<()> { let config_account = &mut ctx.accounts.config_account; config_account.bump = *ctx.bumps.get("config_account").unwrap(); @@ -595,7 +592,7 @@ pub mod staking { // Pre-check utils::risk::validate( - &source_stake_account_positions, + source_stake_account_positions, source_stake_account_custody.amount, source_stake_account_metadata.lock.get_unvested_balance( utils::clock::get_current_time(config), @@ -654,7 +651,7 @@ pub mod staking { // Post-check utils::risk::validate( - &source_stake_account_positions, + source_stake_account_positions, ctx.accounts.source_stake_account_custody.amount, ctx.accounts .source_stake_account_metadata @@ -668,7 +665,7 @@ pub mod staking { )?; utils::risk::validate( - &new_stake_account_positions, + new_stake_account_positions, ctx.accounts.new_stake_account_custody.amount, ctx.accounts .new_stake_account_metadata diff --git a/staking/programs/staking/src/state/vesting.rs b/staking/programs/staking/src/state/vesting.rs index 1be04488..392cad6c 100644 --- a/staking/programs/staking/src/state/vesting.rs +++ b/staking/programs/staking/src/state/vesting.rs @@ -258,48 +258,40 @@ impl VestingSchedule { start_date, period_duration, num_periods, - } => { - return Ok(( - VestingSchedule::PeriodicVesting { - initial_balance: ((remaining_amount as u128) * (*initial_balance as u128) - / (total_amount as u128)) - as u64, - start_date: *start_date, - period_duration: *period_duration, - num_periods: *num_periods, - }, - VestingSchedule::PeriodicVesting { - initial_balance: ((transferred_amount as u128) * (*initial_balance as u128) - / (total_amount as u128)) - as u64, - start_date: *start_date, - period_duration: *period_duration, - num_periods: *num_periods, - }, - )); - } + } => Ok(( + VestingSchedule::PeriodicVesting { + initial_balance: ((remaining_amount as u128) * (*initial_balance as u128) + / (total_amount as u128)) as u64, + start_date: *start_date, + period_duration: *period_duration, + num_periods: *num_periods, + }, + VestingSchedule::PeriodicVesting { + initial_balance: ((transferred_amount as u128) * (*initial_balance as u128) + / (total_amount as u128)) as u64, + start_date: *start_date, + period_duration: *period_duration, + num_periods: *num_periods, + }, + )), VestingSchedule::PeriodicVestingAfterListing { initial_balance, period_duration, num_periods, - } => { - return Ok(( - VestingSchedule::PeriodicVestingAfterListing { - initial_balance: ((remaining_amount as u128) * (*initial_balance as u128) - / (total_amount as u128)) - as u64, - period_duration: *period_duration, - num_periods: *num_periods, - }, - VestingSchedule::PeriodicVestingAfterListing { - initial_balance: ((transferred_amount as u128) * (*initial_balance as u128) - / (total_amount as u128)) - as u64, - period_duration: *period_duration, - num_periods: *num_periods, - }, - )); - } + } => Ok(( + VestingSchedule::PeriodicVestingAfterListing { + initial_balance: ((remaining_amount as u128) * (*initial_balance as u128) + / (total_amount as u128)) as u64, + period_duration: *period_duration, + num_periods: *num_periods, + }, + VestingSchedule::PeriodicVestingAfterListing { + initial_balance: ((transferred_amount as u128) * (*initial_balance as u128) + / (total_amount as u128)) as u64, + period_duration: *period_duration, + num_periods: *num_periods, + }, + )), } } } From 6a15e5db569f58975ef010ce51485d91ca5bfd9d Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 17 Oct 2023 19:21:35 +0100 Subject: [PATCH 32/51] Clippy --- staking/programs/staking/src/state/positions.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/staking/programs/staking/src/state/positions.rs b/staking/programs/staking/src/state/positions.rs index 3d9e8d70..8917b7e9 100644 --- a/staking/programs/staking/src/state/positions.rs +++ b/staking/programs/staking/src/state/positions.rs @@ -1,5 +1,4 @@ use { - super::target, crate::error::ErrorCode, anchor_lang::{ prelude::{ @@ -12,10 +11,7 @@ use { }, }, std::{ - convert::{ - TryFrom, - TryInto, - }, + convert::TryInto, fmt::{ self, Debug, @@ -149,7 +145,7 @@ impl PositionData { let mut excess_governance_exposure = governance_exposure.saturating_sub(remaining_amount); - while (excess_governance_exposure > 0 && *src_next_index > 0) { + while excess_governance_exposure > 0 && *src_next_index > 0 { let index = TryInto::::try_into(*src_next_index - 1) .map_err(|_| ErrorCode::GenericOverflow)?; match self.read_position(index)? { From cbde98feb314e25e9b7234f6de21bc1130954b1a Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 17 Oct 2023 19:41:09 +0100 Subject: [PATCH 33/51] Add actual tests --- staking/programs/staking/src/lib.rs | 2 +- staking/tests/split_vesting_account.ts | 36 +++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index 2f101ae6..8c864b8e 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -574,7 +574,7 @@ pub mod staking { *ctx.bumps.get("new_custody_authority").unwrap(), *ctx.bumps.get("new_voter_record").unwrap(), &split_request.recipient, - None, + Some(current_epoch), ); let new_stake_account_positions = diff --git a/staking/tests/split_vesting_account.ts b/staking/tests/split_vesting_account.ts index eee80703..205f1ea0 100644 --- a/staking/tests/split_vesting_account.ts +++ b/staking/tests/split_vesting_account.ts @@ -13,6 +13,7 @@ import { StakeConnection, PythBalance, VestingAccountState } from "../app"; import { BN, Wallet } from "@project-serum/anchor"; import { assertBalanceMatches } from "./utils/api_utils"; import assert from "assert"; +import { blob } from "stream/consumers"; const ONE_MONTH = new BN(3600 * 24 * 30.5); const portNumber = getPortNumber(path.basename(__filename)); @@ -134,11 +135,44 @@ describe("split vesting account", async () => { let stakeAccount = await samConnection.getMainAccount(sam.publicKey); await samConnection.requestSplit( stakeAccount, - PythBalance.fromString("50"), + PythBalance.fromString("33"), alice.publicKey ); await pdaConnection.acceptSplit(stakeAccount); + + let sourceStakeAccount = await samConnection.getMainAccount(sam.publicKey); + let newStakeAccount = await samConnection.getMainAccount(alice.publicKey); + + assert( + VestingAccountState.UnvestedTokensFullyLocked == + sourceStakeAccount.getVestingAccountState(await samConnection.getTime()) + ); + await assertBalanceMatches( + samConnection, + sam.publicKey, + { + unvested: { + locking: PythBalance.fromString("67"), + }, + }, + await samConnection.getTime() + ); + + assert( + VestingAccountState.UnvestedTokensFullyLocked == + newStakeAccount.getVestingAccountState(await samConnection.getTime()) + ); + await assertBalanceMatches( + samConnection, + alice.publicKey, + { + unvested: { + locking: PythBalance.fromString("33"), + }, + }, + await samConnection.getTime() + ); }); after(async () => { From 129aca70d6012d4f1fc5350c7823d87501191acd Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 17 Oct 2023 19:45:34 +0100 Subject: [PATCH 34/51] Clippy --- staking/programs/staking/src/lib.rs | 1 + staking/programs/staking/src/state/positions.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index 8c864b8e..f1e94b40 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -2,6 +2,7 @@ #![allow(dead_code)] #![allow(clippy::upper_case_acronyms)] #![allow(clippy::result_large_err)] +#![allow(clippy::too_many_arguments)] // Objects of type Result must be used, otherwise we might // call a function that returns a Result and not handle the error diff --git a/staking/programs/staking/src/state/positions.rs b/staking/programs/staking/src/state/positions.rs index 8917b7e9..466994fa 100644 --- a/staking/programs/staking/src/state/positions.rs +++ b/staking/programs/staking/src/state/positions.rs @@ -112,7 +112,7 @@ impl PositionData { } } } - return Ok(exposure); + Ok(exposure) } pub fn split( @@ -200,7 +200,7 @@ impl PositionData { } - return Ok(()); + Ok(()) } } From dd22f6ea8a7dc9ffdeba8cd505819aeadc998225 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 24 Oct 2023 16:47:09 +0100 Subject: [PATCH 35/51] Cleanup --- staking/programs/staking/src/lib.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index 20db78e2..b3b34e97 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -116,9 +116,7 @@ pub mod staking { let config = &ctx.accounts.config; config.check_frozen()?; - let stake_account_metadata: &mut Box< - Account<'_, state::stake_account::StakeAccountMetadataV2>, - > = &mut ctx.accounts.stake_account_metadata; + let stake_account_metadata = &mut ctx.accounts.stake_account_metadata; stake_account_metadata.initialize( *ctx.bumps.get("stake_account_metadata").unwrap(), *ctx.bumps.get("stake_account_custody").unwrap(), @@ -574,9 +572,7 @@ pub mod staking { let split_request = &ctx.accounts.source_stake_account_split_request; // Initialize new accounts - let new_stake_account_metadata: &mut Box< - Account<'_, state::stake_account::StakeAccountMetadataV2>, - > = &mut ctx.accounts.new_stake_account_metadata; + let new_stake_account_metadata = &mut ctx.accounts.new_stake_account_metadata; new_stake_account_metadata.initialize( *ctx.bumps.get("new_stake_account_metadata").unwrap(), *ctx.bumps.get("new_stake_account_custody").unwrap(), From 1138fbc603f33bde9554463672ed830712f08ddd Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 24 Oct 2023 18:49:57 +0100 Subject: [PATCH 36/51] Fix tests --- staking/tests/split_vesting_account.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/staking/tests/split_vesting_account.ts b/staking/tests/split_vesting_account.ts index 205f1ea0..a469011f 100644 --- a/staking/tests/split_vesting_account.ts +++ b/staking/tests/split_vesting_account.ts @@ -94,6 +94,11 @@ describe("split vesting account", async () => { } ); + await samConnection.withJoinDaoLlc( + transaction.instructions, + stakeAccountKeypair.publicKey + ); + transaction.instructions.push( await samConnection.buildTransferInstruction( stakeAccountKeypair.publicKey, From a75183dda7db444ba4ec493f49ee7d4d78248483 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Wed, 8 Nov 2023 09:03:52 -0800 Subject: [PATCH 37/51] Split accounts test (#258) * vesting tests * more tests * better invariant tests --- staking/programs/staking/src/error.rs | 7 +- staking/programs/staking/src/lib.rs | 20 +- .../programs/staking/src/state/positions.rs | 89 --------- staking/programs/staking/src/state/vesting.rs | 184 +++++++++++++++++- staking/target/idl/staking.json | 5 + staking/target/types/staking.ts | 10 + 6 files changed, 210 insertions(+), 105 deletions(-) diff --git a/staking/programs/staking/src/error.rs b/staking/programs/staking/src/error.rs index e4b59393..95e21b44 100644 --- a/staking/programs/staking/src/error.rs +++ b/staking/programs/staking/src/error.rs @@ -69,8 +69,11 @@ pub enum ErrorCode { SplitZeroTokens, #[msg("Can't split more tokens than are in the account")] // 6032 SplitTooManyTokens, - #[msg("Sanity check failed")] // 6033 + #[msg("Can't split a token account with staking positions. Unstake your tokens first.")] + // 6033 + SplitWithStake, + #[msg("Sanity check failed")] // 6034 SanityCheckFailed, - #[msg("Other")] //6031 + #[msg("Other")] //6035 Other, } diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index b3b34e97..2d384858 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -607,6 +607,14 @@ pub mod staking { config.unlocking_duration, )?; + // Check that there aren't any positions (i.e., staked tokens) in the source account. + // This check allows us to create an empty positions account on behalf of the recipient and + // not worry about moving positions from the source account to the new account. + require!( + source_stake_account_metadata.next_index == 0, + ErrorCode::SplitWithStake + ); + require!(split_request.amount > 0, ErrorCode::SplitZeroTokens); require!( split_request.amount < source_stake_account_custody.amount, @@ -626,18 +634,6 @@ pub mod staking { source_stake_account_metadata.set_lock(source_vesting_account); new_stake_account_metadata.set_lock(new_vesting_account); - // Split positions - source_stake_account_positions.split( - new_stake_account_positions, - &mut source_stake_account_metadata.next_index, - &mut new_stake_account_metadata.next_index, - remaining_amount, - split_request.amount, - source_stake_account_custody.amount, - current_epoch, - config.unlocking_duration, - )?; - { transfer( diff --git a/staking/programs/staking/src/state/positions.rs b/staking/programs/staking/src/state/positions.rs index 466994fa..0c9ea672 100644 --- a/staking/programs/staking/src/state/positions.rs +++ b/staking/programs/staking/src/state/positions.rs @@ -114,94 +114,6 @@ impl PositionData { } Ok(exposure) } - - pub fn split( - &mut self, - dest_position_data: &mut PositionData, - src_next_index: &mut u8, - dest_next_index: &mut u8, - remaining_amount: u64, - transferred_amount: u64, - total_amount: u64, - current_epoch: u64, - unlocking_duration: u8, - ) -> Result<()> { - require!( - transferred_amount - .checked_add(remaining_amount) - .ok_or(ErrorCode::Other)? - == total_amount, - ErrorCode::SanityCheckFailed - ); - let governance_exposure = - self.get_target_exposure(&Target::VOTING, current_epoch, unlocking_duration)?; - require!( - governance_exposure <= total_amount, - ErrorCode::SanityCheckFailed - ); - - if remaining_amount < governance_exposure { - // We need to transfer some positions over to the new account - let mut excess_governance_exposure = - governance_exposure.saturating_sub(remaining_amount); - - while excess_governance_exposure > 0 && *src_next_index > 0 { - let index = TryInto::::try_into(*src_next_index - 1) - .map_err(|_| ErrorCode::GenericOverflow)?; - match self.read_position(index)? { - Some(position) => { - match position.get_current_position(current_epoch, unlocking_duration)? { - PositionState::UNLOCKED => self.make_none(index, src_next_index)?, - PositionState::LOCKING - | PositionState::LOCKED - | PositionState::PREUNLOCKING - | PositionState::UNLOCKING => { - if excess_governance_exposure < position.amount { - // We need to split the position - self.write_position( - index, - &Position { - amount: position - .amount - .saturating_sub(excess_governance_exposure), - ..position - }, - )?; - - let new_position = Position { - amount: excess_governance_exposure, - ..position - }; - - let new_index = - dest_position_data.reserve_new_index(dest_next_index)?; - dest_position_data.write_position(new_index, &new_position)?; - - excess_governance_exposure = 0; - } else { - // We need to transfer the whole position - let new_index = - dest_position_data.reserve_new_index(dest_next_index)?; - dest_position_data.write_position(new_index, &position)?; - - self.make_none(index, src_next_index)?; - excess_governance_exposure = - excess_governance_exposure.saturating_sub(position.amount); - } - } - } - } - None => { - // This should never happen - return Err(error!(ErrorCode::SanityCheckFailed)); - } - } - } - } - - - Ok(()) - } } pub trait TryBorsh { @@ -526,7 +438,6 @@ pub mod tests { } } - #[quickcheck] fn prop(input: Vec) -> bool { let mut position_data = PositionData::default(); diff --git a/staking/programs/staking/src/state/vesting.rs b/staking/programs/staking/src/state/vesting.rs index a9fb9898..29e2b7f0 100644 --- a/staking/programs/staking/src/state/vesting.rs +++ b/staking/programs/staking/src/state/vesting.rs @@ -20,7 +20,7 @@ use { /// Represents how a given initial balance vests over time /// It is unit-less, but units must be consistent #[repr(u8)] -#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy, BorshSchema)] +#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy, BorshSchema, PartialEq)] pub enum VestingSchedule { /// No vesting, i.e. balance is fully vested at all time FullyVested, @@ -247,6 +247,10 @@ impl VestingSchedule { == total_amount, ErrorCode::SanityCheckFailed ); + // Note that the arithmetic below may lose precision. The calculations round down + // the number of vesting tokens for both of the new accounts, which means that splitting + // may vest some dust (1 of the smallest decimal point) of PYTH for both the source and + // destination accounts. match self { VestingSchedule::FullyVested => { Ok((VestingSchedule::FullyVested, VestingSchedule::FullyVested)) @@ -299,8 +303,14 @@ pub mod tests { use { crate::state::vesting::{ VestingEvent, - VestingSchedule, + VestingSchedule::{ + self, + PeriodicVesting, + PeriodicVestingAfterListing, + }, }, + quickcheck::TestResult, + quickcheck_macros::quickcheck, std::convert::TryInto, }; @@ -629,4 +639,174 @@ pub mod tests { None ); } + + const START_TIMESTAMP: i64 = 10; + const PERIOD_DURATION: u64 = 5; + const NUM_PERIODS: u64 = 4; + + #[quickcheck] + fn test_split_props(transferred: u64, total: u64, initial_balance: u64) -> TestResult { + if transferred > total || total == 0 { + return TestResult::discard(); + } + let received = total - transferred; + + let schedule = VestingSchedule::FullyVested; + let (remaining_schedule, transferred_schedule) = schedule + .split_vesting_schedule(received, transferred, total) + .unwrap(); + + assert_eq!(remaining_schedule, VestingSchedule::FullyVested); + assert_eq!(transferred_schedule, VestingSchedule::FullyVested); + + let schedule = PeriodicVesting { + initial_balance, + // all of these fields should be preserved in the result + start_date: START_TIMESTAMP, + period_duration: PERIOD_DURATION, + num_periods: NUM_PERIODS, + }; + let (remaining_schedule, transferred_schedule) = schedule + .split_vesting_schedule(received, transferred, total) + .unwrap(); + + match (remaining_schedule, transferred_schedule) { + ( + PeriodicVesting { + initial_balance: r, .. + }, + PeriodicVesting { + initial_balance: t, .. + }, + ) => { + let sum = r + t; + assert!(initial_balance.saturating_sub(2) <= sum && sum <= initial_balance); + } + _ => { + panic!("Test failed"); + } + } + + let schedule = PeriodicVestingAfterListing { + initial_balance, + // all of these fields should be preserved in the result + period_duration: PERIOD_DURATION, + num_periods: NUM_PERIODS, + }; + let (remaining_schedule, transferred_schedule) = schedule + .split_vesting_schedule(received, transferred, total) + .unwrap(); + + match (remaining_schedule, transferred_schedule) { + ( + PeriodicVestingAfterListing { + initial_balance: r, .. + }, + PeriodicVestingAfterListing { + initial_balance: t, .. + }, + ) => { + let sum = r + t; + assert!(initial_balance.saturating_sub(2) <= sum && sum <= initial_balance); + } + _ => { + panic!("Test failed"); + } + } + + for timestamp in 0..(START_TIMESTAMP + (PERIOD_DURATION * NUM_PERIODS + 1) as i64) { + let initial_unvested = schedule + .get_unvested_balance(timestamp, Some(START_TIMESTAMP)) + .unwrap(); + let remaining_unvested = remaining_schedule + .get_unvested_balance(timestamp, Some(START_TIMESTAMP)) + .unwrap(); + let transferred_unvested = transferred_schedule + .get_unvested_balance(timestamp, Some(START_TIMESTAMP)) + .unwrap(); + + assert!( + initial_unvested.saturating_sub(2) <= (remaining_unvested + transferred_unvested) + && (remaining_unvested + transferred_unvested) <= initial_unvested + ); + + if initial_unvested <= total { + assert!(transferred_unvested <= transferred); + assert!(remaining_unvested <= received); + } + } + + TestResult::passed() + } + + fn test_split_helper( + transferred: u64, + total: u64, + initial_balance: u64, + expected_remaining: u64, + expected_transferred: u64, + ) { + let schedule = PeriodicVesting { + initial_balance, + // all of these fields should be preserved in the result + start_date: 203, + period_duration: 100, + num_periods: 12, + }; + let (remaining_schedule, transferred_schedule) = schedule + .split_vesting_schedule(total - transferred, transferred, total) + .unwrap(); + + let t = PeriodicVesting { + initial_balance: expected_transferred, + start_date: 203, + period_duration: 100, + num_periods: 12, + }; + let r = PeriodicVesting { + initial_balance: expected_remaining, + start_date: 203, + period_duration: 100, + num_periods: 12, + }; + + assert_eq!(remaining_schedule, r); + assert_eq!(transferred_schedule, t); + + let schedule = PeriodicVestingAfterListing { + initial_balance, + period_duration: 100, + num_periods: 12, + }; + let (remaining_schedule, transferred_schedule) = schedule + .split_vesting_schedule(total - transferred, transferred, total) + .unwrap(); + + let t = PeriodicVestingAfterListing { + initial_balance: expected_transferred, + period_duration: 100, + num_periods: 12, + }; + let r = PeriodicVestingAfterListing { + initial_balance: expected_remaining, + period_duration: 100, + num_periods: 12, + }; + + assert_eq!(remaining_schedule, r); + assert_eq!(transferred_schedule, t); + } + + #[test] + fn test_split() { + test_split_helper(10, 100, 100, 90, 10); + test_split_helper(10, 1000, 100, 99, 1); + test_split_helper(1, 1000, 100, 99, 0); + + test_split_helper(10, 10, 1000, 0, 1000); + test_split_helper(9, 10, 1000, 100, 900); + test_split_helper(10, 100, 1000, 900, 100); + + test_split_helper(1, 3, 1000, 666, 333); + } } diff --git a/staking/target/idl/staking.json b/staking/target/idl/staking.json index d086ac1a..e1201267 100644 --- a/staking/target/idl/staking.json +++ b/staking/target/idl/staking.json @@ -1916,6 +1916,11 @@ "code": 6034, "name": "Other", "msg": "Other" + }, + { + "code": 6035, + "name": "SplitWithStake", + "msg": "Can't split a token account with staking positions. Unstake your tokens first." } ] } \ No newline at end of file diff --git a/staking/target/types/staking.ts b/staking/target/types/staking.ts index 8fcb4954..b84b6354 100644 --- a/staking/target/types/staking.ts +++ b/staking/target/types/staking.ts @@ -1916,6 +1916,11 @@ export type Staking = { "code": 6034, "name": "Other", "msg": "Other" + }, + { + "code": 6035, + "name": "SplitWithStake", + "msg": "Can't split a token account with staking positions. Unstake your tokens first." } ] }; @@ -3838,6 +3843,11 @@ export const IDL: Staking = { "code": 6034, "name": "Other", "msg": "Other" + }, + { + "code": 6035, + "name": "SplitWithStake", + "msg": "Can't split a token account with staking positions. Unstake your tokens first." } ] }; From 953a31813d971c9972c98ff3c8ae6338d944eca2 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Wed, 8 Nov 2023 10:47:53 -0800 Subject: [PATCH 38/51] minor cleanups --- staking/programs/staking/src/context.rs | 1 + staking/programs/staking/src/error.rs | 4 +- staking/programs/staking/src/lib.rs | 25 +++++++------ staking/programs/staking/src/state/vesting.rs | 37 +++++++------------ 4 files changed, 30 insertions(+), 37 deletions(-) diff --git a/staking/programs/staking/src/context.rs b/staking/programs/staking/src/context.rs index c31d1c16..8c94b2a7 100644 --- a/staking/programs/staking/src/context.rs +++ b/staking/programs/staking/src/context.rs @@ -301,6 +301,7 @@ pub struct RequestSplit<'info> { } #[derive(Accounts)] +#[instruction(amount: u64, recipient: Pubkey)] pub struct AcceptSplit<'info> { // Native payer: #[account(mut, address = config.pda_authority)] diff --git a/staking/programs/staking/src/error.rs b/staking/programs/staking/src/error.rs index 95e21b44..53a383fe 100644 --- a/staking/programs/staking/src/error.rs +++ b/staking/programs/staking/src/error.rs @@ -72,8 +72,8 @@ pub enum ErrorCode { #[msg("Can't split a token account with staking positions. Unstake your tokens first.")] // 6033 SplitWithStake, - #[msg("Sanity check failed")] // 6034 - SanityCheckFailed, + #[msg("The approval arguments do not match the split request.")] // 6034 + InvalidApproval, #[msg("Other")] //6035 Other, } diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index 2d384858..435c4a96 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -562,14 +562,21 @@ pub mod staking { * the config account. If accepted, `amount` tokens are transferred to a new stake account * owned by the `recipient` and the split request is reset (by setting `amount` to 0). * The recipient of a transfer can't vote during the epoch of the transfer. + * + * The `pda_authority` must explicitly approve both the amount of tokens and recipient, and + * these parameters must match the request (in the `split_request` account). */ - pub fn accept_split(ctx: Context) -> Result<()> { + pub fn accept_split(ctx: Context, amount: u64, recipient: Pubkey) -> Result<()> { let config = &ctx.accounts.config; config.check_frozen()?; let current_epoch = get_current_epoch(config)?; let split_request = &ctx.accounts.source_stake_account_split_request; + require!( + split_request.amount == amount && split_request.recipient == recipient, + ErrorCode::InvalidApproval + ); // Initialize new accounts let new_stake_account_metadata = &mut ctx.accounts.new_stake_account_metadata; @@ -616,21 +623,15 @@ pub mod staking { ); require!(split_request.amount > 0, ErrorCode::SplitZeroTokens); - require!( - split_request.amount < source_stake_account_custody.amount, - ErrorCode::SplitTooManyTokens - ); let remaining_amount = source_stake_account_custody .amount - .saturating_sub(split_request.amount); + .checked_sub(split_request.amount) + .ok_or(ErrorCode::SplitTooManyTokens)?; // Split vesting account - let (source_vesting_account, new_vesting_account) = - source_stake_account_metadata.lock.split_vesting_schedule( - remaining_amount, - split_request.amount, - source_stake_account_custody.amount, - )?; + let (source_vesting_account, new_vesting_account) = source_stake_account_metadata + .lock + .split_vesting_schedule(split_request.amount, source_stake_account_custody.amount)?; source_stake_account_metadata.set_lock(source_vesting_account); new_stake_account_metadata.set_lock(new_vesting_account); diff --git a/staking/programs/staking/src/state/vesting.rs b/staking/programs/staking/src/state/vesting.rs index 29e2b7f0..fb9e0b53 100644 --- a/staking/programs/staking/src/state/vesting.rs +++ b/staking/programs/staking/src/state/vesting.rs @@ -236,17 +236,13 @@ impl VestingSchedule { pub fn split_vesting_schedule( &self, - remaining_amount: u64, transferred_amount: u64, total_amount: u64, ) -> Result<(VestingSchedule, VestingSchedule)> { - require!( - transferred_amount - .checked_add(remaining_amount) - .ok_or(ErrorCode::Other)? - == total_amount, - ErrorCode::SanityCheckFailed - ); + let remaining_amount = total_amount + .checked_sub(transferred_amount) + .ok_or(ErrorCode::GenericOverflow)?; + // Note that the arithmetic below may lose precision. The calculations round down // the number of vesting tokens for both of the new accounts, which means that splitting // may vest some dust (1 of the smallest decimal point) of PYTH for both the source and @@ -652,9 +648,8 @@ pub mod tests { let received = total - transferred; let schedule = VestingSchedule::FullyVested; - let (remaining_schedule, transferred_schedule) = schedule - .split_vesting_schedule(received, transferred, total) - .unwrap(); + let (remaining_schedule, transferred_schedule) = + schedule.split_vesting_schedule(transferred, total).unwrap(); assert_eq!(remaining_schedule, VestingSchedule::FullyVested); assert_eq!(transferred_schedule, VestingSchedule::FullyVested); @@ -666,9 +661,8 @@ pub mod tests { period_duration: PERIOD_DURATION, num_periods: NUM_PERIODS, }; - let (remaining_schedule, transferred_schedule) = schedule - .split_vesting_schedule(received, transferred, total) - .unwrap(); + let (remaining_schedule, transferred_schedule) = + schedule.split_vesting_schedule(transferred, total).unwrap(); match (remaining_schedule, transferred_schedule) { ( @@ -693,9 +687,8 @@ pub mod tests { period_duration: PERIOD_DURATION, num_periods: NUM_PERIODS, }; - let (remaining_schedule, transferred_schedule) = schedule - .split_vesting_schedule(received, transferred, total) - .unwrap(); + let (remaining_schedule, transferred_schedule) = + schedule.split_vesting_schedule(transferred, total).unwrap(); match (remaining_schedule, transferred_schedule) { ( @@ -753,9 +746,8 @@ pub mod tests { period_duration: 100, num_periods: 12, }; - let (remaining_schedule, transferred_schedule) = schedule - .split_vesting_schedule(total - transferred, transferred, total) - .unwrap(); + let (remaining_schedule, transferred_schedule) = + schedule.split_vesting_schedule(transferred, total).unwrap(); let t = PeriodicVesting { initial_balance: expected_transferred, @@ -778,9 +770,8 @@ pub mod tests { period_duration: 100, num_periods: 12, }; - let (remaining_schedule, transferred_schedule) = schedule - .split_vesting_schedule(total - transferred, transferred, total) - .unwrap(); + let (remaining_schedule, transferred_schedule) = + schedule.split_vesting_schedule(transferred, total).unwrap(); let t = PeriodicVestingAfterListing { initial_balance: expected_transferred, From 98b6c1b441f5da35404da03eef58cc109a382852 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Wed, 8 Nov 2023 10:53:22 -0800 Subject: [PATCH 39/51] cleanup implementation --- staking/programs/staking/src/lib.rs | 43 +++++++++++++++++------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index 435c4a96..89af2c6f 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -579,8 +579,7 @@ pub mod staking { ); // Initialize new accounts - let new_stake_account_metadata = &mut ctx.accounts.new_stake_account_metadata; - new_stake_account_metadata.initialize( + ctx.accounts.new_stake_account_metadata.initialize( *ctx.bumps.get("new_stake_account_metadata").unwrap(), *ctx.bumps.get("new_stake_account_custody").unwrap(), *ctx.bumps.get("new_custody_authority").unwrap(), @@ -597,19 +596,22 @@ pub mod staking { new_voter_record.initialize(config, &split_request.recipient); // Split off source account - let source_stake_account_custody = &ctx.accounts.source_stake_account_custody; - let source_stake_account_metadata = &mut ctx.accounts.source_stake_account_metadata; + // let source_stake_account_custody = &ctx.accounts.source_stake_account_custody; + // let source_stake_account_metadata = &mut ctx.accounts.source_stake_account_metadata; let source_stake_account_positions = &mut ctx.accounts.source_stake_account_positions.load_mut()?; // Pre-check utils::risk::validate( source_stake_account_positions, - source_stake_account_custody.amount, - source_stake_account_metadata.lock.get_unvested_balance( - utils::clock::get_current_time(config), - config.pyth_token_list_time, - )?, + ctx.accounts.source_stake_account_custody.amount, + ctx.accounts + .source_stake_account_metadata + .lock + .get_unvested_balance( + utils::clock::get_current_time(config), + config.pyth_token_list_time, + )?, current_epoch, config.unlocking_duration, )?; @@ -618,22 +620,27 @@ pub mod staking { // This check allows us to create an empty positions account on behalf of the recipient and // not worry about moving positions from the source account to the new account. require!( - source_stake_account_metadata.next_index == 0, + ctx.accounts.source_stake_account_metadata.next_index == 0, ErrorCode::SplitWithStake ); require!(split_request.amount > 0, ErrorCode::SplitZeroTokens); - let remaining_amount = source_stake_account_custody - .amount - .checked_sub(split_request.amount) - .ok_or(ErrorCode::SplitTooManyTokens)?; // Split vesting account - let (source_vesting_account, new_vesting_account) = source_stake_account_metadata + let (source_vesting_account, new_vesting_account) = ctx + .accounts + .source_stake_account_metadata .lock - .split_vesting_schedule(split_request.amount, source_stake_account_custody.amount)?; - source_stake_account_metadata.set_lock(source_vesting_account); - new_stake_account_metadata.set_lock(new_vesting_account); + .split_vesting_schedule( + split_request.amount, + ctx.accounts.source_stake_account_custody.amount, + )?; + ctx.accounts + .source_stake_account_metadata + .set_lock(source_vesting_account); + ctx.accounts + .new_stake_account_metadata + .set_lock(new_vesting_account); { From 1d1b07fdc928d8ede8241164efa89832d8a09255 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Wed, 8 Nov 2023 10:54:14 -0800 Subject: [PATCH 40/51] idl --- .../programs/staking/src/state/positions.rs | 9 ++-- staking/target/idl/staking.json | 25 +++++++--- staking/target/types/staking.ts | 50 +++++++++++++------ 3 files changed, 54 insertions(+), 30 deletions(-) diff --git a/staking/programs/staking/src/state/positions.rs b/staking/programs/staking/src/state/positions.rs index 0c9ea672..9bda769b 100644 --- a/staking/programs/staking/src/state/positions.rs +++ b/staking/programs/staking/src/state/positions.rs @@ -10,12 +10,9 @@ use { wasm_bindgen, }, }, - std::{ - convert::TryInto, - fmt::{ - self, - Debug, - }, + std::fmt::{ + self, + Debug, }, }; diff --git a/staking/target/idl/staking.json b/staking/target/idl/staking.json index e1201267..a2c76707 100644 --- a/staking/target/idl/staking.json +++ b/staking/target/idl/staking.json @@ -880,7 +880,7 @@ { "name": "acceptSplit", "docs": [ - "* A split request can only be accepted by the `pda_authority`` from\n * the config account. If accepted, `amount` tokens are transferred to a new stake account\n * owned by the `recipient` and the split request is reset (by setting `amount` to 0).\n * The recipient of a transfer can't vote during the epoch of the transfer." + "* A split request can only be accepted by the `pda_authority`` from\n * the config account. If accepted, `amount` tokens are transferred to a new stake account\n * owned by the `recipient` and the split request is reset (by setting `amount` to 0).\n * The recipient of a transfer can't vote during the epoch of the transfer.\n *\n * The `pda_authority` must explicitly approve both the amount of tokens and recipient, and\n * these parameters must match the request (in the `split_request` account)." ], "accounts": [ { @@ -1091,7 +1091,16 @@ "isSigner": false } ], - "args": [] + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "recipient", + "type": "publicKey" + } + ] }, { "name": "joinDaoLlc", @@ -1909,18 +1918,18 @@ }, { "code": 6033, - "name": "SanityCheckFailed", - "msg": "Sanity check failed" + "name": "SplitWithStake", + "msg": "Can't split a token account with staking positions. Unstake your tokens first." }, { "code": 6034, - "name": "Other", - "msg": "Other" + "name": "InvalidApproval", + "msg": "The approval arguments do not match the split request." }, { "code": 6035, - "name": "SplitWithStake", - "msg": "Can't split a token account with staking positions. Unstake your tokens first." + "name": "Other", + "msg": "Other" } ] } \ No newline at end of file diff --git a/staking/target/types/staking.ts b/staking/target/types/staking.ts index b84b6354..081bc997 100644 --- a/staking/target/types/staking.ts +++ b/staking/target/types/staking.ts @@ -880,7 +880,7 @@ export type Staking = { { "name": "acceptSplit", "docs": [ - "* A split request can only be accepted by the `pda_authority`` from\n * the config account. If accepted, `amount` tokens are transferred to a new stake account\n * owned by the `recipient` and the split request is reset (by setting `amount` to 0).\n * The recipient of a transfer can't vote during the epoch of the transfer." + "* A split request can only be accepted by the `pda_authority`` from\n * the config account. If accepted, `amount` tokens are transferred to a new stake account\n * owned by the `recipient` and the split request is reset (by setting `amount` to 0).\n * The recipient of a transfer can't vote during the epoch of the transfer.\n *\n * The `pda_authority` must explicitly approve both the amount of tokens and recipient, and\n * these parameters must match the request (in the `split_request` account)." ], "accounts": [ { @@ -1091,7 +1091,16 @@ export type Staking = { "isSigner": false } ], - "args": [] + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "recipient", + "type": "publicKey" + } + ] }, { "name": "joinDaoLlc", @@ -1909,18 +1918,18 @@ export type Staking = { }, { "code": 6033, - "name": "SanityCheckFailed", - "msg": "Sanity check failed" + "name": "SplitWithStake", + "msg": "Can't split a token account with staking positions. Unstake your tokens first." }, { "code": 6034, - "name": "Other", - "msg": "Other" + "name": "InvalidApproval", + "msg": "The approval arguments do not match the split request." }, { "code": 6035, - "name": "SplitWithStake", - "msg": "Can't split a token account with staking positions. Unstake your tokens first." + "name": "Other", + "msg": "Other" } ] }; @@ -2807,7 +2816,7 @@ export const IDL: Staking = { { "name": "acceptSplit", "docs": [ - "* A split request can only be accepted by the `pda_authority`` from\n * the config account. If accepted, `amount` tokens are transferred to a new stake account\n * owned by the `recipient` and the split request is reset (by setting `amount` to 0).\n * The recipient of a transfer can't vote during the epoch of the transfer." + "* A split request can only be accepted by the `pda_authority`` from\n * the config account. If accepted, `amount` tokens are transferred to a new stake account\n * owned by the `recipient` and the split request is reset (by setting `amount` to 0).\n * The recipient of a transfer can't vote during the epoch of the transfer.\n *\n * The `pda_authority` must explicitly approve both the amount of tokens and recipient, and\n * these parameters must match the request (in the `split_request` account)." ], "accounts": [ { @@ -3018,7 +3027,16 @@ export const IDL: Staking = { "isSigner": false } ], - "args": [] + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "recipient", + "type": "publicKey" + } + ] }, { "name": "joinDaoLlc", @@ -3836,18 +3854,18 @@ export const IDL: Staking = { }, { "code": 6033, - "name": "SanityCheckFailed", - "msg": "Sanity check failed" + "name": "SplitWithStake", + "msg": "Can't split a token account with staking positions. Unstake your tokens first." }, { "code": 6034, - "name": "Other", - "msg": "Other" + "name": "InvalidApproval", + "msg": "The approval arguments do not match the split request." }, { "code": 6035, - "name": "SplitWithStake", - "msg": "Can't split a token account with staking positions. Unstake your tokens first." + "name": "Other", + "msg": "Other" } ] }; From 8dbef967be431053c880bfeac524b0bae6d470f8 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Wed, 8 Nov 2023 14:10:14 -0800 Subject: [PATCH 41/51] ok fix the ts tests --- staking/app/StakeConnection.ts | 8 ++++++-- staking/tests/split_vesting_account.ts | 6 +++++- staking/tests/utils/before.ts | 19 +++++++++++++++---- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/staking/app/StakeConnection.ts b/staking/app/StakeConnection.ts index ffb73275..e0657630 100644 --- a/staking/app/StakeConnection.ts +++ b/staking/app/StakeConnection.ts @@ -857,7 +857,11 @@ export class StakeConnection { .rpc(); } - public async acceptSplit(stakeAccount: StakeAccount) { + public async acceptSplit( + stakeAccount: StakeAccount, + amount: PythBalance, + recipient: PublicKey + ) { const newStakeAccountKeypair = new Keypair(); const instructions = []; @@ -869,7 +873,7 @@ export class StakeConnection { ); await this.program.methods - .acceptSplit() + .acceptSplit(amount.toBN(), recipient) .accounts({ sourceStakeAccountPositions: stakeAccount.address, newStakeAccountPositions: newStakeAccountKeypair.publicKey, diff --git a/staking/tests/split_vesting_account.ts b/staking/tests/split_vesting_account.ts index a469011f..e9da661d 100644 --- a/staking/tests/split_vesting_account.ts +++ b/staking/tests/split_vesting_account.ts @@ -144,7 +144,11 @@ describe("split vesting account", async () => { alice.publicKey ); - await pdaConnection.acceptSplit(stakeAccount); + await pdaConnection.acceptSplit( + stakeAccount, + PythBalance.fromString("33"), + alice.publicKey + ); let sourceStakeAccount = await samConnection.getMainAccount(sam.publicKey); let newStakeAccount = await samConnection.getMainAccount(alice.publicKey); diff --git a/staking/tests/utils/before.ts b/staking/tests/utils/before.ts index 0823d215..cca5da40 100644 --- a/staking/tests/utils/before.ts +++ b/staking/tests/utils/before.ts @@ -133,12 +133,23 @@ export async function startValidatorRaw(portNumber: number, otherArgs: string) { ); const controller = new CustomAbortController(internalController); + let numRetries = 0; while (true) { try { await new Promise((resolve) => setTimeout(resolve, 1000)); - await connection.getEpochInfo(); + await connection.getSlot(); break; - } catch (e) {} + } catch (e) { + // Bound the number of retries so the tests don't hang if there's some problem blocking + // the connection to the validator. + if (numRetries == 10) { + console.log( + `Failed to start validator or connect to running validator. Caught exception: ${e}` + ); + throw e; + } + numRetries += 1; + } } return { controller, connection }; } @@ -193,7 +204,7 @@ export async function startValidator(portNumber: number, config: AnchorConfig) { export function getConnection(portNumber: number): Connection { return new Connection( - `http://localhost:${portNumber}`, + `http://127.0.0.1:${portNumber}`, AnchorProvider.defaultOptions().commitment ); } @@ -482,7 +493,7 @@ export async function standardSetup( .rpc(); const connection = new Connection( - `http://localhost:${portNumber}`, + `http://127.0.0.1:${portNumber}`, AnchorProvider.defaultOptions().commitment ); From 623cab903d3ae2111145aea1141eb7db82a7f574 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Thu, 9 Nov 2023 09:36:22 -0800 Subject: [PATCH 42/51] refactor --- staking/app/StakeConnection.ts | 21 ++- staking/package.json | 2 +- staking/tests/split_vesting_account.ts | 191 +++++++++++++++++++++---- 3 files changed, 184 insertions(+), 30 deletions(-) diff --git a/staking/app/StakeConnection.ts b/staking/app/StakeConnection.ts index e0657630..05234e1d 100644 --- a/staking/app/StakeConnection.ts +++ b/staking/app/StakeConnection.ts @@ -123,6 +123,11 @@ export class StakeConnection { ); } + /** The public key of the user of the staking program. This connection sends transactions as this user. */ + public userPublicKey(): PublicKey { + return this.provider.wallet.publicKey; + } + public async getAllStakeAccountAddresses(): Promise { // Use the raw web3.js connection so that anchor doesn't try to borsh deserialize the zero-copy serialized account const allAccts = await this.provider.connection.getProgramAccounts( @@ -530,6 +535,19 @@ export class StakeConnection { * Locks all unvested tokens in governance */ public async lockAllUnvested(stakeAccount: StakeAccount) { + const balanceSummary = stakeAccount.getBalanceSummary(await this.getTime()); + + await this.lockTokens(stakeAccount, balanceSummary.unvested.unlocked); + } + + /** + * Locks all unvested tokens in governance + */ + public async lockTokens(stakeAccount: StakeAccount, amount: PythBalance) { + if (amount.isZero()) { + return; + } + const vestingAccountState = stakeAccount.getVestingAccountState( await this.getTime() ); @@ -541,8 +559,7 @@ export class StakeConnection { throw Error(`Unexpected account state ${vestingAccountState}`); } const owner: PublicKey = stakeAccount.stakeAccountMetadata.owner; - const balanceSummary = stakeAccount.getBalanceSummary(await this.getTime()); - const amountBN = balanceSummary.unvested.unlocked.toBN(); + const amountBN = amount.toBN(); const transaction: Transaction = new Transaction(); diff --git a/staking/package.json b/staking/package.json index a18d30b6..4b0a76db 100644 --- a/staking/package.json +++ b/staking/package.json @@ -30,7 +30,7 @@ "wasm-pack": "^0.10.2" }, "scripts": { - "test": "npm run build_wasm && anchor build -- --features mock-clock && npm run dump_governance && ts-mocha --parallel -p ./tsconfig.json -t 1000000 tests/*.ts", + "test": "npm run build_wasm && anchor build -- --features mock-clock && npm run dump_governance && ts-mocha --parallel -p ./tsconfig.json -t 1000000 tests/split_vesting_account.ts", "build": "npm run build_wasm && tsc -p tsconfig.api.json", "build_wasm": "./scripts/build_wasm.sh", "localnet": "anchor build && npm run dump_governance && ts-node ./app/scripts/localnet.ts", diff --git a/staking/tests/split_vesting_account.ts b/staking/tests/split_vesting_account.ts index e9da661d..2893b658 100644 --- a/staking/tests/split_vesting_account.ts +++ b/staking/tests/split_vesting_account.ts @@ -11,9 +11,13 @@ import path from "path"; import { Keypair, PublicKey, Transaction } from "@solana/web3.js"; import { StakeConnection, PythBalance, VestingAccountState } from "../app"; import { BN, Wallet } from "@project-serum/anchor"; -import { assertBalanceMatches } from "./utils/api_utils"; +import { + assertBalanceMatches, + OptionalBalanceSummary, +} from "./utils/api_utils"; import assert from "assert"; import { blob } from "stream/consumers"; +import { Key } from "@metaplex-foundation/mpl-token-metadata"; const ONE_MONTH = new BN(3600 * 24 * 30.5); const portNumber = getPortNumber(path.basename(__filename)); @@ -31,11 +35,6 @@ describe("split vesting account", async () => { let pdaAuthority = new Keypair(); let pdaConnection: StakeConnection; - let sam = new Keypair(); - let samConnection: StakeConnection; - - let alice = new Keypair(); - before(async () => { const config = readAnchorConfig(ANCHOR_CONFIG_PATH); ({ controller, stakeConnection } = await standardSetup( @@ -53,19 +52,128 @@ describe("split vesting account", async () => { EPOCH_DURATION = stakeConnection.config.epochDuration; owner = stakeConnection.provider.wallet.publicKey; - samConnection = await StakeConnection.createStakeConnection( + pdaConnection = await connect(pdaAuthority); + }); + + /** Create a stake connection for a keypair and airdrop the key some SOL so it can send transactions. */ + async function connect(keypair: Keypair): Promise { + let connection = await StakeConnection.createStakeConnection( stakeConnection.provider.connection, - new Wallet(sam), + new Wallet(keypair), stakeConnection.program.programId ); - pdaConnection = await StakeConnection.createStakeConnection( - stakeConnection.provider.connection, - new Wallet(pdaAuthority), - stakeConnection.program.programId + await connection.provider.connection.requestAirdrop( + keypair.publicKey, + 1_000_000_000_000 + ); + + return connection; + } + + async function setupSplit( + totalBalance: string, + vestingInitialBalance: string, + lockedBalance: string + ): Promise<[StakeConnection, StakeConnection]> { + let samConnection = await connect(new Keypair()); + + await samConnection.provider.connection.requestAirdrop( + samConnection.userPublicKey(), + 1_000_000_000_000 + ); + await requestPythAirdrop( + samConnection.userPublicKey(), + pythMintAccount.publicKey, + pythMintAuthority, + PythBalance.fromString(totalBalance), + samConnection.provider.connection + ); + + const transaction = new Transaction(); + + const stakeAccountKeypair = await samConnection.withCreateAccount( + transaction.instructions, + samConnection.userPublicKey(), + { + periodicVesting: { + initialBalance: PythBalance.fromString(vestingInitialBalance).toBN(), + startDate: await stakeConnection.getTime(), + periodDuration: ONE_MONTH, + numPeriods: new BN(72), + }, + } + ); + + await samConnection.withJoinDaoLlc( + transaction.instructions, + stakeAccountKeypair.publicKey + ); + + transaction.instructions.push( + await samConnection.buildTransferInstruction( + stakeAccountKeypair.publicKey, + PythBalance.fromString(totalBalance).toBN() + ) + ); + + console.log("Create stake account"); + await samConnection.provider.sendAndConfirm( + transaction, + [stakeAccountKeypair], + { skipPreflight: true } ); - }); + let stakeAccount = await samConnection.getMainAccount( + samConnection.userPublicKey() + ); + assert( + VestingAccountState.UnvestedTokensFullyUnlocked == + stakeAccount.getVestingAccountState(await samConnection.getTime()) + ); + await assertBalanceMatches( + samConnection, + samConnection.userPublicKey(), + { + unvested: { + unlocked: PythBalance.fromString(vestingInitialBalance), + }, + }, + await samConnection.getTime() + ); + + console.log("lockTokens"); + await samConnection.lockTokens( + stakeAccount, + PythBalance.fromString(lockedBalance) + ); + + let aliceConnection = await connect(new Keypair()); + return [samConnection, aliceConnection]; + } + + async function assertMainAccountBalance( + samConnection: StakeConnection, + expectedState: VestingAccountState, + expectedBalance: OptionalBalanceSummary + ) { + let sourceStakeAccount = await samConnection.getMainAccount( + samConnection.userPublicKey() + ); + + assert( + expectedState == + sourceStakeAccount.getVestingAccountState(await samConnection.getTime()) + ); + await assertBalanceMatches( + samConnection, + samConnection.userPublicKey(), + expectedBalance, + await samConnection.getTime() + ); + } + + /* it("create a vesting account", async () => { await samConnection.provider.connection.requestAirdrop( sam.publicKey, @@ -130,58 +238,87 @@ describe("split vesting account", async () => { await samConnection.lockAllUnvested(stakeAccount); }); + */ it("request split", async () => { - await pdaConnection.provider.connection.requestAirdrop( - pdaAuthority.publicKey, - 1_000_000_000_000 + console.log("setup split"); + let [samConnection, aliceConnection] = await setupSplit("100", "100", "0"); + + console.log("requestSplit"); + + let stakeAccount = await samConnection.getMainAccount( + samConnection.userPublicKey() ); - let stakeAccount = await samConnection.getMainAccount(sam.publicKey); await samConnection.requestSplit( stakeAccount, PythBalance.fromString("33"), - alice.publicKey + aliceConnection.userPublicKey() ); + console.log("acceptSplit"); + await pdaConnection.acceptSplit( stakeAccount, PythBalance.fromString("33"), - alice.publicKey + aliceConnection.userPublicKey() + ); + + console.log("testing"); + + await assertMainAccountBalance( + samConnection, + VestingAccountState.UnvestedTokensFullyUnlocked, + { + unvested: { + unlocked: PythBalance.fromString("67"), + }, + } + ); + await assertMainAccountBalance( + aliceConnection, + VestingAccountState.UnvestedTokensFullyUnlocked, + { + unvested: { + unlocked: PythBalance.fromString("33"), + }, + } ); - let sourceStakeAccount = await samConnection.getMainAccount(sam.publicKey); - let newStakeAccount = await samConnection.getMainAccount(alice.publicKey); + /* + let sourceStakeAccount = await samConnection.getMainAccount(samConnection.userPublicKey()); + let newStakeAccount = await samConnection.getMainAccount(aliceConnection.userPublicKey()); assert( - VestingAccountState.UnvestedTokensFullyLocked == + VestingAccountState.UnvestedTokensFullyUnlocked == sourceStakeAccount.getVestingAccountState(await samConnection.getTime()) ); await assertBalanceMatches( samConnection, - sam.publicKey, + samConnection.userPublicKey(), { unvested: { - locking: PythBalance.fromString("67"), + unlocked: PythBalance.fromString("67"), }, }, await samConnection.getTime() ); assert( - VestingAccountState.UnvestedTokensFullyLocked == + VestingAccountState.UnvestedTokensFullyUnlocked == newStakeAccount.getVestingAccountState(await samConnection.getTime()) ); await assertBalanceMatches( samConnection, - alice.publicKey, + aliceConnection.userPublicKey(), { unvested: { - locking: PythBalance.fromString("33"), + unlocked: PythBalance.fromString("33"), }, }, await samConnection.getTime() ); + */ }); after(async () => { From faf9b2fd8b617e824dc22ac1056ea176ad9b405d Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Thu, 9 Nov 2023 09:36:36 -0800 Subject: [PATCH 43/51] refactor --- staking/tests/split_vesting_account.ts | 37 +------------------------- 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/staking/tests/split_vesting_account.ts b/staking/tests/split_vesting_account.ts index 2893b658..6f3aceb2 100644 --- a/staking/tests/split_vesting_account.ts +++ b/staking/tests/split_vesting_account.ts @@ -280,45 +280,10 @@ describe("split vesting account", async () => { VestingAccountState.UnvestedTokensFullyUnlocked, { unvested: { - unlocked: PythBalance.fromString("33"), + unlocked: PythBalance.fromString("32"), }, } ); - - /* - let sourceStakeAccount = await samConnection.getMainAccount(samConnection.userPublicKey()); - let newStakeAccount = await samConnection.getMainAccount(aliceConnection.userPublicKey()); - - assert( - VestingAccountState.UnvestedTokensFullyUnlocked == - sourceStakeAccount.getVestingAccountState(await samConnection.getTime()) - ); - await assertBalanceMatches( - samConnection, - samConnection.userPublicKey(), - { - unvested: { - unlocked: PythBalance.fromString("67"), - }, - }, - await samConnection.getTime() - ); - - assert( - VestingAccountState.UnvestedTokensFullyUnlocked == - newStakeAccount.getVestingAccountState(await samConnection.getTime()) - ); - await assertBalanceMatches( - samConnection, - aliceConnection.userPublicKey(), - { - unvested: { - unlocked: PythBalance.fromString("33"), - }, - }, - await samConnection.getTime() - ); - */ }); after(async () => { From 00466b365036a4ce8d623bb36b060e4fde2957c5 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Thu, 9 Nov 2023 09:57:21 -0800 Subject: [PATCH 44/51] fix --- staking/package.json | 2 +- staking/tests/split_vesting_account.ts | 117 ++++++++++++++++++++++--- 2 files changed, 106 insertions(+), 13 deletions(-) diff --git a/staking/package.json b/staking/package.json index 4b0a76db..a18d30b6 100644 --- a/staking/package.json +++ b/staking/package.json @@ -30,7 +30,7 @@ "wasm-pack": "^0.10.2" }, "scripts": { - "test": "npm run build_wasm && anchor build -- --features mock-clock && npm run dump_governance && ts-mocha --parallel -p ./tsconfig.json -t 1000000 tests/split_vesting_account.ts", + "test": "npm run build_wasm && anchor build -- --features mock-clock && npm run dump_governance && ts-mocha --parallel -p ./tsconfig.json -t 1000000 tests/*.ts", "build": "npm run build_wasm && tsc -p tsconfig.api.json", "build_wasm": "./scripts/build_wasm.sh", "localnet": "anchor build && npm run dump_governance && ts-node ./app/scripts/localnet.ts", diff --git a/staking/tests/split_vesting_account.ts b/staking/tests/split_vesting_account.ts index 6f3aceb2..6033ff30 100644 --- a/staking/tests/split_vesting_account.ts +++ b/staking/tests/split_vesting_account.ts @@ -10,7 +10,7 @@ import { import path from "path"; import { Keypair, PublicKey, Transaction } from "@solana/web3.js"; import { StakeConnection, PythBalance, VestingAccountState } from "../app"; -import { BN, Wallet } from "@project-serum/anchor"; +import { ErrorCode, BN, Wallet, AnchorError } from "@project-serum/anchor"; import { assertBalanceMatches, OptionalBalanceSummary, @@ -117,7 +117,6 @@ describe("split vesting account", async () => { ) ); - console.log("Create stake account"); await samConnection.provider.sendAndConfirm( transaction, [stakeAccountKeypair], @@ -142,7 +141,6 @@ describe("split vesting account", async () => { await samConnection.getTime() ); - console.log("lockTokens"); await samConnection.lockTokens( stakeAccount, PythBalance.fromString(lockedBalance) @@ -173,6 +171,26 @@ describe("split vesting account", async () => { ); } + async function assertFailsWithErrorCode( + thunk: () => Promise, + errorCode: string + ) { + let actualErrorCode: string | undefined = undefined; + try { + await thunk(); + } catch (err) { + if (err instanceof AnchorError) { + actualErrorCode = err.error.errorCode.code; + } + } + + assert.equal( + actualErrorCode, + errorCode, + `Call did not fail with the expected error code.` + ); + } + /* it("create a vesting account", async () => { await samConnection.provider.connection.requestAirdrop( @@ -240,12 +258,9 @@ describe("split vesting account", async () => { }); */ - it("request split", async () => { - console.log("setup split"); + it("split/accept flow success", async () => { let [samConnection, aliceConnection] = await setupSplit("100", "100", "0"); - console.log("requestSplit"); - let stakeAccount = await samConnection.getMainAccount( samConnection.userPublicKey() ); @@ -256,16 +271,12 @@ describe("split vesting account", async () => { aliceConnection.userPublicKey() ); - console.log("acceptSplit"); - await pdaConnection.acceptSplit( stakeAccount, PythBalance.fromString("33"), aliceConnection.userPublicKey() ); - console.log("testing"); - await assertMainAccountBalance( samConnection, VestingAccountState.UnvestedTokensFullyUnlocked, @@ -280,12 +291,94 @@ describe("split vesting account", async () => { VestingAccountState.UnvestedTokensFullyUnlocked, { unvested: { - unlocked: PythBalance.fromString("32"), + unlocked: PythBalance.fromString("33"), + }, + } + ); + }); + + it("split/accept flow fails if account has locked tokens", async () => { + let [samConnection, aliceConnection] = await setupSplit("100", "100", "1"); + + let stakeAccount = await samConnection.getMainAccount( + samConnection.userPublicKey() + ); + + await samConnection.requestSplit( + stakeAccount, + PythBalance.fromString("33"), + aliceConnection.userPublicKey() + ); + + await assertFailsWithErrorCode( + () => + pdaConnection.acceptSplit( + stakeAccount, + PythBalance.fromString("33"), + aliceConnection.userPublicKey() + ), + "SplitWithStake" + ); + + await assertMainAccountBalance( + samConnection, + VestingAccountState.UnvestedTokensPartiallyLocked, + { + unvested: { + unlocked: PythBalance.fromString("99"), + locking: PythBalance.fromString("1"), }, } ); }); + it("split/accept flow fails if accept has mismatched args", async () => { + let [samConnection, aliceConnection] = await setupSplit("100", "100", "0"); + + let stakeAccount = await samConnection.getMainAccount( + samConnection.userPublicKey() + ); + + await samConnection.requestSplit( + stakeAccount, + PythBalance.fromString("33"), + aliceConnection.userPublicKey() + ); + + // wrong balance + await assertFailsWithErrorCode( + () => + pdaConnection.acceptSplit( + stakeAccount, + PythBalance.fromString("34"), + aliceConnection.userPublicKey() + ), + "InvalidApproval" + ); + + // wrong recipient + await assertFailsWithErrorCode( + () => + pdaConnection.acceptSplit( + stakeAccount, + PythBalance.fromString("33"), + samConnection.userPublicKey() + ), + "InvalidApproval" + ); + + // wrong signer + await assertFailsWithErrorCode( + () => + aliceConnection.acceptSplit( + stakeAccount, + PythBalance.fromString("33"), + aliceConnection.userPublicKey() + ), + "ConstraintAddress" + ); + }); + after(async () => { controller.abort(); }); From 7fd2503c07546b696ee00934e0e1912600804a5c Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Thu, 9 Nov 2023 11:27:33 -0800 Subject: [PATCH 45/51] cleanup --- staking/app/StakeConnection.ts | 6 +- staking/programs/staking/src/lib.rs | 2 - .../programs/staking/src/state/positions.rs | 28 ------- staking/tests/split_vesting_account.ts | 76 ++----------------- 4 files changed, 6 insertions(+), 106 deletions(-) diff --git a/staking/app/StakeConnection.ts b/staking/app/StakeConnection.ts index 05234e1d..08444821 100644 --- a/staking/app/StakeConnection.ts +++ b/staking/app/StakeConnection.ts @@ -541,13 +541,9 @@ export class StakeConnection { } /** - * Locks all unvested tokens in governance + * Locks the specified amount of tokens in governance. */ public async lockTokens(stakeAccount: StakeAccount, amount: PythBalance) { - if (amount.isZero()) { - return; - } - const vestingAccountState = stakeAccount.getVestingAccountState( await this.getTime() ); diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index 89af2c6f..7ca914e9 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -596,8 +596,6 @@ pub mod staking { new_voter_record.initialize(config, &split_request.recipient); // Split off source account - // let source_stake_account_custody = &ctx.accounts.source_stake_account_custody; - // let source_stake_account_metadata = &mut ctx.accounts.source_stake_account_metadata; let source_stake_account_positions = &mut ctx.accounts.source_stake_account_positions.load_mut()?; diff --git a/staking/programs/staking/src/state/positions.rs b/staking/programs/staking/src/state/positions.rs index 9bda769b..5835054a 100644 --- a/staking/programs/staking/src/state/positions.rs +++ b/staking/programs/staking/src/state/positions.rs @@ -83,34 +83,6 @@ impl PositionData { .ok_or_else(|| error!(ErrorCode::PositionOutOfBounds))?, ) } - - pub fn get_target_exposure( - &self, - target: &Target, - current_epoch: u64, - unlocking_duration: u8, - ) -> Result { - let mut exposure: u64 = 0; - - for i in 0..MAX_POSITIONS { - if let Some(position) = self.read_position(i)? { - match position.get_current_position(current_epoch, unlocking_duration)? { - PositionState::LOCKED - | PositionState::PREUNLOCKING - | PositionState::UNLOCKING - | PositionState::LOCKING => { - if position.target_with_parameters.get_target() == *target { - exposure = exposure - .checked_add(position.amount) - .ok_or(error!(ErrorCode::GenericOverflow))? - }; - } - _ => {} - } - } - } - Ok(exposure) - } } pub trait TryBorsh { diff --git a/staking/tests/split_vesting_account.ts b/staking/tests/split_vesting_account.ts index 6033ff30..cb859271 100644 --- a/staking/tests/split_vesting_account.ts +++ b/staking/tests/split_vesting_account.ts @@ -141,10 +141,11 @@ describe("split vesting account", async () => { await samConnection.getTime() ); - await samConnection.lockTokens( - stakeAccount, - PythBalance.fromString(lockedBalance) - ); + let lockedPythBalance = PythBalance.fromString(lockedBalance); + if (lockedPythBalance.gt(PythBalance.zero())) { + // locking 0 tokens is an error + await samConnection.lockTokens(stakeAccount, lockedPythBalance); + } let aliceConnection = await connect(new Keypair()); return [samConnection, aliceConnection]; @@ -191,73 +192,6 @@ describe("split vesting account", async () => { ); } - /* - it("create a vesting account", async () => { - await samConnection.provider.connection.requestAirdrop( - sam.publicKey, - 1_000_000_000_000 - ); - await requestPythAirdrop( - sam.publicKey, - pythMintAccount.publicKey, - pythMintAuthority, - PythBalance.fromString("200"), - samConnection.provider.connection - ); - - const transaction = new Transaction(); - - const stakeAccountKeypair = await samConnection.withCreateAccount( - transaction.instructions, - sam.publicKey, - { - periodicVesting: { - initialBalance: PythBalance.fromString("100").toBN(), - startDate: await stakeConnection.getTime(), - periodDuration: ONE_MONTH, - numPeriods: new BN(72), - }, - } - ); - - await samConnection.withJoinDaoLlc( - transaction.instructions, - stakeAccountKeypair.publicKey - ); - - transaction.instructions.push( - await samConnection.buildTransferInstruction( - stakeAccountKeypair.publicKey, - PythBalance.fromString("100").toBN() - ) - ); - - await samConnection.provider.sendAndConfirm( - transaction, - [stakeAccountKeypair], - { skipPreflight: true } - ); - - let stakeAccount = await samConnection.getMainAccount(sam.publicKey); - assert( - VestingAccountState.UnvestedTokensFullyUnlocked == - stakeAccount.getVestingAccountState(await samConnection.getTime()) - ); - await assertBalanceMatches( - samConnection, - sam.publicKey, - { - unvested: { - unlocked: PythBalance.fromString("100"), - }, - }, - await samConnection.getTime() - ); - - await samConnection.lockAllUnvested(stakeAccount); - }); - */ - it("split/accept flow success", async () => { let [samConnection, aliceConnection] = await setupSplit("100", "100", "0"); From 82cc49e92731bb8e5d065ae6729030f26723e777 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Thu, 9 Nov 2023 11:53:48 -0800 Subject: [PATCH 46/51] pr comments --- staking/app/StakeConnection.ts | 18 ++-- staking/programs/staking/src/lib.rs | 31 +++---- .../staking/src/state/stake_account.rs | 4 +- staking/tests/split_vesting_account.ts | 88 +++++++------------ staking/tests/utils/utils.ts | 25 ++++++ 5 files changed, 84 insertions(+), 82 deletions(-) diff --git a/staking/app/StakeConnection.ts b/staking/app/StakeConnection.ts index 08444821..c9273539 100644 --- a/staking/app/StakeConnection.ts +++ b/staking/app/StakeConnection.ts @@ -535,15 +535,6 @@ export class StakeConnection { * Locks all unvested tokens in governance */ public async lockAllUnvested(stakeAccount: StakeAccount) { - const balanceSummary = stakeAccount.getBalanceSummary(await this.getTime()); - - await this.lockTokens(stakeAccount, balanceSummary.unvested.unlocked); - } - - /** - * Locks the specified amount of tokens in governance. - */ - public async lockTokens(stakeAccount: StakeAccount, amount: PythBalance) { const vestingAccountState = stakeAccount.getVestingAccountState( await this.getTime() ); @@ -554,6 +545,15 @@ export class StakeConnection { ) { throw Error(`Unexpected account state ${vestingAccountState}`); } + + const balanceSummary = stakeAccount.getBalanceSummary(await this.getTime()); + await this.lockTokens(stakeAccount, balanceSummary.unvested.unlocked); + } + + /** + * Locks the specified amount of tokens in governance. + */ + public async lockTokens(stakeAccount: StakeAccount, amount: PythBalance) { const owner: PublicKey = stakeAccount.stakeAccountMetadata.owner; const amountBN = amount.toBN(); diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index 7ca914e9..e5f77803 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -585,7 +585,7 @@ pub mod staking { *ctx.bumps.get("new_custody_authority").unwrap(), *ctx.bumps.get("new_voter_record").unwrap(), &split_request.recipient, - Some(current_epoch), + None, ); let new_stake_account_positions = @@ -599,7 +599,10 @@ pub mod staking { let source_stake_account_positions = &mut ctx.accounts.source_stake_account_positions.load_mut()?; - // Pre-check + // Pre-check invariants + // Note that the accept operation requires the positions account to be empty, which should trivially + // pass this invariant check. However, we explicitly check invariants everywhere else, so may + // as well check in this operation also. utils::risk::validate( source_stake_account_positions, ctx.accounts.source_stake_account_custody.amount, @@ -625,7 +628,7 @@ pub mod staking { require!(split_request.amount > 0, ErrorCode::SplitZeroTokens); // Split vesting account - let (source_vesting_account, new_vesting_account) = ctx + let (source_vesting_schedule, new_vesting_schedule) = ctx .accounts .source_stake_account_metadata .lock @@ -635,22 +638,20 @@ pub mod staking { )?; ctx.accounts .source_stake_account_metadata - .set_lock(source_vesting_account); + .set_lock(source_vesting_schedule); ctx.accounts .new_stake_account_metadata - .set_lock(new_vesting_account); + .set_lock(new_vesting_schedule); - { - transfer( - CpiContext::from(&*ctx.accounts).with_signer(&[&[ - AUTHORITY_SEED.as_bytes(), - ctx.accounts.source_stake_account_positions.key().as_ref(), - &[ctx.accounts.source_stake_account_metadata.authority_bump], - ]]), - split_request.amount, - )?; - } + transfer( + CpiContext::from(&*ctx.accounts).with_signer(&[&[ + AUTHORITY_SEED.as_bytes(), + ctx.accounts.source_stake_account_positions.key().as_ref(), + &[ctx.accounts.source_stake_account_metadata.authority_bump], + ]]), + split_request.amount, + )?; ctx.accounts.source_stake_account_custody.reload()?; ctx.accounts.new_stake_account_custody.reload()?; diff --git a/staking/programs/staking/src/state/stake_account.rs b/staking/programs/staking/src/state/stake_account.rs index 20a0de97..e1dacbc3 100644 --- a/staking/programs/staking/src/state/stake_account.rs +++ b/staking/programs/staking/src/state/stake_account.rs @@ -47,14 +47,14 @@ impl StakeAccountMetadataV2 { metadata_bump: u8, custody_bump: u8, authority_bump: u8, - voter_record_bump: u8, + voter_bump: u8, owner: &Pubkey, transfer_epoch: Option, ) { self.metadata_bump = metadata_bump; self.custody_bump = custody_bump; self.authority_bump = authority_bump; - self.voter_bump = voter_record_bump; + self.voter_bump = voter_bump; self.owner = *owner; self.next_index = 0; self.transfer_epoch = transfer_epoch; diff --git a/staking/tests/split_vesting_account.ts b/staking/tests/split_vesting_account.ts index cb859271..9ab56ad4 100644 --- a/staking/tests/split_vesting_account.ts +++ b/staking/tests/split_vesting_account.ts @@ -16,8 +16,7 @@ import { OptionalBalanceSummary, } from "./utils/api_utils"; import assert from "assert"; -import { blob } from "stream/consumers"; -import { Key } from "@metaplex-foundation/mpl-token-metadata"; +import { expectFailWithCode } from "./utils/utils"; const ONE_MONTH = new BN(3600 * 24 * 30.5); const portNumber = getPortNumber(path.basename(__filename)); @@ -25,13 +24,10 @@ const portNumber = getPortNumber(path.basename(__filename)); describe("split vesting account", async () => { const pythMintAccount = new Keypair(); const pythMintAuthority = new Keypair(); - let EPOCH_DURATION: BN; let stakeConnection: StakeConnection; let controller: CustomAbortController; - let owner: PublicKey; - let pdaAuthority = new Keypair(); let pdaConnection: StakeConnection; @@ -49,9 +45,6 @@ describe("split vesting account", async () => { ) )); - EPOCH_DURATION = stakeConnection.config.epochDuration; - owner = stakeConnection.provider.wallet.publicKey; - pdaConnection = await connect(pdaAuthority); }); @@ -172,26 +165,6 @@ describe("split vesting account", async () => { ); } - async function assertFailsWithErrorCode( - thunk: () => Promise, - errorCode: string - ) { - let actualErrorCode: string | undefined = undefined; - try { - await thunk(); - } catch (err) { - if (err instanceof AnchorError) { - actualErrorCode = err.error.errorCode.code; - } - } - - assert.equal( - actualErrorCode, - errorCode, - `Call did not fail with the expected error code.` - ); - } - it("split/accept flow success", async () => { let [samConnection, aliceConnection] = await setupSplit("100", "100", "0"); @@ -244,13 +217,12 @@ describe("split vesting account", async () => { aliceConnection.userPublicKey() ); - await assertFailsWithErrorCode( - () => - pdaConnection.acceptSplit( - stakeAccount, - PythBalance.fromString("33"), - aliceConnection.userPublicKey() - ), + await expectFailWithCode( + pdaConnection.acceptSplit( + stakeAccount, + PythBalance.fromString("33"), + aliceConnection.userPublicKey() + ), "SplitWithStake" ); @@ -280,37 +252,41 @@ describe("split vesting account", async () => { ); // wrong balance - await assertFailsWithErrorCode( - () => - pdaConnection.acceptSplit( - stakeAccount, - PythBalance.fromString("34"), - aliceConnection.userPublicKey() - ), + await expectFailWithCode( + pdaConnection.acceptSplit( + stakeAccount, + PythBalance.fromString("34"), + aliceConnection.userPublicKey() + ), "InvalidApproval" ); // wrong recipient - await assertFailsWithErrorCode( - () => - pdaConnection.acceptSplit( - stakeAccount, - PythBalance.fromString("33"), - samConnection.userPublicKey() - ), + await expectFailWithCode( + pdaConnection.acceptSplit( + stakeAccount, + PythBalance.fromString("33"), + samConnection.userPublicKey() + ), "InvalidApproval" ); // wrong signer - await assertFailsWithErrorCode( - () => - aliceConnection.acceptSplit( - stakeAccount, - PythBalance.fromString("33"), - aliceConnection.userPublicKey() - ), + await expectFailWithCode( + aliceConnection.acceptSplit( + stakeAccount, + PythBalance.fromString("33"), + aliceConnection.userPublicKey() + ), "ConstraintAddress" ); + + // Passing the correct arguments should succeed + await pdaConnection.acceptSplit( + stakeAccount, + PythBalance.fromString("33"), + aliceConnection.userPublicKey() + ); }); after(async () => { diff --git a/staking/tests/utils/utils.ts b/staking/tests/utils/utils.ts index b6554d2d..18b3c7be 100644 --- a/staking/tests/utils/utils.ts +++ b/staking/tests/utils/utils.ts @@ -112,3 +112,28 @@ export async function expectFailApi(promise: Promise, error: string) { assert.equal(err.message, error); } } + +/** + * Awaits the api request and checks whether the error message matches the provided string + * @param promise : api promise + * @param errorCode : expected string + */ +export async function expectFailWithCode( + promise: Promise, + errorCode: string +) { + let actualErrorCode: string | undefined = undefined; + try { + await promise; + assert(false, "Operation should fail"); + } catch (err) { + if (err instanceof AnchorError) { + actualErrorCode = err.error.errorCode.code; + } + } + assert.equal( + actualErrorCode, + errorCode, + `Call did not fail with the expected error code.` + ); +} From 418d6649e474f76768f22f9981c4a847cbb8f326 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Thu, 9 Nov 2023 12:34:44 -0800 Subject: [PATCH 47/51] minor --- staking/tests/split_vesting_account.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/staking/tests/split_vesting_account.ts b/staking/tests/split_vesting_account.ts index 9ab56ad4..5e50ab20 100644 --- a/staking/tests/split_vesting_account.ts +++ b/staking/tests/split_vesting_account.ts @@ -10,7 +10,7 @@ import { import path from "path"; import { Keypair, PublicKey, Transaction } from "@solana/web3.js"; import { StakeConnection, PythBalance, VestingAccountState } from "../app"; -import { ErrorCode, BN, Wallet, AnchorError } from "@project-serum/anchor"; +import { BN, Wallet } from "@project-serum/anchor"; import { assertBalanceMatches, OptionalBalanceSummary, From 02810eb2020135adca8300f1f62053022e5221c9 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Fri, 10 Nov 2023 11:50:11 +0000 Subject: [PATCH 48/51] Cleanup --- staking/programs/staking/src/lib.rs | 2 -- staking/programs/staking/src/state/stake_account.rs | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index e5f77803..bddf84e7 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -123,7 +123,6 @@ pub mod staking { *ctx.bumps.get("custody_authority").unwrap(), *ctx.bumps.get("voter_record").unwrap(), &owner, - None, ); stake_account_metadata.set_lock(lock); @@ -585,7 +584,6 @@ pub mod staking { *ctx.bumps.get("new_custody_authority").unwrap(), *ctx.bumps.get("new_voter_record").unwrap(), &split_request.recipient, - None, ); let new_stake_account_positions = diff --git a/staking/programs/staking/src/state/stake_account.rs b/staking/programs/staking/src/state/stake_account.rs index e1dacbc3..c0f840e7 100644 --- a/staking/programs/staking/src/state/stake_account.rs +++ b/staking/programs/staking/src/state/stake_account.rs @@ -49,7 +49,6 @@ impl StakeAccountMetadataV2 { authority_bump: u8, voter_bump: u8, owner: &Pubkey, - transfer_epoch: Option, ) { self.metadata_bump = metadata_bump; self.custody_bump = custody_bump; @@ -57,7 +56,7 @@ impl StakeAccountMetadataV2 { self.voter_bump = voter_bump; self.owner = *owner; self.next_index = 0; - self.transfer_epoch = transfer_epoch; + self.transfer_epoch = None; self.signed_agreement_hash = None; } From 44429c848ec496f726dcf123d66924d73cb711d6 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Fri, 10 Nov 2023 12:12:32 +0000 Subject: [PATCH 49/51] Comment --- staking/programs/staking/src/lib.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index bddf84e7..15b09757 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -593,14 +593,12 @@ pub mod staking { let new_voter_record = &mut ctx.accounts.new_voter_record; new_voter_record.initialize(config, &split_request.recipient); - // Split off source account - let source_stake_account_positions = - &mut ctx.accounts.source_stake_account_positions.load_mut()?; - // Pre-check invariants // Note that the accept operation requires the positions account to be empty, which should trivially // pass this invariant check. However, we explicitly check invariants everywhere else, so may // as well check in this operation also. + let source_stake_account_positions = + &mut ctx.accounts.source_stake_account_positions.load_mut()?; utils::risk::validate( source_stake_account_positions, ctx.accounts.source_stake_account_custody.amount, From 0994df9504ed1e0b1698e5a9ef04be24be2a4418 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Fri, 10 Nov 2023 14:00:12 +0000 Subject: [PATCH 50/51] Add another test with full amount --- staking/package.json | 2 +- staking/tests/split_vesting_account.ts | 50 ++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/staking/package.json b/staking/package.json index a18d30b6..4b0a76db 100644 --- a/staking/package.json +++ b/staking/package.json @@ -30,7 +30,7 @@ "wasm-pack": "^0.10.2" }, "scripts": { - "test": "npm run build_wasm && anchor build -- --features mock-clock && npm run dump_governance && ts-mocha --parallel -p ./tsconfig.json -t 1000000 tests/*.ts", + "test": "npm run build_wasm && anchor build -- --features mock-clock && npm run dump_governance && ts-mocha --parallel -p ./tsconfig.json -t 1000000 tests/split_vesting_account.ts", "build": "npm run build_wasm && tsc -p tsconfig.api.json", "build_wasm": "./scripts/build_wasm.sh", "localnet": "anchor build && npm run dump_governance && ts-node ./app/scripts/localnet.ts", diff --git a/staking/tests/split_vesting_account.ts b/staking/tests/split_vesting_account.ts index 5e50ab20..b34a886f 100644 --- a/staking/tests/split_vesting_account.ts +++ b/staking/tests/split_vesting_account.ts @@ -1,6 +1,7 @@ import { ANCHOR_CONFIG_PATH, CustomAbortController, + getDummyAgreementHash, getPortNumber, makeDefaultConfig, readAnchorConfig, @@ -204,6 +205,55 @@ describe("split vesting account", async () => { ); }); + it("split/accept flow full amount", async () => { + let [samConnection, aliceConnection] = await setupSplit("100", "100", "0"); + + let stakeAccount = await samConnection.getMainAccount( + samConnection.userPublicKey() + ); + + await samConnection.requestSplit( + stakeAccount, + PythBalance.fromString("100"), + aliceConnection.userPublicKey() + ); + + await pdaConnection.acceptSplit( + stakeAccount, + PythBalance.fromString("100"), + aliceConnection.userPublicKey() + ); + + await assertMainAccountBalance( + samConnection, + VestingAccountState.FullyVested, + {} + ); + await assertMainAccountBalance( + aliceConnection, + VestingAccountState.UnvestedTokensFullyUnlocked, + { + unvested: { + unlocked: PythBalance.fromString("100"), + }, + } + ); + + const aliceStakeAccount = await aliceConnection.getMainAccount( + aliceConnection.userPublicKey() + ); + await aliceConnection.program.methods + .joinDaoLlc(getDummyAgreementHash()) + .accounts({ stakeAccountPositions: aliceStakeAccount.address }) + .rpc(); + await aliceConnection.program.methods + .updateVoterWeight({ createGovernance: {} }) + .accounts({ + stakeAccountPositions: aliceStakeAccount.address, + }) + .rpc(); + }); + it("split/accept flow fails if account has locked tokens", async () => { let [samConnection, aliceConnection] = await setupSplit("100", "100", "1"); From b7e6a6901c0cc2ce8293147c204bf5574fc35b91 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Fri, 10 Nov 2023 14:02:48 +0000 Subject: [PATCH 51/51] Sorry --- staking/package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/staking/package.json b/staking/package.json index 4b0a76db..54e1115a 100644 --- a/staking/package.json +++ b/staking/package.json @@ -27,10 +27,12 @@ "prettier": "^2.6.2", "shelljs": "^0.8.5", "ts-mocha": "^9.0.2", - "wasm-pack": "^0.10.2" + "wasm-pack": "^0.10.2", + "@ledgerhq/hw-transport-node-hid": "^6.27.21", + "@ledgerhq/hw-transport": "^6.27.2" }, "scripts": { - "test": "npm run build_wasm && anchor build -- --features mock-clock && npm run dump_governance && ts-mocha --parallel -p ./tsconfig.json -t 1000000 tests/split_vesting_account.ts", + "test": "npm run build_wasm && anchor build -- --features mock-clock && npm run dump_governance && ts-mocha --parallel -p ./tsconfig.json -t 1000000 tests/*.ts", "build": "npm run build_wasm && tsc -p tsconfig.api.json", "build_wasm": "./scripts/build_wasm.sh", "localnet": "anchor build && npm run dump_governance && ts-node ./app/scripts/localnet.ts",