├── Xargo.toml ├── README.md ├── Cargo.toml └── src └── lib.rs /Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vestor 2 | Vestor is a program on Solana that allows you to schedule your token release or transfer. This could be used for investor token release and incentive token grants. 3 | 4 | Devnet: `EYK2eucQ7A3npLEwWHPEqA9GhoieRERPRN6bRPVgocz2` 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "Vestor" 3 | version = "0.1.0" 4 | description = "Created with Anchor" 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "lib"] 9 | name = "vestor" 10 | 11 | [features] 12 | no-entrypoint = [] 13 | no-idl = [] 14 | no-log-ix-name = [] 15 | cpi = ["no-entrypoint"] 16 | default = [] 17 | 18 | [dependencies] 19 | anchor-lang = { version = "0.22.1", features = ["init-if-needed"]} 20 | anchor-spl = "0.22.1" -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use anchor_lang::solana_program::{clock, program_option::COption, sysvar}; 3 | use anchor_spl::token::{self, Mint, Token, TokenAccount}; 4 | 5 | declare_id!("EYK2eucQ7A3npLEwWHPEqA9GhoieRERPRN6bRPVgocz2"); 6 | 7 | pub fn available( 8 | ticket: &mut Box>, 9 | ) -> u64 { 10 | if has_cliffed(ticket) { 11 | return unlocked(ticket).checked_sub(ticket.claimed).unwrap(); 12 | } else { 13 | return 0; 14 | } 15 | } 16 | 17 | pub fn has_cliffed( 18 | ticket: &mut Box>, 19 | ) -> bool { 20 | let clock = clock::Clock::get().unwrap(); 21 | if ticket.cliff == 0 { 22 | return true; 23 | } 24 | 25 | return clock.unix_timestamp as u64 > ticket.created_at.checked_add( 26 | ticket.cliff.checked_mul( 27 | 86400 28 | ).unwrap() 29 | ).unwrap(); 30 | } 31 | 32 | pub fn unlocked( 33 | ticket: &mut Box>, 34 | ) -> u64 { 35 | let clock = clock::Clock::get().unwrap(); 36 | 37 | let timelapsed = (clock.unix_timestamp as u64).checked_sub(ticket.created_at).unwrap(); 38 | let vesting_in_seconds = ticket.vesting.checked_mul(86400).unwrap(); 39 | 40 | return timelapsed.checked_mul(ticket.amount).unwrap().checked_div( 41 | vesting_in_seconds as u64 42 | ).unwrap(); 43 | } 44 | 45 | #[program] 46 | pub mod vestor { 47 | use super::*; 48 | pub fn initialize(ctx: Context, nonce: u8) -> Result<()> { 49 | let vestor = &mut ctx.accounts.vestor; 50 | vestor.current_id = 1; 51 | vestor.nonce = nonce; 52 | 53 | Ok(()) 54 | } 55 | 56 | pub fn create(ctx: Context, beneficiary: Pubkey, cliff: u64, vesting: u64, amount: u64, irrevocable: bool) -> Result<()> { 57 | let vestor = &mut ctx.accounts.vestor; 58 | let clock = clock::Clock::get().unwrap(); 59 | 60 | if amount == 0 { 61 | return Err(ErrorCode::AmountMustBeGreaterThanZero.into()); 62 | } if vesting < cliff { 63 | return Err(ErrorCode::VestingPeriodShouldBeEqualOrLongerThanCliff.into()); 64 | } 65 | 66 | // Transfer tokens to vault. 67 | { 68 | let cpi_ctx = CpiContext::new( 69 | ctx.accounts.token_program.to_account_info(), 70 | token::Transfer { 71 | from: ctx.accounts.grantor_token_vault.to_account_info(), 72 | to: ctx.accounts.token_vault.to_account_info(), 73 | authority: ctx.accounts.grantor.to_account_info(), //todo use user account as signer 74 | }, 75 | ); 76 | token::transfer(cpi_ctx, amount)?; 77 | } 78 | 79 | vestor.current_id += 1; 80 | let ticket = &mut ctx.accounts.ticket; 81 | ticket.token_mint = ctx.accounts.token_mint.key(); 82 | ticket.token_vault = ctx.accounts.token_vault.key(); 83 | ticket.grantor = ctx.accounts.grantor.key(); 84 | ticket.beneficiary = beneficiary; 85 | ticket.cliff = cliff; 86 | ticket.vesting = vesting; 87 | ticket.amount = amount; 88 | ticket.balance = amount; 89 | ticket.created_at = clock.unix_timestamp as u64; 90 | ticket.irrevocable = irrevocable; 91 | ticket.is_revoked = false; 92 | 93 | Ok(()) 94 | } 95 | 96 | pub fn claim(ctx: Context) -> Result<()> { 97 | let vestor = &mut ctx.accounts.vestor; 98 | let ticket = &mut ctx.accounts.ticket; 99 | let clock = clock::Clock::get().unwrap(); 100 | 101 | if ticket.is_revoked == true { 102 | return Err(ErrorCode::TicketRevoked.into()); 103 | } 104 | let amount = available(ticket); 105 | 106 | // Transfer. 107 | { 108 | let seeds = &[vestor.to_account_info().key.as_ref(), &[vestor.nonce]]; 109 | let signer = &[&seeds[..]]; 110 | 111 | let cpi_ctx = CpiContext::new_with_signer( 112 | ctx.accounts.token_program.to_account_info(), 113 | token::Transfer { 114 | from: ctx.accounts.token_vault.to_account_info(), 115 | to: ctx.accounts.beneficiary_token_vault.to_account_info(), 116 | authority: ctx.accounts.signer.to_account_info(), 117 | }, 118 | signer 119 | ); 120 | token::transfer(cpi_ctx, amount)?; 121 | } 122 | 123 | ticket.claimed += amount; 124 | ticket.balance -= amount; 125 | ticket.last_claimed_at = clock.unix_timestamp as u64; 126 | ticket.num_claims += 1; 127 | 128 | Ok(()) 129 | } 130 | 131 | pub fn revoke(ctx: Context) -> Result<()> { 132 | let vestor = &mut ctx.accounts.vestor; 133 | let ticket = &mut ctx.accounts.ticket; 134 | let clock = clock::Clock::get().unwrap(); 135 | 136 | if ticket.is_revoked == true { 137 | return Err(ErrorCode::TicketRevoked.into()); 138 | } if ticket.irrevocable == true { 139 | return Err(ErrorCode::TicketIrrevocable.into()); 140 | } 141 | 142 | // Transfer. 143 | { 144 | let seeds = &[vestor.to_account_info().key.as_ref(), &[vestor.nonce]]; 145 | let signer = &[&seeds[..]]; 146 | 147 | let cpi_ctx = CpiContext::new_with_signer( 148 | ctx.accounts.token_program.to_account_info(), 149 | token::Transfer { 150 | from: ctx.accounts.token_vault.to_account_info(), 151 | to: ctx.accounts.grantor_token_vault.to_account_info(), 152 | authority: ctx.accounts.signer.to_account_info(), 153 | }, 154 | signer 155 | ); 156 | token::transfer(cpi_ctx, ticket.balance)?; 157 | } 158 | 159 | ticket.is_revoked = true; 160 | ticket.balance = 0; 161 | 162 | Ok(()) 163 | } 164 | } 165 | 166 | #[derive(Accounts)] 167 | pub struct Initialize<'info> { 168 | #[account(init, payer = user, space = 8 + 8)] 169 | pub vestor: Account<'info, Vestor>, 170 | #[account(mut)] 171 | pub user: Signer<'info>, 172 | pub system_program: Program<'info, System>, 173 | } 174 | 175 | #[derive(Accounts)] 176 | pub struct Create<'info> { 177 | #[account(mut)] 178 | pub vestor: Account<'info, Vestor>, 179 | 180 | #[account( 181 | init_if_needed, 182 | payer = grantor, 183 | seeds = [ 184 | vestor.to_account_info().key().as_ref(), 185 | vestor.current_id.to_string().as_ref(), 186 | ], 187 | bump 188 | )] 189 | pub ticket: Box>, 190 | 191 | pub token_mint: Box>, 192 | #[account( 193 | constraint = token_vault.mint == token_mint.key(), 194 | constraint = token_vault.owner == signer.key(), 195 | )] 196 | pub token_vault: Box>, 197 | 198 | #[account( 199 | constraint = token_vault.mint == token_mint.key(), 200 | constraint = token_vault.owner == grantor.key(), 201 | )] 202 | pub grantor_token_vault: Box>, 203 | 204 | #[account( 205 | seeds = [ 206 | vestor.to_account_info().key.as_ref() 207 | ], 208 | bump = vestor.nonce, 209 | )] 210 | pub signer: UncheckedAccount<'info>, 211 | 212 | #[account(mut)] 213 | pub grantor: Signer<'info>, 214 | 215 | pub system_program: Program<'info, System>, 216 | pub token_program: Program<'info, Token>, 217 | } 218 | 219 | #[derive(Accounts)] 220 | pub struct Claim<'info> { 221 | #[account(mut)] 222 | pub vestor: Account<'info, Vestor>, 223 | 224 | #[account( 225 | mut, 226 | has_one = beneficiary, 227 | has_one = token_mint, 228 | has_one = token_vault, 229 | constraint = ticket.balance > 0, 230 | constraint = ticket.amount > 0, 231 | )] 232 | pub ticket: Box>, 233 | 234 | pub token_mint: Box>, 235 | #[account( 236 | constraint = token_vault.mint == token_mint.key(), 237 | constraint = token_vault.owner == signer.key(), 238 | )] 239 | pub token_vault: Box>, 240 | 241 | #[account( 242 | constraint = token_vault.mint == token_mint.key(), 243 | constraint = token_vault.owner == beneficiary.key(), 244 | )] 245 | pub beneficiary_token_vault: Box>, 246 | 247 | #[account( 248 | seeds = [ 249 | vestor.to_account_info().key.as_ref() 250 | ], 251 | bump = vestor.nonce, 252 | )] 253 | pub signer: UncheckedAccount<'info>, 254 | 255 | #[account(mut)] 256 | pub beneficiary: Signer<'info>, 257 | 258 | pub system_program: Program<'info, System>, 259 | pub token_program: Program<'info, Token>, 260 | } 261 | 262 | #[derive(Accounts)] 263 | pub struct Revoke<'info> { 264 | #[account(mut)] 265 | pub vestor: Account<'info, Vestor>, 266 | 267 | #[account( 268 | mut, 269 | has_one = grantor, 270 | has_one = token_mint, 271 | has_one = token_vault, 272 | constraint = ticket.balance > 0, 273 | )] 274 | pub ticket: Box>, 275 | 276 | pub token_mint: Box>, 277 | #[account( 278 | constraint = token_vault.mint == token_mint.key(), 279 | constraint = token_vault.owner == signer.key(), 280 | )] 281 | pub token_vault: Box>, 282 | 283 | #[account( 284 | constraint = token_vault.mint == token_mint.key(), 285 | constraint = token_vault.owner == grantor.key(), 286 | )] 287 | pub grantor_token_vault: Box>, 288 | 289 | #[account( 290 | seeds = [ 291 | vestor.to_account_info().key.as_ref() 292 | ], 293 | bump = vestor.nonce, 294 | )] 295 | pub signer: UncheckedAccount<'info>, 296 | 297 | #[account(mut)] 298 | pub grantor: Signer<'info>, 299 | 300 | pub system_program: Program<'info, System>, 301 | pub token_program: Program<'info, Token>, 302 | } 303 | 304 | #[account] 305 | pub struct Vestor { 306 | pub current_id: u64, 307 | 308 | pub nonce: u8 309 | } 310 | 311 | #[account] 312 | #[derive(Default)] 313 | pub struct Ticket { 314 | pub token_mint: Pubkey, 315 | pub token_vault: Pubkey, 316 | pub grantor: Pubkey, 317 | pub beneficiary: Pubkey, 318 | pub cliff: u64, 319 | pub vesting: u64, 320 | pub amount: u64, 321 | pub claimed: u64, 322 | pub balance: u64, 323 | pub created_at: u64, 324 | pub last_claimed_at: u64, 325 | pub num_claims: u64, 326 | pub irrevocable: bool, 327 | pub is_revoked: bool, 328 | pub revoked_at: u64, 329 | } 330 | 331 | #[error_code] 332 | pub enum ErrorCode { 333 | #[msg("Amount must be greater than zero.")] 334 | AmountMustBeGreaterThanZero, 335 | #[msg("Vesting period should be equal or longer to the cliff")] 336 | VestingPeriodShouldBeEqualOrLongerThanCliff, 337 | #[msg("Ticket has been revoked")] 338 | TicketRevoked, 339 | #[msg("Ticket is irrevocable")] 340 | TicketIrrevocable, 341 | } 342 | --------------------------------------------------------------------------------