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 3 #247

Merged
merged 53 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
c0473d7
Draft
guibescos Oct 13, 2023
ae7eddd
Add authority
guibescos Oct 16, 2023
f238967
Clippy
guibescos Oct 16, 2023
a2fbe05
Checkpoint
guibescos Oct 16, 2023
3b688e8
Do it
guibescos Oct 16, 2023
49c2bc7
Rename error
guibescos Oct 16, 2023
76b1edc
Update stuff
guibescos Oct 16, 2023
4642430
Use epoch_of_snapshot
guibescos Oct 16, 2023
aacc7dc
Checkpoint
guibescos Oct 16, 2023
1631311
Scaffolding
guibescos Oct 16, 2023
b5fb191
Checkpoint
guibescos Oct 16, 2023
6752e22
Cleanup
guibescos Oct 16, 2023
e2a0d58
Cleanup test
guibescos Oct 16, 2023
f1f455e
Another round
guibescos Oct 16, 2023
feb94bf
Cleanup
guibescos Oct 16, 2023
229db4d
Restore all tests
guibescos Oct 16, 2023
25229a8
add todos
guibescos Oct 16, 2023
cdca77d
Cleanup idls
guibescos Oct 16, 2023
bf609fb
Throw error since it's not implemented
guibescos Oct 17, 2023
4f40169
Add some comments
guibescos Oct 17, 2023
65f9035
Box everything
guibescos Oct 17, 2023
649c847
Add soruce
guibescos Oct 17, 2023
23ea423
Add more comments
guibescos Oct 17, 2023
413f606
Add another comment
guibescos Oct 17, 2023
89afd64
First implementation
guibescos Oct 17, 2023
9718000
Delete current request
guibescos Oct 17, 2023
a239ddf
add bumps
guibescos Oct 17, 2023
de68014
Fix bug
guibescos Oct 17, 2023
68219a9
Tests works
guibescos Oct 17, 2023
bd06377
Merge branch 'main' into split-accounts-3
guibescos Oct 17, 2023
5dce63f
Update idls
guibescos Oct 17, 2023
90e954c
Clippy
guibescos Oct 17, 2023
6a15e5d
Clippy
guibescos Oct 17, 2023
cbde98f
Add actual tests
guibescos Oct 17, 2023
129aca7
Clippy
guibescos Oct 17, 2023
fae4055
Merge
guibescos Oct 24, 2023
dd22f6e
Cleanup
guibescos Oct 24, 2023
1138fbc
Fix tests
guibescos Oct 24, 2023
a75183d
Split accounts test (#258)
jayantk Nov 8, 2023
953a318
minor cleanups
jayantk Nov 8, 2023
98b6c1b
cleanup implementation
jayantk Nov 8, 2023
1d1b07f
idl
jayantk Nov 8, 2023
8dbef96
ok fix the ts tests
jayantk Nov 8, 2023
623cab9
refactor
jayantk Nov 9, 2023
faf9b2f
refactor
jayantk Nov 9, 2023
00466b3
fix
jayantk Nov 9, 2023
7fd2503
cleanup
jayantk Nov 9, 2023
82cc49e
pr comments
jayantk Nov 9, 2023
418d664
minor
jayantk Nov 9, 2023
02810eb
Cleanup
guibescos Nov 10, 2023
44429c8
Comment
guibescos Nov 10, 2023
0994df9
Add another test with full amount
guibescos Nov 10, 2023
b7e6a69
Sorry
guibescos Nov 10, 2023
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
25 changes: 21 additions & 4 deletions staking/app/StakeConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<PublicKey[]> {
// 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(
Expand Down Expand Up @@ -540,9 +545,17 @@ export class StakeConnection {
) {
throw Error(`Unexpected account state ${vestingAccountState}`);
guibescos marked this conversation as resolved.
Show resolved Hide resolved
}
const owner: PublicKey = stakeAccount.stakeAccountMetadata.owner;

const balanceSummary = stakeAccount.getBalanceSummary(await this.getTime());
const amountBN = balanceSummary.unvested.unlocked.toBN();
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();

const transaction: Transaction = new Transaction();

Expand Down Expand Up @@ -857,7 +870,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 = [];
Expand All @@ -869,7 +886,7 @@ export class StakeConnection {
);

await this.program.methods
.acceptSplit()
.acceptSplit(amount.toBN(), recipient)
.accounts({
sourceStakeAccountPositions: stakeAccount.address,
newStakeAccountPositions: newStakeAccountKeypair.publicKey,
Expand Down
4 changes: 3 additions & 1 deletion staking/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
"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/*.ts",
Expand Down
1 change: 1 addition & 0 deletions staking/programs/staking/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
11 changes: 10 additions & 1 deletion staking/programs/staking/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ pub enum ErrorCode {
NotLlcMember,
#[msg("Invalid LLC agreement")] // 6030
InvalidLlcAgreement,
#[msg("Other")] //6031
#[msg("Can't split 0 tokens from an account")] // 6031
SplitZeroTokens,
#[msg("Can't split more tokens than are in the account")] // 6032
SplitTooManyTokens,
#[msg("Can't split a token account with staking positions. Unstake your tokens first.")]
// 6033
SplitWithStake,
#[msg("The approval arguments do not match the split request.")] // 6034
InvalidApproval,
#[msg("Other")] //6035
Other,
}
162 changes: 129 additions & 33 deletions staking/programs/staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -53,6 +54,7 @@ pub mod staking {

/// Creates a global config for the program
use super::*;

pub fn init_config(ctx: Context<InitConfig>, global_config: GlobalConfig) -> Result<()> {
let config_account = &mut ctx.accounts.config_account;
config_account.bump = *ctx.bumps.get("config_account").unwrap();
Expand Down Expand Up @@ -115,25 +117,20 @@ pub mod staking {
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;
stake_account_metadata.signed_agreement_hash = None;
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,
);
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(())
}
Expand Down Expand Up @@ -564,32 +561,131 @@ 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<AcceptSplit>) -> Result<()> {
// TODO : Split vesting schedule between both accounts
pub fn accept_split(ctx: Context<AcceptSplit>, amount: u64, recipient: Pubkey) -> Result<()> {
let config = &ctx.accounts.config;
guibescos marked this conversation as resolved.
Show resolved Hide resolved
config.check_frozen()?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

what does this check_frozen do and why is it needed? (leave a comment?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let's just get rid of this


// TODO : Transfer stake positions to the new account if need
let current_epoch = get_current_epoch(config)?;

// TODO Check both accounts are valid after the transfer
let split_request = &ctx.accounts.source_stake_account_split_request;
require!(
split_request.amount == amount && split_request.recipient == recipient,
ErrorCode::InvalidApproval
);

// Initialize new accounts
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(),
*ctx.bumps.get("new_voter_record").unwrap(),
&split_request.recipient,
);

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);

// 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 =
guibescos marked this conversation as resolved.
Show resolved Hide resolved
&mut ctx.accounts.source_stake_account_positions.load_mut()?;
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,
)?;

// 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],
]]),
// 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!(
ctx.accounts.source_stake_account_metadata.next_index == 0,
ErrorCode::SplitWithStake
);

