Browse Source

examples/lockup: Specify start date of vesting schedule

Armani Ferrante 4 years ago
parent
commit
2499195523

+ 2 - 0
Cargo.lock

@@ -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 - 6
examples/lockup/programs/lockup/src/calculator.rs

@@ -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> {

+ 31 - 20
examples/lockup/programs/lockup/src/lib.rs

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

+ 24 - 19
examples/lockup/programs/registry/src/lib.rs

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

+ 11 - 7
examples/lockup/tests/lockup.js

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

+ 9 - 0
lang/src/sysvar.rs

@@ -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,

+ 6 - 0
lang/syn/src/codegen/program.rs

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