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:
Greg Fitzgerald 2019-11-20 19:33:17 -07:00 committed by GitHub
parent e66b29943b
commit 96dd044f8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 175 additions and 14 deletions

View File

@ -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)
}

View File

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

View File

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