require!(split_request.amount > 0, ErrorCode::SplitZeroTokens);

// Split vesting account
let (source_vesting_schedule, new_vesting_schedule) = ctx
.accounts
.source_stake_account_metadata
.lock
.split_vesting_schedule(
split_request.amount,
ctx.accounts.source_stake_account_custody.amount,
)?;
}
ctx.accounts
.source_stake_account_metadata
.set_lock(source_vesting_schedule);
ctx.accounts
.new_stake_account_metadata
.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,
)?;

ctx.accounts.source_stake_account_custody.reload()?;
ctx.accounts.new_stake_account_custody.reload()?;


// Post-check
utils::risk::validate(
guibescos marked this conversation as resolved.
Show resolved Hide resolved
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;
}
err!(ErrorCode::NotImplemented)
ctx.accounts.source_stake_account_split_request.amount = 0;

Ok(())
}

/**
Expand Down
5 changes: 4 additions & 1 deletion staking/programs/staking/src/state/positions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,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<usize> {
let res = *next_index as usize;
Expand Down Expand Up @@ -403,7 +407,6 @@ pub mod tests {
}
}


#[quickcheck]
fn prop(input: Vec<DataOperation>) -> bool {
let mut position_data = PositionData::default();
Expand Down
24 changes: 24 additions & 0 deletions staking/programs/staking/src/state/stake_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,30 @@ impl StakeAccountMetadataV2 {
}
}

impl StakeAccountMetadataV2 {
pub fn initialize(
&mut self,
metadata_bump: u8,
custody_bump: u8,
authority_bump: u8,
voter_bump: u8,
owner: &Pubkey,
) {
self.metadata_bump = metadata_bump;
self.custody_bump = custody_bump;
self.authority_bump = authority_bump;
self.voter_bump = voter_bump;
self.owner = *owner;
self.next_index = 0;
self.transfer_epoch = None;
self.signed_agreement_hash = None;
}

pub fn set_lock(&mut self, lock: VestingSchedule) {
self.lock = lock;
}
}

#[cfg(test)]
pub mod tests {
use {
Expand Down
Loading
Loading