diff --git a/addin-vesting/cli/Cargo.toml b/addin-vesting/cli/Cargo.toml index bf767f5..0e4abf0 100644 --- a/addin-vesting/cli/Cargo.toml +++ b/addin-vesting/cli/Cargo.toml @@ -5,13 +5,13 @@ authors = ["Bonfida "] edition = "2018" [dependencies] -solana-program = "1.5.0" -solana-client = "1.5.0" -solana-sdk = "1.5.0" -solana-clap-utils = "1.5.0" +solana-program = "=1.9.13" +solana-client = "=1.9.13" +solana-sdk = "=1.9.13" +solana-clap-utils = "=1.9.13" token-vesting = { version = "0.1.0", path="../program", features=["no-entrypoint"] } -spl-token = {version = "3.0.1", features = ["no-entrypoint"]} +spl-token = {version = "3.2.0", features = ["no-entrypoint"]} spl-associated-token-account = {version = "1.0.2", features = ["no-entrypoint"]} clap = "2.33.3" chrono = "0.4.19" -iso8601-duration = { git = "https://github.com/rrichardson/iso8601-duration.git", rev = "9e01f51ea253e95e0fba5e4d7ad0c537922931e7"} \ No newline at end of file +iso8601-duration = { git = "https://github.com/rrichardson/iso8601-duration.git", rev = "9e01f51ea253e95e0fba5e4d7ad0c537922931e7"} diff --git a/addin-vesting/program/Cargo.toml b/addin-vesting/program/Cargo.toml index 3ca5f4e..a0c013c 100644 --- a/addin-vesting/program/Cargo.toml +++ b/addin-vesting/program/Cargo.toml @@ -21,9 +21,14 @@ thiserror = "1.0.23" num-traits = "0.2" num-derive = "0.3" arrayref = "0.3.6" +borsh = "0.9.1" solana-program = "1.5.6" spl-token = { version = "3.0.1", features = ["no-entrypoint"] } spl-associated-token-account = { version = "1.0.2", features = ["no-entrypoint"] } +#spl-governance = { version = "2.2.2", features = ["no-entrypoint"] } +#spl-governance-tools = { version = "0.1.2" } +spl-governance = { path="/mnt/working/solana/solana-program-library.git/governance/program", features = ["no-entrypoint"] } +spl-governance-tools = { path="/mnt/working/solana/solana-program-library.git/governance/tools", version = "0.1.2" } arbitrary = { version = "0.4", features = ["derive"], optional = true } honggfuzz = { version = "0.5", optional = true } @@ -31,6 +36,7 @@ honggfuzz = { version = "0.5", optional = true } solana-sdk = "1.5.6" solana-program-test = "1.5.6" tokio = { version = "1.0", features = ["macros"]} +hex = "0.4" [lib] crate-type = ["cdylib", "lib"] diff --git a/addin-vesting/program/src/entrypoint.rs b/addin-vesting/program/src/entrypoint.rs index 4d0e18b..3b91dff 100644 --- a/addin-vesting/program/src/entrypoint.rs +++ b/addin-vesting/program/src/entrypoint.rs @@ -1,5 +1,5 @@ use solana_program::{ - account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg, + account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, program_error::PrintProgramError, pubkey::Pubkey, }; @@ -12,7 +12,6 @@ pub fn process_instruction( accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { - msg!("Entrypoint"); if let Err(error) = Processor::process_instruction(program_id, accounts, instruction_data) { // catch the error so we can print it error.print::(); diff --git a/addin-vesting/program/src/instruction.rs b/addin-vesting/program/src/instruction.rs index 0836f10..cb50a0e 100644 --- a/addin-vesting/program/src/instruction.rs +++ b/addin-vesting/program/src/instruction.rs @@ -1,14 +1,14 @@ -use crate::error::VestingError; +use crate::state::VestingSchedule; use solana_program::{ instruction::{AccountMeta, Instruction}, - msg, program_error::ProgramError, - pubkey::Pubkey + pubkey::Pubkey, + system_program, + sysvar, }; -use std::convert::TryInto; -use std::mem::size_of; +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; #[cfg(feature = "fuzz")] use arbitrary::Arbitrary; @@ -47,59 +47,71 @@ impl Arbitrary for VestingInstruction { #[cfg_attr(feature = "fuzz", derive(Arbitrary))] #[repr(C)] -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct Schedule { // Schedule release time in unix timestamp pub release_time: u64, pub amount: u64, } -pub const SCHEDULE_SIZE: usize = 16; - #[repr(C)] -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub enum VestingInstruction { - /// Initializes an empty program account for the token_vesting program + + /// Creates a new vesting schedule contract /// /// Accounts expected by this instruction: /// /// * Single owner /// 0. `[]` The system program account - /// 1. `[]` The sysvar Rent account - /// 1. `[signer]` The fee payer account - /// 1. `[]` The vesting account - Init { - // The seed used to derive the vesting accounts address - seeds: [u8; 32], - // The number of release schedules for this contract to hold - number_of_schedules: u32, - }, - /// Creates a new vesting schedule contract + /// 1. `[]` The spl-token program account + /// 2. `[writable]` The vesting account (vesting owner account: PDA seeds: [seeds]) + /// 3. `[writable]` The vesting spl-token account (vesting balance account) + /// 4. `[signer]` The source spl-token account owner (from account owner) + /// 5. `[writable]` The source spl-token account (from account) + /// 6. `[]` The Vesting Owner account + /// 7. `[signer]` Payer + /// 8. `[]` Rent sysvar /// - /// Accounts expected by this instruction: + /// Optional part (vesting for Realm) + /// 8. `[]` The Governance program account + /// 9. `[]` The Realm account + /// 10. `[writable]` The VoterWeightRecord. PDA seeds: ['voter_weight', realm, token_mint, token_owner] + /// 11. `[writable]` The MaxVoterWeightRecord. PDA seeds: ['max_voter_weight', realm, token_mint] /// - /// * Single owner - /// 0. `[]` The spl-token program account - /// 1. `[writable]` The vesting account - /// 2. `[writable]` The vesting spl-token account - /// 3. `[signer]` The source spl-token account owner - /// 4. `[writable]` The source spl-token account - Create { + Deposit { + #[allow(dead_code)] seeds: [u8; 32], - mint_address: Pubkey, - destination_token_address: Pubkey, - schedules: Vec, + #[allow(dead_code)] + schedules: Vec, }, + + + /// Unlocks a simple vesting contract (SVC) - can only be invoked by the program itself /// Accounts expected by this instruction: /// /// * Single owner /// 0. `[]` The spl-token program account /// 1. `[]` The clock sysvar account - /// 1. `[writable]` The vesting account - /// 2. `[writable]` The vesting spl-token account - /// 3. `[writable]` The destination spl-token account - Unlock { seeds: [u8; 32] }, + /// 2. `[writable]` The vesting account (vesting owner account: PDA [seeds]) + /// 3. `[writable]` The vesting spl-token account (vesting balance account) + /// 4. `[writable]` The destination spl-token account + /// 5. `[signer]` The Vesting Owner account + /// + /// Optional part (vesting for Realm) + /// 6. `[]` The Governance program account + /// 7. `[]` The Realm account + /// 8. `[]` Governing Owner Record. PDA seeds (governance program): ['governance', realm, token_mint, vesting_owner] + /// 9. `[writable]` The VoterWeightRecord. PDA seeds: ['voter_weight', realm, token_mint, vesting_owner] + /// 10. `[writable]` The MaxVoterWeightRecord. PDA seeds: ['max_voter_weight', realm, token_mint] + /// + Withdraw { + #[allow(dead_code)] + seeds: [u8; 32], + }, + + /// Change the destination account of a given simple vesting contract (SVC) /// - can only be invoked by the present destination address of the contract. @@ -107,237 +119,104 @@ pub enum VestingInstruction { /// Accounts expected by this instruction: /// /// * Single owner - /// 0. `[]` The vesting account - /// 1. `[]` The current destination token account - /// 2. `[signer]` The destination spl-token account owner - /// 3. `[]` The new destination spl-token account - ChangeDestination { seeds: [u8; 32] }, -} - -impl VestingInstruction { - pub fn unpack(input: &[u8]) -> Result { - use VestingError::InvalidInstruction; - let (&tag, rest) = input.split_first().ok_or(InvalidInstruction)?; - Ok(match tag { - 0 => { - let seeds: [u8; 32] = rest - .get(..32) - .and_then(|slice| slice.try_into().ok()) - .unwrap(); - let number_of_schedules = rest - .get(32..36) - .and_then(|slice| slice.try_into().ok()) - .map(u32::from_le_bytes) - .ok_or(InvalidInstruction)?; - Self::Init { - seeds, - number_of_schedules, - } - } - 1 => { - let seeds: [u8; 32] = rest - .get(..32) - .and_then(|slice| slice.try_into().ok()) - .unwrap(); - let mint_address = rest - .get(32..64) - .and_then(|slice| slice.try_into().ok()) - .map(Pubkey::new) - .ok_or(InvalidInstruction)?; - let destination_token_address = rest - .get(64..96) - .and_then(|slice| slice.try_into().ok()) - .map(Pubkey::new) - .ok_or(InvalidInstruction)?; - let number_of_schedules = rest[96..].len() / SCHEDULE_SIZE; - let mut schedules: Vec = Vec::with_capacity(number_of_schedules); - let mut offset = 96; - for _ in 0..number_of_schedules { - let release_time = rest - .get(offset..offset + 8) - .and_then(|slice| slice.try_into().ok()) - .map(u64::from_le_bytes) - .ok_or(InvalidInstruction)?; - let amount = rest - .get(offset + 8..offset + 16) - .and_then(|slice| slice.try_into().ok()) - .map(u64::from_le_bytes) - .ok_or(InvalidInstruction)?; - offset += SCHEDULE_SIZE; - schedules.push(Schedule { - release_time, - amount, - }) - } - Self::Create { - seeds, - mint_address, - destination_token_address, - schedules, - } - } - 2 | 3 => { - let seeds: [u8; 32] = rest - .get(..32) - .and_then(|slice| slice.try_into().ok()) - .unwrap(); - match tag { - 2 => Self::Unlock { seeds }, - _ => Self::ChangeDestination { seeds }, - } - } - _ => { - msg!("Unsupported tag"); - return Err(InvalidInstruction.into()); - } - }) - } - - pub fn pack(&self) -> Vec { - let mut buf = Vec::with_capacity(size_of::()); - match self { - &Self::Init { - seeds, - number_of_schedules, - } => { - buf.push(0); - buf.extend_from_slice(&seeds); - buf.extend_from_slice(&number_of_schedules.to_le_bytes()) - } - Self::Create { - seeds, - mint_address, - destination_token_address, - schedules, - } => { - buf.push(1); - buf.extend_from_slice(seeds); - buf.extend_from_slice(&mint_address.to_bytes()); - buf.extend_from_slice(&destination_token_address.to_bytes()); - for s in schedules.iter() { - buf.extend_from_slice(&s.release_time.to_le_bytes()); - buf.extend_from_slice(&s.amount.to_le_bytes()); - } - } - &Self::Unlock { seeds } => { - buf.push(2); - buf.extend_from_slice(&seeds); - } - &Self::ChangeDestination { seeds } => { - buf.push(3); - buf.extend_from_slice(&seeds); - } - }; - buf - } -} - -// Creates a `Init` instruction to create and initialize the vesting token account. -pub fn init( - system_program_id: &Pubkey, - rent_program_id: &Pubkey, - vesting_program_id: &Pubkey, - payer_key: &Pubkey, - vesting_account: &Pubkey, - seeds: [u8; 32], - number_of_schedules: u32, -) -> Result { - let data = VestingInstruction::Init { - seeds, - number_of_schedules, - } - .pack(); - let accounts = vec![ - AccountMeta::new_readonly(*system_program_id, false), - AccountMeta::new_readonly(*rent_program_id, false), - AccountMeta::new(*payer_key, true), - AccountMeta::new(*vesting_account, false), - ]; - Ok(Instruction { - program_id: *vesting_program_id, - accounts, - data, - }) + /// 0. `[writable]` The Vesting account (PDA seeds: [seeds] / [realm, seeds]) + /// 1. `[signer]` The Current Vesting Owner account + /// 2. `[]` The New Vesting Owner account + /// + /// Optional part (vesting for Realm) + /// 3. `[]` The Governance program account + /// 4. `[]` The Realm account + /// 5. `[]` Governing Owner Record. PDA seeds (governance program): ['governance', realm, token_mint, current_vesting_owner] + /// 6. `[writable]` The from VoterWeight Record. PDA seeds: ['voter_weight', realm, token_mint, current_vesting_owner] + /// 7. `[writable]` The to VoterWeight Record. PDA seeds: ['voter_weight', realm, token_mint, new_vesting_owner] + ChangeOwner { + #[allow(dead_code)] + seeds: [u8; 32], + }, } -// Creates a `CreateSchedule` instruction -pub fn create( - vesting_program_id: &Pubkey, +/// Creates a `Deposit` instruction to create and initialize the vesting token account +pub fn deposit( + program_id: &Pubkey, token_program_id: &Pubkey, - vesting_account_key: &Pubkey, - vesting_token_account_key: &Pubkey, - source_token_account_owner_key: &Pubkey, - source_token_account_key: &Pubkey, - destination_token_account_key: &Pubkey, - mint_address: &Pubkey, - schedules: Vec, seeds: [u8; 32], + vesting_token_account: &Pubkey, + source_token_owner: &Pubkey, + source_token_account: &Pubkey, + vesting_owner: &Pubkey, + payer: &Pubkey, + schedules: Vec, ) -> Result { - let data = VestingInstruction::Create { - mint_address: *mint_address, - seeds, - destination_token_address: *destination_token_account_key, - schedules, - } - .pack(); + let vesting_account = Pubkey::create_program_address(&[&seeds], program_id)?; let accounts = vec![ + AccountMeta::new_readonly(system_program::id(), false), AccountMeta::new_readonly(*token_program_id, false), - AccountMeta::new(*vesting_account_key, false), - AccountMeta::new(*vesting_token_account_key, false), - AccountMeta::new_readonly(*source_token_account_owner_key, true), - AccountMeta::new(*source_token_account_key, false), + AccountMeta::new(vesting_account, false), + AccountMeta::new(*vesting_token_account, false), + AccountMeta::new_readonly(*source_token_owner, true), + AccountMeta::new(*source_token_account, false), + AccountMeta::new_readonly(*vesting_owner, false), + AccountMeta::new_readonly(*payer, true), + AccountMeta::new_readonly(sysvar::rent::id(), false), ]; + + let instruction = VestingInstruction::Deposit { seeds, schedules }; + Ok(Instruction { - program_id: *vesting_program_id, + program_id: *program_id, accounts, - data, + data: instruction.try_to_vec().unwrap(), }) } -// Creates an `Unlock` instruction -pub fn unlock( - vesting_program_id: &Pubkey, +/// Creates a `Withdraw` instruction +pub fn withdraw( + program_id: &Pubkey, token_program_id: &Pubkey, - clock_sysvar_id: &Pubkey, - vesting_account_key: &Pubkey, - vesting_token_account_key: &Pubkey, - destination_token_account_key: &Pubkey, seeds: [u8; 32], + vesting_token_account: &Pubkey, + destination_token_account: &Pubkey, + vesting_owner: &Pubkey, ) -> Result { - let data = VestingInstruction::Unlock { seeds }.pack(); + let vesting_account = Pubkey::create_program_address(&[&seeds], program_id)?; let accounts = vec![ AccountMeta::new_readonly(*token_program_id, false), - AccountMeta::new_readonly(*clock_sysvar_id, false), - AccountMeta::new(*vesting_account_key, false), - AccountMeta::new(*vesting_token_account_key, false), - AccountMeta::new(*destination_token_account_key, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new(vesting_account, false), + AccountMeta::new(*vesting_token_account, false), + AccountMeta::new(*destination_token_account, false), + AccountMeta::new_readonly(*vesting_owner, true), ]; + + let instruction = VestingInstruction::Withdraw { seeds }; + Ok(Instruction { - program_id: *vesting_program_id, + program_id: *program_id, accounts, - data, + data: instruction.try_to_vec().unwrap(), }) } -pub fn change_destination( - vesting_program_id: &Pubkey, - vesting_account_key: &Pubkey, - current_destination_token_account_owner: &Pubkey, - current_destination_token_account: &Pubkey, - target_destination_token_account: &Pubkey, +/// Creates a `Withdraw` instruction +pub fn change_owner( + program_id: &Pubkey, seeds: [u8; 32], + vesting_owner: &Pubkey, + new_vesting_owner: &Pubkey, ) -> Result { - let data = VestingInstruction::ChangeDestination { seeds }.pack(); + let vesting_account = Pubkey::create_program_address(&[&seeds], program_id)?; let accounts = vec![ - AccountMeta::new(*vesting_account_key, false), - AccountMeta::new_readonly(*current_destination_token_account, false), - AccountMeta::new_readonly(*current_destination_token_account_owner, true), - AccountMeta::new_readonly(*target_destination_token_account, false), + AccountMeta::new(vesting_account, false), + AccountMeta::new(*vesting_owner, true), + AccountMeta::new(*new_vesting_owner, false), ]; + + let instruction = VestingInstruction::ChangeOwner { seeds }; + Ok(Instruction { - program_id: *vesting_program_id, + program_id: *program_id, accounts, - data, + data: instruction.try_to_vec().unwrap(), }) } @@ -347,41 +226,30 @@ mod test { #[test] fn test_instruction_packing() { - let mint_address = Pubkey::new_unique(); - let destination_token_address = Pubkey::new_unique(); - - let original_create = VestingInstruction::Create { + let original_deposit = VestingInstruction::Deposit { seeds: [50u8; 32], - schedules: vec![Schedule { + schedules: vec![VestingSchedule { amount: 42, release_time: 250, }], - mint_address: mint_address.clone(), - destination_token_address, }; - let packed_create = original_create.pack(); - let unpacked_create = VestingInstruction::unpack(&packed_create).unwrap(); - assert_eq!(original_create, unpacked_create); - - let original_unlock = VestingInstruction::Unlock { seeds: [50u8; 32] }; assert_eq!( - original_unlock, - VestingInstruction::unpack(&original_unlock.pack()).unwrap() + original_deposit, + VestingInstruction::try_from_slice(&original_deposit.try_to_vec().unwrap()).unwrap() ); - let original_init = VestingInstruction::Init { - number_of_schedules: 42, - seeds: [50u8; 32], - }; + + let original_withdraw = VestingInstruction::Withdraw { seeds: [50u8; 32] }; assert_eq!( - original_init, - VestingInstruction::unpack(&original_init.pack()).unwrap() + original_withdraw, + VestingInstruction::try_from_slice(&original_withdraw.try_to_vec().unwrap()).unwrap() ); - let original_change = VestingInstruction::ChangeDestination { seeds: [50u8; 32] }; + + let original_change = VestingInstruction::ChangeOwner { seeds: [50u8; 32] }; assert_eq!( original_change, - VestingInstruction::unpack(&original_change.pack()).unwrap() + VestingInstruction::try_from_slice(&original_change.try_to_vec().unwrap()).unwrap() ); } } diff --git a/addin-vesting/program/src/processor.rs b/addin-vesting/program/src/processor.rs index b33799a..3523399 100644 --- a/addin-vesting/program/src/processor.rs +++ b/addin-vesting/program/src/processor.rs @@ -1,5 +1,6 @@ use solana_program::{ account_info::{next_account_info, AccountInfo}, + borsh::try_from_slice_unchecked, decode_error::DecodeError, entrypoint::ProgramResult, msg, @@ -9,81 +10,44 @@ use solana_program::{ program_pack::Pack, pubkey::Pubkey, rent::Rent, - system_instruction::create_account, sysvar::{clock::Clock, Sysvar}, }; use num_traits::FromPrimitive; +use borsh::BorshSerialize; use spl_token::{instruction::transfer, state::Account}; +use spl_governance_tools::account::{ + get_account_data, + create_and_serialize_account_signed, +}; use crate::{ error::VestingError, - instruction::{Schedule, VestingInstruction, SCHEDULE_SIZE}, - state::{pack_schedules_into_slice, unpack_schedules, VestingSchedule, VestingScheduleHeader}, + instruction::VestingInstruction, + state::{VestingAccountType, VestingRecord, VestingSchedule}, }; pub struct Processor {} impl Processor { - pub fn process_init( - program_id: &Pubkey, - accounts: &[AccountInfo], - seeds: [u8; 32], - schedules: u32 - ) -> ProgramResult { - let accounts_iter = &mut accounts.iter(); - - let system_program_account = next_account_info(accounts_iter)?; - let rent_sysvar_account = next_account_info(accounts_iter)?; - let payer = next_account_info(accounts_iter)?; - let vesting_account = next_account_info(accounts_iter)?; - - let rent = Rent::from_account_info(rent_sysvar_account)?; - - // Find the non reversible public key for the vesting contract via the seed - let vesting_account_key = Pubkey::create_program_address(&[&seeds], &program_id).unwrap(); - if vesting_account_key != *vesting_account.key { - msg!("Provided vesting account is invalid"); - return Err(ProgramError::InvalidArgument); - } - - let state_size = (schedules as usize) * VestingSchedule::LEN + VestingScheduleHeader::LEN; - - let init_vesting_account = create_account( - &payer.key, - &vesting_account_key, - rent.minimum_balance(state_size), - state_size as u64, - &program_id, - ); - - invoke_signed( - &init_vesting_account, - &[ - system_program_account.clone(), - payer.clone(), - vesting_account.clone(), - ], - &[&[&seeds]], - )?; - Ok(()) - } - pub fn process_create( + pub fn process_deposit( program_id: &Pubkey, accounts: &[AccountInfo], seeds: [u8; 32], - mint_address: &Pubkey, - destination_token_address: &Pubkey, - schedules: Vec, + schedules: Vec, ) -> ProgramResult { let accounts_iter = &mut accounts.iter(); + let system_program_account = next_account_info(accounts_iter)?; let spl_token_account = next_account_info(accounts_iter)?; let vesting_account = next_account_info(accounts_iter)?; let vesting_token_account = next_account_info(accounts_iter)?; let source_token_account_owner = next_account_info(accounts_iter)?; let source_token_account = next_account_info(accounts_iter)?; + let vesting_owner_account = next_account_info(accounts_iter)?; + let payer_account = next_account_info(accounts_iter)?; + let rent_sysvar_info = next_account_info(accounts_iter)?; let vesting_account_key = Pubkey::create_program_address(&[&seeds], program_id)?; if vesting_account_key != *vesting_account.key { @@ -96,17 +60,8 @@ impl Processor { return Err(ProgramError::InvalidArgument); } - if *vesting_account.owner != *program_id { - msg!("Program should own vesting account"); - return Err(ProgramError::InvalidArgument); - } - - // Verifying that no SVC was already created with this seed - let is_initialized = - vesting_account.try_borrow_data()?[VestingScheduleHeader::LEN - 1] == 1; - - if is_initialized { - msg!("Cannot overwrite an existing vesting contract."); + if !vesting_account.data_is_empty() { + msg!("Vesting account already exists"); return Err(ProgramError::InvalidArgument); } @@ -127,35 +82,29 @@ impl Processor { return Err(ProgramError::InvalidAccountData); } - let state_header = VestingScheduleHeader { - destination_address: *destination_token_address, - mint_address: *mint_address, - is_initialized: true, - }; - - let mut data = vesting_account.data.borrow_mut(); - if data.len() != VestingScheduleHeader::LEN + schedules.len() * VestingSchedule::LEN { - return Err(ProgramError::InvalidAccountData) - } - state_header.pack_into_slice(&mut data); - - let mut offset = VestingScheduleHeader::LEN; let mut total_amount: u64 = 0; - for s in schedules.iter() { - let state_schedule = VestingSchedule { - release_time: s.release_time, - amount: s.amount, - }; - state_schedule.pack_into_slice(&mut data[offset..]); - let delta = total_amount.checked_add(s.amount); - match delta { - Some(n) => total_amount = n, - None => return Err(ProgramError::InvalidInstructionData), // Total amount overflows u64 - } - offset += SCHEDULE_SIZE; + total_amount = total_amount.checked_add(s.amount).ok_or_else(|| ProgramError::InvalidInstructionData)?; } + let rent = &Rent::from_account_info(rent_sysvar_info)?; + let vesting_record = VestingRecord { + account_type: VestingAccountType::VestingRecord, + owner: *vesting_owner_account.key, + mint: vesting_token_account_data.mint, + realm: None, + schedule: schedules + }; + create_and_serialize_account_signed::( + payer_account, + vesting_account, + &vesting_record, + &[&seeds[..31]], + program_id, + system_program_account, + rent + )?; + if Account::unpack(&source_token_account.data.borrow())?.amount < total_amount { msg!("The source token account has insufficient funds."); return Err(ProgramError::InsufficientFunds) @@ -182,7 +131,7 @@ impl Processor { Ok(()) } - pub fn process_unlock( + pub fn process_withdraw( program_id: &Pubkey, _accounts: &[AccountInfo], seeds: [u8; 32], @@ -194,6 +143,7 @@ impl Processor { let vesting_account = next_account_info(accounts_iter)?; let vesting_token_account = next_account_info(accounts_iter)?; let destination_token_account = next_account_info(accounts_iter)?; + let vesting_owner_account = next_account_info(accounts_iter)?; let vesting_account_key = Pubkey::create_program_address(&[&seeds], program_id)?; if vesting_account_key != *vesting_account.key { @@ -206,17 +156,19 @@ impl Processor { return Err(ProgramError::InvalidArgument) } - let packed_state = &vesting_account.data; - let header_state = - VestingScheduleHeader::unpack(&packed_state.borrow()[..VestingScheduleHeader::LEN])?; + let mut vesting_record = get_account_data::(&program_id, vesting_account)?; - if header_state.destination_address != *destination_token_account.key { - msg!("Contract destination account does not matched provided account"); + if !vesting_owner_account.is_signer { + msg!("Vesting owner should be a signer"); return Err(ProgramError::InvalidArgument); } - let vesting_token_account_data = Account::unpack(&vesting_token_account.data.borrow())?; + if vesting_record.owner != *vesting_owner_account.key { + msg!("Vesting owner does not matched provided account"); + return Err(ProgramError::InvalidArgument); + } + let vesting_token_account_data = Account::unpack(&vesting_token_account.data.borrow())?; if vesting_token_account_data.owner != vesting_account_key { msg!("The vesting token account should be owned by the vesting account."); return Err(ProgramError::InvalidArgument); @@ -225,9 +177,7 @@ impl Processor { // Unlock the schedules that have reached maturity let clock = Clock::from_account_info(&clock_sysvar_account)?; let mut total_amount_to_transfer = 0; - let mut schedules = unpack_schedules(&packed_state.borrow()[VestingScheduleHeader::LEN..])?; - - for s in schedules.iter_mut() { + for s in vesting_record.schedule.iter_mut() { if clock.unix_timestamp as u64 >= s.release_time { total_amount_to_transfer += s.amount; s.amount = 0; @@ -259,15 +209,12 @@ impl Processor { )?; // Reset released amounts to 0. This makes the simple unlock safe with complex scheduling contracts - pack_schedules_into_slice( - schedules, - &mut packed_state.borrow_mut()[VestingScheduleHeader::LEN..], - ); + vesting_record.serialize(&mut *vesting_account.data.borrow_mut())?; Ok(()) } - pub fn process_change_destination( + pub fn process_change_owner( program_id: &Pubkey, accounts: &[AccountInfo], seeds: [u8; 32], @@ -275,44 +222,31 @@ impl Processor { let accounts_iter = &mut accounts.iter(); let vesting_account = next_account_info(accounts_iter)?; - let destination_token_account = next_account_info(accounts_iter)?; - let destination_token_account_owner = next_account_info(accounts_iter)?; - let new_destination_token_account = next_account_info(accounts_iter)?; + let vesting_owner_account = next_account_info(accounts_iter)?; + let new_vesting_owner_account = next_account_info(accounts_iter)?; - if vesting_account.data.borrow().len() < VestingScheduleHeader::LEN { - return Err(ProgramError::InvalidAccountData) - } - let vesting_account_key = Pubkey::create_program_address(&[&seeds], program_id)?; - let state = VestingScheduleHeader::unpack( - &vesting_account.data.borrow()[..VestingScheduleHeader::LEN], - )?; + msg!("Change owner {} -> {}", vesting_owner_account.key, new_vesting_owner_account.key); + let vesting_account_key = Pubkey::create_program_address(&[&seeds], program_id)?; if vesting_account_key != *vesting_account.key { msg!("Invalid vesting account key"); return Err(ProgramError::InvalidArgument); } - if state.destination_address != *destination_token_account.key { - msg!("Contract destination account does not matched provided account"); - return Err(ProgramError::InvalidArgument); - } + let mut vesting_record = get_account_data::(&program_id, vesting_account)?; - if !destination_token_account_owner.is_signer { - msg!("Destination token account owner should be a signer."); + if vesting_record.owner != *vesting_owner_account.key { + msg!("Vesting owner account does not matched provided account"); return Err(ProgramError::InvalidArgument); } - let destination_token_account = Account::unpack(&destination_token_account.data.borrow())?; - - if destination_token_account.owner != *destination_token_account_owner.key { - msg!("The current destination token account isn't owned by the provided owner"); + if !vesting_owner_account.is_signer { + msg!("Vesting owner account should be a signer."); return Err(ProgramError::InvalidArgument); } - let mut new_state = state; - new_state.destination_address = *new_destination_token_account.key; - new_state - .pack_into_slice(&mut vesting_account.data.borrow_mut()[..VestingScheduleHeader::LEN]); + vesting_record.owner = *new_vesting_owner_account.key; + vesting_record.serialize(&mut *vesting_account.data.borrow_mut())?; Ok(()) } @@ -322,40 +256,20 @@ impl Processor { accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { - msg!("Beginning processing"); - let instruction = VestingInstruction::unpack(instruction_data)?; - msg!("Instruction unpacked"); + msg!("VERSION:{:?}", env!("CARGO_PKG_VERSION")); + let instruction: VestingInstruction = + try_from_slice_unchecked(instruction_data).map_err(|_| ProgramError::InvalidInstructionData)?; + msg!("VESTING-INSTRUCTION: {:?}", instruction); + match instruction { - VestingInstruction::Init { - seeds, - number_of_schedules, - } => { - msg!("Instruction: Init"); - Self::process_init(program_id, accounts, seeds, number_of_schedules) - } - VestingInstruction::Unlock { seeds } => { - msg!("Instruction: Unlock"); - Self::process_unlock(program_id, accounts, seeds) + VestingInstruction::Deposit {seeds, schedules} => { + Self::process_deposit(program_id, accounts, seeds, schedules) } - VestingInstruction::ChangeDestination { seeds } => { - msg!("Instruction: Change Destination"); - Self::process_change_destination(program_id, accounts, seeds) + VestingInstruction::Withdraw {seeds} => { + Self::process_withdraw(program_id, accounts, seeds) } - VestingInstruction::Create { - seeds, - mint_address, - destination_token_address, - schedules, - } => { - msg!("Instruction: Create Schedule"); - Self::process_create( - program_id, - accounts, - seeds, - &mint_address, - &destination_token_address, - schedules, - ) + VestingInstruction::ChangeOwner {seeds} => { + Self::process_change_owner(program_id, accounts, seeds) } } } diff --git a/addin-vesting/program/src/state.rs b/addin-vesting/program/src/state.rs index b59605e..2446ec3 100644 --- a/addin-vesting/program/src/state.rs +++ b/addin-vesting/program/src/state.rs @@ -1,160 +1,85 @@ use solana_program::{ - program_error::ProgramError, - program_pack::{IsInitialized, Pack, Sealed}, + program_pack::IsInitialized, pubkey::Pubkey, }; +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use spl_governance_tools::account::AccountMaxSize; -use std::convert::TryInto; -#[derive(Debug, PartialEq)] +#[repr(C)] +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] +pub enum VestingAccountType { + /// Default uninitialized state + Unitialized, + + /// Vesting info account + VestingRecord, +} + +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct VestingSchedule { pub release_time: u64, pub amount: u64, } -#[derive(Debug, PartialEq)] -pub struct VestingScheduleHeader { - pub destination_address: Pubkey, - pub mint_address: Pubkey, - pub is_initialized: bool, +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] +pub struct VestingRecord { + pub account_type: VestingAccountType, + pub owner: Pubkey, + pub mint: Pubkey, + pub realm: Option, + pub schedule: Vec, } -impl Sealed for VestingScheduleHeader {} - -impl Pack for VestingScheduleHeader { - const LEN: usize = 65; - - fn pack_into_slice(&self, target: &mut [u8]) { - let destination_address_bytes = self.destination_address.to_bytes(); - let mint_address_bytes = self.mint_address.to_bytes(); - for i in 0..32 { - target[i] = destination_address_bytes[i]; - } - - for i in 32..64 { - target[i] = mint_address_bytes[i - 32]; - } - - target[64] = self.is_initialized as u8; - } - - fn unpack_from_slice(src: &[u8]) -> Result { - if src.len() < 65 { - return Err(ProgramError::InvalidAccountData) - } - let destination_address = Pubkey::new(&src[..32]); - let mint_address = Pubkey::new(&src[32..64]); - let is_initialized = src[64] == 1; - Ok(Self { - destination_address, - mint_address, - is_initialized, - }) - } -} - -impl Sealed for VestingSchedule {} - -impl Pack for VestingSchedule { - const LEN: usize = 16; - - fn pack_into_slice(&self, dst: &mut [u8]) { - let release_time_bytes = self.release_time.to_le_bytes(); - let amount_bytes = self.amount.to_le_bytes(); - for i in 0..8 { - dst[i] = release_time_bytes[i]; - } - - for i in 8..16 { - dst[i] = amount_bytes[i - 8]; - } - } - - fn unpack_from_slice(src: &[u8]) -> Result { - if src.len() < 16 { - return Err(ProgramError::InvalidAccountData) - } - let release_time = u64::from_le_bytes(src[0..8].try_into().unwrap()); - let amount = u64::from_le_bytes(src[8..16].try_into().unwrap()); - Ok(Self { - release_time, - amount, - }) - } -} - -impl IsInitialized for VestingScheduleHeader { +impl IsInitialized for VestingRecord { fn is_initialized(&self) -> bool { - self.is_initialized - } -} - -pub fn unpack_schedules(input: &[u8]) -> Result, ProgramError> { - let number_of_schedules = input.len() / VestingSchedule::LEN; - let mut output: Vec = Vec::with_capacity(number_of_schedules); - let mut offset = 0; - for _ in 0..number_of_schedules { - output.push(VestingSchedule::unpack_from_slice( - &input[offset..offset + VestingSchedule::LEN], - )?); - offset += VestingSchedule::LEN; + self.account_type == VestingAccountType::VestingRecord } - Ok(output) } -pub fn pack_schedules_into_slice(schedules: Vec, target: &mut [u8]) { - let mut offset = 0; - for s in schedules.iter() { - s.pack_into_slice(&mut target[offset..]); - offset += VestingSchedule::LEN; - } -} +impl AccountMaxSize for VestingRecord {} #[cfg(test)] mod tests { - use super::{unpack_schedules, VestingSchedule, VestingScheduleHeader}; - use solana_program::{program_pack::Pack, pubkey::Pubkey}; + use super::*; + use solana_program::pubkey::Pubkey; + use solana_program::account_info::AccountInfo; + use spl_governance_tools::account::get_account_data; + use solana_program::clock::Epoch; #[test] - fn test_state_packing() { - let header_state = VestingScheduleHeader { - destination_address: Pubkey::new_unique(), - mint_address: Pubkey::new_unique(), - is_initialized: true, + fn test_vesting_record_packing() { + let vesting_record_source = VestingRecord { + account_type: VestingAccountType::VestingRecord, + owner: Pubkey::new_unique(), + mint: Pubkey::new_unique(), + realm: Some(Pubkey::new_unique()), + schedule: vec!( + VestingSchedule {release_time: 30767976, amount: 969}, + VestingSchedule {release_time: 32767076, amount: 420}, + ), }; - let schedule_state_0 = VestingSchedule { - release_time: 30767976, - amount: 969, - }; - let schedule_state_1 = VestingSchedule { - release_time: 32767076, - amount: 420, - }; - let state_size = VestingScheduleHeader::LEN + 2 * VestingSchedule::LEN; - let mut state_array = [0u8; 97]; - header_state.pack_into_slice(&mut state_array[..VestingScheduleHeader::LEN]); - schedule_state_0.pack_into_slice( - &mut state_array - [VestingScheduleHeader::LEN..VestingScheduleHeader::LEN + VestingSchedule::LEN], - ); - schedule_state_1 - .pack_into_slice(&mut state_array[VestingScheduleHeader::LEN + VestingSchedule::LEN..]); - let packed = Vec::from(state_array); - let mut expected = Vec::with_capacity(state_size); - expected.extend_from_slice(&header_state.destination_address.to_bytes()); - expected.extend_from_slice(&header_state.mint_address.to_bytes()); - expected.extend_from_slice(&[header_state.is_initialized as u8]); - expected.extend_from_slice(&schedule_state_0.release_time.to_le_bytes()); - expected.extend_from_slice(&schedule_state_0.amount.to_le_bytes()); - expected.extend_from_slice(&schedule_state_1.release_time.to_le_bytes()); - expected.extend_from_slice(&schedule_state_1.amount.to_le_bytes()); - assert_eq!(expected, packed); - assert_eq!(packed.len(), state_size); - let unpacked_header = - VestingScheduleHeader::unpack(&packed[..VestingScheduleHeader::LEN]).unwrap(); - assert_eq!(unpacked_header, header_state); - let unpacked_schedules = unpack_schedules(&packed[VestingScheduleHeader::LEN..]).unwrap(); - assert_eq!(unpacked_schedules[0], schedule_state_0); - assert_eq!(unpacked_schedules[1], schedule_state_1); + let mut vesting_data = vesting_record_source.try_to_vec().unwrap(); + println!("UNPACKED: {:?}", vesting_record_source); + println!("PACKED: {}", hex::encode(&vesting_data)); + + let program_id = Pubkey::new_unique(); + + let info_key = Pubkey::new_unique(); + let mut lamports = 10u64; + + let account_info = AccountInfo::new( + &info_key, + false, + false, + &mut lamports, + &mut vesting_data[..], + &program_id, + false, + Epoch::default(), + ); + let vesting_record_target = get_account_data::(&program_id, &account_info).unwrap(); + assert_eq!(vesting_record_source, vesting_record_target); } + } diff --git a/addin-vesting/program/tests/functional.rs b/addin-vesting/program/tests/functional.rs index 3038f1f..485ae10 100644 --- a/addin-vesting/program/tests/functional.rs +++ b/addin-vesting/program/tests/functional.rs @@ -1,16 +1,22 @@ #![cfg(feature = "test-bpf")] use std::str::FromStr; -use solana_program::{hash::Hash, +use solana_program::{ + borsh::try_from_slice_unchecked, + hash::Hash, pubkey::Pubkey, rent::Rent, - sysvar, - system_program }; use solana_program_test::{processor, ProgramTest}; -use solana_sdk::{account::Account, keyed_account, signature::Keypair, signature::Signer, system_instruction, transaction::Transaction}; -use token_vesting::{entrypoint::process_instruction, instruction::Schedule}; -use token_vesting::instruction::{init, unlock, change_destination, create}; +use solana_sdk::{ + account::Account, + signature::Keypair, + signature::Signer, + system_instruction, + transaction::Transaction, +}; +use token_vesting::{entrypoint::process_instruction, state::VestingSchedule, state::VestingRecord}; +use token_vesting::instruction::{deposit, withdraw, change_owner}; use spl_token::{self, instruction::{initialize_mint, initialize_account, mint_to}}; #[tokio::test] @@ -52,28 +58,9 @@ async fn test_token_vesting() { // Start and process transactions on the test network let (mut banks_client, payer, recent_blockhash) = program_test.start().await; - - // Initialize the vesting program account - let init_instruction = [init( - &system_program::id(), - &sysvar::rent::id(), - &program_id, - &payer.pubkey(), - &vesting_account_key, - seeds, - 3 - ).unwrap() - ]; - let mut init_transaction = Transaction::new_with_payer( - &init_instruction, - Some(&payer.pubkey()), - ); - init_transaction.partial_sign( - &[&payer], - recent_blockhash - ); - banks_client.process_transaction(init_transaction).await.unwrap(); - + //let mut context = program_test.start_with_context().await; + //let payer = &context.payer; + //let recent_blockhash = context.last_blockhash; // Initialize the token accounts banks_client.process_transaction(mint_init_transaction( @@ -105,96 +92,88 @@ async fn test_token_vesting() { &source_token_account.pubkey(), &mint_authority.pubkey(), &[], - 100 + 60 ).unwrap() ]; + + // Process transaction on test network + let mut setup_transaction = Transaction::new_with_payer( + &setup_instructions, + Some(&payer.pubkey()), + ); + setup_transaction.partial_sign(&[&payer, &mint_authority], recent_blockhash); + banks_client.process_transaction(setup_transaction).await.unwrap(); let schedules = vec![ - Schedule {amount: 20, release_time: 0}, - Schedule {amount: 20, release_time: 2}, - Schedule {amount: 20, release_time: 5} + VestingSchedule {amount: 20, release_time: 0}, + VestingSchedule {amount: 20, release_time: 2}, + VestingSchedule {amount: 20, release_time: 5} ]; - let test_instructions = [ - create( + let deposit_instructions = [ + deposit( &program_id, &spl_token::id(), - &vesting_account_key, + seeds.clone(), &vesting_token_account.pubkey(), &source_account.pubkey(), &source_token_account.pubkey(), - &destination_token_account.pubkey(), - &mint.pubkey(), + &destination_account.pubkey(), + &payer.pubkey(), schedules, - seeds.clone() ).unwrap(), - unlock( - &program_id, - &spl_token::id(), - &sysvar::clock::id(), - &vesting_account_key, - &vesting_token_account.pubkey(), - &destination_token_account.pubkey(), - seeds.clone() - ).unwrap() ]; + // Process transaction on test network + let mut deposit_transaction = Transaction::new_with_payer( + &deposit_instructions, + Some(&payer.pubkey()), + ); + deposit_transaction.partial_sign(&[&payer, &source_account], recent_blockhash); + banks_client.process_transaction(deposit_transaction).await.unwrap(); + - let change_destination_instructions = [ - change_destination( + let change_owner_instructions = [ + change_owner( &program_id, - &vesting_account_key, + seeds.clone(), &destination_account.pubkey(), - &destination_token_account.pubkey(), - &new_destination_token_account.pubkey(), - seeds.clone() - ).unwrap() + &new_destination_account.pubkey(), + ).unwrap(), ]; - - // Process transaction on test network - let mut setup_transaction = Transaction::new_with_payer( - &setup_instructions, + let mut change_owner_transaction = Transaction::new_with_payer( + &change_owner_instructions, Some(&payer.pubkey()), ); - setup_transaction.partial_sign( - &[ - &payer, - &mint_authority - ], - recent_blockhash - ); - - banks_client.process_transaction(setup_transaction).await.unwrap(); + change_owner_transaction.partial_sign(&[&payer, &destination_account], recent_blockhash); + banks_client.process_transaction(change_owner_transaction).await.unwrap(); - // Process transaction on test network - let mut test_transaction = Transaction::new_with_payer( - &test_instructions, + + let withdraw_instrictions = [ + withdraw( + &program_id, + &spl_token::id(), + seeds.clone(), + &vesting_token_account.pubkey(), + &destination_token_account.pubkey(), + &new_destination_account.pubkey(), + ).unwrap(), + ]; + + let mut withdraw_transaction = Transaction::new_with_payer( + &withdraw_instrictions, Some(&payer.pubkey()), ); - test_transaction.partial_sign( - &[ - &payer, - &source_account - ], - recent_blockhash - ); - - banks_client.process_transaction(test_transaction).await.unwrap(); - - let mut change_destination_transaction = Transaction::new_with_payer( - &change_destination_instructions, - Some(&payer.pubkey()) - ); + withdraw_transaction.partial_sign(&[&payer, &new_destination_account], recent_blockhash); + banks_client.process_transaction(withdraw_transaction).await.unwrap(); - change_destination_transaction.partial_sign( - &[ - &payer, - &destination_account - ], - recent_blockhash - ); - banks_client.process_transaction(change_destination_transaction).await.unwrap(); - + let acc = banks_client.get_account(vesting_account_key).await.unwrap(); + println!("Vesting: {:?}", acc); + + if let Some(a) = acc { + let acc_record: VestingRecord = try_from_slice_unchecked(&a.data).unwrap(); + println!(" {:?}", acc_record); + } } fn mint_init_transaction( @@ -267,4 +246,4 @@ fn create_token_account( recent_blockhash ); transaction -} \ No newline at end of file +}