examples/lockup: Specify start date of vesting schedule
This commit is contained in:
parent
a780002683
commit
2499195523
|
@ -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",
|
||||
]
|
||||
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>>
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)]
|
||||
|
|
Loading…
Reference in New Issue