Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Split accounts #246

Merged
merged 25 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 :
guibescos marked this conversation as resolved.
Show resolved Hide resolved
#[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
47 changes: 47 additions & 0 deletions staking/programs/staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -534,4 +534,51 @@ 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.
* 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.
Copy link
Collaborator

Choose a reason for hiding this comment

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

suggest adding: "each user can only have one active split request at a time"

*/
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
guibescos marked this conversation as resolved.
Show resolved Hide resolved

// 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
Loading