├── .gitignore
├── Anchor.toml
├── LICENSE
├── README.md
├── assets
├── diagram.png
├── structure.drawio
├── structure.png
└── structure.svg
├── audit
└── Bonfida_SecurityAssessment_Vesting_Final050521.pdf
├── cli
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── README.md
└── src
│ └── main.rs
├── js
├── .eslintignore
├── .eslintrc.js
├── .prettierrc.yaml
├── README.md
├── package.json
├── rollup.config.mjs
├── src
│ ├── example.ts
│ ├── index.ts
│ ├── instructions.ts
│ ├── main.ts
│ ├── state.ts
│ └── utils.ts
├── tsconfig.json
├── yarn-error.log
└── yarn.lock
└── program
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── Xargo.toml
├── fuzz
├── Cargo.toml
├── README.md
└── src
│ └── vesting_fuzz.rs
├── solana-llvm-linux.tar.bz2
├── src
├── entrypoint.rs
├── error.rs
├── instruction.rs
├── lib.rs
├── processor.rs
└── state.rs
└── tests
└── functional.rs
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | .DS_Store
3 | .vscode
4 |
5 | */node_modules
6 | js/lib
7 |
8 | cli/target
9 | cli/target/.rustc_info.json
10 | /node_modules
11 |
12 | js/dist
--------------------------------------------------------------------------------
/Anchor.toml:
--------------------------------------------------------------------------------
1 | anchor_version = "0.13.2"
2 |
3 | [workspace]
4 | members = ["program"]
5 |
6 | [provider]
7 | cluster = "mainnet"
8 | wallet = "~/.config/solana/id.json"
9 |
10 | [programs.mainnet]
11 | token_vesting = "CChTq6PthWU82YZkbveA3WDf7s97BWhBK4Vx9bmsT743"
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2021 Bonfida
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Token vesting
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | Table of contents
22 |
23 |
24 | 1. [Program ID](#program-id)
25 | 2. [Audit](#audit)
26 | 3. [UI](#ui)
27 | 4. [Overview](#overview)
28 | 5. [Structure](#structure)
29 |
30 |
31 |
32 | Program ID
33 |
34 |
35 | - mainnet: `CChTq6PthWU82YZkbveA3WDf7s97BWhBK4Vx9bmsT743`
36 | - devnet: `DLxB9dSQtA4WJ49hWFhxqiQkD9v6m67Yfk9voxpxrBs4`
37 |
38 |
39 |
40 | Audit
41 |
42 |
43 | This code has been audited by Kudelski ✅
44 |
45 | - Audit report: [Bonfida Token Vesting Report](/audit/Bonfida_SecurityAssessment_Vesting_Final050521.pdf)
46 |
47 |
48 |
49 | UI
50 |
51 |
52 | The [Bonfida Token Vesting UI](https://vesting.bonfida.org) can be used to unlock tokens. The UI **only** works for vesting accounts using the mainnet deployment `CChTq6PthWU82YZkbveA3WDf7s97BWhBK4Vx9bmsT743`
53 |
54 |
55 |
56 | Overview
57 |
58 |
59 | - Simple vesting contract (SVC) that allows you to deposit X SPL tokens that are unlocked to a specified public key at a certain block height/ slot.
60 | - Unlocking works by pushing a permissionless crank on the contract that moves the tokens to the pre-specified address
61 | - Token Address should be derived from https://spl.solana.com/associated-token-account
62 | - 'Vesting Schedule contract' - A contract containing an array of the SVC's that can be used to develop arbitrary- vesting schedules.
63 | - Tooling to easily setup vesting schedule contracts
64 | - Recipient address should be modifiable by the owner of the current recipient key
65 | - Implementation should be a rust spl compatible program, plus client side javascript bindings that include a CLI- interface. Rust program should be unit tested and fuzzed.
66 |
67 |
68 |
69 | Structure
70 |
71 |
72 | - `cli` : CLI tool to interact with on-chain token vesting contract
73 | - `js` : JavaScript binding to interact with on-chain token vesting contract
74 | - `program` : The BPF compatible token vesting on-chain program/smart contract
75 |
76 | 
77 |
--------------------------------------------------------------------------------
/assets/diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Upwork-Job32/token-vesting.blockchain-rust.project/91a042157e0565de676fad81d7bea03aaa25b288/assets/diagram.png
--------------------------------------------------------------------------------
/assets/structure.drawio:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
--------------------------------------------------------------------------------
/assets/structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Upwork-Job32/token-vesting.blockchain-rust.project/91a042157e0565de676fad81d7bea03aaa25b288/assets/structure.png
--------------------------------------------------------------------------------
/assets/structure.svg:
--------------------------------------------------------------------------------
1 | Contr... Destination address Mint address Is initialized ? Array of Schedules Destination address... Token Ve... CLI or JS bindings - Source (sign )
- Destination (Pubkey)
- Mint (Pubkey)
- Fee Payer (sign )
- Schedules
- Source (sign)... Contract Acc... Transfer total vested amount from sender
Transfer total vested amount... Initi... CLI or JS bindings - Contract Seed (256 bits)
- Current Destination (sign )
- New Destination (Pubkey)
- Fee Payer (sign )
- Contract Seed (256 bits)... Crank... CLI or JS bindings - Contract Seeds (256 bits)
- Payer (sign )
- Contract Seeds (256 bits)... Reci... Transfer maximum amount for current timeslot (fail if 0)
Transfer maximum amount for current... Create Operatio... Change Destination Operations
Change Destinat... Permissionless crank (Unlock Operation)
Permissionless... Contract Seed... Contract Seed... Viewer does not support full SVG 1.1
--------------------------------------------------------------------------------
/audit/Bonfida_SecurityAssessment_Vesting_Final050521.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Upwork-Job32/token-vesting.blockchain-rust.project/91a042157e0565de676fad81d7bea03aaa25b288/audit/Bonfida_SecurityAssessment_Vesting_Final050521.pdf
--------------------------------------------------------------------------------
/cli/.gitignore:
--------------------------------------------------------------------------------
1 | target
--------------------------------------------------------------------------------
/cli/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "vesting-contract-cli"
3 | version = "0.1.0"
4 | authors = ["Bonfida "]
5 | edition = "2018"
6 |
7 | [dependencies]
8 | solana-program = "1.5.0"
9 | solana-client = "1.5.0"
10 | solana-sdk = "1.5.0"
11 | solana-clap-utils = "1.5.0"
12 | token-vesting = { version = "0.1.0", path="../program", features=["no-entrypoint"] }
13 | spl-token = {version = "3.0.1", features = ["no-entrypoint"]}
14 | spl-associated-token-account = {version = "1.0.2", features = ["no-entrypoint"]}
15 | clap = "2.33.3"
16 | chrono = "0.4.19"
17 | iso8601-duration = { git = "https://github.com/rrichardson/iso8601-duration.git", rev = "9e01f51ea253e95e0fba5e4d7ad0c537922931e7"}
--------------------------------------------------------------------------------
/cli/README.md:
--------------------------------------------------------------------------------
1 | # Testing
2 |
3 | Create participants accounts:
4 | ```bash
5 | solana-keygen new --outfile ~/.config/solana/id_owner.json --force
6 | solana-keygen new --outfile ~/.config/solana/id_dest.json --force
7 | solana-keygen new --outfile ~/.config/solana/id_new_dest.json --force
8 | ```
9 |
10 | Owner would do all operations, so put some SOL to his account:
11 | ```bash
12 | solana airdrop 2 --url https://api.devnet.solana.com ~/.config/solana/id_owner.json
13 | ```
14 |
15 | Build program:
16 | ```bash
17 | ( cd ../program ; cargo build-bpf; )
18 | ```
19 |
20 | Deploy program and copy `PROGRAM_ID`.
21 | ```bash
22 | solana deploy ../program/target/deploy/token_vesting.so --url https://api.devnet.solana.com --keypair ~/.config/solana/id_owner.json
23 | ```
24 |
25 | Create mint and get its public key(`MINT`):
26 | ```bash
27 | spl-token create-token --url https://api.devnet.solana.com --fee-payer ~/.config/solana/id_owner.json
28 | ```
29 |
30 | Create source token account(`TOKEN_ACCOUNT_SOURCE`)
31 | ```bash
32 | spl-token create-account $MINT --url https://api.devnet.solana.com --owner ~/.config/solana/id_owner.json --fee-payer ~/.config/solana/id_owner.json
33 | ```
34 |
35 | Mint test source token:
36 | ```bash
37 | spl-token mint $MINT 100000 --url https://api.devnet.solana.com $TOKEN_ACCOUNT_SOURCE --fee-payer ~/.config/solana/id_owner.json
38 | ```
39 |
40 | Create vesting destination token account(`ACCOUNT_TOKEN_DEST`):
41 | ```bash
42 | spl-token create-account $MINT --url https://api.devnet.solana.com --owner ~/.config/solana/id_dest.json --fee-payer ~/.config/solana/id_owner.json
43 | ```
44 |
45 | And new one(`ACCOUNT_TOKEN_NEW_DEST`):
46 | ```bash
47 | spl-token create-account $MINT --url https://api.devnet.solana.com --owner ~/.config/solana/id_new_dest.json --fee-payer ~/.config/solana/id_owner.json
48 | ```
49 |
50 | Build CLI:
51 |
52 | ```bash
53 | cargo build
54 | ```
55 |
56 | Create vesting instance and store its SEED value
57 | ```bash
58 | echo "RUST_BACKTRACE=1 ./target/debug/vesting-contract-cli \
59 | --url https://api.devnet.solana.com \
60 | --program_id $PROGRAM_ID \
61 | create \
62 | --mint_address $MINT \
63 | --source_owner ~/.config/solana/id_owner.json \
64 | --source_token_address $TOKEN_ACCOUNT_SOURCE \
65 | --destination_token_address $ACCOUNT_TOKEN_DEST \
66 | --amounts 2,1,3,! \
67 | --release-times 1,28504431,2850600000000000,! \
68 | --payer ~/.config/solana/id_owner.json" \
69 | --verbose | bash
70 | ```
71 |
72 | To use [Associated Token Account](https://spl.solana.com/associated-token-account) as destination use `--destination_address`(with public key of `id_dest`) instead of `--destination_token_address`.
73 |
74 | Observe contract state:
75 | ```bash
76 | echo "RUST_BACKTRACE=1 ./target/debug/vesting-contract-cli \
77 | --url https://api.devnet.solana.com \
78 | --program_id $PROGRAM_ID \
79 | info \
80 | --seed $SEED " | bash
81 | ```
82 |
83 | Change owner:
84 | ```bash
85 | echo "RUST_BACKTRACE=1 ./target/debug/vesting-contract-cli \
86 | --url https://api.devnet.solana.com \
87 | --program_id $PROGRAM_ID \
88 | change-destination \
89 | --seed $SEED \
90 | --current_destination_owner ~/.config/solana/id_dest.json \
91 | --new_destination_token_address $ACCOUNT_TOKEN_NEW_DEST \
92 | --payer ~/.config/solana/id_owner.json" | bash
93 | ```
94 |
95 | And unlock tokens according schedule:
96 | ```bash
97 | echo "RUST_BACKTRACE=1 ./target/debug/vesting-contract-cli \
98 | --url https://api.devnet.solana.com \
99 | --program_id $PROGRAM_ID \
100 | unlock \
101 | --seed $SEED \
102 | --payer ~/.config/solana/id_owner.json" | bash
103 | ```
104 |
105 | Create linear vesting:
106 | ```bash
107 | echo "RUST_BACKTRACE=1 ./target/debug/vesting-contract-cli \
108 | --url https://api.devnet.solana.com \
109 | --program_id $PROGRAM_ID \
110 | create \
111 | --mint_address $MINT \
112 | --source_owner ~/.config/solana/id_owner.json \
113 | --source_token_address $TOKEN_ACCOUNT_SOURCE \
114 | --destination_token_address $ACCOUNT_TOKEN_DEST \
115 | --amounts 42,! \
116 | --release-frequency 'P1D' \
117 | --start-date-time '2022-01-06T20:11:18Z' \
118 | --end-date-time '2022-01-12T20:11:18Z' \
119 | --payer ~/.config/solana/id_owner.json" \
120 | --verbose | bash
121 | ```
122 |
123 | ## Links
124 |
125 | https://spl.solana.com/token
126 |
--------------------------------------------------------------------------------
/cli/src/main.rs:
--------------------------------------------------------------------------------
1 | use chrono::{DateTime, Duration};
2 | use clap::{
3 | crate_description, crate_name, crate_version, value_t, App, AppSettings, Arg, SubCommand,
4 | };
5 | use solana_clap_utils::{
6 | input_parsers::{keypair_of, pubkey_of, value_of, values_of},
7 | input_validators::{is_amount, is_keypair, is_parsable, is_pubkey, is_slot, is_url},
8 | };
9 | use solana_client::rpc_client::RpcClient;
10 | use solana_program::{msg, program_pack::Pack, pubkey::Pubkey, system_program, sysvar};
11 | use solana_sdk::{
12 | self, commitment_config::CommitmentConfig, signature::Keypair, signature::Signer,
13 | transaction::Transaction,
14 | };
15 | use spl_associated_token_account::{create_associated_token_account, get_associated_token_address};
16 | use spl_token;
17 | use std::convert::TryInto;
18 | use token_vesting::{
19 | instruction::{change_destination, create, init, unlock, Schedule},
20 | state::{unpack_schedules, VestingScheduleHeader},
21 | };
22 |
23 | // Lock the vesting contract
24 | fn command_create_svc(
25 | rpc_client: RpcClient,
26 | program_id: Pubkey,
27 | payer: Keypair,
28 | source_token_owner: Keypair,
29 | possible_source_token_pubkey: Option,
30 | destination_token_pubkey: Pubkey,
31 | mint_address: Pubkey,
32 | schedules: Vec,
33 | confirm: bool,
34 | ) {
35 | // If no source token account was given, use the associated source account
36 | let source_token_pubkey = match possible_source_token_pubkey {
37 | None => get_associated_token_address(&source_token_owner.pubkey(), &mint_address),
38 | _ => possible_source_token_pubkey.unwrap(),
39 | };
40 |
41 | // Find a valid seed for the vesting program account key to be non reversible and unused
42 | let mut not_found = true;
43 | let mut vesting_seed: [u8; 32] = [0; 32];
44 | let mut vesting_pubkey = Pubkey::new_unique();
45 | while not_found {
46 | vesting_seed = Pubkey::new_unique().to_bytes();
47 | let program_id_bump = Pubkey::find_program_address(&[&vesting_seed[..31]], &program_id);
48 | vesting_pubkey = program_id_bump.0;
49 | vesting_seed[31] = program_id_bump.1;
50 | not_found = match rpc_client.get_account(&vesting_pubkey) {
51 | Ok(_) => true,
52 | Err(_) => false,
53 | }
54 | }
55 |
56 | let vesting_token_pubkey = get_associated_token_address(&vesting_pubkey, &mint_address);
57 |
58 | let instructions = [
59 | init(
60 | &system_program::id(),
61 | &sysvar::rent::id(),
62 | &program_id,
63 | &payer.pubkey(),
64 | &vesting_pubkey,
65 | vesting_seed,
66 | schedules.len() as u32,
67 | )
68 | .unwrap(),
69 | create_associated_token_account(
70 | &source_token_owner.pubkey(),
71 | &vesting_pubkey,
72 | &mint_address,
73 | ),
74 | create(
75 | &program_id,
76 | &spl_token::id(),
77 | &vesting_pubkey,
78 | &vesting_token_pubkey,
79 | &source_token_owner.pubkey(),
80 | &source_token_pubkey,
81 | &destination_token_pubkey,
82 | &mint_address,
83 | schedules,
84 | vesting_seed,
85 | )
86 | .unwrap(),
87 | ];
88 |
89 | let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey()));
90 |
91 | let recent_blockhash = rpc_client.get_recent_blockhash().unwrap().0;
92 | transaction.sign(&[&payer], recent_blockhash);
93 |
94 | msg!(
95 | "\nThe seed of the contract is: {:?}",
96 | Pubkey::new_from_array(vesting_seed)
97 | );
98 | msg!("Please write it down as it is needed to interact with the contract!");
99 |
100 | msg!("The vesting account pubkey: {:?}", vesting_pubkey,);
101 |
102 | if confirm {
103 | rpc_client
104 | .send_and_confirm_transaction_with_spinner_and_commitment(
105 | &transaction,
106 | CommitmentConfig::finalized(),
107 | )
108 | .unwrap();
109 | } else {
110 | rpc_client.send_transaction(&transaction).unwrap();
111 | }
112 | }
113 |
114 | fn command_unlock_svc(
115 | rpc_client: RpcClient,
116 | program_id: Pubkey,
117 | vesting_seed: [u8; 32],
118 | payer: Keypair,
119 | ) {
120 | // Find the non reversible public key for the vesting contract via the seed
121 | let (vesting_pubkey, _) = Pubkey::find_program_address(&[&vesting_seed[..31]], &program_id);
122 |
123 | let packed_state = rpc_client.get_account_data(&vesting_pubkey).unwrap();
124 | let header_state =
125 | VestingScheduleHeader::unpack(&packed_state[..VestingScheduleHeader::LEN]).unwrap();
126 | let destination_token_pubkey = header_state.destination_address;
127 |
128 | let vesting_token_pubkey =
129 | get_associated_token_address(&vesting_pubkey, &header_state.mint_address);
130 |
131 | let unlock_instruction = unlock(
132 | &program_id,
133 | &spl_token::id(),
134 | &sysvar::clock::id(),
135 | &vesting_pubkey,
136 | &vesting_token_pubkey,
137 | &destination_token_pubkey,
138 | vesting_seed,
139 | )
140 | .unwrap();
141 |
142 | let mut transaction = Transaction::new_with_payer(&[unlock_instruction], Some(&payer.pubkey()));
143 |
144 | let recent_blockhash = rpc_client.get_recent_blockhash().unwrap().0;
145 | transaction.sign(&[&payer], recent_blockhash);
146 |
147 | rpc_client.send_transaction(&transaction).unwrap();
148 | }
149 |
150 | fn command_change_destination(
151 | rpc_client: RpcClient,
152 | program_id: Pubkey,
153 | destination_token_account_owner: Keypair,
154 | opt_new_destination_account: Option,
155 | opt_new_destination_token_account: Option,
156 | vesting_seed: [u8; 32],
157 | payer: Keypair,
158 | ) {
159 | // Find the non reversible public key for the vesting contract via the seed
160 | let (vesting_pubkey, _) = Pubkey::find_program_address(&[&vesting_seed[..31]], &program_id);
161 |
162 | let packed_state = rpc_client.get_account_data(&vesting_pubkey).unwrap();
163 | let state_header =
164 | VestingScheduleHeader::unpack(&packed_state[..VestingScheduleHeader::LEN]).unwrap();
165 | let destination_token_pubkey = state_header.destination_address;
166 |
167 | let new_destination_token_account = match opt_new_destination_token_account {
168 | None => get_associated_token_address(
169 | &opt_new_destination_account.unwrap(),
170 | &state_header.mint_address,
171 | ),
172 | Some(new_destination_token_account) => new_destination_token_account,
173 | };
174 |
175 | let unlock_instruction = change_destination(
176 | &program_id,
177 | &vesting_pubkey,
178 | &destination_token_account_owner.pubkey(),
179 | &destination_token_pubkey,
180 | &new_destination_token_account,
181 | vesting_seed,
182 | )
183 | .unwrap();
184 |
185 | let mut transaction = Transaction::new_with_payer(&[unlock_instruction], Some(&payer.pubkey()));
186 |
187 | let recent_blockhash = rpc_client.get_recent_blockhash().unwrap().0;
188 | transaction.sign(
189 | &[&payer, &destination_token_account_owner],
190 | recent_blockhash,
191 | );
192 |
193 | rpc_client.send_transaction(&transaction).unwrap();
194 | }
195 |
196 | fn command_info(
197 | rpc_client: RpcClient,
198 | rpc_url: String,
199 | program_id: Pubkey,
200 | vesting_seed: [u8; 32],
201 | ) {
202 | msg!("\n---------------VESTING--CONTRACT--INFO-----------------\n");
203 | msg!("RPC URL: {:?}", &rpc_url);
204 | msg!("Program ID: {:?}", &program_id);
205 | msg!("Vesting Seed: {:?}", Pubkey::new_from_array(vesting_seed));
206 |
207 | // Find the non reversible public key for the vesting contract via the seed
208 | let (vesting_pubkey, _) = Pubkey::find_program_address(&[&vesting_seed[..31]], &program_id);
209 | msg!("Vesting Account Pubkey: {:?}", &vesting_pubkey);
210 |
211 | let packed_state = rpc_client.get_account_data(&vesting_pubkey).unwrap();
212 | let state_header =
213 | VestingScheduleHeader::unpack(&packed_state[..VestingScheduleHeader::LEN]).unwrap();
214 | let vesting_token_pubkey =
215 | get_associated_token_address(&vesting_pubkey, &state_header.mint_address);
216 | msg!("Vesting Token Account Pubkey: {:?}", &vesting_token_pubkey);
217 | msg!("Initialized: {:?}", &state_header.is_initialized);
218 | msg!("Mint Address: {:?}", &state_header.mint_address);
219 | msg!(
220 | "Destination Token Address: {:?}",
221 | &state_header.destination_address
222 | );
223 |
224 | let schedules = unpack_schedules(&packed_state[VestingScheduleHeader::LEN..]).unwrap();
225 |
226 | for i in 0..schedules.len() {
227 | msg!("\nSCHEDULE {:?}", i);
228 | msg!("Release Height: {:?}", &schedules[i].release_time);
229 | msg!("Amount: {:?}", &schedules[i].amount);
230 | }
231 | }
232 |
233 | fn main() {
234 | let matches = App::new(crate_name!())
235 | .about(crate_description!())
236 | .version(crate_version!())
237 | .setting(AppSettings::SubcommandRequiredElseHelp)
238 | .arg(
239 | Arg::with_name("verbose")
240 | .long("verbose")
241 | .short("v")
242 | .takes_value(false)
243 | .global(true)
244 | .help("Show additional information"),
245 | )
246 | .arg(
247 | Arg::with_name("rpc_url")
248 | .long("url")
249 | .value_name("URL")
250 | .validator(is_url)
251 | .takes_value(true)
252 | .global(true)
253 | .help(
254 | "Specify the url of the rpc client (solana network).",
255 | ),
256 | )
257 | .arg(
258 | Arg::with_name("program_id")
259 | .long("program_id")
260 | .value_name("ADDRESS")
261 | .validator(is_pubkey)
262 | .takes_value(true)
263 | .help(
264 | "Specify the address (public key) of the program.",
265 | ),
266 | )
267 | .subcommand(SubCommand::with_name("create").about("Create a new vesting contract with an optional release schedule")
268 | .arg(
269 | Arg::with_name("mint_address")
270 | .long("mint_address")
271 | .value_name("ADDRESS")
272 | .validator(is_pubkey)
273 | .takes_value(true)
274 | .help(
275 | "Specify the address (publickey) of the mint for the token that should be used.",
276 | ),
277 | )
278 | .arg(
279 | Arg::with_name("source_owner")
280 | .long("source_owner")
281 | .value_name("KEYPAIR")
282 | .validator(is_keypair)
283 | .takes_value(true)
284 | .help(
285 | "Specify the source account owner. \
286 | This may be a keypair file, the ASK keyword. \
287 | Defaults to the client keypair.",
288 | ),
289 | )
290 | .arg(
291 | Arg::with_name("source_token_address")
292 | .long("source_token_address")
293 | .value_name("ADDRESS")
294 | .validator(is_pubkey)
295 | .takes_value(true)
296 | .help(
297 | "Specify the source token account address.",
298 | ),
299 | )
300 | .arg(
301 | Arg::with_name("destination_address")
302 | .long("destination_address")
303 | .value_name("ADDRESS")
304 | .validator(is_pubkey)
305 | .takes_value(true)
306 | .help(
307 | "Specify the destination (non-token) account address. \
308 | If specified, the vesting destination will be the associated \
309 | token account for the mint of the contract."
310 | ),
311 | )
312 | .arg(
313 | Arg::with_name("destination_token_address")
314 | .long("destination_token_address")
315 | .value_name("ADDRESS")
316 | .validator(is_pubkey)
317 | .takes_value(true)
318 | .help(
319 | "Specify the destination token account address. \
320 | If specified, this address will be used as a destination, \
321 | and overwrite the associated token account.",
322 | ),
323 | )
324 | .arg(
325 | Arg::with_name("amounts")
326 | .long("amounts")
327 | .value_name("AMOUNT")
328 | .validator(is_amount)
329 | .takes_value(true)
330 | .multiple(true)
331 | .use_delimiter(true)
332 | .value_terminator("!")
333 | .allow_hyphen_values(true)
334 | .help(
335 | "Amounts of tokens to transfer via the vesting \
336 | contract. Multiple inputs separated by a comma are
337 | accepted for the creation of multiple schedules. The sequence of inputs \
338 | needs to end with an exclamation mark ( e.g. 1,2,3,! )",
339 | ),
340 | )
341 | // scheduled vesting
342 | .arg(
343 | Arg::with_name("release-times")
344 | .long("release-times")
345 | .conflicts_with("release-frequency")
346 | .value_name("SLOT")
347 | .validator(is_slot)
348 | .takes_value(true)
349 | .multiple(true)
350 | .use_delimiter(true)
351 | .value_terminator("!")
352 | .allow_hyphen_values(true)
353 | .help(
354 | "Release times in unix timestamp to decide when the contract is \
355 | unlockable. Multiple inputs separated by a comma are
356 | accepted for the creation of multiple schedules. The sequence of inputs \
357 | needs to end with an exclamation mark ( e.g. 1,2,3,! ).",
358 | ),
359 | )
360 | // linear vesting
361 | .arg(
362 | Arg::with_name("release-frequency")
363 | .long("release-frequency")
364 | .value_name("RELEASE_FREQUENCY")
365 | .takes_value(true)
366 | .conflicts_with("release-times")
367 | .help(
368 | "Frequency of release amount. \
369 | You start on 1sth of Nov and end on 5th of Nov. \
370 | With 1 day frequency it will vest from total amount 5 times \
371 | splitted linearly.
372 | Duration must be ISO8601 duration format. Example, P1D.
373 | Internally all dates will be transformed into schedule.",
374 | ),
375 | )
376 | .arg(
377 | Arg::with_name("start-date-time")
378 | .long("start-date-time")
379 | .value_name("START_DATE_TIME")
380 | .takes_value(true)
381 | .help(
382 | "First time of release in linear vesting. \
383 | Must be RFC 3339 and ISO 8601 sortable date time. \
384 | Example, 2022-01-06T20:11:18Z",
385 | ),
386 | )
387 | .arg(
388 | Arg::with_name("end-date-time")
389 | .long("end-date-time")
390 | .value_name("END_DATE_TIME")
391 | .takes_value(true)
392 | .help(
393 | "Last time of release in linear vesting. \
394 | If frequency will go over last release time, \
395 | tokens will be released later than end date.
396 | Must be RFC 3339 and ISO 8601 sortable date time. \
397 | Example, 2022-17-06T20:11:18Z",
398 | ),
399 | )
400 | .arg(
401 | Arg::with_name("payer")
402 | .long("payer")
403 | .value_name("KEYPAIR")
404 | .validator(is_keypair)
405 | .takes_value(true)
406 | .help(
407 | "Specify the transaction fee payer account address. \
408 | This may be a keypair file, the ASK keyword. \
409 | Defaults to the client keypair.",
410 | ),
411 | )
412 | .arg(
413 | Arg::with_name("confirm")
414 | .long("confirm")
415 | .value_name("CONFIRM")
416 | .takes_value(true)
417 | .default_value("true")
418 | .help(
419 | "Specify whether to wait transaction confirmation"
420 | ),
421 | )
422 | )
423 | .subcommand(SubCommand::with_name("unlock").about("Unlock a vesting contract. This will only release \
424 | the schedules that have reached maturity.")
425 | .arg(
426 | Arg::with_name("seed")
427 | .long("seed")
428 | .value_name("SEED")
429 | .validator(is_parsable::)
430 | .takes_value(true)
431 | .help(
432 | "Specify the seed for the vesting contract.",
433 | ),
434 | )
435 | .arg(
436 | Arg::with_name("payer")
437 | .long("payer")
438 | .value_name("KEYPAIR")
439 | .validator(is_keypair)
440 | .takes_value(true)
441 | .help(
442 | "Specify the transaction fee payer account address. \
443 | This may be a keypair file, the ASK keyword. \
444 | Defaults to the client keypair.",
445 | ),
446 | )
447 | )
448 | .subcommand(SubCommand::with_name("change-destination").about("Change the destination of a vesting contract")
449 | .arg(
450 | Arg::with_name("seed")
451 | .long("seed")
452 | .value_name("SEED")
453 | .validator(is_parsable::)
454 | .takes_value(true)
455 | .help(
456 | "Specify the seed for the vesting contract.",
457 | ),
458 | )
459 | .arg(
460 | Arg::with_name("current_destination_owner")
461 | .long("current_destination_owner")
462 | .value_name("KEYPAIR")
463 | .validator(is_keypair)
464 | .takes_value(true)
465 | .help(
466 | "Specify the current destination owner account keypair. \
467 | This may be a keypair file, the ASK keyword. \
468 | Defaults to the client keypair.",
469 | ),
470 | )
471 | .arg(
472 | Arg::with_name("new_destination_address")
473 | .long("new_destination_address")
474 | .value_name("ADDRESS")
475 | .validator(is_pubkey)
476 | .takes_value(true)
477 | .help(
478 | "Specify the new destination (non-token) account address. \
479 | If specified, the vesting destination will be the associated \
480 | token account for the mint of the contract."
481 | ),
482 | )
483 | .arg(
484 | Arg::with_name("new_destination_token_address")
485 | .long("new_destination_token_address")
486 | .value_name("ADDRESS")
487 | .validator(is_pubkey)
488 | .takes_value(true)
489 | .help(
490 | "Specify the new destination token account address. \
491 | If specified, this address will be used as a destination, \
492 | and overwrite the associated token account.",
493 | ),
494 | )
495 | .arg(
496 | Arg::with_name("payer")
497 | .long("payer")
498 | .value_name("KEYPAIR")
499 | .validator(is_keypair)
500 | .takes_value(true)
501 | .help(
502 | "Specify the transaction fee payer account address. \
503 | This may be a keypair file, the ASK keyword. \
504 | Defaults to the client keypair.",
505 | ),
506 | )
507 | )
508 | .subcommand(SubCommand::with_name("info").about("Print information about a vesting contract")
509 | .arg(
510 | Arg::with_name("seed")
511 | .long("seed")
512 | .value_name("SEED")
513 | .validator(is_parsable::)
514 | .takes_value(true)
515 | .help(
516 | "Specify the seed for the vesting contract.",
517 | ),
518 | )
519 | )
520 | .get_matches();
521 |
522 | let rpc_url = value_t!(matches, "rpc_url", String).unwrap();
523 | let rpc_client = RpcClient::new(rpc_url);
524 | let program_id = pubkey_of(&matches, "program_id").unwrap();
525 |
526 | let _ = match matches.subcommand() {
527 | ("create", Some(arg_matches)) => {
528 | let source_keypair = keypair_of(arg_matches, "source_owner").unwrap();
529 | let source_token_pubkey = pubkey_of(arg_matches, "source_token_address");
530 | let mint_address = pubkey_of(arg_matches, "mint_address").unwrap();
531 | let destination_pubkey = match pubkey_of(arg_matches, "destination_token_address") {
532 | None => get_associated_token_address(
533 | &pubkey_of(arg_matches, "destination_address").unwrap(),
534 | &mint_address,
535 | ),
536 | Some(destination_token_pubkey) => destination_token_pubkey,
537 | };
538 | let payer_keypair = keypair_of(arg_matches, "payer").unwrap();
539 |
540 | // Parsing schedules
541 | let mut schedule_amounts: Vec = values_of(arg_matches, "amounts").unwrap();
542 | let confirm: bool = value_of(arg_matches, "confirm").unwrap();
543 | let release_frequency: Option = value_of(arg_matches, "release-frequency");
544 |
545 | let schedule_times = if release_frequency.is_some() {
546 | // best found in rust
547 | let release_frequency: iso8601_duration::Duration =
548 | release_frequency.unwrap().parse().unwrap();
549 | let release_frequency: u64 = Duration::from_std(release_frequency.to_std())
550 | .unwrap()
551 | .num_seconds()
552 | .try_into()
553 | .unwrap();
554 | if schedule_amounts.len() > 1 {
555 | panic!("Linear vesting must have one amount which will split into parts per period")
556 | }
557 | let start: u64 = DateTime::parse_from_rfc3339(
558 | &value_of::(arg_matches, "start-date-time").unwrap(),
559 | )
560 | .unwrap()
561 | .timestamp()
562 | .try_into()
563 | .unwrap();
564 | let end: u64 = DateTime::parse_from_rfc3339(
565 | &value_of::(arg_matches, "end-date-time").unwrap(),
566 | )
567 | .unwrap()
568 | .timestamp()
569 | .try_into()
570 | .unwrap();
571 | let total = schedule_amounts[0];
572 | let part = (((total as u128) * (release_frequency as u128))
573 | / ((end - start) as u128))
574 | .try_into()
575 | .unwrap();
576 | schedule_amounts.clear();
577 | let mut linear_vesting = Vec::new();
578 |
579 | let q = total / part;
580 | let r = total % part;
581 |
582 | for n in 0..q {
583 | linear_vesting.push(start + n * release_frequency);
584 | schedule_amounts.push(part);
585 | }
586 |
587 | if r != 0 {
588 | schedule_amounts[(q - 1) as usize] += r;
589 | }
590 |
591 | if linear_vesting.len() > 365 {
592 | panic!("Total count of vesting periods is more than 365. Not sure if you want to do that.")
593 | }
594 |
595 | assert_eq!(schedule_amounts.iter().sum::(), total);
596 |
597 | linear_vesting
598 | } else {
599 | values_of(arg_matches, "release-times").unwrap()
600 | };
601 |
602 | if schedule_amounts.len() != schedule_times.len() {
603 | eprintln!("error: Number of amounts given is not equal to number of release heights given.");
604 | std::process::exit(1);
605 | }
606 | let mut schedules: Vec = Vec::with_capacity(schedule_amounts.len());
607 | for (&a, &h) in schedule_amounts.iter().zip(schedule_times.iter()) {
608 | schedules.push(Schedule {
609 | release_time: h,
610 | amount: a,
611 | });
612 | }
613 |
614 | command_create_svc(
615 | rpc_client,
616 | program_id,
617 | payer_keypair,
618 | source_keypair,
619 | source_token_pubkey,
620 | destination_pubkey,
621 | mint_address,
622 | schedules,
623 | confirm,
624 | )
625 | }
626 | ("unlock", Some(arg_matches)) => {
627 | // The seed is given in the format of a pubkey on the user side but it's handled as a [u8;32] in the program
628 | let vesting_seed = pubkey_of(arg_matches, "seed").unwrap().to_bytes();
629 | let payer_keypair = keypair_of(arg_matches, "payer").unwrap();
630 | command_unlock_svc(rpc_client, program_id, vesting_seed, payer_keypair)
631 | }
632 | ("change-destination", Some(arg_matches)) => {
633 | let vesting_seed = pubkey_of(arg_matches, "seed").unwrap().to_bytes();
634 | let destination_account_owner =
635 | keypair_of(arg_matches, "current_destination_owner").unwrap();
636 | let opt_new_destination_account = pubkey_of(arg_matches, "new_destination_address");
637 | let opt_new_destination_token_account =
638 | pubkey_of(arg_matches, "new_destination_token_address");
639 | let payer_keypair = keypair_of(arg_matches, "payer").unwrap();
640 | command_change_destination(
641 | rpc_client,
642 | program_id,
643 | destination_account_owner,
644 | opt_new_destination_account,
645 | opt_new_destination_token_account,
646 | vesting_seed,
647 | payer_keypair,
648 | )
649 | }
650 | ("info", Some(arg_matches)) => {
651 | let vesting_seed = pubkey_of(arg_matches, "seed").unwrap().to_bytes();
652 | let rpcurl = value_of(arg_matches, "rpc_url").unwrap();
653 | command_info(rpc_client, rpcurl, program_id, vesting_seed)
654 | }
655 | _ => unreachable!(),
656 | };
657 | }
658 |
--------------------------------------------------------------------------------
/js/.eslintignore:
--------------------------------------------------------------------------------
1 | /lib
2 |
--------------------------------------------------------------------------------
/js/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line
2 | module.exports = {
3 | // eslint-disable-line import/no-commonjs
4 | env: {
5 | browser: true,
6 | es6: true,
7 | node: true,
8 | },
9 | extends: [
10 | 'eslint:recommended',
11 | 'plugin:import/errors',
12 | 'plugin:import/warnings',
13 | ],
14 | parser: 'babel-eslint',
15 | parserOptions: {
16 | sourceType: 'module',
17 | ecmaVersion: 8,
18 | },
19 | rules: {
20 | 'no-trailing-spaces': ['error'],
21 | 'import/first': ['error'],
22 | 'import/no-commonjs': ['error'],
23 | 'import/order': [
24 | 'error',
25 | {
26 | groups: [
27 | ['internal', 'external', 'builtin'],
28 | ['index', 'sibling', 'parent'],
29 | ],
30 | 'newlines-between': 'always',
31 | },
32 | ],
33 | indent: [
34 | 'error',
35 | 2,
36 | {
37 | MemberExpression: 1,
38 | SwitchCase: 1,
39 | },
40 | ],
41 | 'linebreak-style': ['error', 'unix'],
42 | 'no-console': [0],
43 | quotes: [
44 | 'error',
45 | 'single',
46 | {avoidEscape: true, allowTemplateLiterals: true},
47 | ],
48 | 'require-await': ['error'],
49 | semi: ['error', 'always'],
50 | },
51 | settings: {
52 | react: {
53 | version: 'detect',
54 | },
55 | },
56 | };
57 |
--------------------------------------------------------------------------------
/js/.prettierrc.yaml:
--------------------------------------------------------------------------------
1 | arrowParens: 'avoid'
2 | bracketSpacing: true
3 | jsxBracketSameLine: false
4 | semi: true
5 | singleQuote: true
6 | tabWidth: 2
7 | trailingComma: 'all'
8 |
--------------------------------------------------------------------------------
/js/README.md:
--------------------------------------------------------------------------------
1 | # Simple JS bindings
2 |
3 | ## Example
4 |
5 | A linear unlock example can be found in `example.ts`
6 |
7 | ## Quickstart
8 |
9 | Contract address on Devnet
10 |
11 | ```
12 | DLxB9dSQtA4WJ49hWFhxqiQkD9v6m67Yfk9voxpxrBs4
13 | ```
14 |
15 | Contract address on Mainnet
16 |
17 | ```
18 | CChTq6PthWU82YZkbveA3WDf7s97BWhBK4Vx9bmsT743
19 | ```
20 |
21 | The code allows you to
22 |
23 | - Create vesting instructions for any SPL token: `createCreateInstruction`
24 | - Create unlock instructions: `createUnlockInstruction`
25 | - Change the destination of the vested tokens: `createChangeDestinationInstruction`
26 |
27 | (To import Solana accounts created with [Sollet](https://sollet.io) you can use `getAccountFromSeed`)
28 |
29 | ```
30 | Seed 9043936629442508205162695100279588102353854608998701852963634059
31 | Vesting contract account pubkey: r2p2mLJvyrTzetxxsttQ54CS1m18zMgYqKSRzxP9WpE
32 | contract ID: 90439366294425082051626951002795881023538546089987018529636340fe
33 | ✅ Successfully created vesting instructions
34 | 🚚 Transaction signature: 2uypTM3QcroR7uk6g9Y4eLdniCHqdQBDq4XyrFM7hCtTbb4rftkEHMM6vJ6tTYpihYubHt55xWD86vHB857bqXXb
35 |
36 | Fetching contract r2p2mLJvyrTzetxxsttQ54CS1m18zMgYqKSRzxP9WpE
37 | ✅ Successfully created unlocking instructions
38 | 🚚 Transaction signature: 2Vg3W1w8WBdRAWBEwFTn2BtMkKPD3Xor7SRvzC193UnsUnhmneUChPHe7vLF9Lfw9BKxWH5JbbJmnda4XztHMVHz
39 |
40 | Fetching contract r2p2mLJvyrTzetxxsttQ54CS1m18zMgYqKSRzxP9WpE
41 | ✅ Successfully changed destination
42 | 🚚 Transaction signature: 4tgPgCdM5ubaSKNLKD1WrfAJPZgRajxRSnmcPkHcN1TCeCRmq3cUCYVdCzsYwr63JRf4D2K1UZnt8rwu2pkGxeYe
43 | ```
44 |
--------------------------------------------------------------------------------
/js/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@bonfida/token-vesting",
3 | "version": "0.0.9",
4 | "license": "MIT",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/bonfida/token-vesting"
8 | },
9 | "publishConfig": {
10 | "access": "public"
11 | },
12 | "main": "dist/index.js",
13 | "types": "dist/index.d.ts",
14 | "files": [
15 | "dist",
16 | "src"
17 | ],
18 | "scripts": {
19 | "dev": "tsc && node dist/dev.js",
20 | "build": "rm -rf dist && rollup -c",
21 | "prepublish": "rm -rf dist && rollup -c",
22 | "lint": "npm run pretty && eslint .",
23 | "lint:fix": "npm run pretty:fix && eslint . --fix",
24 | "pretty": "prettier --check 'src/*.[jt]s'",
25 | "pretty:fix": "prettier --write 'src/*.[jt]s'"
26 | },
27 | "devDependencies": {
28 | "@bonfida/utils": "0.0.4",
29 | "@rollup/plugin-commonjs": "^24.0.1",
30 | "@rollup/plugin-typescript": "^11.0.0",
31 | "@solana/spl-token": "0.3.7",
32 | "@solana/web3.js": "^1.73.2",
33 | "@tsconfig/recommended": "^1.0.1",
34 | "@types/node": "^14.14.20",
35 | "babel-eslint": "^10.1.0",
36 | "eslint": "^7.17.0",
37 | "eslint-plugin-import": "^2.22.1",
38 | "nodemon": "^2.0.7",
39 | "prettier": "^2.2.1",
40 | "rollup": "^3.14.0",
41 | "rollup-plugin-terser": "^7.0.2",
42 | "ts-node": "^9.1.1",
43 | "tslib": "^2.1.0",
44 | "typescript": "^4.1.3"
45 | },
46 | "peerDependencies": {
47 | "@solana/spl-token": "0.3.7",
48 | "@solana/web3.js": "^1.73.2"
49 | },
50 | "dependencies": {
51 | "bip32": "^2.0.6",
52 | "bn.js": "^5.1.3",
53 | "bs58": "^4.0.1",
54 | "buffer-layout": "^1.2.0",
55 | "tweetnacl": "^1.0.3"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/js/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import typescript from "@rollup/plugin-typescript";
2 | import commonjs from "@rollup/plugin-commonjs";
3 | import { terser } from "rollup-plugin-terser";
4 |
5 | export default {
6 | input: "src/index.ts",
7 | output: {
8 | dir: "dist",
9 | format: "cjs",
10 | },
11 | plugins: [typescript(), commonjs(), terser()],
12 | };
13 |
--------------------------------------------------------------------------------
/js/src/example.ts:
--------------------------------------------------------------------------------
1 | import { Connection, PublicKey, Keypair } from '@solana/web3.js';
2 | import fs from 'fs';
3 | import { Numberu64, generateRandomSeed } from './utils';
4 | import { Schedule } from './state';
5 | import { create, TOKEN_VESTING_PROGRAM_ID } from './main';
6 | import { signAndSendInstructions } from '@bonfida/utils';
7 |
8 | /**
9 | *
10 | * Simple example of a linear unlock.
11 | *
12 | * This is just an example, please be careful using the vesting contract and test it first with test tokens.
13 | *
14 | */
15 |
16 | /** Path to your wallet */
17 | const WALLET_PATH = '';
18 | const wallet = Keypair.fromSecretKey(
19 | new Uint8Array(JSON.parse(fs.readFileSync(WALLET_PATH).toString())),
20 | );
21 |
22 | /** There are better way to generate an array of dates but be careful as it's irreversible */
23 | const DATES = [
24 | new Date(2022, 12),
25 | new Date(2023, 1),
26 | new Date(2023, 2),
27 | new Date(2023, 3),
28 | new Date(2023, 4),
29 | new Date(2023, 5),
30 | new Date(2023, 6),
31 | new Date(2023, 7),
32 | new Date(2023, 8),
33 | new Date(2023, 9),
34 | new Date(2023, 10),
35 | new Date(2023, 11),
36 | new Date(2024, 12),
37 | new Date(2024, 2),
38 | new Date(2024, 3),
39 | new Date(2024, 4),
40 | new Date(2024, 5),
41 | new Date(2024, 6),
42 | new Date(2024, 7),
43 | new Date(2024, 8),
44 | new Date(2024, 9),
45 | new Date(2024, 10),
46 | new Date(2024, 11),
47 | new Date(2024, 12),
48 | ];
49 |
50 | /** Info about the desintation */
51 | const DESTINATION_OWNER = new PublicKey('');
52 | const DESTINATION_TOKEN_ACCOUNT = new PublicKey('');
53 |
54 | /** Token info */
55 | const MINT = new PublicKey('');
56 | const DECIMALS = 0;
57 |
58 | /** Info about the source */
59 | const SOURCE_TOKEN_ACCOUNT = new PublicKey('');
60 |
61 | /** Amount to give per schedule */
62 | const AMOUNT_PER_SCHEDULE = 0;
63 |
64 | /** Your RPC connection */
65 | const connection = new Connection('');
66 |
67 | /** Do some checks before sending the tokens */
68 | const checks = async () => {
69 | const tokenInfo = await connection.getParsedAccountInfo(
70 | DESTINATION_TOKEN_ACCOUNT,
71 | );
72 |
73 | // @ts-ignore
74 | const parsed = tokenInfo.value.data.parsed;
75 | if (parsed.info.mint !== MINT.toBase58()) {
76 | throw new Error('Invalid mint');
77 | }
78 | if (parsed.info.owner !== DESTINATION_OWNER.toBase58()) {
79 | throw new Error('Invalid owner');
80 | }
81 | if (parsed.info.tokenAmount.decimals !== DECIMALS) {
82 | throw new Error('Invalid decimals');
83 | }
84 | };
85 |
86 | /** Function that locks the tokens */
87 | const lock = async () => {
88 | await checks();
89 | const schedules: Schedule[] = [];
90 | for (let date of DATES) {
91 | schedules.push(
92 | new Schedule(
93 | /** Has to be in seconds */
94 | // @ts-ignore
95 | new Numberu64(date.getTime() / 1_000),
96 | /** Don't forget to add decimals */
97 | // @ts-ignore
98 | new Numberu64(AMOUNT_PER_SCHEDULE * Math.pow(10, DECIMALS)),
99 | ),
100 | );
101 | }
102 | const seed = generateRandomSeed();
103 |
104 | console.log(`Seed: ${seed}`);
105 |
106 | const instruction = await create(
107 | connection,
108 | TOKEN_VESTING_PROGRAM_ID,
109 | Buffer.from(seed),
110 | wallet.publicKey,
111 | wallet.publicKey,
112 | SOURCE_TOKEN_ACCOUNT,
113 | DESTINATION_TOKEN_ACCOUNT,
114 | MINT,
115 | schedules,
116 | );
117 |
118 | const tx = await signAndSendInstructions(connection, [], wallet, instruction);
119 |
120 | console.log(`Transaction: ${tx}`);
121 |
122 | const txInfo = await connection.getConfirmedTransaction(tx, 'confirmed');
123 | if (txInfo && !txInfo.meta?.err) {
124 | console.log(
125 | txInfo?.transaction.instructions[2].data.slice(1, 32 + 1).toString('hex'),
126 | );
127 | } else {
128 | throw new Error('Transaction not confirmed.');
129 | }
130 | };
131 |
132 | lock();
133 |
--------------------------------------------------------------------------------
/js/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './instructions';
2 | export * from './main';
3 | export * from './state';
4 | export * from './utils';
5 |
--------------------------------------------------------------------------------
/js/src/instructions.ts:
--------------------------------------------------------------------------------
1 | import { PublicKey, SYSVAR_RENT_PUBKEY, TransactionInstruction } from '@solana/web3.js';
2 | import { Schedule } from './state';
3 | import { Numberu32 } from './utils';
4 |
5 | export enum Instruction {
6 | Init,
7 | Create,
8 | }
9 |
10 | export function createInitInstruction(
11 | systemProgramId: PublicKey,
12 | vestingProgramId: PublicKey,
13 | payerKey: PublicKey,
14 | vestingAccountKey: PublicKey,
15 | seeds: Array,
16 | numberOfSchedules: number,
17 | ): TransactionInstruction {
18 | let buffers = [
19 | Buffer.from(Int8Array.from([0]).buffer),
20 | Buffer.concat(seeds),
21 | // @ts-ignore
22 | new Numberu32(numberOfSchedules).toBuffer(),
23 | ];
24 |
25 | const data = Buffer.concat(buffers);
26 | const keys = [
27 | {
28 | pubkey: systemProgramId,
29 | isSigner: false,
30 | isWritable: false,
31 | },
32 | {
33 | pubkey: SYSVAR_RENT_PUBKEY,
34 | isSigner: false,
35 | isWritable: false,
36 | },
37 | {
38 | pubkey: payerKey,
39 | isSigner: true,
40 | isWritable: true,
41 | },
42 | {
43 | pubkey: vestingAccountKey,
44 | isSigner: false,
45 | isWritable: true,
46 | },
47 | ];
48 |
49 | return new TransactionInstruction({
50 | keys,
51 | programId: vestingProgramId,
52 | data,
53 | });
54 | }
55 |
56 | export function createCreateInstruction(
57 | vestingProgramId: PublicKey,
58 | tokenProgramId: PublicKey,
59 | vestingAccountKey: PublicKey,
60 | vestingTokenAccountKey: PublicKey,
61 | sourceTokenAccountOwnerKey: PublicKey,
62 | sourceTokenAccountKey: PublicKey,
63 | destinationTokenAccountKey: PublicKey,
64 | mintAddress: PublicKey,
65 | schedules: Array,
66 | seeds: Array,
67 | ): TransactionInstruction {
68 | let buffers = [
69 | Buffer.from(Int8Array.from([1]).buffer),
70 | Buffer.concat(seeds),
71 | mintAddress.toBuffer(),
72 | destinationTokenAccountKey.toBuffer(),
73 | ];
74 |
75 | schedules.forEach(s => {
76 | buffers.push(s.toBuffer());
77 | });
78 |
79 | const data = Buffer.concat(buffers);
80 | const keys = [
81 | {
82 | pubkey: tokenProgramId,
83 | isSigner: false,
84 | isWritable: false,
85 | },
86 | {
87 | pubkey: vestingAccountKey,
88 | isSigner: false,
89 | isWritable: true,
90 | },
91 | {
92 | pubkey: vestingTokenAccountKey,
93 | isSigner: false,
94 | isWritable: true,
95 | },
96 | {
97 | pubkey: sourceTokenAccountOwnerKey,
98 | isSigner: true,
99 | isWritable: false,
100 | },
101 | {
102 | pubkey: sourceTokenAccountKey,
103 | isSigner: false,
104 | isWritable: true,
105 | },
106 | ];
107 | return new TransactionInstruction({
108 | keys,
109 | programId: vestingProgramId,
110 | data,
111 | });
112 | }
113 |
114 | export function createUnlockInstruction(
115 | vestingProgramId: PublicKey,
116 | tokenProgramId: PublicKey,
117 | clockSysvarId: PublicKey,
118 | vestingAccountKey: PublicKey,
119 | vestingTokenAccountKey: PublicKey,
120 | destinationTokenAccountKey: PublicKey,
121 | seeds: Array,
122 | ): TransactionInstruction {
123 | const data = Buffer.concat([
124 | Buffer.from(Int8Array.from([2]).buffer),
125 | Buffer.concat(seeds),
126 | ]);
127 |
128 | const keys = [
129 | {
130 | pubkey: tokenProgramId,
131 | isSigner: false,
132 | isWritable: false,
133 | },
134 | {
135 | pubkey: clockSysvarId,
136 | isSigner: false,
137 | isWritable: false,
138 | },
139 | {
140 | pubkey: vestingAccountKey,
141 | isSigner: false,
142 | isWritable: true,
143 | },
144 | {
145 | pubkey: vestingTokenAccountKey,
146 | isSigner: false,
147 | isWritable: true,
148 | },
149 | {
150 | pubkey: destinationTokenAccountKey,
151 | isSigner: false,
152 | isWritable: true,
153 | },
154 | ];
155 | return new TransactionInstruction({
156 | keys,
157 | programId: vestingProgramId,
158 | data,
159 | });
160 | }
161 |
162 | export function createChangeDestinationInstruction(
163 | vestingProgramId: PublicKey,
164 | vestingAccountKey: PublicKey,
165 | currentDestinationTokenAccountOwner: PublicKey,
166 | currentDestinationTokenAccount: PublicKey,
167 | targetDestinationTokenAccount: PublicKey,
168 | seeds: Array,
169 | ): TransactionInstruction {
170 | const data = Buffer.concat([
171 | Buffer.from(Int8Array.from([3]).buffer),
172 | Buffer.concat(seeds),
173 | ]);
174 |
175 | const keys = [
176 | {
177 | pubkey: vestingAccountKey,
178 | isSigner: false,
179 | isWritable: true,
180 | },
181 | {
182 | pubkey: currentDestinationTokenAccount,
183 | isSigner: false,
184 | isWritable: false,
185 | },
186 | {
187 | pubkey: currentDestinationTokenAccountOwner,
188 | isSigner: true,
189 | isWritable: false,
190 | },
191 | {
192 | pubkey: targetDestinationTokenAccount,
193 | isSigner: false,
194 | isWritable: false,
195 | },
196 | ];
197 | return new TransactionInstruction({
198 | keys,
199 | programId: vestingProgramId,
200 | data,
201 | });
202 | }
203 |
--------------------------------------------------------------------------------
/js/src/main.ts:
--------------------------------------------------------------------------------
1 | import {
2 | PublicKey,
3 | SystemProgram,
4 | SYSVAR_CLOCK_PUBKEY,
5 | TransactionInstruction,
6 | Connection,
7 | } from '@solana/web3.js';
8 | import {
9 | createAssociatedTokenAccountInstruction,
10 | TOKEN_PROGRAM_ID,
11 | getAssociatedTokenAddress,
12 | } from '@solana/spl-token';
13 | import {
14 | createChangeDestinationInstruction,
15 | createCreateInstruction,
16 | createInitInstruction,
17 | createUnlockInstruction,
18 | } from './instructions';
19 | import { ContractInfo, Schedule } from './state';
20 | import { assert } from 'console';
21 | import bs58 from 'bs58';
22 |
23 | /**
24 | * The vesting schedule program ID on mainnet
25 | */
26 | export const TOKEN_VESTING_PROGRAM_ID = new PublicKey(
27 | 'CChTq6PthWU82YZkbveA3WDf7s97BWhBK4Vx9bmsT743',
28 | );
29 |
30 | /**
31 | * This function can be used to lock tokens
32 | * @param connection The Solana RPC connection object
33 | * @param programId The token vesting program ID
34 | * @param seedWord Seed words used to derive the vesting account
35 | * @param payer The fee payer of the transaction
36 | * @param sourceTokenOwner The owner of the source token account (i.e where locked tokens are originating from)
37 | * @param possibleSourceTokenPubkey The source token account (i.e where locked tokens are originating from), if null it defaults to the ATA
38 | * @param destinationTokenPubkey The destination token account i.e where unlocked tokens will be transfered
39 | * @param mintAddress The mint of the tokens being vested
40 | * @param schedules The array of vesting schedules
41 | * @returns An array of `TransactionInstruction`
42 | */
43 | export async function create(
44 | connection: Connection,
45 | programId: PublicKey,
46 | seedWord: Buffer | Uint8Array,
47 | payer: PublicKey,
48 | sourceTokenOwner: PublicKey,
49 | possibleSourceTokenPubkey: PublicKey | null,
50 | destinationTokenPubkey: PublicKey,
51 | mintAddress: PublicKey,
52 | schedules: Array,
53 | ): Promise> {
54 | // If no source token account was given, use the associated source account
55 | if (possibleSourceTokenPubkey == null) {
56 | possibleSourceTokenPubkey = await getAssociatedTokenAddress(
57 | mintAddress,
58 | sourceTokenOwner,
59 | true,
60 | );
61 | }
62 |
63 | // Find the non reversible public key for the vesting contract via the seed
64 | seedWord = seedWord.slice(0, 31);
65 | const [vestingAccountKey, bump] = await PublicKey.findProgramAddress(
66 | [seedWord],
67 | programId,
68 | );
69 |
70 | const vestingTokenAccountKey = await getAssociatedTokenAddress(
71 | mintAddress,
72 | vestingAccountKey,
73 | true,
74 | );
75 |
76 | seedWord = Buffer.from(seedWord.toString('hex') + bump.toString(16), 'hex');
77 |
78 | console.log(
79 | 'Vesting contract account pubkey: ',
80 | vestingAccountKey.toBase58(),
81 | );
82 |
83 | console.log('contract ID: ', bs58.encode(seedWord));
84 |
85 | const check_existing = await connection.getAccountInfo(vestingAccountKey);
86 | if (!!check_existing) {
87 | throw 'Contract already exists.';
88 | }
89 |
90 | let instruction = [
91 | createInitInstruction(
92 | SystemProgram.programId,
93 | programId,
94 | payer,
95 | vestingAccountKey,
96 | [seedWord],
97 | schedules.length,
98 | ),
99 | createAssociatedTokenAccountInstruction(
100 | payer,
101 | vestingTokenAccountKey,
102 | vestingAccountKey,
103 | mintAddress,
104 | ),
105 | createCreateInstruction(
106 | programId,
107 | TOKEN_PROGRAM_ID,
108 | vestingAccountKey,
109 | vestingTokenAccountKey,
110 | sourceTokenOwner,
111 | possibleSourceTokenPubkey,
112 | destinationTokenPubkey,
113 | mintAddress,
114 | schedules,
115 | [seedWord],
116 | ),
117 | ];
118 | return instruction;
119 | }
120 |
121 | /**
122 | * This function can be used to unlock vested tokens
123 | * @param connection The Solana RPC connection object
124 | * @param programId The token vesting program ID
125 | * @param seedWord Seed words used to derive the vesting account
126 | * @param mintAddress The mint of the vested tokens
127 | * @returns An array of `TransactionInstruction`
128 | */
129 | export async function unlock(
130 | connection: Connection,
131 | programId: PublicKey,
132 | seedWord: Buffer | Uint8Array,
133 | mintAddress: PublicKey,
134 | ): Promise> {
135 | seedWord = seedWord.slice(0, 31);
136 | const [vestingAccountKey, bump] = await PublicKey.findProgramAddress(
137 | [seedWord],
138 | programId,
139 | );
140 | seedWord = Buffer.from(seedWord.toString('hex') + bump.toString(16), 'hex');
141 |
142 | const vestingTokenAccountKey = await getAssociatedTokenAddress(
143 | mintAddress,
144 | vestingAccountKey,
145 | true,
146 | );
147 |
148 | const vestingInfo = await getContractInfo(connection, vestingAccountKey);
149 |
150 | let instruction = [
151 | createUnlockInstruction(
152 | programId,
153 | TOKEN_PROGRAM_ID,
154 | SYSVAR_CLOCK_PUBKEY,
155 | vestingAccountKey,
156 | vestingTokenAccountKey,
157 | vestingInfo.destinationAddress,
158 | [seedWord],
159 | ),
160 | ];
161 |
162 | return instruction;
163 | }
164 |
165 | /**
166 | * This function can be used retrieve information about a vesting account
167 | * @param connection The Solana RPC connection object
168 | * @param vestingAccountKey The vesting account public key
169 | * @returns A `ContractInfo` object
170 | */
171 | export async function getContractInfo(
172 | connection: Connection,
173 | vestingAccountKey: PublicKey,
174 | ): Promise {
175 | console.log('Fetching contract ', vestingAccountKey.toBase58());
176 | const vestingInfo = await connection.getAccountInfo(
177 | vestingAccountKey,
178 | 'single',
179 | );
180 | if (!vestingInfo) {
181 | throw new Error('Vesting contract account is unavailable');
182 | }
183 | const info = ContractInfo.fromBuffer(vestingInfo!.data);
184 | if (!info) {
185 | throw new Error('Vesting contract account is not initialized');
186 | }
187 | return info!;
188 | }
189 |
190 | /**
191 | * This function can be used to transfer a vesting account to a new wallet. It requires the current owner to sign.
192 | * @param connection The Solana RPC connection object
193 | * @param programId The token vesting program ID
194 | * @param currentDestinationTokenAccountPublicKey The current token account to which the vested tokens are transfered to as they unlock
195 | * @param newDestinationTokenAccountOwner The new owner of the vesting account
196 | * @param newDestinationTokenAccount The new token account to which the vested tokens will be transfered to as they unlock
197 | * @param vestingSeed Seed words used to derive the vesting account
198 | * @returns An array of `TransactionInstruction`
199 | */
200 | export async function changeDestination(
201 | connection: Connection,
202 | programId: PublicKey,
203 | currentDestinationTokenAccountPublicKey: PublicKey,
204 | newDestinationTokenAccountOwner: PublicKey | undefined,
205 | newDestinationTokenAccount: PublicKey | undefined,
206 | vestingSeed: Array,
207 | ): Promise> {
208 | let seedWord = vestingSeed[0];
209 | seedWord = seedWord.slice(0, 31);
210 | const [vestingAccountKey, bump] = await PublicKey.findProgramAddress(
211 | [seedWord],
212 | programId,
213 | );
214 | seedWord = Buffer.from(seedWord.toString('hex') + bump.toString(16), 'hex');
215 |
216 | const contractInfo = await getContractInfo(connection, vestingAccountKey);
217 | if (!newDestinationTokenAccount) {
218 | assert(
219 | !!newDestinationTokenAccountOwner,
220 | 'At least one of newDestinationTokenAccount and newDestinationTokenAccountOwner must be provided!',
221 | );
222 | newDestinationTokenAccount = await getAssociatedTokenAddress(
223 | contractInfo.mintAddress,
224 | newDestinationTokenAccountOwner!,
225 | true,
226 | );
227 | }
228 |
229 | return [
230 | createChangeDestinationInstruction(
231 | programId,
232 | vestingAccountKey,
233 | currentDestinationTokenAccountPublicKey,
234 | contractInfo.destinationAddress,
235 | newDestinationTokenAccount,
236 | [seedWord],
237 | ),
238 | ];
239 | }
240 |
--------------------------------------------------------------------------------
/js/src/state.ts:
--------------------------------------------------------------------------------
1 | import { PublicKey } from '@solana/web3.js';
2 | import { Numberu64 } from './utils';
3 |
4 | export class Schedule {
5 | // Release time in unix timestamp
6 | releaseTime!: Numberu64;
7 | amount!: Numberu64;
8 |
9 | constructor(releaseTime: Numberu64, amount: Numberu64) {
10 | this.releaseTime = releaseTime;
11 | this.amount = amount;
12 | }
13 |
14 | public toBuffer(): Buffer {
15 | return Buffer.concat([this.releaseTime.toBuffer(), this.amount.toBuffer()]);
16 | }
17 |
18 | static fromBuffer(buf: Buffer): Schedule {
19 | const releaseTime: Numberu64 = Numberu64.fromBuffer(buf.slice(0, 8));
20 | const amount: Numberu64 = Numberu64.fromBuffer(buf.slice(8, 16));
21 | return new Schedule(releaseTime, amount);
22 | }
23 | }
24 |
25 | export class VestingScheduleHeader {
26 | destinationAddress!: PublicKey;
27 | mintAddress!: PublicKey;
28 | isInitialized!: boolean;
29 |
30 | constructor(
31 | destinationAddress: PublicKey,
32 | mintAddress: PublicKey,
33 | isInitialized: boolean,
34 | ) {
35 | this.destinationAddress = destinationAddress;
36 | this.mintAddress = mintAddress;
37 | this.isInitialized = isInitialized;
38 | }
39 |
40 | static fromBuffer(buf: Buffer): VestingScheduleHeader {
41 | const destinationAddress = new PublicKey(buf.slice(0, 32));
42 | const mintAddress = new PublicKey(buf.slice(32, 64));
43 | const isInitialized = buf[64] == 1;
44 | const header: VestingScheduleHeader = {
45 | destinationAddress,
46 | mintAddress,
47 | isInitialized,
48 | };
49 | return header;
50 | }
51 | }
52 |
53 | export class ContractInfo {
54 | destinationAddress!: PublicKey;
55 | mintAddress!: PublicKey;
56 | schedules!: Array;
57 |
58 | constructor(
59 | destinationAddress: PublicKey,
60 | mintAddress: PublicKey,
61 | schedules: Array,
62 | ) {
63 | this.destinationAddress = destinationAddress;
64 | this.mintAddress = mintAddress;
65 | this.schedules = schedules;
66 | }
67 |
68 | static fromBuffer(buf: Buffer): ContractInfo | undefined {
69 | const header = VestingScheduleHeader.fromBuffer(buf.slice(0, 65));
70 | if (!header.isInitialized) {
71 | return undefined;
72 | }
73 | const schedules: Array = [];
74 | for (let i = 65; i < buf.length; i += 16) {
75 | schedules.push(Schedule.fromBuffer(buf.slice(i, i + 16)));
76 | }
77 | return new ContractInfo(
78 | header.destinationAddress,
79 | header.mintAddress,
80 | schedules,
81 | );
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/js/src/utils.ts:
--------------------------------------------------------------------------------
1 | import BN from 'bn.js';
2 |
3 | export const generateRandomSeed = () => {
4 | // Generate a random seed
5 | let seed = '';
6 | for (let i = 0; i < 64; i++) {
7 | seed += Math.floor(Math.random() * 10);
8 | }
9 | return seed;
10 | };
11 |
12 | export class Numberu64 extends BN {
13 | /**
14 | * Convert to Buffer representation
15 | */
16 | toBuffer(): Buffer {
17 | const a = super.toArray().reverse();
18 | const b = Buffer.from(a);
19 | if (b.length === 8) {
20 | return b;
21 | }
22 | if (b.length > 8) {
23 | throw new Error('Numberu64 too large');
24 | }
25 |
26 | const zeroPad = Buffer.alloc(8);
27 | b.copy(zeroPad);
28 | return zeroPad;
29 | }
30 |
31 | /**
32 | * Construct a Numberu64 from Buffer representation
33 | */
34 | static fromBuffer(buffer): any {
35 | if (buffer.length !== 8) {
36 | throw new Error(`Invalid buffer length: ${buffer.length}`);
37 | }
38 |
39 | return new BN(
40 | [...buffer]
41 | .reverse()
42 | .map(i => `00${i.toString(16)}`.slice(-2))
43 | .join(''),
44 | 16,
45 | );
46 | }
47 | }
48 |
49 | export class Numberu32 extends BN {
50 | /**
51 | * Convert to Buffer representation
52 | */
53 | toBuffer(): Buffer {
54 | const a = super.toArray().reverse();
55 | const b = Buffer.from(a);
56 | if (b.length === 4) {
57 | return b;
58 | }
59 | if (b.length > 4) {
60 | throw new Error('Numberu32 too large');
61 | }
62 |
63 | const zeroPad = Buffer.alloc(4);
64 | b.copy(zeroPad);
65 | return zeroPad;
66 | }
67 |
68 | /**
69 | * Construct a Numberu32 from Buffer representation
70 | */
71 | static fromBuffer(buffer): any {
72 | if (buffer.length !== 4) {
73 | throw new Error(`Invalid buffer length: ${buffer.length}`);
74 | }
75 |
76 | return new BN(
77 | [...buffer]
78 | .reverse()
79 | .map(i => `00${i.toString(16)}`.slice(-2))
80 | .join(''),
81 | 16,
82 | );
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/js/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/recommended/tsconfig.json",
3 | "ts-node": {
4 | "compilerOptions": {
5 | "module": "commonjs",
6 | "baseUrl": "./",
7 | "paths": {
8 | "*": ["types/*"]
9 | }
10 | }
11 | },
12 | "compilerOptions": {
13 | "allowJs": true,
14 | "module": "esnext",
15 | "esModuleInterop": true,
16 | "allowSyntheticDefaultImports": true,
17 | "target": "es2019",
18 | "outDir": "dist",
19 | "rootDir": "./src",
20 | "declaration": true,
21 | "noImplicitAny": false,
22 | "moduleResolution": "node",
23 | "sourceMap": false,
24 | "baseUrl": ".",
25 | "resolveJsonModule": true
26 | },
27 | "include": ["src/*", "src/.ts"],
28 | "exclude": ["**/node_modules", "dist", "tests", "src/example.ts"]
29 | }
30 |
--------------------------------------------------------------------------------
/program/.gitignore:
--------------------------------------------------------------------------------
1 | target
2 | .vscode
3 | hfuzz*
--------------------------------------------------------------------------------
/program/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "token-vesting"
3 | version = "0.1.0"
4 | authors = ["Elliott Benisty ", "Lucas Chaumeny "]
5 | edition = "2018"
6 |
7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8 |
9 | [workspace]
10 | members = [
11 | "fuzz"
12 | ]
13 |
14 | [features]
15 | no-entrypoint = []
16 | test-bpf = []
17 | fuzz = ["arbitrary", "honggfuzz"]
18 |
19 | [dependencies]
20 | thiserror = "1.0.23"
21 | num-traits = "0.2"
22 | num-derive = "0.3"
23 | arrayref = "0.3.6"
24 | solana-program = "1.5.6"
25 | spl-token = { version = "3.0.1", features = ["no-entrypoint"] }
26 | spl-associated-token-account = { version = "1.0.2", features = ["no-entrypoint"] }
27 | arbitrary = { version = "0.4", features = ["derive"], optional = true }
28 | honggfuzz = { version = "0.5", optional = true }
29 |
30 | [dev-dependencies]
31 | solana-sdk = "1.5.6"
32 | solana-program-test = "1.5.6"
33 | tokio = { version = "1.0", features = ["macros"]}
34 |
35 | [lib]
36 | crate-type = ["cdylib", "lib"]
37 |
--------------------------------------------------------------------------------
/program/Xargo.toml:
--------------------------------------------------------------------------------
1 | [target.bpfel-unknown-unknown.dependencies.std]
2 | features = []
--------------------------------------------------------------------------------
/program/fuzz/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "token-vesting-fuzz"
3 | version = "0.1.0"
4 | authors = ["Elliott Benisty ", "Lucas Chaumeny "]
5 | publish = false
6 | edition = "2018"
7 |
8 | [dependencies]
9 | honggfuzz = { version = "0.5" }
10 | arbitrary = { version = "0.4", features = ["derive"] }
11 | solana-program = "1.5.6"
12 | solana-sdk = "1.5.6"
13 | solana-program-test = "1.5.6"
14 | spl-token = { version = "3.0.1", features = ["no-entrypoint"] }
15 | spl-associated-token-account = { version = "1.0.2", features = ["no-entrypoint"] }
16 | token-vesting = { version = "0.1.0", path="..", features=["fuzz", "no-entrypoint"] }
17 | tokio = { version = "1.0", features = ["macros"]}
18 |
19 | [[bin]]
20 | name = "vesting_fuzz"
21 | path = "src/vesting_fuzz.rs"
22 | test = false
23 | doc = false
24 |
--------------------------------------------------------------------------------
/program/fuzz/README.md:
--------------------------------------------------------------------------------
1 | CMD used to run the fuzzing (run cargo build-bpf at least once before):
2 | ```
3 | BPF_OUT_DIR="/home/lcchy/repos/bonfida/token-vesting/program/target/deploy" HFUZZ_RUN_ARGS="-t 10 -n 32 -N 1000000" cargo hfuzz run vesting_fuzz
4 | ```
5 |
6 | CMD used to debug the last crash:
7 | ```
8 | BPF_OUT_DIR="/home/lcchy/repos/bonfida/token-vesting/program/target/deploy" cargo hfuzz run-debug vesting_fuzz hfuzz_workspace/*/*.fuzz
9 | ```
10 |
11 | `BPF_OUT_DIR` is there to force ProgramTest to load token_vesting as BPF code (see bug mentioned in /README.md).
12 |
--------------------------------------------------------------------------------
/program/fuzz/src/vesting_fuzz.rs:
--------------------------------------------------------------------------------
1 | use token_vesting::instruction;
2 | use spl_token::instruction::{initialize_mint, mint_to};
3 |
4 | use std::{convert::TryInto, str::FromStr};
5 | use spl_associated_token_account::{get_associated_token_address, create_associated_token_account};
6 |
7 | use solana_program::{hash::Hash, instruction::Instruction, pubkey::Pubkey, rent::Rent, system_program, sysvar};
8 | use honggfuzz::fuzz;
9 | use solana_program_test::{BanksClient, ProgramTest, processor};
10 | use solana_sdk::{signature::Keypair, signature::Signer, system_instruction, transaction::Transaction, transport::TransportError};
11 | use arbitrary::Arbitrary;
12 | use std::collections::HashMap;
13 | use token_vesting::{instruction::{Schedule, VestingInstruction}, processor::Processor};
14 | use token_vesting::instruction::{init, unlock, change_destination, create};
15 | use solana_sdk::{account::Account, instruction::InstructionError, transaction::TransactionError};
16 | struct TokenVestingEnv {
17 | system_program_id: Pubkey,
18 | token_program_id: Pubkey,
19 | sysvarclock_program_id: Pubkey,
20 | rent_program_id: Pubkey,
21 | vesting_program_id: Pubkey,
22 | mint_authority: Keypair
23 | }
24 |
25 | #[derive(Debug, Arbitrary, Clone)]
26 | struct FuzzInstruction {
27 | vesting_account_key: AccountId,
28 | vesting_token_account_key: AccountId,
29 | source_token_account_owner_key: AccountId,
30 | source_token_account_key: AccountId,
31 | source_token_amount: u64,
32 | destination_token_owner_key: AccountId,
33 | destination_token_key: AccountId,
34 | new_destination_token_key: AccountId,
35 | mint_key: AccountId,
36 | schedules: Vec,
37 | payer_key: AccountId,
38 | vesting_program_account: AccountId,
39 | seeds:[u8; 32],
40 | number_of_schedules: u8,
41 | instruction: instruction::VestingInstruction,
42 | // This flag decides wether the instruction will be executed with inputs that should
43 | // not provoke any errors. (The accounts and contracts will be set up before if needed)
44 | correct_inputs: bool
45 | }
46 | /// Use u8 as an account id to simplify the address space and re-use accounts
47 | /// more often.
48 | type AccountId = u8;
49 |
50 |
51 | fn main() {
52 | let rt = tokio::runtime::Runtime::new().unwrap();
53 |
54 | // Set up the fixed test environment
55 | let token_vesting_testenv = TokenVestingEnv {
56 | system_program_id: system_program::id(),
57 | sysvarclock_program_id: sysvar::clock::id(),
58 | rent_program_id: sysvar::rent::id(),
59 | token_program_id: spl_token::id(),
60 | vesting_program_id: Pubkey::from_str("VestingbGKPFXCWuBvfkegQfZyiNwAJb9Ss623VQ5DA").unwrap(),
61 | mint_authority: Keypair::new()
62 | };
63 |
64 |
65 | loop {
66 | fuzz!(|fuzz_instructions: Vec| {
67 | // Initialize and start the test network
68 | let mut program_test = ProgramTest::new(
69 | "token_vesting",
70 | token_vesting_testenv.vesting_program_id,
71 | processor!(Processor::process_instruction),
72 | );
73 |
74 | program_test.add_account(token_vesting_testenv.mint_authority.pubkey(), Account {
75 | lamports: u32::MAX as u64,
76 | ..Account::default()
77 | });
78 | let mut test_state = rt.block_on(program_test.start_with_context());
79 |
80 | rt.block_on(run_fuzz_instructions(&token_vesting_testenv, &mut test_state.banks_client, fuzz_instructions, &test_state.payer, test_state.last_blockhash));
81 | });
82 | }
83 | }
84 |
85 |
86 | async fn run_fuzz_instructions(
87 | token_vesting_testenv: &TokenVestingEnv,
88 | banks_client: &mut BanksClient,
89 | fuzz_instructions: Vec,
90 | correct_payer: &Keypair,
91 | recent_blockhash: Hash
92 | ) {
93 | // keep track of all accounts
94 | let mut vesting_account_keys: HashMap = HashMap::new();
95 | let mut vesting_token_account_keys: HashMap = HashMap::new();
96 | let mut source_token_account_owner_keys: HashMap = HashMap::new();
97 | let mut destination_token_owner_keys: HashMap = HashMap::new();
98 | let mut destination_token_keys: HashMap = HashMap::new();
99 | let mut new_destination_token_keys: HashMap = HashMap::new();
100 | let mut mint_keys: HashMap = HashMap::new();
101 | let mut payer_keys: HashMap = HashMap::new();
102 |
103 | let mut global_output_instructions = vec![];
104 | let mut global_signer_keys = vec![];
105 |
106 | for fuzz_instruction in fuzz_instructions {
107 |
108 | // Add accounts
109 | vesting_account_keys
110 | .entry(fuzz_instruction.vesting_account_key)
111 | .or_insert_with(|| Pubkey::new_unique());
112 | vesting_token_account_keys
113 | .entry(fuzz_instruction.vesting_token_account_key)
114 | .or_insert_with(|| Pubkey::new_unique());
115 | source_token_account_owner_keys
116 | .entry(fuzz_instruction.source_token_account_owner_key)
117 | .or_insert_with(|| Keypair::new());
118 | destination_token_owner_keys
119 | .entry(fuzz_instruction.destination_token_owner_key)
120 | .or_insert_with(|| Keypair::new());
121 | destination_token_keys
122 | .entry(fuzz_instruction.destination_token_key)
123 | .or_insert_with(|| Pubkey::new_unique());
124 | new_destination_token_keys
125 | .entry(fuzz_instruction.new_destination_token_key)
126 | .or_insert_with(|| Pubkey::new_unique());
127 | mint_keys
128 | .entry(fuzz_instruction.mint_key)
129 | .or_insert_with(|| Keypair::new());
130 | payer_keys
131 | .entry(fuzz_instruction.payer_key)
132 | .or_insert_with(|| Keypair::new());
133 |
134 | let (mut output_instructions, mut signer_keys) = run_fuzz_instruction(
135 | &token_vesting_testenv,
136 | &fuzz_instruction,
137 | &correct_payer,
138 | mint_keys.get(&fuzz_instruction.mint_key).unwrap(),
139 | vesting_account_keys.get(&fuzz_instruction.vesting_account_key).unwrap(),
140 | vesting_token_account_keys.get(&fuzz_instruction.vesting_token_account_key).unwrap(),
141 | source_token_account_owner_keys.get(
142 | &fuzz_instruction.source_token_account_owner_key
143 | ).unwrap(),
144 | destination_token_owner_keys.get(
145 | &fuzz_instruction.destination_token_owner_key
146 | ).unwrap(),
147 | destination_token_keys.get(
148 | &fuzz_instruction.destination_token_key
149 | ).unwrap(),
150 | new_destination_token_keys.get(
151 | &fuzz_instruction.new_destination_token_key
152 | ).unwrap(),
153 | payer_keys.get(&fuzz_instruction.payer_key).unwrap()
154 | );
155 | global_output_instructions.append(&mut output_instructions);
156 | global_signer_keys.append(&mut signer_keys);
157 | }
158 | // Process transaction on test network
159 | let mut transaction = Transaction::new_with_payer(
160 | &global_output_instructions,
161 | Some(&correct_payer.pubkey()),
162 | );
163 | let signers = [correct_payer].iter().map(|&v| v).chain(global_signer_keys.iter()).collect::>();
164 | transaction.partial_sign(
165 | &signers,
166 | recent_blockhash
167 | );
168 |
169 | banks_client.process_transaction(transaction).await.unwrap_or_else(|e| {
170 | if let TransportError::TransactionError(te) = e {
171 | match te {
172 | TransactionError::InstructionError(_, ie) => {
173 | match ie {
174 | InstructionError::InvalidArgument
175 | | InstructionError::InvalidInstructionData
176 | | InstructionError::InvalidAccountData
177 | | InstructionError::InsufficientFunds
178 | | InstructionError::AccountAlreadyInitialized
179 | | InstructionError::InvalidSeeds
180 | | InstructionError::Custom(0) => {},
181 | _ => {
182 | print!("{:?}", ie);
183 | Err(ie).unwrap()
184 | }
185 | }
186 | },
187 | TransactionError::SignatureFailure
188 | | TransactionError::InvalidAccountForFee
189 | | TransactionError::InsufficientFundsForFee => {},
190 | _ => {
191 | print!("{:?}", te);
192 | panic!()
193 | }
194 | }
195 |
196 | } else {
197 | print!("{:?}", e);
198 | panic!()
199 | }
200 | });
201 | }
202 |
203 |
204 | fn run_fuzz_instruction(
205 | token_vesting_testenv: &TokenVestingEnv,
206 | fuzz_instruction: &FuzzInstruction,
207 | correct_payer: &Keypair,
208 | mint_key: &Keypair,
209 | vesting_account_key: &Pubkey,
210 | vesting_token_account_key: &Pubkey,
211 | source_token_account_owner_key: &Keypair,
212 | destination_token_owner_key: &Keypair,
213 | destination_token_key: &Pubkey,
214 | new_destination_token_key: &Pubkey,
215 | payer_key: &Keypair
216 | ) -> (Vec, Vec) {
217 |
218 | // Execute the fuzzing in a more restrained way in order to go deeper into the program branches.
219 | // For each possible fuzz instruction we first instantiate the needed accounts for the instruction
220 | if fuzz_instruction.correct_inputs {
221 |
222 | let mut correct_seeds = fuzz_instruction.seeds;
223 | let (correct_vesting_account_key, bump) = Pubkey::find_program_address(
224 | &[&correct_seeds[..31]],
225 | &token_vesting_testenv.vesting_program_id
226 | );
227 | correct_seeds[31] = bump;
228 | let correct_vesting_token_key = get_associated_token_address(
229 | &correct_vesting_account_key,
230 | &mint_key.pubkey()
231 | );
232 | let correct_source_token_account_key = get_associated_token_address(
233 | &source_token_account_owner_key.pubkey(),
234 | &mint_key.pubkey()
235 | );
236 |
237 | match fuzz_instruction {
238 | FuzzInstruction {
239 | instruction: VestingInstruction::Init{ .. },
240 | ..
241 | } => {
242 | return (vec![init(
243 | &token_vesting_testenv.system_program_id,
244 | &token_vesting_testenv.rent_program_id,
245 | &token_vesting_testenv.vesting_program_id,
246 | &correct_payer.pubkey(),
247 | &correct_vesting_account_key,
248 | correct_seeds,
249 | fuzz_instruction.number_of_schedules as u32
250 | ).unwrap()], vec![]);
251 | },
252 |
253 | FuzzInstruction {
254 | instruction: VestingInstruction::Create { .. },
255 | ..
256 | } => {
257 | let mut instructions_acc = vec![init(
258 | &token_vesting_testenv.system_program_id,
259 | &token_vesting_testenv.rent_program_id,
260 | &token_vesting_testenv.vesting_program_id,
261 | &correct_payer.pubkey(),
262 | &correct_vesting_account_key,
263 | correct_seeds,
264 | fuzz_instruction.number_of_schedules as u32
265 | ).unwrap()];
266 | let mut create_instructions = create_fuzzinstruction(
267 | token_vesting_testenv,
268 | fuzz_instruction,
269 | correct_payer,
270 | &correct_source_token_account_key,
271 | source_token_account_owner_key,
272 | destination_token_key,
273 | &destination_token_owner_key.pubkey(),
274 | &correct_vesting_account_key,
275 | &correct_vesting_token_key,
276 | correct_seeds,
277 | mint_key,
278 | fuzz_instruction.source_token_amount
279 | );
280 | instructions_acc.append(&mut create_instructions);
281 | return (instructions_acc, vec![clone_keypair(mint_key),
282 | clone_keypair(&token_vesting_testenv.mint_authority),
283 | clone_keypair(source_token_account_owner_key)]);
284 | },
285 |
286 | FuzzInstruction {
287 | instruction: VestingInstruction::Unlock{ .. },
288 | ..
289 | } => {
290 | let mut instructions_acc = vec![init(
291 | &token_vesting_testenv.system_program_id,
292 | &token_vesting_testenv.rent_program_id,
293 | &token_vesting_testenv.vesting_program_id,
294 | &correct_payer.pubkey(),
295 | &correct_vesting_account_key,
296 | correct_seeds,
297 | fuzz_instruction.number_of_schedules as u32
298 | ).unwrap()];
299 | let mut create_instructions = create_fuzzinstruction(
300 | token_vesting_testenv,
301 | fuzz_instruction,
302 | correct_payer,
303 | &correct_source_token_account_key,
304 | source_token_account_owner_key,
305 | destination_token_key,
306 | &destination_token_owner_key.pubkey(),
307 | &correct_vesting_account_key,
308 | &correct_vesting_token_key,
309 | correct_seeds,
310 | mint_key,
311 | fuzz_instruction.source_token_amount
312 | );
313 | instructions_acc.append(&mut create_instructions);
314 |
315 | let unlock_instruction = unlock(
316 | &token_vesting_testenv.vesting_program_id,
317 | &token_vesting_testenv.token_program_id,
318 | &token_vesting_testenv.sysvarclock_program_id,
319 | &correct_vesting_account_key,
320 | &correct_vesting_token_key,
321 | destination_token_key,
322 | correct_seeds
323 | ).unwrap();
324 | instructions_acc.push(unlock_instruction);
325 | return (instructions_acc, vec![
326 | clone_keypair(mint_key),
327 | clone_keypair(&token_vesting_testenv.mint_authority),
328 | clone_keypair(source_token_account_owner_key),
329 | ]);
330 | },
331 |
332 | FuzzInstruction {
333 | instruction: VestingInstruction::ChangeDestination{ .. },
334 | ..
335 | } => {
336 | let mut instructions_acc = vec![init(
337 | &token_vesting_testenv.system_program_id,
338 | &token_vesting_testenv.rent_program_id,
339 | &token_vesting_testenv.vesting_program_id,
340 | &correct_payer.pubkey(),
341 | &correct_vesting_account_key,
342 | correct_seeds,
343 | fuzz_instruction.number_of_schedules as u32
344 | ).unwrap()];
345 | let mut create_instructions = create_fuzzinstruction(
346 | token_vesting_testenv,
347 | fuzz_instruction,
348 | correct_payer,
349 | &correct_source_token_account_key,
350 | source_token_account_owner_key,
351 | destination_token_key,
352 | &destination_token_owner_key.pubkey(),
353 | &correct_vesting_account_key,
354 | &correct_vesting_token_key,
355 | correct_seeds,
356 | mint_key,
357 | fuzz_instruction.source_token_amount
358 | );
359 | instructions_acc.append(&mut create_instructions);
360 |
361 | let new_destination_instruction = create_associated_token_account(
362 | &correct_payer.pubkey(),
363 | &Pubkey::new_unique(), // Arbitrary
364 | &mint_key.pubkey()
365 | );
366 | instructions_acc.push(new_destination_instruction);
367 |
368 | let change_instruction = change_destination(
369 | &token_vesting_testenv.vesting_program_id,
370 | &correct_vesting_account_key,
371 | &destination_token_owner_key.pubkey(),
372 | &destination_token_key,
373 | new_destination_token_key,
374 | correct_seeds
375 | ).unwrap();
376 | instructions_acc.push(change_instruction);
377 | return (instructions_acc, vec![
378 | clone_keypair(mint_key),
379 | clone_keypair(&token_vesting_testenv.mint_authority),
380 | clone_keypair(source_token_account_owner_key),
381 | clone_keypair(destination_token_owner_key),
382 | ]);
383 | }
384 | };
385 |
386 | // Execute a more random input fuzzing (these should give an error almost surely)
387 | } else {
388 | match fuzz_instruction {
389 |
390 | FuzzInstruction {
391 | instruction: VestingInstruction::Init{ .. },
392 | ..
393 | } => {
394 | return (vec![init(
395 | &token_vesting_testenv.system_program_id,
396 | &token_vesting_testenv.rent_program_id,
397 | &token_vesting_testenv.vesting_program_id,
398 | &payer_key.pubkey(),
399 | vesting_account_key,
400 | fuzz_instruction.seeds,
401 | fuzz_instruction.number_of_schedules as u32
402 | ).unwrap()], vec![]);
403 | },
404 |
405 | FuzzInstruction {
406 | instruction: VestingInstruction::Create { .. },
407 | ..
408 | } => {
409 | let create_instructions = create(
410 | &token_vesting_testenv.vesting_program_id,
411 | &token_vesting_testenv.token_program_id,
412 | vesting_account_key,
413 | vesting_token_account_key,
414 | &source_token_account_owner_key.pubkey(),
415 | &destination_token_owner_key.pubkey(),
416 | destination_token_key,
417 | &mint_key.pubkey(),
418 | fuzz_instruction.schedules.clone(),
419 | fuzz_instruction.seeds
420 | ).unwrap();
421 | return (
422 | vec![create_instructions],
423 | vec![clone_keypair(source_token_account_owner_key)]
424 | );
425 | },
426 |
427 | FuzzInstruction {
428 | instruction: VestingInstruction::Unlock{ .. },
429 | ..
430 | } => {
431 | let unlock_instruction = unlock(
432 | &token_vesting_testenv.vesting_program_id,
433 | &token_vesting_testenv.token_program_id,
434 | &token_vesting_testenv.sysvarclock_program_id,
435 | vesting_account_key,
436 | vesting_token_account_key,
437 | destination_token_key,
438 | fuzz_instruction.seeds,
439 | ).unwrap();
440 | return (
441 | vec![unlock_instruction],
442 | vec![]
443 | );
444 | },
445 |
446 | FuzzInstruction {
447 | instruction: VestingInstruction::ChangeDestination{ .. },
448 | ..
449 | } => {
450 | let change_instruction = change_destination(
451 | &token_vesting_testenv.vesting_program_id,
452 | vesting_account_key,
453 | &destination_token_owner_key.pubkey(),
454 | &destination_token_key,
455 | new_destination_token_key,
456 | fuzz_instruction.seeds,
457 | ).unwrap();
458 | return (
459 | vec![change_instruction],
460 | vec![clone_keypair(destination_token_owner_key)]
461 | );
462 | }
463 | };
464 | }
465 |
466 | }
467 |
468 |
469 | // A correct vesting create fuzz instruction
470 | fn create_fuzzinstruction(
471 | token_vesting_testenv: &TokenVestingEnv,
472 | fuzz_instruction: &FuzzInstruction,
473 | payer: &Keypair,
474 | correct_source_token_account_key: &Pubkey,
475 | source_token_account_owner_key: &Keypair,
476 | destination_token_key: &Pubkey,
477 | destination_token_owner_key: &Pubkey,
478 | correct_vesting_account_key: &Pubkey,
479 | correct_vesting_token_key: &Pubkey,
480 | correct_seeds: [u8; 32],
481 | mint_key: &Keypair,
482 | source_amount: u64
483 | ) -> Vec {
484 |
485 | // Initialize the token mint account
486 | let mut instructions_acc = mint_init_instruction(
487 | &payer,
488 | &mint_key,
489 | &token_vesting_testenv.mint_authority
490 | );
491 |
492 | // Create the associated token accounts
493 | let source_instruction = create_associated_token_account(
494 | &payer.pubkey(),
495 | &source_token_account_owner_key.pubkey(),
496 | &mint_key.pubkey()
497 | );
498 | instructions_acc.push(source_instruction);
499 |
500 | let vesting_instruction = create_associated_token_account(
501 | &payer.pubkey(),
502 | &correct_vesting_account_key,
503 | &mint_key.pubkey()
504 | );
505 | instructions_acc.push(vesting_instruction);
506 |
507 | let destination_instruction = create_associated_token_account(
508 | &payer.pubkey(),
509 | &destination_token_owner_key,
510 | &mint_key.pubkey()
511 | );
512 | instructions_acc.push(destination_instruction);
513 |
514 | // Credit the source account
515 | let setup_instruction = mint_to(
516 | &spl_token::id(),
517 | &mint_key.pubkey(),
518 | &correct_source_token_account_key,
519 | &token_vesting_testenv.mint_authority.pubkey(),
520 | &[],
521 | source_amount
522 | ).unwrap();
523 | instructions_acc.push(setup_instruction);
524 |
525 | let used_number_of_schedules = fuzz_instruction.number_of_schedules.min(
526 | fuzz_instruction.schedules.len().try_into().unwrap_or(u8::MAX)
527 | );
528 | // Initialize the vesting program account
529 | let create_instruction = create(
530 | &token_vesting_testenv.vesting_program_id,
531 | &token_vesting_testenv.token_program_id,
532 | &correct_vesting_account_key,
533 | &correct_vesting_token_key,
534 | &source_token_account_owner_key.pubkey(),
535 | &correct_source_token_account_key,
536 | &destination_token_key,
537 | &mint_key.pubkey(),
538 | fuzz_instruction.schedules.clone()[..used_number_of_schedules.into()].into(),
539 | correct_seeds,
540 | ).unwrap();
541 | instructions_acc.push(create_instruction);
542 |
543 | return instructions_acc;
544 | }
545 |
546 |
547 | // Helper functions
548 | fn mint_init_instruction(
549 | payer: &Keypair,
550 | mint:&Keypair,
551 | mint_authority: &Keypair) -> Vec {
552 | let instructions = vec![
553 | system_instruction::create_account(
554 | &payer.pubkey(),
555 | &mint.pubkey(),
556 | Rent::default().minimum_balance(82),
557 | 82,
558 | &spl_token::id()
559 |
560 | ),
561 | initialize_mint(
562 | &spl_token::id(),
563 | &mint.pubkey(),
564 | &mint_authority.pubkey(),
565 | None,
566 | 0
567 | ).unwrap(),
568 | ];
569 | return instructions;
570 | }
571 |
572 | fn clone_keypair(keypair: &Keypair) -> Keypair {
573 | return Keypair::from_bytes(&keypair.to_bytes().clone()).unwrap();
574 | }
575 |
--------------------------------------------------------------------------------
/program/solana-llvm-linux.tar.bz2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Upwork-Job32/token-vesting.blockchain-rust.project/91a042157e0565de676fad81d7bea03aaa25b288/program/solana-llvm-linux.tar.bz2
--------------------------------------------------------------------------------
/program/src/entrypoint.rs:
--------------------------------------------------------------------------------
1 | use solana_program::{
2 | account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg,
3 | program_error::PrintProgramError, pubkey::Pubkey,
4 | };
5 |
6 | use crate::{error::VestingError, processor::Processor};
7 |
8 | entrypoint!(process_instruction);
9 |
10 | pub fn process_instruction(
11 | program_id: &Pubkey,
12 | accounts: &[AccountInfo],
13 | instruction_data: &[u8],
14 | ) -> ProgramResult {
15 | msg!("Entrypoint");
16 | if let Err(error) = Processor::process_instruction(program_id, accounts, instruction_data) {
17 | // catch the error so we can print it
18 | error.print::();
19 | return Err(error);
20 | }
21 | Ok(())
22 | }
23 |
24 | // Deploy the program with the following id:
25 | // solana_program::declare_id!("VestingbGKPFXCWuBvfkegQfZyiNwAJb9Ss623VQ5DA");
26 |
--------------------------------------------------------------------------------
/program/src/error.rs:
--------------------------------------------------------------------------------
1 | use num_derive::FromPrimitive;
2 | use solana_program::{decode_error::DecodeError, program_error::ProgramError};
3 | use thiserror::Error;
4 |
5 | /// Errors that may be returned by the Token vesting program.
6 | #[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]
7 | pub enum VestingError {
8 | // Invalid instruction
9 | #[error("Invalid Instruction")]
10 | InvalidInstruction
11 | }
12 |
13 | impl From for ProgramError {
14 | fn from(e: VestingError) -> Self {
15 | ProgramError::Custom(e as u32)
16 | }
17 | }
18 |
19 | impl DecodeError for VestingError {
20 | fn type_of() -> &'static str {
21 | "VestingError"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/program/src/instruction.rs:
--------------------------------------------------------------------------------
1 | use crate::error::VestingError;
2 |
3 | use solana_program::{
4 | instruction::{AccountMeta, Instruction},
5 | msg,
6 | program_error::ProgramError,
7 | pubkey::Pubkey
8 | };
9 |
10 | use std::convert::TryInto;
11 | use std::mem::size_of;
12 |
13 | #[cfg(feature = "fuzz")]
14 | use arbitrary::Arbitrary;
15 |
16 | #[cfg(feature = "fuzz")]
17 | impl Arbitrary for VestingInstruction {
18 | fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result {
19 | let seeds: [u8; 32] = u.arbitrary()?;
20 | let choice = u.choose(&[0, 1, 2, 3])?;
21 | match choice {
22 | 0 => {
23 | let number_of_schedules = u.arbitrary()?;
24 | return Ok(Self::Init {
25 | seeds,
26 | number_of_schedules,
27 | });
28 | }
29 | 1 => {
30 | let schedules: [Schedule; 10] = u.arbitrary()?;
31 | let key_bytes: [u8; 32] = u.arbitrary()?;
32 | let mint_address: Pubkey = Pubkey::new(&key_bytes);
33 | let key_bytes: [u8; 32] = u.arbitrary()?;
34 | let destination_token_address: Pubkey = Pubkey::new(&key_bytes);
35 | return Ok(Self::Create {
36 | seeds,
37 | mint_address,
38 | destination_token_address,
39 | schedules: schedules.to_vec(),
40 | });
41 | }
42 | 2 => return Ok(Self::Unlock { seeds }),
43 | _ => return Ok(Self::ChangeDestination { seeds }),
44 | }
45 | }
46 | }
47 |
48 | #[cfg_attr(feature = "fuzz", derive(Arbitrary))]
49 | #[repr(C)]
50 | #[derive(Clone, Debug, PartialEq)]
51 | pub struct Schedule {
52 | // Schedule release time in unix timestamp
53 | pub release_time: u64,
54 | pub amount: u64,
55 | }
56 |
57 | pub const SCHEDULE_SIZE: usize = 16;
58 |
59 | #[repr(C)]
60 | #[derive(Clone, Debug, PartialEq)]
61 | pub enum VestingInstruction {
62 | /// Initializes an empty program account for the token_vesting program
63 | ///
64 | /// Accounts expected by this instruction:
65 | ///
66 | /// * Single owner
67 | /// 0. `[]` The system program account
68 | /// 1. `[]` The sysvar Rent account
69 | /// 1. `[signer]` The fee payer account
70 | /// 1. `[]` The vesting account
71 | Init {
72 | // The seed used to derive the vesting accounts address
73 | seeds: [u8; 32],
74 | // The number of release schedules for this contract to hold
75 | number_of_schedules: u32,
76 | },
77 | /// Creates a new vesting schedule contract
78 | ///
79 | /// Accounts expected by this instruction:
80 | ///
81 | /// * Single owner
82 | /// 0. `[]` The spl-token program account
83 | /// 1. `[writable]` The vesting account
84 | /// 2. `[writable]` The vesting spl-token account
85 | /// 3. `[signer]` The source spl-token account owner
86 | /// 4. `[writable]` The source spl-token account
87 | Create {
88 | seeds: [u8; 32],
89 | mint_address: Pubkey,
90 | destination_token_address: Pubkey,
91 | schedules: Vec,
92 | },
93 | /// Unlocks a simple vesting contract (SVC) - can only be invoked by the program itself
94 | /// Accounts expected by this instruction:
95 | ///
96 | /// * Single owner
97 | /// 0. `[]` The spl-token program account
98 | /// 1. `[]` The clock sysvar account
99 | /// 1. `[writable]` The vesting account
100 | /// 2. `[writable]` The vesting spl-token account
101 | /// 3. `[writable]` The destination spl-token account
102 | Unlock { seeds: [u8; 32] },
103 |
104 | /// Change the destination account of a given simple vesting contract (SVC)
105 | /// - can only be invoked by the present destination address of the contract.
106 | ///
107 | /// Accounts expected by this instruction:
108 | ///
109 | /// * Single owner
110 | /// 0. `[]` The vesting account
111 | /// 1. `[]` The current destination token account
112 | /// 2. `[signer]` The destination spl-token account owner
113 | /// 3. `[]` The new destination spl-token account
114 | ChangeDestination { seeds: [u8; 32] },
115 | }
116 |
117 | impl VestingInstruction {
118 | pub fn unpack(input: &[u8]) -> Result {
119 | use VestingError::InvalidInstruction;
120 | let (&tag, rest) = input.split_first().ok_or(InvalidInstruction)?;
121 | Ok(match tag {
122 | 0 => {
123 | let seeds: [u8; 32] = rest
124 | .get(..32)
125 | .and_then(|slice| slice.try_into().ok())
126 | .unwrap();
127 | let number_of_schedules = rest
128 | .get(32..36)
129 | .and_then(|slice| slice.try_into().ok())
130 | .map(u32::from_le_bytes)
131 | .ok_or(InvalidInstruction)?;
132 | Self::Init {
133 | seeds,
134 | number_of_schedules,
135 | }
136 | }
137 | 1 => {
138 | let seeds: [u8; 32] = rest
139 | .get(..32)
140 | .and_then(|slice| slice.try_into().ok())
141 | .unwrap();
142 | let mint_address = rest
143 | .get(32..64)
144 | .and_then(|slice| slice.try_into().ok())
145 | .map(Pubkey::new)
146 | .ok_or(InvalidInstruction)?;
147 | let destination_token_address = rest
148 | .get(64..96)
149 | .and_then(|slice| slice.try_into().ok())
150 | .map(Pubkey::new)
151 | .ok_or(InvalidInstruction)?;
152 | let number_of_schedules = rest[96..].len() / SCHEDULE_SIZE;
153 | let mut schedules: Vec = Vec::with_capacity(number_of_schedules);
154 | let mut offset = 96;
155 | for _ in 0..number_of_schedules {
156 | let release_time = rest
157 | .get(offset..offset + 8)
158 | .and_then(|slice| slice.try_into().ok())
159 | .map(u64::from_le_bytes)
160 | .ok_or(InvalidInstruction)?;
161 | let amount = rest
162 | .get(offset + 8..offset + 16)
163 | .and_then(|slice| slice.try_into().ok())
164 | .map(u64::from_le_bytes)
165 | .ok_or(InvalidInstruction)?;
166 | offset += SCHEDULE_SIZE;
167 | schedules.push(Schedule {
168 | release_time,
169 | amount,
170 | })
171 | }
172 | Self::Create {
173 | seeds,
174 | mint_address,
175 | destination_token_address,
176 | schedules,
177 | }
178 | }
179 | 2 | 3 => {
180 | let seeds: [u8; 32] = rest
181 | .get(..32)
182 | .and_then(|slice| slice.try_into().ok())
183 | .unwrap();
184 | match tag {
185 | 2 => Self::Unlock { seeds },
186 | _ => Self::ChangeDestination { seeds },
187 | }
188 | }
189 | _ => {
190 | msg!("Unsupported tag");
191 | return Err(InvalidInstruction.into());
192 | }
193 | })
194 | }
195 |
196 | pub fn pack(&self) -> Vec {
197 | let mut buf = Vec::with_capacity(size_of::());
198 | match self {
199 | &Self::Init {
200 | seeds,
201 | number_of_schedules,
202 | } => {
203 | buf.push(0);
204 | buf.extend_from_slice(&seeds);
205 | buf.extend_from_slice(&number_of_schedules.to_le_bytes())
206 | }
207 | Self::Create {
208 | seeds,
209 | mint_address,
210 | destination_token_address,
211 | schedules,
212 | } => {
213 | buf.push(1);
214 | buf.extend_from_slice(seeds);
215 | buf.extend_from_slice(&mint_address.to_bytes());
216 | buf.extend_from_slice(&destination_token_address.to_bytes());
217 | for s in schedules.iter() {
218 | buf.extend_from_slice(&s.release_time.to_le_bytes());
219 | buf.extend_from_slice(&s.amount.to_le_bytes());
220 | }
221 | }
222 | &Self::Unlock { seeds } => {
223 | buf.push(2);
224 | buf.extend_from_slice(&seeds);
225 | }
226 | &Self::ChangeDestination { seeds } => {
227 | buf.push(3);
228 | buf.extend_from_slice(&seeds);
229 | }
230 | };
231 | buf
232 | }
233 | }
234 |
235 | // Creates a `Init` instruction to create and initialize the vesting token account.
236 | pub fn init(
237 | system_program_id: &Pubkey,
238 | rent_program_id: &Pubkey,
239 | vesting_program_id: &Pubkey,
240 | payer_key: &Pubkey,
241 | vesting_account: &Pubkey,
242 | seeds: [u8; 32],
243 | number_of_schedules: u32,
244 | ) -> Result {
245 | let data = VestingInstruction::Init {
246 | seeds,
247 | number_of_schedules,
248 | }
249 | .pack();
250 | let accounts = vec![
251 | AccountMeta::new_readonly(*system_program_id, false),
252 | AccountMeta::new_readonly(*rent_program_id, false),
253 | AccountMeta::new(*payer_key, true),
254 | AccountMeta::new(*vesting_account, false),
255 | ];
256 | Ok(Instruction {
257 | program_id: *vesting_program_id,
258 | accounts,
259 | data,
260 | })
261 | }
262 |
263 | // Creates a `CreateSchedule` instruction
264 | pub fn create(
265 | vesting_program_id: &Pubkey,
266 | token_program_id: &Pubkey,
267 | vesting_account_key: &Pubkey,
268 | vesting_token_account_key: &Pubkey,
269 | source_token_account_owner_key: &Pubkey,
270 | source_token_account_key: &Pubkey,
271 | destination_token_account_key: &Pubkey,
272 | mint_address: &Pubkey,
273 | schedules: Vec,
274 | seeds: [u8; 32],
275 | ) -> Result {
276 | let data = VestingInstruction::Create {
277 | mint_address: *mint_address,
278 | seeds,
279 | destination_token_address: *destination_token_account_key,
280 | schedules,
281 | }
282 | .pack();
283 | let accounts = vec![
284 | AccountMeta::new_readonly(*token_program_id, false),
285 | AccountMeta::new(*vesting_account_key, false),
286 | AccountMeta::new(*vesting_token_account_key, false),
287 | AccountMeta::new_readonly(*source_token_account_owner_key, true),
288 | AccountMeta::new(*source_token_account_key, false),
289 | ];
290 | Ok(Instruction {
291 | program_id: *vesting_program_id,
292 | accounts,
293 | data,
294 | })
295 | }
296 |
297 | // Creates an `Unlock` instruction
298 | pub fn unlock(
299 | vesting_program_id: &Pubkey,
300 | token_program_id: &Pubkey,
301 | clock_sysvar_id: &Pubkey,
302 | vesting_account_key: &Pubkey,
303 | vesting_token_account_key: &Pubkey,
304 | destination_token_account_key: &Pubkey,
305 | seeds: [u8; 32],
306 | ) -> Result {
307 | let data = VestingInstruction::Unlock { seeds }.pack();
308 | let accounts = vec![
309 | AccountMeta::new_readonly(*token_program_id, false),
310 | AccountMeta::new_readonly(*clock_sysvar_id, false),
311 | AccountMeta::new(*vesting_account_key, false),
312 | AccountMeta::new(*vesting_token_account_key, false),
313 | AccountMeta::new(*destination_token_account_key, false),
314 | ];
315 | Ok(Instruction {
316 | program_id: *vesting_program_id,
317 | accounts,
318 | data,
319 | })
320 | }
321 |
322 | pub fn change_destination(
323 | vesting_program_id: &Pubkey,
324 | vesting_account_key: &Pubkey,
325 | current_destination_token_account_owner: &Pubkey,
326 | current_destination_token_account: &Pubkey,
327 | target_destination_token_account: &Pubkey,
328 | seeds: [u8; 32],
329 | ) -> Result {
330 | let data = VestingInstruction::ChangeDestination { seeds }.pack();
331 | let accounts = vec![
332 | AccountMeta::new(*vesting_account_key, false),
333 | AccountMeta::new_readonly(*current_destination_token_account, false),
334 | AccountMeta::new_readonly(*current_destination_token_account_owner, true),
335 | AccountMeta::new_readonly(*target_destination_token_account, false),
336 | ];
337 | Ok(Instruction {
338 | program_id: *vesting_program_id,
339 | accounts,
340 | data,
341 | })
342 | }
343 |
344 | #[cfg(test)]
345 | mod test {
346 | use super::*;
347 |
348 | #[test]
349 | fn test_instruction_packing() {
350 | let mint_address = Pubkey::new_unique();
351 | let destination_token_address = Pubkey::new_unique();
352 |
353 | let original_create = VestingInstruction::Create {
354 | seeds: [50u8; 32],
355 | schedules: vec![Schedule {
356 | amount: 42,
357 | release_time: 250,
358 | }],
359 | mint_address: mint_address.clone(),
360 | destination_token_address,
361 | };
362 | let packed_create = original_create.pack();
363 | let unpacked_create = VestingInstruction::unpack(&packed_create).unwrap();
364 | assert_eq!(original_create, unpacked_create);
365 |
366 | let original_unlock = VestingInstruction::Unlock { seeds: [50u8; 32] };
367 | assert_eq!(
368 | original_unlock,
369 | VestingInstruction::unpack(&original_unlock.pack()).unwrap()
370 | );
371 |
372 | let original_init = VestingInstruction::Init {
373 | number_of_schedules: 42,
374 | seeds: [50u8; 32],
375 | };
376 | assert_eq!(
377 | original_init,
378 | VestingInstruction::unpack(&original_init.pack()).unwrap()
379 | );
380 |
381 | let original_change = VestingInstruction::ChangeDestination { seeds: [50u8; 32] };
382 | assert_eq!(
383 | original_change,
384 | VestingInstruction::unpack(&original_change.pack()).unwrap()
385 | );
386 | }
387 | }
388 |
--------------------------------------------------------------------------------
/program/src/lib.rs:
--------------------------------------------------------------------------------
1 | #[cfg(not(feature = "no-entrypoint"))]
2 | pub mod entrypoint;
3 |
4 | pub mod error;
5 | pub mod instruction;
6 | pub mod state;
7 |
8 | pub mod processor;
9 |
--------------------------------------------------------------------------------
/program/src/processor.rs:
--------------------------------------------------------------------------------
1 | use solana_program::{
2 | account_info::{next_account_info, AccountInfo},
3 | decode_error::DecodeError,
4 | entrypoint::ProgramResult,
5 | msg,
6 | program::{invoke, invoke_signed},
7 | program_error::PrintProgramError,
8 | program_error::ProgramError,
9 | program_pack::Pack,
10 | pubkey::Pubkey,
11 | rent::Rent,
12 | system_instruction::create_account,
13 | sysvar::{clock::Clock, Sysvar},
14 | };
15 |
16 | use num_traits::FromPrimitive;
17 | use spl_token::{instruction::transfer, state::Account};
18 |
19 | use crate::{
20 | error::VestingError,
21 | instruction::{Schedule, VestingInstruction, SCHEDULE_SIZE},
22 | state::{pack_schedules_into_slice, unpack_schedules, VestingSchedule, VestingScheduleHeader},
23 | };
24 |
25 | pub struct Processor {}
26 |
27 | impl Processor {
28 | pub fn process_init(
29 | program_id: &Pubkey,
30 | accounts: &[AccountInfo],
31 | seeds: [u8; 32],
32 | schedules: u32
33 | ) -> ProgramResult {
34 | let accounts_iter = &mut accounts.iter();
35 |
36 | let system_program_account = next_account_info(accounts_iter)?;
37 | let rent_sysvar_account = next_account_info(accounts_iter)?;
38 | let payer = next_account_info(accounts_iter)?;
39 | let vesting_account = next_account_info(accounts_iter)?;
40 |
41 | let rent = Rent::from_account_info(rent_sysvar_account)?;
42 |
43 | // Find the non reversible public key for the vesting contract via the seed
44 | let vesting_account_key = Pubkey::create_program_address(&[&seeds], &program_id).unwrap();
45 | if vesting_account_key != *vesting_account.key {
46 | msg!("Provided vesting account is invalid");
47 | return Err(ProgramError::InvalidArgument);
48 | }
49 |
50 | let state_size = (schedules as usize) * VestingSchedule::LEN + VestingScheduleHeader::LEN;
51 |
52 | let init_vesting_account = create_account(
53 | &payer.key,
54 | &vesting_account_key,
55 | rent.minimum_balance(state_size),
56 | state_size as u64,
57 | &program_id,
58 | );
59 |
60 | invoke_signed(
61 | &init_vesting_account,
62 | &[
63 | system_program_account.clone(),
64 | payer.clone(),
65 | vesting_account.clone(),
66 | ],
67 | &[&[&seeds]],
68 | )?;
69 | Ok(())
70 | }
71 |
72 | pub fn process_create(
73 | program_id: &Pubkey,
74 | accounts: &[AccountInfo],
75 | seeds: [u8; 32],
76 | mint_address: &Pubkey,
77 | destination_token_address: &Pubkey,
78 | schedules: Vec,
79 | ) -> ProgramResult {
80 | let accounts_iter = &mut accounts.iter();
81 |
82 | let spl_token_account = next_account_info(accounts_iter)?;
83 | let vesting_account = next_account_info(accounts_iter)?;
84 | let vesting_token_account = next_account_info(accounts_iter)?;
85 | let source_token_account_owner = next_account_info(accounts_iter)?;
86 | let source_token_account = next_account_info(accounts_iter)?;
87 |
88 | let vesting_account_key = Pubkey::create_program_address(&[&seeds], program_id)?;
89 | if vesting_account_key != *vesting_account.key {
90 | msg!("Provided vesting account is invalid");
91 | return Err(ProgramError::InvalidArgument);
92 | }
93 |
94 | if !source_token_account_owner.is_signer {
95 | msg!("Source token account owner should be a signer.");
96 | return Err(ProgramError::InvalidArgument);
97 | }
98 |
99 | if *vesting_account.owner != *program_id {
100 | msg!("Program should own vesting account");
101 | return Err(ProgramError::InvalidArgument);
102 | }
103 |
104 | // Verifying that no SVC was already created with this seed
105 | let is_initialized =
106 | vesting_account.try_borrow_data()?[VestingScheduleHeader::LEN - 1] == 1;
107 |
108 | if is_initialized {
109 | msg!("Cannot overwrite an existing vesting contract.");
110 | return Err(ProgramError::InvalidArgument);
111 | }
112 |
113 | let vesting_token_account_data = Account::unpack(&vesting_token_account.data.borrow())?;
114 |
115 | if vesting_token_account_data.owner != vesting_account_key {
116 | msg!("The vesting token account should be owned by the vesting account.");
117 | return Err(ProgramError::InvalidArgument);
118 | }
119 |
120 | if vesting_token_account_data.delegate.is_some() {
121 | msg!("The vesting token account should not have a delegate authority");
122 | return Err(ProgramError::InvalidAccountData);
123 | }
124 |
125 | if vesting_token_account_data.close_authority.is_some() {
126 | msg!("The vesting token account should not have a close authority");
127 | return Err(ProgramError::InvalidAccountData);
128 | }
129 |
130 | let state_header = VestingScheduleHeader {
131 | destination_address: *destination_token_address,
132 | mint_address: *mint_address,
133 | is_initialized: true,
134 | };
135 |
136 | let mut data = vesting_account.data.borrow_mut();
137 | if data.len() != VestingScheduleHeader::LEN + schedules.len() * VestingSchedule::LEN {
138 | return Err(ProgramError::InvalidAccountData)
139 | }
140 | state_header.pack_into_slice(&mut data);
141 |
142 | let mut offset = VestingScheduleHeader::LEN;
143 | let mut total_amount: u64 = 0;
144 |
145 | for s in schedules.iter() {
146 | let state_schedule = VestingSchedule {
147 | release_time: s.release_time,
148 | amount: s.amount,
149 | };
150 | state_schedule.pack_into_slice(&mut data[offset..]);
151 | let delta = total_amount.checked_add(s.amount);
152 | match delta {
153 | Some(n) => total_amount = n,
154 | None => return Err(ProgramError::InvalidInstructionData), // Total amount overflows u64
155 | }
156 | offset += SCHEDULE_SIZE;
157 | }
158 |
159 | if Account::unpack(&source_token_account.data.borrow())?.amount < total_amount {
160 | msg!("The source token account has insufficient funds.");
161 | return Err(ProgramError::InsufficientFunds)
162 | };
163 |
164 | let transfer_tokens_to_vesting_account = transfer(
165 | spl_token_account.key,
166 | source_token_account.key,
167 | vesting_token_account.key,
168 | source_token_account_owner.key,
169 | &[],
170 | total_amount,
171 | )?;
172 |
173 | invoke(
174 | &transfer_tokens_to_vesting_account,
175 | &[
176 | source_token_account.clone(),
177 | vesting_token_account.clone(),
178 | spl_token_account.clone(),
179 | source_token_account_owner.clone(),
180 | ],
181 | )?;
182 | Ok(())
183 | }
184 |
185 | pub fn process_unlock(
186 | program_id: &Pubkey,
187 | _accounts: &[AccountInfo],
188 | seeds: [u8; 32],
189 | ) -> ProgramResult {
190 | let accounts_iter = &mut _accounts.iter();
191 |
192 | let spl_token_account = next_account_info(accounts_iter)?;
193 | let clock_sysvar_account = next_account_info(accounts_iter)?;
194 | let vesting_account = next_account_info(accounts_iter)?;
195 | let vesting_token_account = next_account_info(accounts_iter)?;
196 | let destination_token_account = next_account_info(accounts_iter)?;
197 |
198 | let vesting_account_key = Pubkey::create_program_address(&[&seeds], program_id)?;
199 | if vesting_account_key != *vesting_account.key {
200 | msg!("Invalid vesting account key");
201 | return Err(ProgramError::InvalidArgument);
202 | }
203 |
204 | if spl_token_account.key != &spl_token::id() {
205 | msg!("The provided spl token program account is invalid");
206 | return Err(ProgramError::InvalidArgument)
207 | }
208 |
209 | let packed_state = &vesting_account.data;
210 | let header_state =
211 | VestingScheduleHeader::unpack(&packed_state.borrow()[..VestingScheduleHeader::LEN])?;
212 |
213 | if header_state.destination_address != *destination_token_account.key {
214 | msg!("Contract destination account does not matched provided account");
215 | return Err(ProgramError::InvalidArgument);
216 | }
217 |
218 | let vesting_token_account_data = Account::unpack(&vesting_token_account.data.borrow())?;
219 |
220 | if vesting_token_account_data.owner != vesting_account_key {
221 | msg!("The vesting token account should be owned by the vesting account.");
222 | return Err(ProgramError::InvalidArgument);
223 | }
224 |
225 | // Unlock the schedules that have reached maturity
226 | let clock = Clock::from_account_info(&clock_sysvar_account)?;
227 | let mut total_amount_to_transfer = 0;
228 | let mut schedules = unpack_schedules(&packed_state.borrow()[VestingScheduleHeader::LEN..])?;
229 |
230 | for s in schedules.iter_mut() {
231 | if clock.unix_timestamp as u64 >= s.release_time {
232 | total_amount_to_transfer += s.amount;
233 | s.amount = 0;
234 | }
235 | }
236 | if total_amount_to_transfer == 0 {
237 | msg!("Vesting contract has not yet reached release time");
238 | return Err(ProgramError::InvalidArgument);
239 | }
240 |
241 | let transfer_tokens_from_vesting_account = transfer(
242 | &spl_token_account.key,
243 | &vesting_token_account.key,
244 | destination_token_account.key,
245 | &vesting_account_key,
246 | &[],
247 | total_amount_to_transfer,
248 | )?;
249 |
250 | invoke_signed(
251 | &transfer_tokens_from_vesting_account,
252 | &[
253 | spl_token_account.clone(),
254 | vesting_token_account.clone(),
255 | destination_token_account.clone(),
256 | vesting_account.clone(),
257 | ],
258 | &[&[&seeds]],
259 | )?;
260 |
261 | // Reset released amounts to 0. This makes the simple unlock safe with complex scheduling contracts
262 | pack_schedules_into_slice(
263 | schedules,
264 | &mut packed_state.borrow_mut()[VestingScheduleHeader::LEN..],
265 | );
266 |
267 | Ok(())
268 | }
269 |
270 | pub fn process_change_destination(
271 | program_id: &Pubkey,
272 | accounts: &[AccountInfo],
273 | seeds: [u8; 32],
274 | ) -> ProgramResult {
275 | let accounts_iter = &mut accounts.iter();
276 |
277 | let vesting_account = next_account_info(accounts_iter)?;
278 | let destination_token_account = next_account_info(accounts_iter)?;
279 | let destination_token_account_owner = next_account_info(accounts_iter)?;
280 | let new_destination_token_account = next_account_info(accounts_iter)?;
281 |
282 | if vesting_account.data.borrow().len() < VestingScheduleHeader::LEN {
283 | return Err(ProgramError::InvalidAccountData)
284 | }
285 | let vesting_account_key = Pubkey::create_program_address(&[&seeds], program_id)?;
286 | let state = VestingScheduleHeader::unpack(
287 | &vesting_account.data.borrow()[..VestingScheduleHeader::LEN],
288 | )?;
289 |
290 | if vesting_account_key != *vesting_account.key {
291 | msg!("Invalid vesting account key");
292 | return Err(ProgramError::InvalidArgument);
293 | }
294 |
295 | if state.destination_address != *destination_token_account.key {
296 | msg!("Contract destination account does not matched provided account");
297 | return Err(ProgramError::InvalidArgument);
298 | }
299 |
300 | if !destination_token_account_owner.is_signer {
301 | msg!("Destination token account owner should be a signer.");
302 | return Err(ProgramError::InvalidArgument);
303 | }
304 |
305 | let destination_token_account = Account::unpack(&destination_token_account.data.borrow())?;
306 |
307 | if destination_token_account.owner != *destination_token_account_owner.key {
308 | msg!("The current destination token account isn't owned by the provided owner");
309 | return Err(ProgramError::InvalidArgument);
310 | }
311 |
312 | let mut new_state = state;
313 | new_state.destination_address = *new_destination_token_account.key;
314 | new_state
315 | .pack_into_slice(&mut vesting_account.data.borrow_mut()[..VestingScheduleHeader::LEN]);
316 |
317 | Ok(())
318 | }
319 |
320 | pub fn process_instruction(
321 | program_id: &Pubkey,
322 | accounts: &[AccountInfo],
323 | instruction_data: &[u8],
324 | ) -> ProgramResult {
325 | msg!("Beginning processing");
326 | let instruction = VestingInstruction::unpack(instruction_data)?;
327 | msg!("Instruction unpacked");
328 | match instruction {
329 | VestingInstruction::Init {
330 | seeds,
331 | number_of_schedules,
332 | } => {
333 | msg!("Instruction: Init");
334 | Self::process_init(program_id, accounts, seeds, number_of_schedules)
335 | }
336 | VestingInstruction::Unlock { seeds } => {
337 | msg!("Instruction: Unlock");
338 | Self::process_unlock(program_id, accounts, seeds)
339 | }
340 | VestingInstruction::ChangeDestination { seeds } => {
341 | msg!("Instruction: Change Destination");
342 | Self::process_change_destination(program_id, accounts, seeds)
343 | }
344 | VestingInstruction::Create {
345 | seeds,
346 | mint_address,
347 | destination_token_address,
348 | schedules,
349 | } => {
350 | msg!("Instruction: Create Schedule");
351 | Self::process_create(
352 | program_id,
353 | accounts,
354 | seeds,
355 | &mint_address,
356 | &destination_token_address,
357 | schedules,
358 | )
359 | }
360 | }
361 | }
362 | }
363 |
364 | impl PrintProgramError for VestingError {
365 | fn print(&self)
366 | where
367 | E: 'static + std::error::Error + DecodeError + PrintProgramError + FromPrimitive,
368 | {
369 | match self {
370 | VestingError::InvalidInstruction => msg!("Error: Invalid instruction!"),
371 | }
372 | }
373 | }
374 |
--------------------------------------------------------------------------------
/program/src/state.rs:
--------------------------------------------------------------------------------
1 | use solana_program::{
2 | program_error::ProgramError,
3 | program_pack::{IsInitialized, Pack, Sealed},
4 | pubkey::Pubkey,
5 | };
6 |
7 | use std::convert::TryInto;
8 | #[derive(Debug, PartialEq)]
9 | pub struct VestingSchedule {
10 | pub release_time: u64,
11 | pub amount: u64,
12 | }
13 |
14 | #[derive(Debug, PartialEq)]
15 | pub struct VestingScheduleHeader {
16 | pub destination_address: Pubkey,
17 | pub mint_address: Pubkey,
18 | pub is_initialized: bool,
19 | }
20 |
21 | impl Sealed for VestingScheduleHeader {}
22 |
23 | impl Pack for VestingScheduleHeader {
24 | const LEN: usize = 65;
25 |
26 | fn pack_into_slice(&self, target: &mut [u8]) {
27 | let destination_address_bytes = self.destination_address.to_bytes();
28 | let mint_address_bytes = self.mint_address.to_bytes();
29 | for i in 0..32 {
30 | target[i] = destination_address_bytes[i];
31 | }
32 |
33 | for i in 32..64 {
34 | target[i] = mint_address_bytes[i - 32];
35 | }
36 |
37 | target[64] = self.is_initialized as u8;
38 | }
39 |
40 | fn unpack_from_slice(src: &[u8]) -> Result {
41 | if src.len() < 65 {
42 | return Err(ProgramError::InvalidAccountData)
43 | }
44 | let destination_address = Pubkey::new(&src[..32]);
45 | let mint_address = Pubkey::new(&src[32..64]);
46 | let is_initialized = src[64] == 1;
47 | Ok(Self {
48 | destination_address,
49 | mint_address,
50 | is_initialized,
51 | })
52 | }
53 | }
54 |
55 | impl Sealed for VestingSchedule {}
56 |
57 | impl Pack for VestingSchedule {
58 | const LEN: usize = 16;
59 |
60 | fn pack_into_slice(&self, dst: &mut [u8]) {
61 | let release_time_bytes = self.release_time.to_le_bytes();
62 | let amount_bytes = self.amount.to_le_bytes();
63 | for i in 0..8 {
64 | dst[i] = release_time_bytes[i];
65 | }
66 |
67 | for i in 8..16 {
68 | dst[i] = amount_bytes[i - 8];
69 | }
70 | }
71 |
72 | fn unpack_from_slice(src: &[u8]) -> Result {
73 | if src.len() < 16 {
74 | return Err(ProgramError::InvalidAccountData)
75 | }
76 | let release_time = u64::from_le_bytes(src[0..8].try_into().unwrap());
77 | let amount = u64::from_le_bytes(src[8..16].try_into().unwrap());
78 | Ok(Self {
79 | release_time,
80 | amount,
81 | })
82 | }
83 | }
84 |
85 | impl IsInitialized for VestingScheduleHeader {
86 | fn is_initialized(&self) -> bool {
87 | self.is_initialized
88 | }
89 | }
90 |
91 | pub fn unpack_schedules(input: &[u8]) -> Result, ProgramError> {
92 | let number_of_schedules = input.len() / VestingSchedule::LEN;
93 | let mut output: Vec = Vec::with_capacity(number_of_schedules);
94 | let mut offset = 0;
95 | for _ in 0..number_of_schedules {
96 | output.push(VestingSchedule::unpack_from_slice(
97 | &input[offset..offset + VestingSchedule::LEN],
98 | )?);
99 | offset += VestingSchedule::LEN;
100 | }
101 | Ok(output)
102 | }
103 |
104 | pub fn pack_schedules_into_slice(schedules: Vec, target: &mut [u8]) {
105 | let mut offset = 0;
106 | for s in schedules.iter() {
107 | s.pack_into_slice(&mut target[offset..]);
108 | offset += VestingSchedule::LEN;
109 | }
110 | }
111 |
112 | #[cfg(test)]
113 | mod tests {
114 | use super::{unpack_schedules, VestingSchedule, VestingScheduleHeader};
115 | use solana_program::{program_pack::Pack, pubkey::Pubkey};
116 |
117 | #[test]
118 | fn test_state_packing() {
119 | let header_state = VestingScheduleHeader {
120 | destination_address: Pubkey::new_unique(),
121 | mint_address: Pubkey::new_unique(),
122 | is_initialized: true,
123 | };
124 | let schedule_state_0 = VestingSchedule {
125 | release_time: 30767976,
126 | amount: 969,
127 | };
128 | let schedule_state_1 = VestingSchedule {
129 | release_time: 32767076,
130 | amount: 420,
131 | };
132 | let state_size = VestingScheduleHeader::LEN + 2 * VestingSchedule::LEN;
133 | let mut state_array = [0u8; 97];
134 | header_state.pack_into_slice(&mut state_array[..VestingScheduleHeader::LEN]);
135 | schedule_state_0.pack_into_slice(
136 | &mut state_array
137 | [VestingScheduleHeader::LEN..VestingScheduleHeader::LEN + VestingSchedule::LEN],
138 | );
139 | schedule_state_1
140 | .pack_into_slice(&mut state_array[VestingScheduleHeader::LEN + VestingSchedule::LEN..]);
141 | let packed = Vec::from(state_array);
142 | let mut expected = Vec::with_capacity(state_size);
143 | expected.extend_from_slice(&header_state.destination_address.to_bytes());
144 | expected.extend_from_slice(&header_state.mint_address.to_bytes());
145 | expected.extend_from_slice(&[header_state.is_initialized as u8]);
146 | expected.extend_from_slice(&schedule_state_0.release_time.to_le_bytes());
147 | expected.extend_from_slice(&schedule_state_0.amount.to_le_bytes());
148 | expected.extend_from_slice(&schedule_state_1.release_time.to_le_bytes());
149 | expected.extend_from_slice(&schedule_state_1.amount.to_le_bytes());
150 |
151 | assert_eq!(expected, packed);
152 | assert_eq!(packed.len(), state_size);
153 | let unpacked_header =
154 | VestingScheduleHeader::unpack(&packed[..VestingScheduleHeader::LEN]).unwrap();
155 | assert_eq!(unpacked_header, header_state);
156 | let unpacked_schedules = unpack_schedules(&packed[VestingScheduleHeader::LEN..]).unwrap();
157 | assert_eq!(unpacked_schedules[0], schedule_state_0);
158 | assert_eq!(unpacked_schedules[1], schedule_state_1);
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/program/tests/functional.rs:
--------------------------------------------------------------------------------
1 | #![cfg(feature = "test-bpf")]
2 | use std::str::FromStr;
3 |
4 | use solana_program::{hash::Hash,
5 | pubkey::Pubkey,
6 | rent::Rent,
7 | sysvar,
8 | system_program
9 | };
10 | use solana_program_test::{processor, ProgramTest};
11 | use solana_sdk::{account::Account, keyed_account, signature::Keypair, signature::Signer, system_instruction, transaction::Transaction};
12 | use token_vesting::{entrypoint::process_instruction, instruction::Schedule};
13 | use token_vesting::instruction::{init, unlock, change_destination, create};
14 | use spl_token::{self, instruction::{initialize_mint, initialize_account, mint_to}};
15 |
16 | #[tokio::test]
17 | async fn test_token_vesting() {
18 |
19 | // Create program and test environment
20 | let program_id = Pubkey::from_str("VestingbGKPFXCWuBvfkegQfZyiNwAJb9Ss623VQ5DA").unwrap();
21 | let mint_authority = Keypair::new();
22 | let mint = Keypair::new();
23 |
24 | let source_account = Keypair::new();
25 | let source_token_account = Keypair::new();
26 |
27 | let destination_account = Keypair::new();
28 | let destination_token_account = Keypair::new();
29 |
30 | let new_destination_account = Keypair::new();
31 | let new_destination_token_account = Keypair::new();
32 |
33 | let mut seeds = [42u8; 32];
34 | let (vesting_account_key, bump) = Pubkey::find_program_address(&[&seeds[..31]], &program_id);
35 | seeds[31] = bump;
36 | let vesting_token_account = Keypair::new();
37 |
38 | let mut program_test = ProgramTest::new(
39 | "token_vesting",
40 | program_id,
41 | processor!(process_instruction),
42 | );
43 |
44 | // Add accounts
45 | program_test.add_account(
46 | source_account.pubkey(),
47 | Account {
48 | lamports: 5000000,
49 | ..Account::default()
50 | },
51 | );
52 |
53 | // Start and process transactions on the test network
54 | let (mut banks_client, payer, recent_blockhash) = program_test.start().await;
55 |
56 | // Initialize the vesting program account
57 | let init_instruction = [init(
58 | &system_program::id(),
59 | &sysvar::rent::id(),
60 | &program_id,
61 | &payer.pubkey(),
62 | &vesting_account_key,
63 | seeds,
64 | 3
65 | ).unwrap()
66 | ];
67 | let mut init_transaction = Transaction::new_with_payer(
68 | &init_instruction,
69 | Some(&payer.pubkey()),
70 | );
71 | init_transaction.partial_sign(
72 | &[&payer],
73 | recent_blockhash
74 | );
75 | banks_client.process_transaction(init_transaction).await.unwrap();
76 |
77 |
78 | // Initialize the token accounts
79 | banks_client.process_transaction(mint_init_transaction(
80 | &payer,
81 | &mint,
82 | &mint_authority,
83 | recent_blockhash
84 | )).await.unwrap();
85 |
86 | banks_client.process_transaction(
87 | create_token_account(&payer, &mint, recent_blockhash, &source_token_account, &source_account.pubkey())
88 | ).await.unwrap();
89 | banks_client.process_transaction(
90 | create_token_account(&payer, &mint, recent_blockhash, &vesting_token_account, &vesting_account_key)
91 | ).await.unwrap();
92 | banks_client.process_transaction(
93 | create_token_account(&payer, &mint, recent_blockhash, &destination_token_account, &destination_account.pubkey())
94 | ).await.unwrap();
95 | banks_client.process_transaction(
96 | create_token_account(&payer, &mint, recent_blockhash, &new_destination_token_account, &new_destination_account.pubkey())
97 | ).await.unwrap();
98 |
99 |
100 | // Create and process the vesting transactions
101 | let setup_instructions = [
102 | mint_to(
103 | &spl_token::id(),
104 | &mint.pubkey(),
105 | &source_token_account.pubkey(),
106 | &mint_authority.pubkey(),
107 | &[],
108 | 100
109 | ).unwrap()
110 | ];
111 |
112 | let schedules = vec![
113 | Schedule {amount: 20, release_time: 0},
114 | Schedule {amount: 20, release_time: 2},
115 | Schedule {amount: 20, release_time: 5}
116 | ];
117 |
118 | let test_instructions = [
119 | create(
120 | &program_id,
121 | &spl_token::id(),
122 | &vesting_account_key,
123 | &vesting_token_account.pubkey(),
124 | &source_account.pubkey(),
125 | &source_token_account.pubkey(),
126 | &destination_token_account.pubkey(),
127 | &mint.pubkey(),
128 | schedules,
129 | seeds.clone()
130 | ).unwrap(),
131 | unlock(
132 | &program_id,
133 | &spl_token::id(),
134 | &sysvar::clock::id(),
135 | &vesting_account_key,
136 | &vesting_token_account.pubkey(),
137 | &destination_token_account.pubkey(),
138 | seeds.clone()
139 | ).unwrap()
140 | ];
141 |
142 | let change_destination_instructions = [
143 | change_destination(
144 | &program_id,
145 | &vesting_account_key,
146 | &destination_account.pubkey(),
147 | &destination_token_account.pubkey(),
148 | &new_destination_token_account.pubkey(),
149 | seeds.clone()
150 | ).unwrap()
151 | ];
152 |
153 | // Process transaction on test network
154 | let mut setup_transaction = Transaction::new_with_payer(
155 | &setup_instructions,
156 | Some(&payer.pubkey()),
157 | );
158 | setup_transaction.partial_sign(
159 | &[
160 | &payer,
161 | &mint_authority
162 | ],
163 | recent_blockhash
164 | );
165 |
166 | banks_client.process_transaction(setup_transaction).await.unwrap();
167 |
168 | // Process transaction on test network
169 | let mut test_transaction = Transaction::new_with_payer(
170 | &test_instructions,
171 | Some(&payer.pubkey()),
172 | );
173 | test_transaction.partial_sign(
174 | &[
175 | &payer,
176 | &source_account
177 | ],
178 | recent_blockhash
179 | );
180 |
181 | banks_client.process_transaction(test_transaction).await.unwrap();
182 |
183 | let mut change_destination_transaction = Transaction::new_with_payer(
184 | &change_destination_instructions,
185 | Some(&payer.pubkey())
186 | );
187 |
188 | change_destination_transaction.partial_sign(
189 | &[
190 | &payer,
191 | &destination_account
192 | ],
193 | recent_blockhash
194 | );
195 |
196 | banks_client.process_transaction(change_destination_transaction).await.unwrap();
197 |
198 | }
199 |
200 | fn mint_init_transaction(
201 | payer: &Keypair,
202 | mint:&Keypair,
203 | mint_authority: &Keypair,
204 | recent_blockhash: Hash) -> Transaction{
205 | let instructions = [
206 | system_instruction::create_account(
207 | &payer.pubkey(),
208 | &mint.pubkey(),
209 | Rent::default().minimum_balance(82),
210 | 82,
211 | &spl_token::id()
212 |
213 | ),
214 | initialize_mint(
215 | &spl_token::id(),
216 | &mint.pubkey(),
217 | &mint_authority.pubkey(),
218 | None,
219 | 0
220 | ).unwrap(),
221 | ];
222 | let mut transaction = Transaction::new_with_payer(
223 | &instructions,
224 | Some(&payer.pubkey()),
225 | );
226 | transaction.partial_sign(
227 | &[
228 | payer,
229 | mint
230 | ],
231 | recent_blockhash
232 | );
233 | transaction
234 | }
235 |
236 | fn create_token_account(
237 | payer: &Keypair,
238 | mint:&Keypair,
239 | recent_blockhash: Hash,
240 | token_account:&Keypair,
241 | token_account_owner: &Pubkey
242 | ) -> Transaction {
243 | let instructions = [
244 | system_instruction::create_account(
245 | &payer.pubkey(),
246 | &token_account.pubkey(),
247 | Rent::default().minimum_balance(165),
248 | 165,
249 | &spl_token::id()
250 | ),
251 | initialize_account(
252 | &spl_token::id(),
253 | &token_account.pubkey(),
254 | &mint.pubkey(),
255 | token_account_owner
256 | ).unwrap()
257 | ];
258 | let mut transaction = Transaction::new_with_payer(
259 | &instructions,
260 | Some(&payer.pubkey()),
261 | );
262 | transaction.partial_sign(
263 | &[
264 | payer,
265 | token_account
266 | ],
267 | recent_blockhash
268 | );
269 | transaction
270 | }
--------------------------------------------------------------------------------