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.
This commit is contained in:
		@@ -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)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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<Utc>,
 | 
			
		||||
        payee_account: &mut Account,
 | 
			
		||||
    ) {
 | 
			
		||||
    fn calc_vested_lamports(&self, current_date: Date<Utc>) -> 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::<u64>();
 | 
			
		||||
 | 
			
		||||
        min(vested_lamports, total_lamports_after_reneged)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Redeem vested tokens.
 | 
			
		||||
    pub fn redeem_tokens(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        contract_account: &mut Account,
 | 
			
		||||
        current_date: Date<Utc>,
 | 
			
		||||
        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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user