From 96dd044f8e72b6e8fd64ec7c0f2b7255146518fd Mon Sep 17 00:00:00 2001 From: Greg Fitzgerald Date: Wed, 20 Nov 2019 19:33:17 -0700 Subject: [PATCH] Allow vest's terminator to recapture tokens (#7071) * Allow vest's terminator to recapture tokens * Less code * Add a VestAll instruction The terminator may decide it's impractical to maintain a vest contract and want to make all tokens immediately redeemable. --- programs/vest/src/vest_instruction.rs | 27 ++++++++ programs/vest/src/vest_processor.rs | 63 ++++++++++++++++- programs/vest/src/vest_state.rs | 99 ++++++++++++++++++++++++--- 3 files changed, 175 insertions(+), 14 deletions(-) diff --git a/programs/vest/src/vest_instruction.rs b/programs/vest/src/vest_instruction.rs index a8abbcf7ff..ba3a013987 100644 --- a/programs/vest/src/vest_instruction.rs +++ b/programs/vest/src/vest_instruction.rs @@ -52,6 +52,14 @@ pub enum VestInstruction { /// Tell the contract that the `InitializeAccount` with `Signature` has been /// signed by the containing transaction's `Pubkey`. Terminate, + + /// Reduce total_lamports by the given number of lamports. Tokens that have + /// already vested are unaffected. Use this instead of `Terminate` to minimize + /// the number of token transfers. + Renege(u64), + + /// Mark all available tokens as redeemable, regardless of the date. + VestAll, } fn initialize_account( @@ -138,3 +146,22 @@ pub fn terminate(contract: &Pubkey, from: &Pubkey, to: &Pubkey) -> Instruction { } Instruction::new(id(), &VestInstruction::Terminate, account_metas) } + +pub fn renege(contract: &Pubkey, from: &Pubkey, to: &Pubkey, lamports: u64) -> Instruction { + let mut account_metas = vec![ + AccountMeta::new(*contract, false), + AccountMeta::new(*from, true), + ]; + if from != to { + account_metas.push(AccountMeta::new(*to, false)); + } + Instruction::new(id(), &VestInstruction::Renege(lamports), account_metas) +} + +pub fn vest_all(contract: &Pubkey, from: &Pubkey) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*contract, false), + AccountMeta::new(*from, true), + ]; + Instruction::new(id(), &VestInstruction::VestAll, account_metas) +} diff --git a/programs/vest/src/vest_processor.rs b/programs/vest/src/vest_processor.rs index fc1ce422e3..8983c9aa6f 100644 --- a/programs/vest/src/vest_processor.rs +++ b/programs/vest/src/vest_processor.rs @@ -77,7 +77,7 @@ pub fn process_instruction( start_date_time, date_pubkey, total_lamports, - redeemed_lamports: 0, + ..VestState::default() } } else { VestState::deserialize(&contract_account.data)? @@ -110,7 +110,12 @@ pub fn process_instruction( )?; vest_state.redeem_tokens(contract_account, current_date, payee_account); } - VestInstruction::Terminate => { + VestInstruction::Terminate | VestInstruction::Renege(_) => { + let lamports = if let VestInstruction::Renege(lamports) = instruction { + lamports + } else { + contract_account.lamports + }; let terminator_account = verify_signed_account( next_keyed_account(keyed_accounts_iter)?, &vest_state.terminator_pubkey, @@ -121,7 +126,14 @@ pub fn process_instruction( } else { terminator_account }; - vest_state.terminate(contract_account, payee_account); + vest_state.renege(contract_account, payee_account, lamports); + } + VestInstruction::VestAll => { + verify_signed_account( + next_keyed_account(keyed_accounts_iter)?, + &vest_state.terminator_pubkey, + )?; + vest_state.vest_all(); } } @@ -631,4 +643,49 @@ mod tests { ); assert_eq!(bank_client.get_account_data(&bob_pubkey).unwrap(), None); } + + #[test] + fn test_renege_and_send_funds() { + let (bank_client, alice_keypair) = create_bank_client(3); + let alice_pubkey = alice_keypair.pubkey(); + let contract_keypair = Keypair::new(); + let contract_pubkey = contract_keypair.pubkey(); + let bob_pubkey = Pubkey::new_rand(); + let start_date = Utc::now().date(); + + let date_keypair = Keypair::new(); + let date_pubkey = date_keypair.pubkey(); + + let current_date = Utc.ymd(2019, 1, 1); + create_date_account(&bank_client, &date_keypair, &alice_keypair, current_date).unwrap(); + + create_vest_account( + &bank_client, + &contract_keypair, + &alice_keypair, + &alice_pubkey, + &bob_pubkey, + start_date, + &date_pubkey, + 1, + ) + .unwrap(); + assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 1); + assert_eq!(bank_client.get_balance(&contract_pubkey).unwrap(), 1); + + // Now, renege on a token. carol gets it. + let carol_pubkey = Pubkey::new_rand(); + let instruction = + vest_instruction::renege(&contract_pubkey, &alice_pubkey, &carol_pubkey, 1); + bank_client + .send_instruction(&alice_keypair, instruction) + .unwrap(); + assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 1); + assert_eq!(bank_client.get_balance(&carol_pubkey).unwrap(), 1); + assert_eq!( + bank_client.get_account_data(&contract_pubkey).unwrap(), + None + ); + assert_eq!(bank_client.get_account_data(&bob_pubkey).unwrap(), None); + } } diff --git a/programs/vest/src/vest_state.rs b/programs/vest/src/vest_state.rs index 2677dab6e8..b835e63a4b 100644 --- a/programs/vest/src/vest_state.rs +++ b/programs/vest/src/vest_state.rs @@ -8,6 +8,7 @@ use chrono::{ }; use serde_derive::{Deserialize, Serialize}; use solana_sdk::{account::Account, instruction::InstructionError, pubkey::Pubkey}; +use std::cmp::min; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct VestState { @@ -29,6 +30,12 @@ pub struct VestState { /// The number of lamports the payee has already redeemed pub redeemed_lamports: u64, + + /// The number of lamports the terminator repurchased + pub reneged_lamports: u64, + + /// True if the terminator has declared this contract fully vested. + pub is_fully_vested: bool, } impl Default for VestState { @@ -40,6 +47,8 @@ impl Default for VestState { date_pubkey: Pubkey::default(), total_lamports: 0, redeemed_lamports: 0, + reneged_lamports: 0, + is_fully_vested: false, } } } @@ -53,13 +62,12 @@ impl VestState { deserialize(input).map_err(|_| InstructionError::InvalidAccountData) } - /// Redeem vested tokens. - pub fn redeem_tokens( - &mut self, - contract_account: &mut Account, - current_date: Date, - payee_account: &mut Account, - ) { + fn calc_vested_lamports(&self, current_date: Date) -> u64 { + let total_lamports_after_reneged = self.total_lamports - self.reneged_lamports; + if self.is_fully_vested { + return total_lamports_after_reneged; + } + let schedule = create_vesting_schedule(self.start_date_time.date(), self.total_lamports); let vested_lamports = schedule @@ -68,6 +76,17 @@ impl VestState { .map(|(_, lamports)| lamports) .sum::(); + min(vested_lamports, total_lamports_after_reneged) + } + + /// Redeem vested tokens. + pub fn redeem_tokens( + &mut self, + contract_account: &mut Account, + current_date: Date, + payee_account: &mut Account, + ) { + let vested_lamports = self.calc_vested_lamports(current_date); let redeemable_lamports = vested_lamports.saturating_sub(self.redeemed_lamports); contract_account.lamports -= redeemable_lamports; @@ -76,10 +95,23 @@ impl VestState { self.redeemed_lamports += redeemable_lamports; } - /// Terminate the contract and return all tokens to the given pubkey. - pub fn terminate(&mut self, contract_account: &mut Account, payee_account: &mut Account) { - payee_account.lamports += contract_account.lamports; - contract_account.lamports = 0; + /// Renege on the given number of tokens and send them to the given payee. + pub fn renege( + &mut self, + contract_account: &mut Account, + payee_account: &mut Account, + lamports: u64, + ) { + let reneged_lamports = min(contract_account.lamports, lamports); + payee_account.lamports += reneged_lamports; + contract_account.lamports -= reneged_lamports; + + self.reneged_lamports += reneged_lamports; + } + + /// Mark this contract as fully vested, regardless of the date. + pub fn vest_all(&mut self) { + self.is_fully_vested = true; } } @@ -88,6 +120,7 @@ mod test { use super::*; use crate::id; use solana_sdk::account::Account; + use solana_sdk::system_program; #[test] fn test_serializer() { @@ -107,4 +140,48 @@ mod test { Err(InstructionError::AccountDataTooSmall) ); } + + #[test] + fn test_schedule_after_renege() { + let total_lamports = 3; + let mut contract_account = Account::new(total_lamports, 512, &id()); + let mut payee_account = Account::new(0, 0, &system_program::id()); + let mut vest_state = VestState { + total_lamports, + start_date_time: Utc.ymd(2019, 1, 1).and_hms(0, 0, 0), + ..VestState::default() + }; + vest_state.serialize(&mut contract_account.data).unwrap(); + let current_date = Utc.ymd(2020, 1, 1); + assert_eq!(vest_state.calc_vested_lamports(current_date), 1); + + // Verify vesting schedule is calculated with original amount. + vest_state.renege(&mut contract_account, &mut payee_account, 1); + assert_eq!(vest_state.calc_vested_lamports(current_date), 1); + assert_eq!(vest_state.reneged_lamports, 1); + + // Verify reneged tokens aren't redeemable. + assert_eq!(vest_state.calc_vested_lamports(Utc.ymd(2022, 1, 1)), 2); + + // Verify reneged tokens aren't redeemable after fully vesting. + vest_state.vest_all(); + assert_eq!(vest_state.calc_vested_lamports(Utc.ymd(2022, 1, 1)), 2); + } + + #[test] + fn test_vest_all() { + let total_lamports = 3; + let mut contract_account = Account::new(total_lamports, 512, &id()); + let mut vest_state = VestState { + total_lamports, + start_date_time: Utc.ymd(2019, 1, 1).and_hms(0, 0, 0), + ..VestState::default() + }; + vest_state.serialize(&mut contract_account.data).unwrap(); + let current_date = Utc.ymd(2020, 1, 1); + assert_eq!(vest_state.calc_vested_lamports(current_date), 1); + + vest_state.vest_all(); + assert_eq!(vest_state.calc_vested_lamports(current_date), 3); + } }