examples/lockup: Specify start date of vesting schedule

This commit is contained in:
Armani Ferrante 2021-02-09 16:54:37 +08:00
parent a780002683
commit 2499195523
No known key found for this signature in database
GPG Key ID: D597A80BCF8E12B7
7 changed files with 90 additions and 52 deletions

2
Cargo.lock generated
View File

@ -57,6 +57,7 @@ dependencies = [
"anyhow",
"proc-macro2 1.0.24",
"quote 1.0.8",
"regex",
"syn 1.0.57",
]
@ -181,6 +182,7 @@ name = "anchor-spl"
version = "0.2.0"
dependencies = [
"anchor-lang",
"solana-program",
"spl-token 3.0.1",
]

View File

@ -7,7 +7,7 @@ pub fn available_for_withdrawal(vesting: &Vesting, current_ts: i64) -> u64 {
}
// The amount of funds currently in the vault.
pub fn balance(vesting: &Vesting) -> u64 {
fn balance(vesting: &Vesting) -> u64 {
vesting
.outstanding
.checked_sub(vesting.whitelist_owned)
@ -33,12 +33,13 @@ fn withdrawn_amount(vesting: &Vesting) -> u64 {
// Returns the total vested amount up to the given ts, assuming zero
// withdrawals and zero funds sent to other programs.
fn total_vested(vesting: &Vesting, current_ts: i64) -> u64 {
assert!(current_ts >= vesting.start_ts);
if current_ts >= vesting.end_ts {
return vesting.start_balance;
if current_ts < vesting.start_ts {
0
} else if current_ts >= vesting.end_ts {
vesting.start_balance
} else {
linear_unlock(vesting, current_ts).unwrap()
}
linear_unlock(vesting, current_ts).unwrap()
}
fn linear_unlock(vesting: &Vesting, current_ts: i64) -> Option<u64> {

View File

@ -4,8 +4,8 @@
#![feature(proc_macro_hygiene)]
use anchor_lang::prelude::*;
use anchor_lang::solana_program;
use anchor_lang::solana_program::instruction::Instruction;
use anchor_lang::solana_program::program;
use anchor_spl::token::{self, TokenAccount, Transfer};
mod calculator;
@ -18,7 +18,8 @@ pub mod lockup {
pub struct Lockup {
/// The key with the ability to change the whitelist.
pub authority: Pubkey,
/// Valid programs the program can relay transactions to.
/// List of programs locked tokens can be sent to. These programs
/// are completely trusted to maintain the locked property.
pub whitelist: Vec<WhitelistEntry>,
}
@ -70,25 +71,19 @@ pub mod lockup {
pub fn create_vesting(
ctx: Context<CreateVesting>,
beneficiary: Pubkey,
end_ts: i64,
period_count: u64,
deposit_amount: u64,
nonce: u8,
start_ts: i64,
end_ts: i64,
period_count: u64,
realizor: Option<Realizor>,
) -> Result<()> {
if end_ts <= ctx.accounts.clock.unix_timestamp {
return Err(ErrorCode::InvalidTimestamp.into());
}
if period_count > (end_ts - ctx.accounts.clock.unix_timestamp) as u64 {
return Err(ErrorCode::InvalidPeriod.into());
}
if period_count == 0 {
return Err(ErrorCode::InvalidPeriod.into());
}
if deposit_amount == 0 {
return Err(ErrorCode::InvalidDepositAmount.into());
}
if !is_valid_schedule(start_ts, end_ts, period_count) {
return Err(ErrorCode::InvalidSchedule.into());
}
let vesting = &mut ctx.accounts.vesting;
vesting.beneficiary = beneficiary;
vesting.mint = ctx.accounts.vault.mint;
@ -96,7 +91,8 @@ pub mod lockup {
vesting.period_count = period_count;
vesting.start_balance = deposit_amount;
vesting.end_ts = end_ts;
vesting.start_ts = ctx.accounts.clock.unix_timestamp;
vesting.start_ts = start_ts;
vesting.created_ts = ctx.accounts.clock.unix_timestamp;
vesting.outstanding = deposit_amount;
vesting.whitelist_owned = 0;
vesting.grantor = *ctx.accounts.depositor_authority.key;
@ -321,9 +317,10 @@ pub struct Vesting {
/// originally deposited.
pub start_balance: u64,
/// The unix timestamp at which this vesting account was created.
pub created_ts: i64,
/// The time at which vesting begins.
pub start_ts: i64,
/// The ts at which all the tokens associated with this account
/// should be vested.
/// The time at which all tokens are vested.
pub end_ts: i64,
/// The number of times vesting will occur. For example, if vesting
/// is once a year over seven years, this will be 7.
@ -400,6 +397,8 @@ pub enum ErrorCode {
InvalidLockRealizor,
#[msg("You have not realized this vesting account.")]
UnrealizedVesting,
#[msg("Invalid vesting schedule given.")]
InvalidSchedule,
}
impl<'a, 'b, 'c, 'info> From<&mut CreateVesting<'info>>
@ -471,8 +470,7 @@ pub fn whitelist_relay_cpi<'info>(
let signer = &[&seeds[..]];
let mut accounts = transfer.to_account_infos();
accounts.extend_from_slice(&remaining_accounts);
solana_program::program::invoke_signed(&relay_instruction, &accounts, signer)
.map_err(Into::into)
program::invoke_signed(&relay_instruction, &accounts, signer).map_err(Into::into)
}
pub fn is_whitelisted<'info>(transfer: &WhitelistTransfer<'info>) -> Result<()> {
@ -491,10 +489,23 @@ fn whitelist_auth(lockup: &Lockup, ctx: &Context<Auth>) -> Result<()> {
Ok(())
}
pub fn is_valid_schedule(start_ts: i64, end_ts: i64, period_count: u64) -> bool {
if end_ts <= start_ts {
return false;
}
if period_count > (end_ts - start_ts) as u64 {
return false;
}
if period_count == 0 {
return false;
}
true
}
// Returns Ok if the locked vesting account has been "realized". Realization
// is application dependent. For example, in the case of staking, one must first
// unstake before being able to earn locked tokens.
fn is_realized<'info>(ctx: &Context<Withdraw>) -> Result<()> {
fn is_realized(ctx: &Context<Withdraw>) -> Result<()> {
if let Some(realizor) = &ctx.accounts.vesting.realizor {
let cpi_program = {
let p = ctx.remaining_accounts[0].clone();

View File

@ -358,6 +358,16 @@ mod registry {
if ctx.accounts.clock.unix_timestamp >= expiry_ts {
return Err(ErrorCode::InvalidExpiry.into());
}
if let RewardVendorKind::Locked {
start_ts,
end_ts,
period_count,
} = kind
{
if !lockup::is_valid_schedule(start_ts, end_ts, period_count) {
return Err(ErrorCode::InvalidVestingSchedule.into());
}
}
// Transfer funds into the vendor's vault.
token::transfer(ctx.accounts.into(), total)?;
@ -384,7 +394,7 @@ mod registry {
vendor.from = *ctx.accounts.depositor_authority.key;
vendor.total = total;
vendor.expired = false;
vendor.kind = kind.clone();
vendor.kind = kind;
Ok(())
}
@ -434,12 +444,13 @@ mod registry {
ctx: Context<'a, 'b, 'c, 'info, ClaimRewardLocked<'info>>,
nonce: u8,
) -> Result<()> {
let (end_ts, period_count) = match ctx.accounts.cmn.vendor.kind {
let (start_ts, end_ts, period_count) = match ctx.accounts.cmn.vendor.kind {
RewardVendorKind::Unlocked => return Err(ErrorCode::ExpectedLockedVendor.into()),
RewardVendorKind::Locked {
start_ts,
end_ts,
period_count,
} => (end_ts, period_count),
} => (start_ts, end_ts, period_count),
};
// Reward distribution.
@ -452,19 +463,6 @@ mod registry {
.unwrap();
assert!(reward_amount > 0);
// The lockup program requires the timestamp to be >= clock's timestamp.
// So update if the time has already passed.
//
// If the reward is within `period_count` seconds of fully vesting, then
// we bump the `end_ts` because, otherwise, the vesting account would
// fail to be created. Vesting must have no more frequently than the
// smallest unit of time, once per second, expressed as
// `period_count <= end_ts - start_ts`.
let end_ts = match end_ts < ctx.accounts.cmn.clock.unix_timestamp + period_count as i64 {
true => ctx.accounts.cmn.clock.unix_timestamp + period_count as i64,
false => end_ts,
};
// Specify the vesting account's realizor, so that unlocks can only
// execute once completely unstaked.
let realizor = Some(Realizor {
@ -487,10 +485,11 @@ mod registry {
lockup::cpi::create_vesting(
cpi_ctx,
ctx.accounts.cmn.member.beneficiary,
end_ts,
period_count,
reward_amount,
nonce,
start_ts,
end_ts,
period_count,
realizor,
)?;
@ -1165,7 +1164,11 @@ pub struct RewardVendor {
#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq)]
pub enum RewardVendorKind {
Unlocked,
Locked { end_ts: i64, period_count: u64 },
Locked {
start_ts: i64,
end_ts: i64,
period_count: u64,
},
}
#[error]
@ -1216,6 +1219,8 @@ pub enum ErrorCode {
InvalidBeneficiary,
#[msg("The given member account does not match the realizor metadata.")]
InvalidRealizorMetadata,
#[msg("Invalid vesting schedule for the locked reward.")]
InvalidVestingSchedule,
}
impl<'a, 'b, 'c, 'info> From<&mut Deposit<'info>>

View File

@ -12,6 +12,7 @@ describe("Lockup and Registry", () => {
anchor.setProvider(provider);
const lockup = anchor.workspace.Lockup;
const linear = anchor.workspace.Linear;
const registry = anchor.workspace.Registry;
let lockupAddress = null;
@ -138,9 +139,10 @@ describe("Lockup and Registry", () => {
let vestingSigner = null;
it("Creates a vesting account", async () => {
const beneficiary = provider.wallet.publicKey;
const endTs = new anchor.BN(Date.now() / 1000 + 5);
const startTs = new anchor.BN(Date.now() / 1000);
const endTs = new anchor.BN(startTs.toNumber() + 5);
const periodCount = new anchor.BN(2);
const beneficiary = provider.wallet.publicKey;
const depositAmount = new anchor.BN(100);
const vault = new anchor.web3.Account();
@ -155,10 +157,11 @@ describe("Lockup and Registry", () => {
await lockup.rpc.createVesting(
beneficiary,
endTs,
periodCount,
depositAmount,
nonce,
startTs,
endTs,
periodCount,
null, // Lock realizor is None.
{
accounts: {
@ -190,11 +193,11 @@ describe("Lockup and Registry", () => {
assert.ok(vestingAccount.grantor.equals(provider.wallet.publicKey));
assert.ok(vestingAccount.outstanding.eq(depositAmount));
assert.ok(vestingAccount.startBalance.eq(depositAmount));
assert.ok(vestingAccount.endTs.eq(endTs));
assert.ok(vestingAccount.periodCount.eq(periodCount));
assert.ok(vestingAccount.whitelistOwned.eq(new anchor.BN(0)));
assert.equal(vestingAccount.nonce, nonce);
assert.ok(endTs.gt(vestingAccount.startTs));
assert.ok(vestingAccount.createdTs.gt(new anchor.BN(0)));
assert.ok(vestingAccount.startTs.eq(startTs));
assert.ok(vestingAccount.endTs.eq(endTs));
assert.ok(vestingAccount.realizor === null);
});
@ -582,6 +585,7 @@ describe("Lockup and Registry", () => {
it("Drops a locked reward", async () => {
lockedRewardKind = {
locked: {
startTs: new anchor.BN(Date.now() / 1000),
endTs: new anchor.BN(Date.now() / 1000 + 6),
periodCount: new anchor.BN(2),
},

View File

@ -23,6 +23,15 @@ impl<'info, T: solana_program::sysvar::Sysvar> Sysvar<'info, T> {
}
}
impl<'info, T: solana_program::sysvar::Sysvar> Clone for Sysvar<'info, T> {
fn clone(&self) -> Self {
Self {
info: self.info.clone(),
account: T::from_account_info(&self.info).unwrap(),
}
}
}
impl<'info, T: solana_program::sysvar::Sysvar> Accounts<'info> for Sysvar<'info, T> {
fn try_accounts(
_program_id: &Pubkey,

View File

@ -273,6 +273,12 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
Ok(())
}
#[inline(never)]
#[cfg(feature = "no-idl")]
pub fn __idl(program_id: &Pubkey, accounts: &[AccountInfo], idl_ix_data: &[u8]) -> ProgramResult {
Err(anchor_lang::solana_program::program_error::ProgramError::Custom(99))
}
// One time IDL account initializer. Will faill on subsequent
// invocations.
#[inline(never)]