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
 | 
					    /// Tell the contract that the `InitializeAccount` with `Signature` has been
 | 
				
			||||||
    /// signed by the containing transaction's `Pubkey`.
 | 
					    /// signed by the containing transaction's `Pubkey`.
 | 
				
			||||||
    Terminate,
 | 
					    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(
 | 
					fn initialize_account(
 | 
				
			||||||
@@ -138,3 +146,22 @@ pub fn terminate(contract: &Pubkey, from: &Pubkey, to: &Pubkey) -> Instruction {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    Instruction::new(id(), &VestInstruction::Terminate, account_metas)
 | 
					    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,
 | 
					            start_date_time,
 | 
				
			||||||
            date_pubkey,
 | 
					            date_pubkey,
 | 
				
			||||||
            total_lamports,
 | 
					            total_lamports,
 | 
				
			||||||
            redeemed_lamports: 0,
 | 
					            ..VestState::default()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        VestState::deserialize(&contract_account.data)?
 | 
					        VestState::deserialize(&contract_account.data)?
 | 
				
			||||||
@@ -110,7 +110,12 @@ pub fn process_instruction(
 | 
				
			|||||||
            )?;
 | 
					            )?;
 | 
				
			||||||
            vest_state.redeem_tokens(contract_account, current_date, payee_account);
 | 
					            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(
 | 
					            let terminator_account = verify_signed_account(
 | 
				
			||||||
                next_keyed_account(keyed_accounts_iter)?,
 | 
					                next_keyed_account(keyed_accounts_iter)?,
 | 
				
			||||||
                &vest_state.terminator_pubkey,
 | 
					                &vest_state.terminator_pubkey,
 | 
				
			||||||
@@ -121,7 +126,14 @@ pub fn process_instruction(
 | 
				
			|||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                terminator_account
 | 
					                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);
 | 
					        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 serde_derive::{Deserialize, Serialize};
 | 
				
			||||||
use solana_sdk::{account::Account, instruction::InstructionError, pubkey::Pubkey};
 | 
					use solana_sdk::{account::Account, instruction::InstructionError, pubkey::Pubkey};
 | 
				
			||||||
 | 
					use std::cmp::min;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
 | 
					#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
 | 
				
			||||||
pub struct VestState {
 | 
					pub struct VestState {
 | 
				
			||||||
@@ -29,6 +30,12 @@ pub struct VestState {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    /// The number of lamports the payee has already redeemed
 | 
					    /// The number of lamports the payee has already redeemed
 | 
				
			||||||
    pub redeemed_lamports: u64,
 | 
					    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 {
 | 
					impl Default for VestState {
 | 
				
			||||||
@@ -40,6 +47,8 @@ impl Default for VestState {
 | 
				
			|||||||
            date_pubkey: Pubkey::default(),
 | 
					            date_pubkey: Pubkey::default(),
 | 
				
			||||||
            total_lamports: 0,
 | 
					            total_lamports: 0,
 | 
				
			||||||
            redeemed_lamports: 0,
 | 
					            redeemed_lamports: 0,
 | 
				
			||||||
 | 
					            reneged_lamports: 0,
 | 
				
			||||||
 | 
					            is_fully_vested: false,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -53,13 +62,12 @@ impl VestState {
 | 
				
			|||||||
        deserialize(input).map_err(|_| InstructionError::InvalidAccountData)
 | 
					        deserialize(input).map_err(|_| InstructionError::InvalidAccountData)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Redeem vested tokens.
 | 
					    fn calc_vested_lamports(&self, current_date: Date<Utc>) -> u64 {
 | 
				
			||||||
    pub fn redeem_tokens(
 | 
					        let total_lamports_after_reneged = self.total_lamports - self.reneged_lamports;
 | 
				
			||||||
        &mut self,
 | 
					        if self.is_fully_vested {
 | 
				
			||||||
        contract_account: &mut Account,
 | 
					            return total_lamports_after_reneged;
 | 
				
			||||||
        current_date: Date<Utc>,
 | 
					        }
 | 
				
			||||||
        payee_account: &mut Account,
 | 
					
 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
        let schedule = create_vesting_schedule(self.start_date_time.date(), self.total_lamports);
 | 
					        let schedule = create_vesting_schedule(self.start_date_time.date(), self.total_lamports);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let vested_lamports = schedule
 | 
					        let vested_lamports = schedule
 | 
				
			||||||
@@ -68,6 +76,17 @@ impl VestState {
 | 
				
			|||||||
            .map(|(_, lamports)| lamports)
 | 
					            .map(|(_, lamports)| lamports)
 | 
				
			||||||
            .sum::<u64>();
 | 
					            .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);
 | 
					        let redeemable_lamports = vested_lamports.saturating_sub(self.redeemed_lamports);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        contract_account.lamports -= redeemable_lamports;
 | 
					        contract_account.lamports -= redeemable_lamports;
 | 
				
			||||||
@@ -76,10 +95,23 @@ impl VestState {
 | 
				
			|||||||
        self.redeemed_lamports += redeemable_lamports;
 | 
					        self.redeemed_lamports += redeemable_lamports;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Terminate the contract and return all tokens to the given pubkey.
 | 
					    /// Renege on the given number of tokens and send them to the given payee.
 | 
				
			||||||
    pub fn terminate(&mut self, contract_account: &mut Account, payee_account: &mut Account) {
 | 
					    pub fn renege(
 | 
				
			||||||
        payee_account.lamports += contract_account.lamports;
 | 
					        &mut self,
 | 
				
			||||||
        contract_account.lamports = 0;
 | 
					        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 super::*;
 | 
				
			||||||
    use crate::id;
 | 
					    use crate::id;
 | 
				
			||||||
    use solana_sdk::account::Account;
 | 
					    use solana_sdk::account::Account;
 | 
				
			||||||
 | 
					    use solana_sdk::system_program;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #[test]
 | 
					    #[test]
 | 
				
			||||||
    fn test_serializer() {
 | 
					    fn test_serializer() {
 | 
				
			||||||
@@ -107,4 +140,48 @@ mod test {
 | 
				
			|||||||
            Err(InstructionError::AccountDataTooSmall)
 | 
					            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