Skip to content

Commit

Permalink
Split accounts (#246)
Browse files Browse the repository at this point in the history
* Draft

* Add authority

* Clippy

* Checkpoint

* Do it

* Rename error

* Update stuff

* Use epoch_of_snapshot

* Checkpoint

* Scaffolding

* Checkpoint

* Cleanup

* Cleanup test

* Another round

* Cleanup

* Restore all tests

* add todos

* Cleanup idls

* Throw error since it's not implemented

* Add some comments

* Box everything

* Add soruce

* Add more comments

* Add another comment

* Update idls
  • Loading branch information
guibescos authored Oct 17, 2023
1 parent d8c7aed commit a34d07d
Show file tree
Hide file tree
Showing 9 changed files with 1,634 additions and 340 deletions.
36 changes: 36 additions & 0 deletions staking/app/StakeConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,42 @@ export class StakeConnection {
})
.rpc();
}

public async requestSplit(
stakeAccount: StakeAccount,
amount: PythBalance,
recipient: PublicKey
) {
await this.program.methods
.requestSplit(amount.toBN(), recipient)
.accounts({
stakeAccountPositions: stakeAccount.address,
})
.rpc();
}

public async acceptSplit(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
.acceptSplit()
.accounts({
sourceStakeAccountPositions: stakeAccount.address,
newStakeAccountPositions: newStakeAccountKeypair.publicKey,
mint: this.config.pythTokenMint,
})
.signers([newStakeAccountKeypair])
.preInstructions(instructions)
.rpc();
}
}
export interface BalanceSummary {
withdrawable: PythBalance;
Expand Down
92 changes: 92 additions & 0 deletions staking/programs/staking/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8> {
Expand Down Expand Up @@ -278,6 +279,97 @@ pub struct CreateTarget<'info> {
pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
#[instruction(amount : u64, recipient : Pubkey)]
pub struct RequestSplit<'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>,
// Primitive accounts :
pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct AcceptSplit<'info> {
// Native payer:
#[account(mut, address = config.pda_authority)]
pub payer: Signer<'info>,
// Current stake accounts:
#[account(mut)]
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<'info, stake_account::StakeAccountMetadataV2>>,
#[account(seeds = [SPLIT_REQUEST.as_bytes(), source_stake_account_positions.key().as_ref()], bump)]
pub source_stake_account_split_request: Box<Account<'info, split_request::SplitRequest>>,
#[account(
mut,
seeds = [CUSTODY_SEED.as_bytes(), source_stake_account_positions.key().as_ref()],
bump = source_stake_account_metadata.custody_bump,
)]
pub source_stake_account_custody: Box<Account<'info, TokenAccount>>,
/// 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>,

// 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<Account<'info, stake_account::StakeAccountMetadataV2>>,
#[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: Box<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: Box<Account<'info, voter_weight_record::VoterWeightRecord>>,

#[account(seeds = [CONFIG_SEED.as_bytes()], bump = config.bump)]
pub config: Box<Account<'info, global_config::GlobalConfig>>,

// Pyth token mint:
#[account(address = config.pyth_token_mint)]
pub mint: Box<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>>
for 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.source_stake_account_custody.to_account_info(),
to: accounts.new_stake_account_custody.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)
}
}

// 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)]
Expand Down
48 changes: 48 additions & 0 deletions staking/programs/staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -534,4 +534,52 @@ pub mod staking {
Err(error!(ErrorCode::DebuggingOnly))
}
}

/**
* 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. 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.
*/
pub fn request_split(ctx: Context<RequestSplit>, amount: u64, recipient: Pubkey) -> Result<()> {
ctx.accounts.stake_account_split_request.amount = amount;
ctx.accounts.stake_account_split_request.recipient = recipient;
Ok(())
}


/**
* 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<AcceptSplit>) -> Result<()> {
// TODO : Split vesting schedule between both accounts

// 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.source_stake_account_split_request;
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,
)?;
}

// Delete current request
{
ctx.accounts.source_stake_account_split_request.amount = 0;
}
err!(ErrorCode::NotImplemented)
}
}
1 change: 1 addition & 0 deletions staking/programs/staking/src/state/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
17 changes: 17 additions & 0 deletions staking/programs/staking/src/state/split_request.rs
Original file line number Diff line number Diff line change
@@ -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
}
Loading

1 comment on commit a34d07d

@vercel
Copy link

@vercel vercel bot commented on a34d07d Oct 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

staking-devnet – ./

staking-devnet-pyth-web.vercel.app
governance-nu.vercel.app
staking-devnet-git-main-pyth-web.vercel.app

Please sign in to comment.