├── .gitignore
├── .gitmodules
├── LICENSE.md
├── README.md
├── scripts
├── create-devnet-spl-token.ts
├── deploy-contract.sh
├── deploy-full-devnet.sh
├── drop-bond-v2.sh
├── drop-bond-v2.ts
├── init-central-state.sh
├── init-central-state.ts
├── ledger-transfer-program-authority.sh
├── migrate-central-state-v2.sh
├── migrate-central-state-v2.ts
├── package-lock.json
├── package.json
├── spl-mint.sh
├── spl-set-authority.sh
└── spl-set-authority.ts
└── smart-contract
├── js
├── .ammanrc.cjs
├── README.md
├── compile-mpl-token-metadata-program.sh
├── fixup.sh
├── jest.config.cjs
├── package.json
├── programs
│ └── mpl_token_metadata.so
├── src
│ ├── bindings.ts
│ ├── index.ts
│ ├── raw_instructions.ts
│ ├── secondary_bindings.ts
│ ├── state.ts
│ ├── u64.ts
│ ├── utils.ts
│ └── v1_bindings.ts
├── tests
│ ├── change-central-state-auth.ts
│ ├── end_to_end.test.ts
│ ├── poc.ts
│ └── utils.ts
├── tsconfig.base.json
├── tsconfig.cjs.json
├── tsconfig.esm.json
└── yarn.lock
└── program
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── Makefile
├── src
├── cpi.rs
├── entrypoint.rs
├── error.rs
├── instruction.rs
├── lib.rs
├── processor.rs
├── processor
│ ├── activate_stake_pool.rs
│ ├── add_to_bond_v2.rs
│ ├── admin_change_freeze_authority.rs
│ ├── admin_freeze.rs
│ ├── admin_mint.rs
│ ├── admin_program_freeze.rs
│ ├── admin_renounce.rs
│ ├── admin_set_protocol_fee.rs
│ ├── admin_setup_fee_split.rs
│ ├── change_central_state_authority.rs
│ ├── change_inflation.rs
│ ├── change_pool_minimum.rs
│ ├── change_pool_multiplier.rs
│ ├── claim_bond.rs
│ ├── claim_bond_rewards.rs
│ ├── claim_bond_v2_rewards.rs
│ ├── claim_pool_rewards.rs
│ ├── claim_rewards.rs
│ ├── close_royalty_account.rs
│ ├── close_stake_account.rs
│ ├── close_stake_pool.rs
│ ├── crank.rs
│ ├── create_bond.rs
│ ├── create_bond_v2.rs
│ ├── create_central_state.rs
│ ├── create_royalty_account.rs
│ ├── create_stake_account.rs
│ ├── create_stake_pool.rs
│ ├── distribute_fees.rs
│ ├── edit_metadata.rs
│ ├── migrate_central_state_v2.rs
│ ├── sign_bond.rs
│ ├── stake.rs
│ ├── unlock_bond_tokens.rs
│ ├── unlock_bond_v2.rs
│ └── unstake.rs
├── state.rs
└── utils.rs
└── tests
├── basic_functionality.rs
├── bonds.rs
├── claim-before-crank.rs
├── common
├── mod.rs
├── test_runner.rs
└── utils.rs
├── common_stake_limit.rs
├── common_unstake_limit.rs
├── devnet.rs
├── end_to_end.rs
├── fee_split.rs
├── freeze_and_renounce.rs
├── full.rs
├── functional.rs
├── later_creations.rs
├── overflow.rs
├── pool_settings.rs
├── pool_setup.rs
├── protocol_fee.rs
├── renounce.rs
├── repeated_claim.rs
├── rewards_bonds.rs
├── rewards_wrap.rs
├── royalties.rs
├── v1_bonds.rs
└── v2_bonds.rs
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
3 | dist
4 | .vscode
5 | __pycache__
6 | target/
7 | bin/
8 | coverage/
9 | wallet.json
10 | scripts/artifacts*
11 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "metaplex-program-library"]
2 | path = metaplex-program-library
3 | url = git@github.com:metaplex-foundation/metaplex-program-library.git
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
ACCESS Protocol
2 |
3 | Table of content
4 |
5 | 1. Concepts
6 | 2. Backends
7 | - Javascript
8 | - Rust
9 | - Go
10 | - Python
11 | 3. Smart contract
12 | - Program
13 | - Javascript bindings
14 |
15 | Concepts
16 |
17 | ACCESS Protocol lays the foundation for digital content monetization using a web3 wallet. Users get access to content by staking ACCESS tokens (through their `StakeAccount`) into the content publisher's pool (`StakePool`). ACCESS tokens are distributed to stakers and content publishers through an inflation schedule defined in the `CentralState`.
18 |
19 | The protocol also has the possibility to create and sell bonds (`BondAccount`). Bonds allow the protocol to sell locked tokens with linear vesting. The locked tokens can be staked and used to access content of a staked pool.
20 |
21 | Publishers need to adapt their backend infrastructures to support authentication and authorization via a web3 wallet. This authentication process relies on signature verifications and a demo example is implemented in the `backends` folder in JS, Rust, Go and Python.
22 |
23 | Backends
24 |
25 | The `backends` folder contains an example implementation of a REST API using a Solana wallet for authentication and JWT authorization. The example is implemented in Javascript, Rust, Go and Python.
26 |
27 | It is strongly recommended to use either the Javascript or Rust implementation as these two language have the best Solana tooling.
28 |
29 | Smart contract
30 |
31 | The smart contract folder contains two subfolders: `js` and `program`.
32 |
33 | ### Program
34 |
35 | The `program` folder contains the Solana smart contract code, documentation can be generated using Rust doc
36 |
37 | ```
38 | cargo doc
39 | ```
40 |
41 | `functional.rs` test can be run using Solana program test
42 |
43 | ```
44 | BPF_OUT_DIR=target/deploy cargo test-bpf --features days-to-sec-10s no-mint-check no-bond-signer --test functional
45 | ```
46 |
47 | Other Rust tests can be run using
48 |
49 | ```
50 | BPF_OUT_DIR=target/deploy cargo test-bpf --features no-mint-check no-bond-signer -- --skip functional_10s
51 | ```
52 |
53 | ```
54 |
55 | ```
56 |
57 | ### JS
58 |
59 | The `js` folder contains the Javascript bindings of the smart contract. This package is published on NPM
60 |
61 | ```
62 | npm i @accessprotocol/js
63 | ```
64 |
65 | ```
66 | yarn add @accessprotocol/js
67 | ```
68 |
69 | End to end tests are implemented using `jest`, they can be run using
70 |
71 | ```
72 | yarn amman:start
73 | yarn jest
74 | ```
75 |
76 | This will:
77 |
78 | - Spawn a local solana test validator via Amman
79 | - Deploy the program
80 | - Run all the instructions of the protocol
81 | - Verify the states of each account at each step
82 |
83 | ### Devnet deployment
84 |
85 | To deploy the program on devnet run the `yarn deploy` command inside the `scripts` folder. This will:
86 |
87 | - Create an SPL token with appropriate metadata.
88 | - Build and deploy the Solana program (smart contract).
89 | - Create a `CentralState` data account for the global state of the program.
90 | - Transfer the SPL token authority to the program (central state).
91 |
92 | The following artifacts will be created during the deployment in the `scripts/artifacts` folder:
93 |
94 | - `program.json` - Keypair of the program
95 | - `authority.json` - Keypair of the program update authority
96 | - `spl_authority.json` - Keypair of the SPL token authority, not used anymore after the authority transfer
97 | - `central_state_pubkey.txt` - Pubkey of the `CentralState` data account
98 | - `mint_address.txt` - Pubkey of the SPL token mint
99 |
100 | ### Known shortcomings
101 |
102 | - Cannot create two bonds tied to a different pool with the same amount
103 | - Bond functionality is counterintuitive
104 | - Bond unlocking does not start at `unlock_start_date`, but at `unlock_start_date + unlock_period`
105 | - If bond is claimed after the `unlock_start_date`, the offset of unlock times is counted relative to this date instead of the `unlock_start_date`
106 |
--------------------------------------------------------------------------------
/scripts/create-devnet-spl-token.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import {
3 | Connection,
4 | Keypair,
5 | LAMPORTS_PER_SOL,
6 | PublicKey,
7 | TransactionMessage,
8 | VersionedTransaction
9 | } from "@solana/web3.js";
10 | import { createMint } from "@solana/spl-token";
11 |
12 | import { keypairIdentity, Metaplex } from '@metaplex-foundation/js';
13 | import { createCreateMetadataAccountV3Instruction } from '@metaplex-foundation/mpl-token-metadata';
14 |
15 |
16 | const createDevnetSplToken = async (
17 | connection: Connection,
18 | decimals: number,
19 | mintAuthority: Keypair,
20 | ) => {
21 | const mintAuthorityKey = mintAuthority.publicKey;
22 | console.log(`Mint authority key: ${mintAuthorityKey.toBase58()}`);
23 |
24 | const balance = await connection.getBalance(mintAuthorityKey);
25 | console.log(`Balance of mint authority wallet: ${(balance / LAMPORTS_PER_SOL).toFixed(2)} SOL`);
26 |
27 | return await createMint(
28 | connection,
29 | mintAuthority,
30 | mintAuthority.publicKey,
31 | null,
32 | decimals
33 | );
34 | }
35 |
36 |
37 | const updateMetadata = async (
38 | connection: Connection,
39 | mintAddress: PublicKey,
40 | mintAuthority: Keypair,
41 | metadata: Metadata,
42 | ) => {
43 | const mintAuthorityKey = await mintAuthority.publicKey;
44 | const metaplex = Metaplex.make(connection).use(keypairIdentity(mintAuthority))
45 | const metadataPDA = metaplex.nfts().pdas().metadata({mint:mintAddress});
46 |
47 | console.log("Metadata: ", metadata);
48 |
49 | // create v0 compatible message
50 | const messageV0 = new TransactionMessage({
51 | payerKey: mintAuthorityKey,
52 | recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
53 | instructions: [createCreateMetadataAccountV3Instruction({
54 | metadata: metadataPDA,
55 | mint: mintAddress,
56 | mintAuthority: mintAuthorityKey,
57 | payer: mintAuthorityKey,
58 | updateAuthority: mintAuthorityKey,
59 | },
60 | {
61 | createMetadataAccountArgsV3:
62 | {
63 | data: {
64 | name: metadata.name,
65 | symbol: metadata.symbol,
66 | uri: metadata.uri,
67 | sellerFeeBasisPoints: 0,
68 | creators: null,
69 | collection: null,
70 | uses: null
71 | },
72 | isMutable: true,
73 | collectionDetails: null
74 | },
75 | })
76 | ],
77 | }).compileToV0Message();
78 |
79 | const transaction = new VersionedTransaction(messageV0);
80 | transaction.sign([mintAuthority]);
81 |
82 | await connection.sendTransaction(transaction, {
83 | preflightCommitment: "confirmed",
84 | skipPreflight: false
85 | });
86 | }
87 |
88 | // Decimals of the ACS token
89 | const TOKEN_DECIMALS = 6;
90 |
91 | const main = async () => {
92 | console.log("Minting a new SPL token");
93 | // The Solana RPC connection
94 | const rpcProviderUrl = 'https://api.devnet.solana.com'
95 | console.log("RPC provider: ", rpcProviderUrl);
96 | const connection = new Connection(rpcProviderUrl);
97 |
98 | // Mint authority keypair
99 | const authorityKeypair = Keypair.generate();
100 | console.log(`Created new Mint Authority Wallet into artifacts/spl_authority.json: ${authorityKeypair.publicKey.toBase58()}`);
101 | fs.writeFileSync(`artifacts/spl_authority.json`,
102 | JSON.stringify(authorityKeypair.secretKey
103 | .toString() //convert secret key to string
104 | .split(',') //delimit string by commas and convert to an array of strings
105 | .map(value => Number(value))
106 | )
107 | );
108 |
109 | console.log(`Funding mint authority wallet with 1 SOL...`)
110 | const airdropSignature = await connection.requestAirdrop(authorityKeypair.publicKey, LAMPORTS_PER_SOL);
111 | await connection.confirmTransaction(airdropSignature);
112 |
113 | console.log(`Waiting for 20 seconds to ensure that the airdrop succeeds...`);
114 | await new Promise(resolve => setTimeout(resolve, 20000));
115 |
116 | // Initialize mint
117 | const tokenPubkey = await createDevnetSplToken(
118 | connection,
119 | TOKEN_DECIMALS, // Decimals of the token
120 | authorityKeypair, // mint authority keypair
121 | );
122 |
123 | console.log(`Token initiated successfully on address ${tokenPubkey.toBase58()}`);
124 | // write token address to file mint_address.txt
125 | fs.writeFileSync(`artifacts/mint_address.txt`, tokenPubkey.toBase58());
126 |
127 | await updateMetadata(connection, tokenPubkey, authorityKeypair, {
128 | name: "Access Protocol",
129 | symbol: "ACS",
130 | image: "https://ap-staging.fra1.digitaloceanspaces.com/1663691449945",
131 | uri: "https://accessprotocol.s3.eu-central-1.amazonaws.com/testing_token.json",
132 | });
133 | };
134 |
135 |
136 | type Metadata = {
137 | name: string,
138 | symbol: string,
139 | image: string,
140 | uri: string,
141 | }
142 |
143 | // Run:
144 | // NETWORK=devnet yarn create-spl-token [EXISTING_WALLET_PATH]
145 | main()
146 | .then(() => process.exit(0))
147 | .catch((error) => {
148 | console.error(error);
149 | process.exit(1);
150 | });
--------------------------------------------------------------------------------
/scripts/deploy-contract.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # set -x
3 |
4 | pwd=$(pwd)
5 | NETWORK=${NETWORK:-devnet}
6 | ALLOW_V1=${ALLOW_V1:-false}
7 |
8 | pushd ../smart-contract/program
9 | echo "Building smart contract program..."
10 | # IMPORTANT: If not no-mint-check present you need to update MINT_ADDRESS in (state.rs)
11 | FEATURES=""
12 | if [ "$ALLOW_V1" == "true" ];
13 | then
14 | FEATURES="no-bond-signer no-mint-check v1-instructions-allowed"
15 | fi
16 | cargo build-bpf --features ${FEATURES}
17 |
18 | PROGRAM_KEYPAIR=${PROGRAM_KEYPAIR:-"$pwd/artifacts/program.json"}
19 | echo "Check program keypair file exists..."
20 | if test -f "$PROGRAM_KEYPAIR"
21 | then
22 | echo "Program ID keypair exists at: $PROGRAM_KEYPAIR"
23 | else
24 | echo "Creating program keypair..."
25 | solana-keygen new --outfile $PROGRAM_KEYPAIR --no-bip39-passphrase
26 | fi
27 |
28 | AUTHORITY_KEYPAIR=${AUTHORITY_KEYPAIR:-"$pwd/artifacts/authority.json"}
29 | echo "Authority ${AUTHORITY_KEYPAIR}"
30 | echo "Check fee payer keypair file exists..."
31 | if test -f "$AUTHORITY_KEYPAIR"
32 | then
33 | echo "Authority ID keypair exists at: $AUTHORITY_KEYPAIR"
34 | else
35 | if [ "$NETWORK" == "devnet" ];
36 | then
37 | echo "Creating authority keypair..."
38 | solana-keygen new --outfile $AUTHORITY_KEYPAIR --no-bip39-passphrase
39 | else
40 | echo "For production env. you need to provide the AUTHORITY_KEYPAIR with SOL inside!"
41 | exit 1
42 | fi
43 | fi
44 |
45 | solana config set --keypair $AUTHORITY_KEYPAIR
46 |
47 | echo "authority: $(solana address)"
48 |
49 | echo "Checking your account balance..."
50 | balance=$(solana balance -u ${NETWORK} | rev | grep -Eo '[^ ]+$' | rev)
51 | echo $balance
52 | if (( $(echo "$balance > 6" | bc -l) ))
53 | then
54 | echo "Balance is good."
55 | else
56 | if [ "$NETWORK" == "devnet" ];
57 | then
58 | while [ ${balance%.*} -lt 6 ]
59 | do
60 | echo "Not enough SOL in your wallet, airdropping. If this keeps failing, fund the authority wallet manually."
61 | solana airdrop 1
62 | sleep 2
63 | balance=$(solana balance -u ${NETWORK} | rev | grep -Eo '[^ ]+$' | rev)
64 | done
65 | else
66 | echo "You need at least 6 SOL in the wallet to be able to deploy!"
67 | exit 1
68 | fi
69 | fi
70 |
71 | echo "Deploying contract..."
72 | program_pubkey=$(solana-keygen pubkey ${PROGRAM_KEYPAIR})
73 | echo "Program pubkey: $program_pubkey"
74 | authority_pubkey=$(solana-keygen pubkey ${AUTHORITY_KEYPAIR})
75 | echo "Authority pubkey: $authority_pubkey"
76 | solana config get
77 | solana program deploy ./target/deploy/access_protocol.so \
78 | --program-id ${PROGRAM_KEYPAIR} \
79 | --upgrade-authority ${authority_pubkey} \
80 | --keypair ${AUTHORITY_KEYPAIR} \
81 | -u ${NETWORK}
82 |
83 | popd
84 |
--------------------------------------------------------------------------------
/scripts/deploy-full-devnet.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -x
3 | set -o allexport -o notify
4 | set -e
5 |
6 | mkdir -p artifacts
7 |
8 | echo "STEP 1: Creating an SPL Token"
9 | ts-node create-devnet-spl-token.ts
10 |
11 | echo "STEP 2: Deploying the Devnet contract with V1 instructions allowed"
12 | # the ALLOW_V1 flag is only needed if we want to keep the V1 instructions enabled
13 | # ALLOW_V1=true ./deploy-contract.sh
14 | ./deploy-contract.sh
15 |
16 | echo "STEP 3: Initializing the central state"
17 | ./init-central-state.sh
18 |
19 | echo "STEP 4: Minting 1,000,000 tokens to token bank as minting is disabled in V2 (if ALLOW_V1=false)"
20 | ./spl-mint.sh
21 |
22 | echo "STEP 5: Transferring the SPL token authority to the central state"
23 | ./spl-set-authority.sh
24 |
25 | echo "STEP 6: Migrating the central state to the V2 format"
26 | ./migrate-central-state-v2.sh
--------------------------------------------------------------------------------
/scripts/drop-bond-v2.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -x
3 | set -o allexport -o notify
4 |
5 | SOLANA_RPC_PROVIDER_URL=
6 | PROGRAM_ID=
7 | POOL_PUBKEY=
8 | USER_PUBKEY=
9 | PAYER_KEYPAIR=
10 | AMOUNT=10
11 | # Sun Mar 24 2024 09:45:57 GMT+0000
12 | UNLOCK_TIMESTAMP=1711273557
13 |
14 | ts-node drop-bond-v2.ts
--------------------------------------------------------------------------------
/scripts/drop-bond-v2.ts:
--------------------------------------------------------------------------------
1 | import { addToBondV2, createBondV2 } from "../smart-contract/js";
2 | import {
3 | ComputeBudgetProgram,
4 | Connection,
5 | Keypair,
6 | PublicKey,
7 | Signer,
8 | TransactionInstruction,
9 | TransactionMessage,
10 | VersionedTransaction,
11 | } from "@solana/web3.js";
12 | import fs from "fs";
13 | import BN from "bn.js";
14 |
15 | // get from env variables
16 | const {
17 | SOLANA_RPC_PROVIDER_URL,
18 | PROGRAM_ID,
19 | POOL_PUBKEY,
20 | USER_PUBKEY,
21 | PAYER_KEYPAIR,
22 | AMOUNT,
23 | UNLOCK_TIMESTAMP,
24 | } = process.env;
25 |
26 | if (!SOLANA_RPC_PROVIDER_URL) {
27 | throw new Error("SOLANA_RPC_PROVIDER_URL is required");
28 | }
29 | if (!PROGRAM_ID) {
30 | throw new Error("PROGRAM_ID is required");
31 | }
32 | if (!POOL_PUBKEY) {
33 | throw new Error("POOL_PUBKEY is required");
34 | }
35 | if (!USER_PUBKEY) {
36 | throw new Error("USER_PUBKEY is required");
37 | }
38 | if (!PAYER_KEYPAIR) {
39 | throw new Error("PAYER_KEYPAIR is required");
40 | }
41 | if (!AMOUNT) {
42 | throw new Error("AMOUNT is required");
43 | }
44 | if (!UNLOCK_TIMESTAMP) {
45 | throw new Error("UNLOCK_TIMESTAMP is required");
46 | }
47 |
48 | const connection = new Connection(SOLANA_RPC_PROVIDER_URL);
49 | const programId = new PublicKey(PROGRAM_ID);
50 | const user = new PublicKey(USER_PUBKEY);
51 | const payer = Keypair.fromSecretKey(
52 | Uint8Array.from(JSON.parse(fs.readFileSync(PAYER_KEYPAIR).toString()))
53 | );
54 | const pool = new PublicKey(POOL_PUBKEY);
55 | const amount = parseInt(AMOUNT);
56 | if (isNaN(amount)) {
57 | throw new Error("AMOUNT must be a number");
58 | }
59 | const unlockTimestamp = parseInt(UNLOCK_TIMESTAMP);
60 | if (isNaN(unlockTimestamp)) {
61 | throw new Error("UNLOCK_TIMESTAMP must be a number");
62 | }
63 |
64 | export const sendIxs = async (connection: Connection, ixs: TransactionInstruction[], signers: Signer[]) => {
65 | const computePriceIx = ComputeBudgetProgram.setComputeUnitPrice({
66 | microLamports: 10_000_000,
67 | });
68 |
69 | const computeLimitIx = ComputeBudgetProgram.setComputeUnitLimit({
70 | units: 300_000,
71 | });
72 |
73 | const messageV0 = new TransactionMessage({
74 | payerKey: signers[0].publicKey,
75 | recentBlockhash: (await connection.getLatestBlockhash('max')).blockhash,
76 | instructions: [computePriceIx, computeLimitIx, ...ixs],
77 | }).compileToV0Message();
78 |
79 | const transaction = new VersionedTransaction(messageV0);
80 | transaction.sign(signers);
81 |
82 | const txl = transaction.serialize().length;
83 | console.log("Tx size:", txl);
84 |
85 | return await connection.sendTransaction(transaction,
86 | {
87 | skipPreflight: false,
88 | }
89 | );
90 | }
91 |
92 | const main = async () => {
93 | const createBondIx = createBondV2(
94 | user,
95 | payer.publicKey,
96 | pool,
97 | new BN.BN(unlockTimestamp),
98 | programId,
99 | );
100 |
101 | // Add to bond V2
102 | const addBondIx = await addToBondV2(
103 | connection,
104 | user,
105 | payer.publicKey,
106 | pool,
107 | new BN.BN(amount * 1e6),
108 | new BN.BN(unlockTimestamp),
109 | programId,
110 | );
111 |
112 | const sx = await sendIxs(connection, [createBondIx, addBondIx], [payer]);
113 | console.log("Transaction:", sx);
114 | };
115 |
116 | main()
117 | .then(() => process.exit(0))
118 | .catch((err) => {
119 | console.error(err);
120 | process.exit(1);
121 | });
122 |
--------------------------------------------------------------------------------
/scripts/init-central-state.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -x
3 | set -o allexport -o notify
4 |
5 | pwd=$(pwd)
6 | mint_address=$(cat $pwd/artifacts/mint_address.txt)
7 | echo "mint_address: $mint_address"
8 |
9 | PROGRAM_PUBKEY=$(solana-keygen pubkey $pwd/artifacts/program.json)
10 | AUTHORITY_KEYPAIR="$pwd/artifacts/authority.json"
11 | MINT_ADDRESS=$mint_address
12 | YEARLY_INFLATION_IN_ACS=2000000000
13 | SOLANA_RPC_PROVIDER_URL=https://api.devnet.solana.com
14 |
15 | ts-node init-central-state
--------------------------------------------------------------------------------
/scripts/init-central-state.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import { Connection, Keypair, PublicKey, TransactionMessage, VersionedTransaction } from "@solana/web3.js";
3 |
4 | import { createCentralState, } from "../smart-contract/js";
5 |
6 | const {
7 | SOLANA_RPC_PROVIDER_URL, PROGRAM_PUBKEY, AUTHORITY_KEYPAIR, MINT_ADDRESS, YEARLY_INFLATION_IN_ACS
8 | } = process.env;
9 |
10 | if (SOLANA_RPC_PROVIDER_URL == null)
11 | throw new Error("SOLANA_RPC_PROVIDER_URL must be set.");
12 | if (PROGRAM_PUBKEY == null)
13 | throw new Error("PROGRAM_PUBKEY must be set.");
14 | if (AUTHORITY_KEYPAIR == null)
15 | throw new Error("AUTHORITY_KEYPAIR must be set.");
16 | if (MINT_ADDRESS == null)
17 | throw new Error("MINT_ADDRESS must be set.");
18 | if (YEARLY_INFLATION_IN_ACS == null)
19 | throw new Error("YEARLY_INFLATION_IN_ACS must be set.");
20 |
21 | // The Solana RPC connection
22 | const connection = new Connection(SOLANA_RPC_PROVIDER_URL);
23 |
24 | // The wallet used to initialize the central state
25 | // This wallet will be the central state authority
26 | const authorityKeypair = Keypair.fromSecretKey(
27 | Uint8Array.from(JSON.parse(fs.readFileSync(AUTHORITY_KEYPAIR).toString()))
28 | );
29 |
30 | // 🚨 The initial inflation in tokens/day (raw amount i.e need to contain decimals)
31 | const dailyInflation = Math.floor((parseInt(YEARLY_INFLATION_IN_ACS) * (10 ** 6)) / 365);
32 | console.log("Daily inflation at: ", Number(dailyInflation));
33 | console.log("Program ID: ", PROGRAM_PUBKEY);
34 | console.log("Mint address: ", MINT_ADDRESS);
35 |
36 | const initCentralState = async () => {
37 | const ix = await createCentralState(
38 | Number(dailyInflation),
39 | authorityKeypair.publicKey, // Central state authority
40 | new PublicKey(MINT_ADDRESS), // Key to token program
41 | new PublicKey(PROGRAM_PUBKEY), // Program ID
42 | );
43 |
44 | const messageV0 = new TransactionMessage({
45 | payerKey: authorityKeypair.publicKey,
46 | recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
47 | instructions: [ix],
48 | }).compileToV0Message();
49 |
50 | const transaction = new VersionedTransaction(messageV0);
51 | transaction.sign([authorityKeypair]);
52 |
53 | const tx = await connection.sendTransaction(transaction, {
54 | preflightCommitment: "confirmed",
55 | skipPreflight: false
56 | });
57 |
58 | console.log(`Created central state ${tx}`);
59 |
60 | const [centralKey] = PublicKey.findProgramAddressSync(
61 | [new PublicKey(PROGRAM_PUBKEY).toBuffer()],
62 | new PublicKey(PROGRAM_PUBKEY)
63 | );
64 | // write central state key to file
65 | fs.writeFileSync("artifacts/central_state_pubkey.txt", centralKey.toString());
66 | };
67 |
68 | initCentralState()
69 | .then(() => process.exit(0))
70 | .catch((error) => {
71 | console.error(error);
72 | process.exit(1);
73 | });
74 |
--------------------------------------------------------------------------------
/scripts/ledger-transfer-program-authority.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Change these variables
4 | LEDGER_ACCOUNT_INDEX=0
5 | PROGRAM_PUBKEY=
6 | SOLANA_RPC_PROVIDER_URL=
7 | NEW_AUTHORITY_ADDRESS=
8 |
9 | # Don't change anything below this line
10 | LEDGER_KEYPAIR_URL=usb://ledger\?key=$LEDGER_ACCOUNT_INDEX
11 | LEDGER_PUBKEY=$(solana-keygen pubkey "$LEDGER_KEYPAIR_URL")
12 | if [ $? -eq 0 ]; then
13 | echo Ledger pubkey: $LEDGER_PUBKEY
14 | else
15 | echo "Is your Ledger connected and unlocked?"
16 | exit 1
17 | fi
18 |
19 | CURRENT_AUTHORITY_ADDRESS=$(solana program show --url $SOLANA_RPC_PROVIDER_URL $PROGRAM_PUBKEY | grep "Authority" | awk '{print $2}')
20 | if [ "$LEDGER_PUBKEY" != "$CURRENT_AUTHORITY_ADDRESS" ]; then
21 | echo "The Ledger pubkey $LEDGER_PUBKEY does not match the current authority $CURRENT_AUTHORITY_ADDRESS"
22 | exit 1
23 | fi
24 |
25 | echo "Transferring authority out of the Ledger..."
26 | solana program set-upgrade-authority $PROGRAM_PUBKEY \
27 | --new-upgrade-authority $NEW_AUTHORITY_ADDRESS \
28 | -k "$LEDGER_KEYPAIR_URL" \
29 | --url $SOLANA_RPC_PROVIDER_URL \
30 | --skip-new-upgrade-authority-signer-check
31 |
--------------------------------------------------------------------------------
/scripts/migrate-central-state-v2.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -x
3 | set -o allexport -o notify
4 |
5 | pwd=$(pwd)
6 | mint_address=$(cat $pwd/artifacts/mint_address.txt)
7 | echo "mint_address: $mint_address"
8 |
9 | PROGRAM_PUBKEY=$(solana-keygen pubkey $pwd/artifacts/program.json)
10 | AUTHORITY_KEYPAIR="$pwd/artifacts/authority.json"
11 | MINT_ADDRESS=$mint_address
12 | SOLANA_RPC_PROVIDER_URL=https://api.devnet.solana.com
13 |
14 | ts-node migrate-central-state-v2
--------------------------------------------------------------------------------
/scripts/migrate-central-state-v2.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import {
3 | Connection,
4 | Keypair,
5 | PublicKey,
6 | sendAndConfirmTransaction,
7 | Transaction,
8 | TransactionMessage,
9 | VersionedTransaction
10 | } from "@solana/web3.js";
11 |
12 | import { migrateCentralStateV2 } from "../smart-contract/js";
13 | import { createAssociatedTokenAccountInstruction, getAssociatedTokenAddressSync } from "@solana/spl-token";
14 |
15 | const {
16 | SOLANA_RPC_PROVIDER_URL, PROGRAM_PUBKEY, AUTHORITY_KEYPAIR, MINT_ADDRESS
17 | } = process.env;
18 |
19 | if (SOLANA_RPC_PROVIDER_URL == null)
20 | throw new Error("SOLANA_RPC_PROVIDER_URL must be set.");
21 | if (PROGRAM_PUBKEY == null)
22 | throw new Error("PROGRAM_PUBKEY must be set.");
23 | if (AUTHORITY_KEYPAIR == null)
24 | throw new Error("AUTHORITY_KEYPAIR must be set.");
25 | if (MINT_ADDRESS == null)
26 | throw new Error("MINT_ADDRESS must be set.");
27 |
28 | // The Solana RPC connection
29 | const connection = new Connection(SOLANA_RPC_PROVIDER_URL);
30 |
31 | // The wallet used to initialize the central state
32 | // This wallet will be the central state authority
33 | const authorityKeypair = Keypair.fromSecretKey(
34 | Uint8Array.from(JSON.parse(fs.readFileSync(AUTHORITY_KEYPAIR).toString()))
35 | );
36 |
37 | const migrateCentralState = async () => {
38 | const [centralKey] = PublicKey.findProgramAddressSync(
39 | [new PublicKey(PROGRAM_PUBKEY).toBuffer()],
40 | new PublicKey(PROGRAM_PUBKEY)
41 | );
42 |
43 | try {
44 | const ata = getAssociatedTokenAddressSync(
45 | new PublicKey(MINT_ADDRESS), centralKey, true
46 | );
47 |
48 | console.log(`Associated token account: ${ata.toBase58()}`);
49 |
50 | const transaction = new Transaction().add(
51 | createAssociatedTokenAccountInstruction(
52 | authorityKeypair.publicKey,
53 | ata,
54 | centralKey,
55 | new PublicKey(MINT_ADDRESS),
56 | )
57 | );
58 |
59 | await sendAndConfirmTransaction(connection, transaction, [authorityKeypair]);
60 | } catch (e) {
61 | console.log("Associated token account not created, it might already exist", e)
62 | }
63 |
64 | const ix = migrateCentralStateV2(
65 | authorityKeypair.publicKey, // Central state authority
66 | new PublicKey(PROGRAM_PUBKEY), // Program ID
67 | );
68 |
69 | const messageV0 = new TransactionMessage({
70 | payerKey: authorityKeypair.publicKey,
71 | recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
72 | instructions: [ix],
73 | }).compileToV0Message();
74 |
75 | const transaction = new VersionedTransaction(messageV0);
76 | transaction.sign([authorityKeypair]);
77 |
78 | const tx = await connection.sendTransaction(transaction, {
79 | preflightCommitment: "confirmed",
80 | skipPreflight: false
81 | });
82 |
83 | console.log(`Migrated central state to v2 ${tx}`);
84 |
85 | // write central state key to file
86 | fs.writeFileSync("artifacts/central_state_pubkey.txt", centralKey.toString());
87 | };
88 |
89 | migrateCentralState()
90 | .then(() => process.exit(0))
91 | .catch((error) => {
92 | console.error(error);
93 | process.exit(1);
94 | });
95 |
--------------------------------------------------------------------------------
/scripts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "spl-token",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "create-devnet-spl-token": "ts-node create-devnet-spl-token.ts",
8 | "deploy-contract": "./deploy-contract.sh",
9 | "init-central-state": "./init-central-state.sh",
10 | "spl-set-authority": "./spl-set-authority.sh",
11 | "deploy-full-devnet": "./deploy-full-devnet.sh"
12 | },
13 | "author": "",
14 | "license": "ISC",
15 | "dependencies": {
16 | "@metaplex-foundation/js": "^0.19.3",
17 | "@metaplex-foundation/mpl-token-metadata": "^2.11.1",
18 | "@solana/spl-token": "^0.3.6",
19 | "@solana/web3.js": "^1.66.2",
20 | "js": "^0.1.0",
21 | "ts-node": "^10.9.1",
22 | "typescript": "^4.8.4"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/scripts/spl-mint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -x
3 | set -o allexport -o notify
4 |
5 | pwd=$(pwd)
6 | SOLANA_RPC_PROVIDER_URL=https://api.devnet.solana.com
7 | MINT_ADDRESS=$(cat $pwd/artifacts/mint_address.txt)
8 |
9 | solana-keygen new --outfile $pwd/artifacts/token_bank.json --no-bip39-passphrase
10 | PUBKEY=$(solana-keygen pubkey $pwd/artifacts/token_bank.json)
11 | echo "Token bank pubkey: $PUBKEY"
12 | echo "Mint address: $MINT_ADDRESS"
13 | spl-token create-account --fee-payer $pwd/artifacts/spl_authority.json --owner $pwd/artifacts/token_bank.json -u $SOLANA_RPC_PROVIDER_URL $MINT_ADDRESS
14 | ATA_ADDRESS=$(spl-token accounts -v --owner $pwd/artifacts/token_bank.json | grep $MINT_ADDRESS | awk '{print $2}')
15 | spl-token mint --fee-payer $pwd/artifacts/spl_authority.json --mint-authority $pwd/artifacts/spl_authority.json -u $SOLANA_RPC_PROVIDER_URL $MINT_ADDRESS 1000000000000 $ATA_ADDRESS
--------------------------------------------------------------------------------
/scripts/spl-set-authority.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -x
3 | set -o allexport -o notify
4 |
5 | pwd=$(pwd)
6 | mint_address=$(cat $pwd/artifacts/mint_address.txt)
7 | central_state_pubkey=$(cat $pwd/artifacts/central_state_pubkey.txt)
8 |
9 | SPL_AUTHORITY_KEYPAIR="$pwd/artifacts/spl_authority.json"
10 | SOLANA_RPC_PROVIDER_URL=https://api.devnet.solana.com
11 | MINT_ADDRESS=$mint_address
12 | NEW_AUTHORITY_ADDRESS=$central_state_pubkey
13 |
14 | ts-node spl-set-authority.ts
--------------------------------------------------------------------------------
/scripts/spl-set-authority.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Connection,
3 | Keypair,
4 | LAMPORTS_PER_SOL,
5 | PublicKey,
6 | Signer,
7 | TransactionMessage,
8 | VersionedTransaction
9 | } from "@solana/web3.js";
10 | import { AuthorityType, createSetAuthorityInstruction } from "@solana/spl-token";
11 | import fs from "fs";
12 |
13 | const {
14 | SOLANA_RPC_PROVIDER_URL, SPL_AUTHORITY_KEYPAIR, MINT_ADDRESS, NEW_AUTHORITY_ADDRESS
15 | } = process.env;
16 |
17 | if (SOLANA_RPC_PROVIDER_URL == null)
18 | throw new Error("SOLANA_RPC_PROVIDER_URL must be set.");
19 | if (SPL_AUTHORITY_KEYPAIR == null)
20 | throw new Error("SPL_AUTHORITY_KEYPAIR must be set.");
21 | if (MINT_ADDRESS == null)
22 | throw new Error("MINT_ADDRESS must be set.");
23 | if (NEW_AUTHORITY_ADDRESS == null)
24 | throw new Error("NEW_AUTHORITY_ADDRESS must be set.");
25 |
26 | const setNewAuthority = async (
27 | connection: Connection,
28 | mintAddress: PublicKey,
29 | mintAuthority: Signer,
30 | newAuthorityWallet: PublicKey,
31 | ) => {
32 | const mintAuthorityKey = mintAuthority.publicKey;
33 | console.log(`Mint authority key: ${mintAuthorityKey.toBase58()}`);
34 |
35 | const balance = await connection.getBalance(mintAuthorityKey);
36 | console.log(`Balance of mint authority wallet: ${(balance / LAMPORTS_PER_SOL).toFixed(2)} SOL`);
37 |
38 | const messageV0 = new TransactionMessage({
39 | payerKey: mintAuthorityKey,
40 | recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
41 | instructions: [
42 | createSetAuthorityInstruction(
43 | mintAddress,
44 | mintAuthorityKey,
45 | AuthorityType.MintTokens,
46 | newAuthorityWallet,
47 | )
48 | ]
49 | }
50 | ).compileToV0Message();
51 |
52 | const transaction = new VersionedTransaction(messageV0);
53 | transaction.sign([mintAuthority]);
54 |
55 | return await connection.sendTransaction(transaction, {
56 | preflightCommitment: "confirmed",
57 | skipPreflight: false
58 | });
59 | }
60 |
61 | const main = async () => {
62 | const mintWallet = MINT_ADDRESS
63 | const newAuthorityWallet = NEW_AUTHORITY_ADDRESS
64 | // The Solana RPC connection
65 | const connection = new Connection(SOLANA_RPC_PROVIDER_URL);
66 |
67 | const authorityKeypair = Keypair.fromSecretKey(
68 | Uint8Array.from(JSON.parse(fs.readFileSync(SPL_AUTHORITY_KEYPAIR).toString()))
69 | );
70 |
71 | // Set authority
72 | const signature = await setNewAuthority(
73 | connection,
74 | new PublicKey(mintWallet),
75 | authorityKeypair,
76 | new PublicKey(newAuthorityWallet)
77 | );
78 |
79 | console.log(`New authority set successfully with signature: ${signature}`);
80 | };
81 |
82 | main()
83 | .then(() => process.exit(0))
84 | .catch((error) => {
85 | console.error(error);
86 | process.exit(1);
87 | });
--------------------------------------------------------------------------------
/smart-contract/js/.ammanrc.cjs:
--------------------------------------------------------------------------------
1 | const { LOCALHOST, tmpLedgerDir } = require('@metaplex-foundation/amman');
2 | const mplTokenMetadata = require('@metaplex-foundation/mpl-token-metadata');
3 | const path = require('path');
4 | const MOCK_STORAGE_ID = 'js-next-sdk';
5 |
6 | function localDeployPath(programName) {
7 | return path.join(__dirname, 'programs', `${programName}.so`);
8 | }
9 |
10 | const programs = [
11 | {
12 | label: 'Token Metadata',
13 | programId: mplTokenMetadata.PROGRAM_ADDRESS,
14 | deployPath: localDeployPath('mpl_token_metadata'),
15 | },
16 | ];
17 |
18 | module.exports = {
19 | validator: {
20 | killRunningValidators: true,
21 | programs,
22 | jsonRpcUrl: LOCALHOST,
23 | websocketUrl: '',
24 | commitment: 'finalized',
25 | ledgerDir: tmpLedgerDir(),
26 | resetLedger: true,
27 | verifyFees: false,
28 | },
29 | relay: {
30 | accountProviders: {
31 | ...mplTokenMetadata.accountProviders,
32 | },
33 | },
34 | storage: {
35 | storageId: MOCK_STORAGE_ID,
36 | clearOnStart: true,
37 | },
38 | snapshot: {
39 | snapshotFolder: path.join(__dirname, 'snapshots'),
40 | },
41 | };
--------------------------------------------------------------------------------
/smart-contract/js/README.md:
--------------------------------------------------------------------------------
1 | # Access Protocol - Javascript bindings for Solana smart contract
2 |
3 | ## Install
4 | ```bash
5 | npm install @accessprotocol/js
6 | ```
7 | or
8 | ```bash
9 | yarn add @accessprotocol/js
10 | ```
11 |
12 | ## Importing stuff
13 | ```javascript
14 | import { stake, unstake, ... } from "@accessprotocol/js"
15 | ```
16 |
17 | ## Relaseing new NPM package
18 | ```
19 | yarn install
20 | npm login
21 | yarn version
22 | yarn publish --dry-run
23 | yarn publish
24 | ```
--------------------------------------------------------------------------------
/smart-contract/js/compile-mpl-token-metadata-program.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -x
3 |
4 | pwd=$(pwd)
5 | export BPF_OUT_DIR=$pwd/../program/target/deploy
6 |
7 | pushd ../../metaplex-program-library/token-metadata/program
8 | echo "Building metaplex token metadata program..."
9 | cargo build-sbf --sbf-out-dir $BPF_OUT_DIR --arch bpf
10 | popd
11 |
12 | cp $pwd/../program/target/deploy/mpl_token_metadata.so $pwd/programs/
--------------------------------------------------------------------------------
/smart-contract/js/fixup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | cat >dist/cjs/package.json <dist/esm/package.json < 8) {
26 | throw new Error("u64 too large");
27 | }
28 |
29 | const zeroPad = Buffer.alloc(8);
30 | b.copy(zeroPad);
31 | return zeroPad;
32 | }
33 |
34 | /**
35 | * Construct a u64 from Buffer representation
36 | */
37 | static fromBuffer(buffer: Buffer): u64 {
38 | if (buffer.length !== 8) {
39 | throw new Error(`Invalid buffer length: ${buffer.length}`);
40 | }
41 | return new u64(
42 | [...buffer]
43 | .reverse()
44 | .map((i) => `00${i.toString(16)}`.slice(-2))
45 | .join(""),
46 | 16
47 | );
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/smart-contract/js/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { Connection, Keypair, Transaction, TransactionInstruction, } from "@solana/web3.js";
2 | import { TaggedInstruction } from "./raw_instructions.js";
3 | import BN from "bn.js";
4 |
5 | export async function sleep(ms: number) {
6 | console.log("Sleeping for ", ms, " ms");
7 | return await new Promise((resolve) => setTimeout(resolve, ms));
8 | }
9 |
10 | export const signAndSendTransactionInstructions = async (
11 | // sign and send transaction
12 | connection: Connection,
13 | signers: Array,
14 | feePayer: Keypair,
15 | txInstructions: Array
16 | ): Promise => {
17 | const tx = new Transaction();
18 | tx.feePayer = feePayer.publicKey;
19 | signers.push(feePayer);
20 | tx.add(...txInstructions);
21 | return await connection.sendTransaction(tx, signers, {
22 | skipPreflight: false,
23 | });
24 | };
25 |
26 | export const getUnfreezeMask = (ixs: TaggedInstruction[]) => {
27 | if (ixs.length === 0) {
28 | return new BN.BN(0).notn(128);
29 | }
30 | return ixs.reduce((acc, ix) =>
31 | acc.or(new BN.BN(1).shln(ix.tag)), new BN.BN(0));
32 | }
33 |
34 | export const getFreezeMask = (ixs: TaggedInstruction[]) => {
35 | return getUnfreezeMask(ixs).notn(128);
36 | }
37 |
--------------------------------------------------------------------------------
/smart-contract/js/tests/change-central-state-auth.ts:
--------------------------------------------------------------------------------
1 | import {
2 | changeCentralStateAuthority,
3 | } from "../src/bindings";
4 | import {
5 | Connection,
6 | Keypair,
7 | PublicKey,
8 | } from "@solana/web3.js";
9 | import { CentralState, Tag } from "../src/state";
10 | import { TokenMint } from "./utils";
11 | import { signAndSendTransactionInstructions } from "./utils";
12 | import { expect } from "@jest/globals";
13 |
14 | export const changeCentralStateAuth = async (
15 | connection: Connection,
16 | programId: PublicKey,
17 | feePayer: Keypair,
18 | centralStateAuthority: Keypair,
19 | accessToken: TokenMint
20 | ) => {
21 | /**
22 | * Test variables
23 | */
24 | const [centralKey, centralNonce] = await CentralState.getKey(programId);
25 | const nextAuth = Keypair.generate();
26 |
27 | let cs = await CentralState.retrieve(connection, centralKey);
28 |
29 | expect(cs.authority.toBase58()).toBe(
30 | centralStateAuthority.publicKey.toBase58()
31 | );
32 | expect(cs.signerNonce).toBe(centralNonce);
33 | expect(cs.tag).toBe(Tag.CentralState);
34 | expect(cs.tokenMint.toBase58()).toBe(accessToken.token.publicKey.toBase58());
35 |
36 | /**
37 | * Change central state
38 | */
39 | const ix = await changeCentralStateAuthority(
40 | connection,
41 | nextAuth.publicKey,
42 | programId
43 | );
44 | const tx = await signAndSendTransactionInstructions(
45 | connection,
46 | [centralStateAuthority],
47 | feePayer,
48 | [ix]
49 | );
50 |
51 | console.log("Central state auth changed: ", tx)
52 |
53 | cs = await CentralState.retrieve(connection, centralKey);
54 |
55 | expect(cs.authority.toBase58()).toBe(nextAuth.publicKey.toBase58());
56 | expect(cs.signerNonce).toBe(centralNonce);
57 | expect(cs.tag).toBe(Tag.CentralState);
58 | expect(cs.tokenMint.toBase58()).toBe(accessToken.token.publicKey.toBase58());
59 | };
60 |
--------------------------------------------------------------------------------
/smart-contract/js/tests/utils.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Connection,
3 | Keypair,
4 | LAMPORTS_PER_SOL,
5 | PublicKey,
6 | TransactionInstruction,
7 | TransactionMessage,
8 | VersionedTransaction,
9 | } from "@solana/web3.js";
10 | import * as path from "path";
11 | import { closeSync, readFileSync, writeSync } from "fs";
12 | import { execSync } from "child_process";
13 | import * as tmp from "tmp";
14 | import {
15 | AuthorityType,
16 | createMint,
17 | createSetAuthorityInstruction,
18 | getOrCreateAssociatedTokenAccount,
19 | mintTo,
20 | TOKEN_PROGRAM_ID
21 | } from "@solana/spl-token";
22 | import { sleep } from "../src/utils";
23 |
24 | const programName = "access_protocol";
25 |
26 | // Returns a keypair and key file name.
27 | export function initializePayer(): [Keypair, string] {
28 | const key = new Keypair();
29 | const tmpobj = tmp.fileSync();
30 | writeSync(tmpobj.fd, JSON.stringify(Array.from(key.secretKey)));
31 | closeSync(tmpobj.fd);
32 | return [key, tmpobj.name];
33 | }
34 |
35 | // Deploys the agnostic order book program. Fees are paid with the fee payer
36 | // whose key is in the given key file.
37 | export function deployProgram(
38 | payerKeyFile: string,
39 | compile: boolean,
40 | compileFlag?: string,
41 | testBpf?: boolean
42 | ): PublicKey {
43 | const programDirectory = path.join(path.dirname(__filename), "../../program");
44 | const stakingSo = path.join(
45 | programDirectory,
46 | `target/deploy/${programName}.so`
47 | );
48 | const keyfile = path.join(
49 | path.dirname(stakingSo),
50 | `${programName}-keypair.json`
51 | );
52 | let compileCmd = "cargo build-bpf";
53 | if (compileFlag) {
54 | compileCmd += ` --features ${compileFlag}`;
55 | }
56 | if (compile) {
57 | execSync(compileCmd, {
58 | cwd: programDirectory,
59 | });
60 | }
61 | if (testBpf) {
62 | execSync(
63 | "cargo test-bpf --features days-to-sec-10s no-mint-check no-bond-signer",
64 | {
65 | cwd: programDirectory,
66 | }
67 | );
68 | }
69 |
70 | const bytes = readFileSync(keyfile, "utf-8");
71 | const keypair = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(bytes)));
72 | const cmd = [
73 | "solana program deploy",
74 | stakingSo,
75 | "--program-id",
76 | keyfile,
77 | "-u localhost",
78 | "-k",
79 | payerKeyFile,
80 | "--commitment finalized",
81 | ].join(" ")
82 | execSync(cmd);
83 | return keypair.publicKey;
84 | }
85 |
86 | // Funds the given account. Sleeps until the connection is ready.
87 | export async function airdropPayer(connection: Connection, key: PublicKey) {
88 | while (true) {
89 | try {
90 | const signature = await connection.requestAirdrop(
91 | key,
92 | 10 * LAMPORTS_PER_SOL
93 | );
94 | console.log(`Airdrop signature ${signature}`);
95 | await connection.confirmTransaction(signature, "finalized");
96 | return;
97 | } catch (e) {
98 | console.log(`Error airdropping ${e}`);
99 | await new Promise((resolve) => setTimeout(resolve, 1000));
100 | }
101 | }
102 | }
103 |
104 | export const signAndSendTransactionInstructions = async (
105 | // sign and send transaction
106 | connection: Connection,
107 | signers: Array | undefined,
108 | feePayer: Keypair,
109 | txInstructions: Array,
110 | skipPreflight?: boolean
111 | ): Promise => {
112 | const messageV0 = new TransactionMessage({
113 | payerKey: feePayer.publicKey,
114 | recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
115 | instructions: [...txInstructions],
116 | }).compileToV0Message();
117 |
118 | const transaction = new VersionedTransaction(messageV0);
119 | transaction.sign([...signers, feePayer]);
120 |
121 | const sig = await connection.sendTransaction(transaction, {
122 | preflightCommitment: "finalized",
123 | skipPreflight: false
124 | });
125 |
126 | let status = await connection.getSignatureStatus(sig);
127 | console.log("Signature status: ", status.value?.confirmationStatus);
128 | let attempt = 1;
129 | while (status.value?.confirmationStatus !== 'finalized') {
130 | if (attempt > 20) {
131 | throw new Error(`Confirmation failed after ${attempt} attempts`);
132 | }
133 | await sleep(5000);
134 | console.log(`waiting for confirmation... (${attempt})`);
135 | status = await connection.getSignatureStatus(sig);
136 | attempt++;
137 | }
138 |
139 | return sig;
140 | };
141 |
142 | export class TokenMint {
143 | token: Keypair;
144 | connection: Connection;
145 | feePayer: Keypair;
146 | mintAuthority: PublicKey;
147 | centralStateAuthority?: Keypair;
148 |
149 | constructor(token: Keypair, connection: Connection, feePayer: Keypair, mintAuthority: PublicKey) {
150 | this.token = token;
151 | this.connection = connection;
152 | this.feePayer = feePayer;
153 | this.mintAuthority = mintAuthority;
154 | }
155 |
156 | async updateAuthorityToCentralState(
157 | connection: Connection,
158 | mintAuthorityKeypair: Keypair,
159 | feePayer: Keypair,
160 | centralKey: PublicKey
161 | ) {
162 | const txs = [
163 | createSetAuthorityInstruction(
164 | this.token.publicKey,
165 | this.mintAuthority,
166 | AuthorityType.MintTokens,
167 | centralKey,
168 | )
169 | ];
170 |
171 | const tx = await signAndSendTransactionInstructions(connection, [mintAuthorityKeypair], feePayer,
172 | txs
173 | );
174 | console.log(`Move mint authority to central key ${tx}`);
175 | }
176 |
177 | static async init(
178 | connection: Connection,
179 | feePayer: Keypair,
180 | mintAuthority: Keypair | null = null,
181 | ) {
182 | const tokenKeypair = new Keypair();
183 | await createMint(
184 | connection,
185 | feePayer,
186 | mintAuthority?.publicKey ?? tokenKeypair.publicKey,
187 | null,
188 | 6,
189 | tokenKeypair
190 | );
191 | const token = new TokenMint(
192 | tokenKeypair, connection, feePayer, mintAuthority?.publicKey ?? tokenKeypair.publicKey,
193 | );
194 | return token;
195 | }
196 |
197 | async getAssociatedTokenAccount(wallet: PublicKey): Promise {
198 | const acc = await getOrCreateAssociatedTokenAccount(
199 | this.connection,
200 | this.feePayer,
201 | this.token.publicKey,
202 | wallet
203 | );
204 | return acc.address;
205 | }
206 |
207 | async mintInto(tokenAccount: PublicKey, amount: number): Promise {
208 | return await mintTo(
209 | this.connection,
210 | this.feePayer,
211 | this.token.publicKey,
212 | tokenAccount,
213 | this.token,
214 | amount,
215 | [],
216 | { skipPreflight: false },
217 | TOKEN_PROGRAM_ID
218 | );
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/smart-contract/js/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "esModuleInterop": true,
5 | "forceConsistentCasingInFileNames": true,
6 | "skipLibCheck": true,
7 | "checkJs": true,
8 | "allowJs": true,
9 | "declaration": true,
10 | "declarationMap": true,
11 | "allowSyntheticDefaultImports": true
12 | },
13 | "files": ["./src/index.ts"]
14 | }
--------------------------------------------------------------------------------
/smart-contract/js/tsconfig.cjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base.json",
3 | "compilerOptions": {
4 | "lib": ["ES6", "DOM"],
5 | "target": "ES6",
6 | "module": "CommonJS",
7 | "moduleResolution": "Node",
8 | "outDir": "./dist/cjs",
9 | "declarationDir": "./dist/cjs/types"
10 | }
11 | }
--------------------------------------------------------------------------------
/smart-contract/js/tsconfig.esm.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base.json",
3 | "compilerOptions": {
4 | "lib": ["ES2015", "DOM"],
5 | "target": "ES2015",
6 | "module": "ESNext",
7 | "moduleResolution": "node",
8 | "outDir": "./dist/esm",
9 | "declarationDir": "./dist/esm/types"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/smart-contract/program/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | .DS_Store
3 | coverage
--------------------------------------------------------------------------------
/smart-contract/program/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "access-protocol"
3 | version = "0.7.15"
4 | edition = "2018"
5 | description = "Access Protocol"
6 | license = "GPL-3.0"
7 |
8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
9 |
10 | [features]
11 | no-entrypoint = []
12 | test-bpf = []
13 | no-mint-check = []
14 | days-to-sec-10s = []
15 | days-to-sec-15m = []
16 | no-bond-signer = []
17 | claim-bond-rewards-off = []
18 | v1-instructions-allowed = []
19 |
20 | [dependencies]
21 | borsh = "0.10.3"
22 | solana-program = "1.16.16"
23 | num_enum = "0.5.4"
24 | thiserror = "1.0.24"
25 | num-traits = "0.2"
26 | num-derive = "0.3"
27 | enumflags2 = "0.7.1"
28 | spl-token = {version="3.3.0", features= ["no-entrypoint"]}
29 | bonfida-utils = "0.4.0"
30 | spl-associated-token-account = {version = "1.0.5", features = ["no-entrypoint"]}
31 | bytemuck = {version = "1.7.2", features = ["derive"]}
32 | mpl-token-metadata = { version = "^1.11.0", features = ["no-entrypoint"] }
33 | spl-math = {version="0.1.0", features= ["no-entrypoint"]}
34 |
35 | [dev-dependencies]
36 | hexdump = "0.1.0"
37 | solana-sdk = "1.10.30"
38 | rand = "0.8.4"
39 | arrayref = "0.3.6"
40 | solana-client = "1.10.0"
41 | solana-program-test = "1.10.12"
42 | tokio = {version="1.6", features = ["macros"]}
43 | solana-test-framework = { git = "https://github.com/halbornteam/solana-test-framework"}
44 |
45 |
46 | [lib]
47 | crate-type = ["cdylib", "lib"]
48 |
49 | [profile.release]
50 | overflow-checks = true
51 |
--------------------------------------------------------------------------------
/smart-contract/program/Makefile:
--------------------------------------------------------------------------------
1 | test:
2 | cargo test-bpf --features no-mint-check no-bond-signer v1-instructions-allowed -- --nocapture --skip functional --skip devnet
3 | cargo test-bpf --features no-mint-check no-bond-signer v1-instructions-allowed days-to-sec-10s --test functional -- --nocapture
4 |
5 | test_devnet:
6 | cargo test --test devnet -- --nocapture
--------------------------------------------------------------------------------
/smart-contract/program/src/cpi.rs:
--------------------------------------------------------------------------------
1 | use solana_program::{
2 | account_info::AccountInfo, entrypoint::ProgramResult, program::invoke_signed, pubkey::Pubkey,
3 | rent::Rent, system_instruction::create_account, sysvar::Sysvar,
4 | };
5 |
6 | #[allow(missing_docs)]
7 | pub struct Cpi {}
8 |
9 | impl Cpi {
10 | #[allow(missing_docs)]
11 | pub fn create_account<'a>(
12 | program_id: &Pubkey,
13 | system_program: &AccountInfo<'a>,
14 | fee_payer: &AccountInfo<'a>,
15 | account_to_create: &AccountInfo<'a>,
16 | signer_seeds: &[&[u8]],
17 | space: usize,
18 | ) -> ProgramResult {
19 | let create_state_instruction = create_account(
20 | fee_payer.key,
21 | account_to_create.key,
22 | Rent::get()?.minimum_balance(space),
23 | space as u64,
24 | program_id,
25 | );
26 |
27 | invoke_signed(
28 | &create_state_instruction,
29 | &[
30 | system_program.clone(),
31 | fee_payer.clone(),
32 | account_to_create.clone(),
33 | ],
34 | &[signer_seeds],
35 | )
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/smart-contract/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 | #[derive(Clone, Debug, Error, FromPrimitive)]
6 | pub enum AccessError {
7 | #[error("This account is already initialized")]
8 | AlreadyInitialized,
9 | #[error("Data type mismatch")]
10 | DataTypeMismatch,
11 | #[error("Wrong system program key")]
12 | WrongSystemProgram,
13 | #[error("Wrong rent sysvar key")]
14 | WrongRent,
15 | #[error("Wrong account owner")]
16 | WrongOwner,
17 | #[error("Account not generated deterministically")]
18 | AccountNotDeterministic,
19 | #[error("The stake pool owner must sign")]
20 | StakePoolOwnerMustSign,
21 | #[error("Wrong stake pool owner")]
22 | WrongStakePoolOwner,
23 | #[error("The stake pool must be empty")]
24 | StakePoolMustBeEmpty,
25 | #[error("The stake account must be empty")]
26 | StakeAccountMustBeEmpty,
27 | #[error("The stake account owner must sign")]
28 | StakeAccountOwnerMustSign,
29 | #[error("Wrong SPL token program ID")]
30 | WrongSplTokenProgramId,
31 | #[error("Token account must be owned by SPL Token")]
32 | WrongTokenAccountOwner,
33 | #[error("Bond account must be owned by the program")]
34 | WrongBondAccountOwner,
35 | #[error("Stake account must be program owned")]
36 | WrongStakeAccountOwner,
37 | #[error("Stake pool account must be program owned")]
38 | WrongStakePoolAccountOwner,
39 | #[error("Stake account owner mismatch")]
40 | StakeAccountOwnerMismatch,
41 | #[error("Stake pool mismatch")]
42 | StakePoolMismatch,
43 | #[error("Stake pool vault mismatch")]
44 | StakePoolVaultMismatch,
45 | #[error("Wrong central state authority")]
46 | WrongCentralStateAuthority,
47 | #[error("The central state authority must sign")]
48 | CentralStateAuthorityMustSign,
49 | #[error("Overflow")]
50 | Overflow,
51 | #[error("Operation is a no-op")]
52 | NoOp,
53 | #[error("Wrong mint")]
54 | WrongMint,
55 | #[error("Wrong central vault")]
56 | WrongCentralVault,
57 | #[error("Wrong stake pool")]
58 | WrongStakePool,
59 | #[error("Unauthorized seller")]
60 | UnauthorizedSeller,
61 | #[error("Bond seller must sign")]
62 | BondSellerMustSign,
63 | #[error("Bond seller has already signed")]
64 | BondSellerAlreadySigner,
65 | #[error("The bond does not have enough sellers")]
66 | NotEnoughSellers,
67 | #[error("The bond buyer must sign")]
68 | BuyerMustSign,
69 | #[error("Wrong quote token destination")]
70 | WrongQuoteDestination,
71 | #[error("Rewards must be claimed first")]
72 | UnclaimedRewards,
73 | #[error("Unstake period not over")]
74 | CannotUnstake,
75 | #[error("Invalid unstake amount")]
76 | InvalidUnstakeAmount,
77 | #[error("Invalid amount")]
78 | InvalidAmount,
79 | #[error("Inactive stake pool not allowed")]
80 | InactiveStakePoolNotAllowed,
81 | #[error("Active stake pool not allowed")]
82 | ActiveStakePoolNotAllowed,
83 | #[error("Invalid tag change")]
84 | InvalidTagChange,
85 | #[error("Too many unstake requests")]
86 | TooManyUnstakeRequests,
87 | #[error("Pool must be cranked")]
88 | PoolMustBeCranked,
89 | #[error("Pending unstake request")]
90 | PendingUnstakeRequests,
91 | #[error("Cannot stake 0 token")]
92 | CannotStakeZero,
93 | #[error("Unlock period cannot be 0")]
94 | ForbiddenUnlockPeriodZero,
95 | #[error("Wrong MPL metadata program")]
96 | WrongMplProgram,
97 | #[error("Unsupported instruction")]
98 | UnsupportedInstruction,
99 | #[error("Deprecated instruction")]
100 | DeprecatedInstruction,
101 | #[error("Too many recipients")]
102 | TooManyRecipients,
103 | #[error("No recipients")]
104 | NoRecipients,
105 | #[error("Invalid percentages")]
106 | InvalidPercentages,
107 | #[error("Invalid token account")]
108 | InvalidTokenAccount,
109 | #[error("Nonzero balance")]
110 | NonzeroBallance,
111 | #[error("Delay too long")]
112 | DelayTooLong,
113 | #[error("Frozen instruction")]
114 | FrozenInstruction,
115 | #[error("Invalid renounce params")]
116 | InvalidRenounceParams,
117 | #[error("Already renounced")]
118 | AlreadyRenounced,
119 | #[error("Royalty account mismatch")]
120 | RoyaltyAccountMismatch,
121 | #[error("Royalty ata mismatch")]
122 | RoyaltyAtaMismatch,
123 | #[error("Wrong royalty account owner")]
124 | WrongRoyaltyAccountOwner,
125 | #[error("Owner must sign")]
126 | OwnerMustSign,
127 | #[error("Royalty ata not provided")]
128 | RoyaltyAtaNotProvided,
129 | #[error("Royalty ata not deterministic")]
130 | RoyaltyAtaNotDeterministic,
131 | #[error("Wrong sysvar instructions id")]
132 | WrongSysvarInstructionsId,
133 | #[error("Wrong Access cnft authority")]
134 | WrongAccessCnftAuthority,
135 | #[error("Access cnft authority must sign")]
136 | AccessCnftAuthorityMustSign,
137 | }
138 |
139 | impl From for ProgramError {
140 | fn from(e: AccessError) -> Self {
141 | ProgramError::Custom(e as u32)
142 | }
143 | }
144 |
145 | impl DecodeError for AccessError {
146 | fn type_of() -> &'static str {
147 | "AccessError"
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/smart-contract/program/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![warn(missing_docs)]
2 | /*!
3 | Access protocol
4 |
5 | ## Overview
6 |
7 | ## Central state
8 |
9 | The [`CentralState`][`state::CentralState`] contains the information about:
10 |
11 | - ACCESS token mint
12 | - ACCESS token inflation
13 |
14 | The inflation schedule can be modified by the `authority` key contained in the [`CentralState`][`state::CentralState`] by using the [`change_inflation`][`fn@instruction::change_inflation`] instruction.
15 |
16 | The [`CentralState`][`state::CentralState`] is the mint authority of the ACCESS token.
17 |
18 | ## Stake pool
19 |
20 | [`Stake pools`][`state::StakePool`] are created by content publishers. In order to get access to the publisher's content users need to stake ACCESS tokens in the [`StakePool`][`state::StakePool`] of the publisher.
21 |
22 | A [`Stake pools`][`state::StakePool`] is made of a header ([`StakePoolHeader`][`state::StakePoolHeader`]) and circular buffer that contains the pool balances multiplied by the current inflation at each crank time.
23 |
24 | The circular buffer is updated using a permissionless [`crank`][`fn@instruction::crank`].
25 |
26 |
27 | ## Stake accounts
28 |
29 | [`Stake accounts`][`state::StakeAccount`] are used to deposit funds in a stake pool. Stake accounts allow users to access the content of the publisher and earn yield in ACCESS tokens at the same time.
30 |
31 | ## Bonds
32 |
33 | [`Bonds`][`state::Bonds`] represent locked ACCESS tokens sold by the ACCESS DAO. The lifecycle of a bond is as follow:
34 |
35 | - [`create_bond`][`fn@instruction::create_bond`]: This instruction creates an inactive bond. The bond account contains the information about the price of the bond, the buyer, the unlock schedule and the sellers.
36 | - [`sign_bond`][`fn@instruction::sign_bond`]: This instruction allows DAO members to approve the sell.
37 | - [`claim_bond`][`fn@instruction::claim_bond`]: Once the bond has been signed by enough DAO members, the buyer can claim the bond.
38 |
39 | Bond tokens can be staked like regular ACCESS tokens.
40 |
41 | */
42 |
43 | use solana_program::declare_id;
44 | #[doc(hidden)]
45 | pub mod entrypoint;
46 | #[doc(hidden)]
47 | pub mod error;
48 | /// Program instructions and their CPI-compatible bindings
49 | pub mod instruction;
50 | /// Describes the different data structres that the program uses to encode state
51 | pub mod state;
52 |
53 | #[doc(hidden)]
54 | pub(crate) mod processor;
55 | pub mod utils;
56 |
57 | #[allow(missing_docs)]
58 | pub mod cpi;
59 |
60 | declare_id!("6HW8dXjtiTGkD4jzXs7igdFmZExPpmwUrRN5195xGup");
61 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/activate_stake_pool.rs:
--------------------------------------------------------------------------------
1 | //! Activate a stake pool
2 | use bonfida_utils::{BorshSize, InstructionsAccount};
3 | use borsh::{BorshDeserialize, BorshSerialize};
4 | use solana_program::{
5 | account_info::{AccountInfo, next_account_info},
6 | entrypoint::ProgramResult,
7 | program_error::ProgramError,
8 | pubkey::Pubkey,
9 | };
10 |
11 | use crate::error::AccessError;
12 | use crate::instruction::ProgramInstruction::ActivateStakePool;
13 | use crate::state::{CentralStateV2, StakePool, Tag};
14 | use crate::utils::{check_account_owner};
15 |
16 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
17 | pub struct Params {}
18 |
19 | #[derive(InstructionsAccount)]
20 | pub struct Accounts<'a, T> {
21 | /// The stake pool to activate
22 | #[cons(writable)]
23 | pub stake_pool: &'a T,
24 |
25 | /// The central state account
26 | pub central_state: &'a T,
27 | }
28 |
29 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
30 | pub fn parse(
31 | accounts: &'a [AccountInfo<'b>],
32 | program_id: &Pubkey,
33 | ) -> Result {
34 | let accounts_iter = &mut accounts.iter();
35 | let accounts = Accounts {
36 | stake_pool: next_account_info(accounts_iter)?,
37 | central_state: next_account_info(accounts_iter)?,
38 | };
39 |
40 | // Check ownership
41 | check_account_owner(accounts.stake_pool, program_id, AccessError::WrongOwner)?;
42 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
43 |
44 | Ok(accounts)
45 | }
46 | }
47 |
48 | pub fn process_activate_stake_pool(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
49 | let accounts = Accounts::parse(accounts, program_id)?;
50 |
51 | let mut stake_pool = StakePool::get_checked(accounts.stake_pool, vec![Tag::InactiveStakePool])?;
52 | let central_state = CentralStateV2::from_account_info(accounts.central_state)?;
53 | central_state.assert_instruction_allowed(&ActivateStakePool)?;
54 |
55 | if stake_pool.header.tag != Tag::InactiveStakePool as u8 {
56 | return Err(AccessError::ActiveStakePoolNotAllowed.into());
57 | }
58 |
59 | stake_pool.header.tag = Tag::StakePool as u8;
60 | stake_pool.header.last_claimed_offset = central_state.last_snapshot_offset;
61 | if central_state.last_snapshot_offset > u16::MAX as u64 {
62 | return Err(AccessError::Overflow.into());
63 | }
64 | stake_pool.header.current_day_idx = central_state.last_snapshot_offset as u16;
65 |
66 | Ok(())
67 | }
68 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/admin_change_freeze_authority.rs:
--------------------------------------------------------------------------------
1 | //! Change freeze authority
2 | use borsh::{BorshDeserialize, BorshSerialize};
3 | use solana_program::{
4 | account_info::{next_account_info, AccountInfo},
5 | entrypoint::ProgramResult,
6 | program_error::ProgramError,
7 | pubkey::Pubkey,
8 | };
9 |
10 | use crate::{error::AccessError};
11 | use bonfida_utils::{BorshSize, InstructionsAccount};
12 | use crate::instruction::ProgramInstruction::AdminChangeFreezeAuthority;
13 |
14 | use crate::utils::{check_account_key, check_account_owner, check_signer};
15 | use crate::state:: CentralStateV2;
16 |
17 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
18 | /// The required parameters for the `change_freeze_authority` instruction
19 | pub struct Params {
20 | /// The new freeze authority
21 | pub new_freeze_authority: Pubkey,
22 | }
23 |
24 | #[derive(InstructionsAccount)]
25 | /// The required accounts for the `change_freeze_authority` instruction
26 | pub struct Accounts<'a, T> {
27 | /// The central state account
28 | #[cons(writable)]
29 | pub central_state: &'a T,
30 |
31 | /// The central state account authority
32 | #[cons(signer)]
33 | pub authority: &'a T,
34 | }
35 |
36 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
37 | pub fn parse(
38 | accounts: &'a [AccountInfo<'b>],
39 | program_id: &Pubkey,
40 | ) -> Result {
41 | let accounts_iter = &mut accounts.iter();
42 | let accounts = Accounts {
43 | central_state: next_account_info(accounts_iter)?,
44 | authority: next_account_info(accounts_iter)?,
45 | };
46 |
47 | // Check ownership
48 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
49 |
50 | // Check signer
51 | check_signer(
52 | accounts.authority,
53 | AccessError::CentralStateAuthorityMustSign,
54 | )?;
55 |
56 | Ok(accounts)
57 | }
58 | }
59 |
60 | pub fn process_admin_change_freeze_authority(
61 | program_id: &Pubkey,
62 | accounts: &[AccountInfo],
63 | params: Params,
64 | ) -> ProgramResult {
65 | let accounts = Accounts::parse(accounts, program_id)?;
66 |
67 | let mut central_state = CentralStateV2::from_account_info(accounts.central_state)?;
68 | central_state.assert_instruction_allowed(&AdminChangeFreezeAuthority)?;
69 |
70 | check_account_key(
71 | accounts.authority,
72 | ¢ral_state.authority,
73 | AccessError::WrongCentralStateAuthority,
74 | )?;
75 |
76 | central_state.freeze_authority = params.new_freeze_authority;
77 | central_state.save(&mut accounts.central_state.data.borrow_mut())?;
78 |
79 | Ok(())
80 | }
81 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/admin_freeze.rs:
--------------------------------------------------------------------------------
1 | //! Freeze and unfreeze a program account
2 | //! This admin instruction can be dangereous 💀
3 | use bonfida_utils::{BorshSize, InstructionsAccount};
4 | use borsh::{BorshDeserialize, BorshSerialize};
5 | use num_traits::FromPrimitive;
6 | use solana_program::{
7 | account_info::{AccountInfo, next_account_info},
8 | entrypoint::ProgramResult,
9 | program_error::ProgramError,
10 | pubkey::Pubkey,
11 | };
12 |
13 | use crate::error::AccessError;
14 | use crate::instruction::ProgramInstruction::AdminFreeze;
15 | use crate::state::{CentralStateV2, Tag, V1_INSTRUCTIONS_ALLOWED};
16 | use crate::utils::{check_account_key, check_account_owner, check_signer};
17 |
18 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
19 | pub struct Params {}
20 |
21 | #[derive(InstructionsAccount)]
22 | pub struct Accounts<'a, T> {
23 | /// The central state authority
24 | #[cons(signer)]
25 | pub authority: &'a T,
26 |
27 | /// The account to freeze (or unfreeze)
28 | #[cons(writable)]
29 | pub account_to_freeze: &'a T,
30 |
31 | /// The central state account
32 | pub central_state: &'a T,
33 | }
34 |
35 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
36 | pub fn parse(
37 | accounts: &'a [AccountInfo<'b>],
38 | program_id: &Pubkey,
39 | ) -> Result {
40 | let accounts_iter = &mut accounts.iter();
41 | let accounts = Accounts {
42 | authority: next_account_info(accounts_iter)?,
43 | account_to_freeze: next_account_info(accounts_iter)?,
44 | central_state: next_account_info(accounts_iter)?,
45 | };
46 |
47 | // Check ownership
48 | check_account_owner(
49 | accounts.account_to_freeze,
50 | program_id,
51 | AccessError::WrongOwner,
52 | )?;
53 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
54 |
55 | // Check signer
56 | check_signer(
57 | accounts.authority,
58 | AccessError::CentralStateAuthorityMustSign,
59 | )?;
60 |
61 | Ok(accounts)
62 | }
63 | }
64 |
65 | pub fn process_admin_freeze(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
66 | if !V1_INSTRUCTIONS_ALLOWED {
67 | return Err(AccessError::DeprecatedInstruction.into());
68 | }
69 |
70 | let accounts = Accounts::parse(accounts, program_id)?;
71 |
72 | let central_state = CentralStateV2::from_account_info(accounts.central_state)?;
73 | central_state.assert_instruction_allowed(&AdminFreeze)?;
74 |
75 | check_account_key(
76 | accounts.authority,
77 | ¢ral_state.authority,
78 | AccessError::WrongCentralStateAuthority,
79 | )?;
80 |
81 | let mut data = accounts.account_to_freeze.data.borrow_mut();
82 |
83 | let current_tag = Tag::from_u8(data[0]).ok_or(ProgramError::InvalidAccountData)?;
84 | let new_tag = Tag::opposite(¤t_tag)?;
85 |
86 | data[0] = new_tag as u8;
87 |
88 | Ok(())
89 | }
90 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/admin_mint.rs:
--------------------------------------------------------------------------------
1 | //! Allows central state authority to mint ACCESS tokens
2 | use bonfida_utils::{BorshSize, InstructionsAccount};
3 | use borsh::{BorshDeserialize, BorshSerialize};
4 | use solana_program::{
5 | account_info::{AccountInfo, next_account_info},
6 | entrypoint::ProgramResult,
7 | program::invoke_signed,
8 | program_error::ProgramError,
9 | pubkey::Pubkey,
10 | };
11 |
12 | use crate::error::AccessError;
13 | use crate::instruction::ProgramInstruction::AdminMint;
14 | use crate::state::{CentralStateV2, V1_INSTRUCTIONS_ALLOWED};
15 | use crate::utils::{check_account_key, check_account_owner, check_signer};
16 |
17 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
18 | pub struct Params {
19 | /// The amount to be minted
20 | pub amount: u64,
21 | }
22 |
23 | #[derive(InstructionsAccount)]
24 | pub struct Accounts<'a, T> {
25 | /// The central state authority
26 | #[cons(signer)]
27 | pub authority: &'a T,
28 |
29 | /// The ACCESS mint token
30 | #[cons(writable)]
31 | pub mint: &'a T,
32 |
33 | /// The ACCESS token destination
34 | #[cons(writable)]
35 | pub access_token_destination: &'a T,
36 |
37 | /// The central state account
38 | pub central_state: &'a T,
39 |
40 | /// The SPL token program account
41 | pub spl_token_program: &'a T,
42 | }
43 |
44 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
45 | pub fn parse(
46 | accounts: &'a [AccountInfo<'b>],
47 | program_id: &Pubkey,
48 | ) -> Result {
49 | let accounts_iter = &mut accounts.iter();
50 | let accounts = Accounts {
51 | authority: next_account_info(accounts_iter)?,
52 | mint: next_account_info(accounts_iter)?,
53 | access_token_destination: next_account_info(accounts_iter)?,
54 | central_state: next_account_info(accounts_iter)?,
55 | spl_token_program: next_account_info(accounts_iter)?,
56 | };
57 |
58 | // Check keys
59 | check_account_key(
60 | accounts.spl_token_program,
61 | &spl_token::ID,
62 | AccessError::WrongSplTokenProgramId,
63 | )?;
64 |
65 | // Check ownership
66 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
67 |
68 | // Check signer
69 | check_signer(
70 | accounts.authority,
71 | AccessError::CentralStateAuthorityMustSign,
72 | )?;
73 |
74 | Ok(accounts)
75 | }
76 | }
77 |
78 | pub fn process_admin_mint(
79 | program_id: &Pubkey,
80 | accounts: &[AccountInfo],
81 | params: Params,
82 | ) -> ProgramResult {
83 | if !V1_INSTRUCTIONS_ALLOWED {
84 | return Err(AccessError::DeprecatedInstruction.into());
85 | }
86 |
87 | let accounts = Accounts::parse(accounts, program_id)?;
88 | let central_state = CentralStateV2::from_account_info(accounts.central_state)?;
89 | central_state.assert_instruction_allowed(&AdminMint)?;
90 |
91 | check_account_key(
92 | accounts.mint,
93 | ¢ral_state.token_mint,
94 | AccessError::WrongMint,
95 | )?;
96 | check_account_key(
97 | accounts.authority,
98 | ¢ral_state.authority,
99 | AccessError::WrongCentralStateAuthority,
100 | )?;
101 |
102 | // Transfer tokens
103 | let mint_ix = spl_token::instruction::mint_to(
104 | &spl_token::ID,
105 | accounts.mint.key,
106 | accounts.access_token_destination.key,
107 | accounts.central_state.key,
108 | &[],
109 | params.amount,
110 | )?;
111 | invoke_signed(
112 | &mint_ix,
113 | &[
114 | accounts.spl_token_program.clone(),
115 | accounts.central_state.clone(),
116 | accounts.mint.clone(),
117 | accounts.access_token_destination.clone(),
118 | ],
119 | &[&[&program_id.to_bytes(), &[central_state.bump_seed]]],
120 | )?;
121 |
122 | Ok(())
123 | }
124 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/admin_program_freeze.rs:
--------------------------------------------------------------------------------
1 | //! Admin program freeze instruction.
2 | use bonfida_utils::{BorshSize, InstructionsAccount};
3 | use borsh::{BorshDeserialize, BorshSerialize};
4 | use solana_program::{account_info::{AccountInfo, next_account_info}, entrypoint::ProgramResult, program_error::ProgramError, pubkey::Pubkey};
5 |
6 | use crate::error::AccessError;
7 | use crate::instruction::ProgramInstruction;
8 | use crate::state::CentralStateV2;
9 | use crate::utils::{check_account_owner, check_signer};
10 |
11 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
12 | /// The required parameters for the `admin_program_freeze` instruction
13 | pub struct Params {
14 | /// The new ix gate
15 | pub ix_gate: u128,
16 | }
17 |
18 | #[derive(InstructionsAccount)]
19 | /// The required accounts for the `admin_program_freeze` instruction
20 | pub struct Accounts<'a, T> {
21 | /// The central state account
22 | #[cons(writable)]
23 | pub central_state: &'a T,
24 |
25 | /// The central state account authority or freeze authority
26 | #[cons(signer)]
27 | pub authority: &'a T,
28 | }
29 |
30 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
31 | pub fn parse(
32 | accounts: &'a [AccountInfo<'b>],
33 | program_id: &Pubkey,
34 | ) -> Result {
35 | let accounts_iter = &mut accounts.iter();
36 | let accounts = Accounts {
37 | central_state: next_account_info(accounts_iter)?,
38 | authority: next_account_info(accounts_iter)?,
39 | };
40 |
41 | // Check ownership
42 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
43 |
44 | // Check signer
45 | check_signer(
46 | accounts.authority,
47 | AccessError::CentralStateAuthorityMustSign,
48 | )?;
49 |
50 | Ok(accounts)
51 | }
52 | }
53 |
54 | pub fn process_admin_program_freeze(
55 | program_id: &Pubkey,
56 | accounts: &[AccountInfo],
57 | params: Params,
58 | ) -> ProgramResult {
59 | let Params { ix_gate } = params;
60 | let accounts = Accounts::parse(accounts, program_id)?;
61 |
62 | let mut central_state = CentralStateV2::from_account_info(accounts.central_state)?;
63 | central_state.assert_instruction_allowed(&ProgramInstruction::AdminProgramFreeze)?;
64 |
65 | // Only central state authority can unfreeze, the freeze authority is only allowed to freeze everything
66 | if accounts.authority.key != ¢ral_state.authority && (accounts.authority.key != ¢ral_state.freeze_authority || ix_gate > 0) {
67 | return Err(AccessError::WrongCentralStateAuthority.into());
68 | }
69 |
70 | central_state.ix_gate = ix_gate;
71 | central_state.save(&mut accounts.central_state.data.borrow_mut())?;
72 |
73 | Ok(())
74 | }
75 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/admin_renounce.rs:
--------------------------------------------------------------------------------
1 | //! Admin renounce functionality
2 | use bonfida_utils::{BorshSize, InstructionsAccount};
3 | use borsh::{BorshDeserialize, BorshSerialize};
4 |
5 |
6 | use solana_program::{account_info::{AccountInfo, next_account_info}, entrypoint::ProgramResult, program_error::ProgramError, pubkey::Pubkey};
7 |
8 | use crate::error::AccessError;
9 | use crate::instruction::ProgramInstruction;
10 | use crate::instruction::ProgramInstruction::AdminRenounce;
11 | use crate::state::CentralStateV2;
12 | use crate::utils::{check_account_key, check_account_owner, check_signer, get_freeze_mask, is_admin_renouncable_instruction};
13 |
14 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
15 | /// The required parameters for the `admin_renounce` instruction
16 | pub struct Params {
17 | /// The instruction to be renounced
18 | pub ix: ProgramInstruction,
19 | }
20 |
21 | #[derive(InstructionsAccount)]
22 | /// The required accounts for the `admin_renounce` instruction
23 | pub struct Accounts<'a, T> {
24 | /// The central state account
25 | #[cons(writable)]
26 | pub central_state: &'a T,
27 |
28 | /// The central state account authority
29 | #[cons(signer)]
30 | pub authority: &'a T,
31 | }
32 |
33 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
34 | pub fn parse(
35 | accounts: &'a [AccountInfo<'b>],
36 | program_id: &Pubkey,
37 | ) -> Result {
38 | let accounts_iter = &mut accounts.iter();
39 | let accounts = Accounts {
40 | central_state: next_account_info(accounts_iter)?,
41 | authority: next_account_info(accounts_iter)?,
42 | };
43 |
44 | // Check ownership
45 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
46 |
47 | // Check signer
48 | check_signer(
49 | accounts.authority,
50 | AccessError::CentralStateAuthorityMustSign,
51 | )?;
52 |
53 | Ok(accounts)
54 | }
55 | }
56 |
57 | pub fn process_admin_renounce(
58 | program_id: &Pubkey,
59 | accounts: &[AccountInfo],
60 | params: Params,
61 | ) -> ProgramResult {
62 | let Params { ix } = params;
63 | let accounts = Accounts::parse(accounts, program_id)?;
64 |
65 | let mut central_state = CentralStateV2::from_account_info(accounts.central_state)?;
66 | central_state.assert_instruction_allowed(&AdminRenounce)?;
67 | check_account_key(
68 | accounts.authority,
69 | ¢ral_state.authority,
70 | AccessError::WrongCentralStateAuthority,
71 | )?;
72 |
73 | if !is_admin_renouncable_instruction(&ix) {
74 | return Err(AccessError::InvalidRenounceParams.into());
75 | }
76 |
77 | let renounce_mask = get_freeze_mask(vec![ix]);
78 | let admin_ix_gate = central_state.admin_ix_gate & renounce_mask;
79 |
80 | if admin_ix_gate == central_state.admin_ix_gate {
81 | return Err(AccessError::AlreadyRenounced.into());
82 | }
83 |
84 | central_state.admin_ix_gate = admin_ix_gate;
85 | central_state.save(&mut accounts.central_state.data.borrow_mut())?;
86 |
87 | Ok(())
88 | }
89 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/admin_set_protocol_fee.rs:
--------------------------------------------------------------------------------
1 | //! Admin set protocol fee
2 | use bonfida_utils::{BorshSize, InstructionsAccount};
3 | use borsh::{BorshDeserialize, BorshSerialize};
4 | use solana_program::{account_info::{AccountInfo, next_account_info}, entrypoint::ProgramResult, program_error::ProgramError, pubkey::Pubkey};
5 |
6 | use crate::error::AccessError;
7 | use crate::instruction::ProgramInstruction::AdminSetProtocolFee;
8 | use crate::state::CentralStateV2;
9 | use crate::utils::{check_account_key, check_account_owner, check_signer};
10 |
11 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
12 | /// The required parameters for the `admin_set_protocol_fee` instruction
13 | pub struct Params {
14 | // The new protocol fee basis points
15 | pub protocol_fee_basis_points: u16,
16 | }
17 |
18 | #[derive(InstructionsAccount)]
19 | /// The required accounts for the `admin_set_protocol_fee` instruction
20 | pub struct Accounts<'a, T> {
21 | /// The central state authority
22 | #[cons(signer)]
23 | pub authority: &'a T,
24 |
25 | /// The central state account
26 | #[cons(writable)]
27 | pub central_state: &'a T,
28 | }
29 |
30 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
31 | pub fn parse(
32 | accounts: &'a [AccountInfo<'b>],
33 | program_id: &Pubkey,
34 | ) -> Result {
35 | let accounts_iter = &mut accounts.iter();
36 | let accounts = Accounts {
37 | authority: next_account_info(accounts_iter)?,
38 | central_state: next_account_info(accounts_iter)?,
39 | };
40 |
41 | // Check ownership
42 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
43 |
44 | // Check signer
45 | check_signer(
46 | accounts.authority,
47 | AccessError::CentralStateAuthorityMustSign,
48 | )?;
49 |
50 | Ok(accounts)
51 | }
52 | }
53 |
54 | pub fn process_admin_set_protocol_fee(
55 | program_id: &Pubkey,
56 | accounts: &[AccountInfo],
57 | params: Params,
58 | ) -> ProgramResult {
59 | let Params { protocol_fee_basis_points } = params;
60 | let accounts = Accounts::parse(accounts, program_id)?;
61 |
62 | let mut central_state = CentralStateV2::from_account_info(accounts.central_state)?;
63 | central_state.assert_instruction_allowed(&AdminSetProtocolFee)?;
64 |
65 | check_account_key(
66 | accounts.authority,
67 | ¢ral_state.authority,
68 | AccessError::WrongCentralStateAuthority,
69 | )?;
70 |
71 | // Even though the protocol fee is added to the transaction (so we could do more than 100%),
72 | // we don't want to allow this.
73 | if protocol_fee_basis_points > 10000 {
74 | return Err(AccessError::InvalidAmount.into());
75 | }
76 |
77 | central_state.fee_basis_points = protocol_fee_basis_points;
78 | central_state.save(&mut accounts.central_state.data.borrow_mut())?;
79 |
80 | Ok(())
81 | }
82 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/admin_setup_fee_split.rs:
--------------------------------------------------------------------------------
1 | //! Setup fee split
2 |
3 | use bonfida_utils::{BorshSize, InstructionsAccount};
4 | use borsh::{BorshDeserialize, BorshSerialize};
5 | use solana_program::{
6 | account_info::{AccountInfo, next_account_info},
7 | entrypoint::ProgramResult,
8 | msg,
9 | program_error::ProgramError,
10 | pubkey::Pubkey,
11 | };
12 | use solana_program::clock::Clock;
13 | use solana_program::sysvar::Sysvar;
14 |
15 | use crate::error::AccessError;
16 | use crate::instruction::ProgramInstruction::AdminSetupFeeSplit;
17 | use crate::state::{
18 | FeeRecipient, MAX_FEE_RECIPIENTS, MAX_FEE_SPLIT_SETUP_DELAY,
19 | };
20 | use crate::state::CentralStateV2;
21 | use crate::utils::{check_account_key, check_account_owner, check_signer};
22 |
23 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
24 | /// The required parameters for the `admin_setup_fee_split` instruction
25 | pub struct Params {
26 | pub recipients: Vec,
27 | }
28 |
29 | #[derive(InstructionsAccount)]
30 | /// The required accounts for the `admin_setup_fee_split` instruction
31 | pub struct Accounts<'a, T> {
32 | /// The central state authority
33 | #[cons(signer)]
34 | pub authority: &'a T,
35 |
36 | /// The central state account
37 | #[cons(writable)]
38 | pub central_state: &'a T,
39 | }
40 |
41 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
42 | pub fn parse(
43 | accounts: &'a [AccountInfo<'b>],
44 | program_id: &Pubkey,
45 | ) -> Result {
46 | let accounts_iter = &mut accounts.iter();
47 | let accounts = Accounts {
48 | authority: next_account_info(accounts_iter)?,
49 | central_state: next_account_info(accounts_iter)?,
50 | };
51 |
52 | // Check ownership
53 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
54 |
55 | // Check signer
56 | check_signer(
57 | accounts.authority,
58 | AccessError::CentralStateAuthorityMustSign,
59 | )?;
60 |
61 | Ok(accounts)
62 | }
63 | }
64 |
65 | pub fn process_admin_setup_fee_split(
66 | program_id: &Pubkey,
67 | accounts: &[AccountInfo],
68 | params: Params,
69 | ) -> ProgramResult {
70 | let Params { recipients } = params;
71 | let accounts = Accounts::parse(accounts, program_id)?;
72 | let mut central_state = CentralStateV2::from_account_info(accounts.central_state)?;
73 | central_state.assert_instruction_allowed(&AdminSetupFeeSplit)?;
74 |
75 | check_account_key(
76 | accounts.authority,
77 | ¢ral_state.authority,
78 | AccessError::WrongCentralStateAuthority,
79 | )?;
80 |
81 | // Check if right number of recipients
82 | if recipients.len() > MAX_FEE_RECIPIENTS {
83 | msg!("Too many recipients");
84 | return Err(AccessError::TooManyRecipients.into());
85 | }
86 |
87 | // Check recipients
88 | let mut percentage_sum: u64 = 0;
89 | recipients.iter().try_for_each(|r| -> ProgramResult {
90 | if r.percentage == 0 {
91 | msg!("Recipient percentage 0 not allowed");
92 | return Err(AccessError::InvalidPercentages.into());
93 | }
94 | percentage_sum = percentage_sum
95 | .checked_add(r.percentage)
96 | .ok_or(AccessError::Overflow)?;
97 | if percentage_sum > 100 {
98 | msg!("Percentages add up to more than 100");
99 | return Err(AccessError::InvalidPercentages.into());
100 | }
101 | Ok(())
102 | })?;
103 |
104 | // The recipient setup must be done within 5 minutes after the fee distribution
105 | let current_time = Clock::get()?.unix_timestamp as u64;
106 | if current_time - central_state.last_fee_distribution_time as u64 > MAX_FEE_SPLIT_SETUP_DELAY {
107 | msg!("Delay between fee distribution and fee split setup too long");
108 | return Err(AccessError::DelayTooLong.into());
109 | }
110 |
111 | central_state.recipients = recipients;
112 |
113 | // replace the recipients
114 | central_state.save(&mut accounts.central_state.data.borrow_mut())?;
115 | Ok(())
116 | }
117 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/change_central_state_authority.rs:
--------------------------------------------------------------------------------
1 | //! Change central state authority
2 | use borsh::{BorshDeserialize, BorshSerialize};
3 | use solana_program::{
4 | account_info::{next_account_info, AccountInfo},
5 | entrypoint::ProgramResult,
6 | program_error::ProgramError,
7 | pubkey::Pubkey,
8 | };
9 |
10 | use crate::{error::AccessError};
11 | use bonfida_utils::{BorshSize, InstructionsAccount};
12 | use crate::instruction::ProgramInstruction::ChangeCentralStateAuthority;
13 |
14 | use crate::utils::{check_account_key, check_account_owner, check_signer};
15 | use crate::state:: CentralStateV2;
16 |
17 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
18 | /// The required parameters for the `change_central_state_authority` instruction
19 | pub struct Params {
20 | // The new central state authority
21 | pub new_authority: Pubkey,
22 | }
23 |
24 | #[derive(InstructionsAccount)]
25 | /// The required accounts for the `change_central_state_authority` instruction
26 | pub struct Accounts<'a, T> {
27 | /// The central state account
28 | #[cons(writable)]
29 | pub central_state: &'a T,
30 |
31 | /// The central state account authority
32 | #[cons(signer)]
33 | pub authority: &'a T,
34 | }
35 |
36 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
37 | pub fn parse(
38 | accounts: &'a [AccountInfo<'b>],
39 | program_id: &Pubkey,
40 | ) -> Result {
41 | let accounts_iter = &mut accounts.iter();
42 | let accounts = Accounts {
43 | central_state: next_account_info(accounts_iter)?,
44 | authority: next_account_info(accounts_iter)?,
45 | };
46 |
47 | // Check ownership
48 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
49 |
50 | // Check signer
51 | check_signer(
52 | accounts.authority,
53 | AccessError::CentralStateAuthorityMustSign,
54 | )?;
55 |
56 | Ok(accounts)
57 | }
58 | }
59 |
60 | pub fn process_change_central_state_auth(
61 | program_id: &Pubkey,
62 | accounts: &[AccountInfo],
63 | params: Params,
64 | ) -> ProgramResult {
65 | let accounts = Accounts::parse(accounts, program_id)?;
66 |
67 | let mut central_state = CentralStateV2::from_account_info(accounts.central_state)?;
68 | central_state.assert_instruction_allowed(&ChangeCentralStateAuthority)?;
69 |
70 | check_account_key(
71 | accounts.authority,
72 | ¢ral_state.authority,
73 | AccessError::WrongCentralStateAuthority,
74 | )?;
75 |
76 | central_state.authority = params.new_authority;
77 | central_state.save(&mut accounts.central_state.data.borrow_mut())?;
78 |
79 | Ok(())
80 | }
81 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/change_inflation.rs:
--------------------------------------------------------------------------------
1 | //! Change central state inflation
2 | use bonfida_utils::{BorshSize, InstructionsAccount};
3 | use borsh::{BorshDeserialize, BorshSerialize};
4 | use solana_program::{account_info::{AccountInfo, next_account_info}, entrypoint::ProgramResult, msg, program_error::ProgramError, pubkey::Pubkey};
5 | use solana_program::program_pack::Pack;
6 |
7 | use crate::{error::AccessError};
8 | use crate::instruction::ProgramInstruction::ChangeInflation;
9 | use crate::utils::{check_account_key, check_account_owner, check_signer};
10 | use crate::state:: CentralStateV2;
11 |
12 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
13 | /// The required parameters for the `change_inflation` instruction
14 | pub struct Params {
15 | // The new daily inflation token amount
16 | pub daily_inflation: u64,
17 | }
18 |
19 | #[derive(InstructionsAccount)]
20 | /// The required accounts for the `change_inflation` instruction
21 | pub struct Accounts<'a, T> {
22 | /// The central state account
23 | #[cons(writable)]
24 | pub central_state: &'a T,
25 |
26 | /// The central state account authority
27 | #[cons(signer)]
28 | pub authority: &'a T,
29 |
30 | /// The mint address of the ACCESS token
31 | pub mint: &'a T,
32 | }
33 |
34 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
35 | pub fn parse(
36 | accounts: &'a [AccountInfo<'b>],
37 | program_id: &Pubkey,
38 | ) -> Result {
39 | let accounts_iter = &mut accounts.iter();
40 | let accounts = Accounts {
41 | central_state: next_account_info(accounts_iter)?,
42 | authority: next_account_info(accounts_iter)?,
43 | mint: next_account_info(accounts_iter)?,
44 | };
45 |
46 | // Check ownership
47 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
48 | check_account_owner(accounts.mint, &spl_token::ID, AccessError::WrongOwner)?;
49 |
50 | // Check signer
51 | check_signer(
52 | accounts.authority,
53 | AccessError::CentralStateAuthorityMustSign,
54 | )?;
55 |
56 | Ok(accounts)
57 | }
58 | }
59 |
60 | pub fn process_change_inflation(
61 | program_id: &Pubkey,
62 | accounts: &[AccountInfo],
63 | params: Params,
64 | ) -> ProgramResult {
65 | let accounts = Accounts::parse(accounts, program_id)?;
66 |
67 | let mut central_state = CentralStateV2::from_account_info(accounts.central_state)?;
68 | central_state.assert_instruction_allowed(&ChangeInflation)?;
69 |
70 | check_account_key(
71 | accounts.mint,
72 | ¢ral_state.token_mint,
73 | AccessError::WrongMint,
74 | )?;
75 |
76 | let current_offset = central_state.get_current_offset()?;
77 | // check if we need to do a system wide snapshot
78 | if central_state.last_snapshot_offset < current_offset {
79 | msg!("System snapshot out of date, crank needed");
80 | return Err(AccessError::PoolMustBeCranked.into());
81 | }
82 |
83 | let token_mint = spl_token::state::Mint::unpack_from_slice(&accounts.mint.data.clone().borrow_mut())?;
84 |
85 | let supply = token_mint.supply;
86 | let annual_inflation = params.daily_inflation * 365;
87 | if annual_inflation > supply {
88 | msg!("Inflation is too high, maximum annual {}, requested {}", supply, annual_inflation);
89 | return Err(AccessError::InvalidAmount.into());
90 | }
91 |
92 | check_account_key(
93 | accounts.authority,
94 | ¢ral_state.authority,
95 | AccessError::WrongCentralStateAuthority,
96 | )?;
97 |
98 | central_state.daily_inflation = params.daily_inflation;
99 | central_state.save(&mut accounts.central_state.data.borrow_mut())?;
100 |
101 | Ok(())
102 | }
103 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/change_pool_minimum.rs:
--------------------------------------------------------------------------------
1 | //! Change the minimum stakeable amount of a pool
2 | //! This instruction allows a pool owner to adjust the price of its subscription for new joiners without impacting people who already subscribed
3 | use borsh::{BorshDeserialize, BorshSerialize};
4 | use solana_program::{
5 | account_info::{next_account_info, AccountInfo},
6 | entrypoint::ProgramResult,
7 | program_error::ProgramError,
8 | pubkey::Pubkey,
9 | };
10 |
11 | use crate::state::StakePool;
12 | use crate::{error::AccessError, state::Tag};
13 | use bonfida_utils::{BorshSize, InstructionsAccount};
14 | use crate::instruction::ProgramInstruction::ChangePoolMinimum;
15 |
16 | use crate::utils::{check_account_key, check_account_owner, check_signer};
17 | use crate::state:: CentralStateV2;
18 |
19 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
20 | /// The required parameters for the `change_pool_minimum` instruction
21 | pub struct Params {
22 | pub new_minimum: u64,
23 | }
24 |
25 | #[derive(InstructionsAccount)]
26 | /// The required accounts for the `change_pool_minimum` instruction
27 | pub struct Accounts<'a, T> {
28 | /// The stake pool account
29 | #[cons(writable)]
30 | pub stake_pool: &'a T,
31 |
32 | /// The stake pool owner account
33 | #[cons(signer)]
34 | pub stake_pool_owner: &'a T,
35 |
36 | /// The central state account
37 | pub central_state: &'a T,
38 | }
39 |
40 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
41 | pub fn parse(
42 | accounts: &'a [AccountInfo<'b>],
43 | program_id: &Pubkey,
44 | ) -> Result {
45 | let accounts_iter = &mut accounts.iter();
46 | let accounts = Accounts {
47 | stake_pool: next_account_info(accounts_iter)?,
48 | stake_pool_owner: next_account_info(accounts_iter)?,
49 | central_state: next_account_info(accounts_iter)?,
50 | };
51 |
52 | // Check keys
53 |
54 | // Check ownership
55 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
56 | check_account_owner(
57 | accounts.stake_pool,
58 | program_id,
59 | AccessError::WrongStakePoolAccountOwner,
60 | )?;
61 |
62 | // Check signer
63 | check_signer(
64 | accounts.stake_pool_owner,
65 | AccessError::StakePoolOwnerMustSign,
66 | )?;
67 |
68 | Ok(accounts)
69 | }
70 | }
71 |
72 | pub fn process_change_pool_minimum(
73 | program_id: &Pubkey,
74 | accounts: &[AccountInfo],
75 | params: Params,
76 | ) -> ProgramResult {
77 | let Params { new_minimum } = params;
78 | let accounts = Accounts::parse(accounts, program_id)?;
79 | let central_state = CentralStateV2::from_account_info(accounts.central_state)?;
80 | central_state.assert_instruction_allowed(&ChangePoolMinimum)?;
81 |
82 | let mut stake_pool = StakePool::get_checked(accounts.stake_pool, vec![Tag::StakePool])?;
83 |
84 | check_account_key(
85 | accounts.stake_pool_owner,
86 | &Pubkey::from(stake_pool.header.owner),
87 | AccessError::StakeAccountOwnerMismatch,
88 | )?;
89 |
90 | stake_pool.header.minimum_stake_amount = new_minimum;
91 |
92 | Ok(())
93 | }
94 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/change_pool_multiplier.rs:
--------------------------------------------------------------------------------
1 | /// Change the stake part multiplier of a pool
2 | /// This instruction allows a pool owner to adjust the percentage of the pool rewards that go to the pool stakers.
3 | use borsh::{BorshDeserialize, BorshSerialize};
4 | use solana_program::{
5 | account_info::{next_account_info, AccountInfo},
6 | entrypoint::ProgramResult,
7 | msg,
8 | program_error::ProgramError,
9 | pubkey::Pubkey,
10 | };
11 | use crate::state:: CentralStateV2;
12 | use crate::state::StakePool;
13 | use crate::{error::AccessError, state::Tag};
14 | use bonfida_utils::{BorshSize, InstructionsAccount};
15 | use crate::instruction::ProgramInstruction::ChangePoolMultiplier;
16 |
17 | use crate::utils::{check_account_key, check_account_owner, check_signer};
18 |
19 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
20 | /// The required parameters for the `change_pool_multiplier` instruction
21 | pub struct Params {
22 | pub new_multiplier: u64,
23 | }
24 |
25 | #[derive(InstructionsAccount)]
26 | /// The required accounts for the `change_pool_multiplier` instruction
27 | pub struct Accounts<'a, T> {
28 | /// The stake pool account
29 | #[cons(writable)]
30 | pub stake_pool: &'a T,
31 |
32 | /// The stake pool owner account
33 | #[cons(signer)]
34 | pub stake_pool_owner: &'a T,
35 |
36 | /// The central state account
37 | pub central_state: &'a T,
38 | }
39 |
40 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
41 | pub fn parse(
42 | accounts: &'a [AccountInfo<'b>],
43 | program_id: &Pubkey,
44 | ) -> Result {
45 | let accounts_iter = &mut accounts.iter();
46 | let accounts = Accounts {
47 | stake_pool: next_account_info(accounts_iter)?,
48 | stake_pool_owner: next_account_info(accounts_iter)?,
49 | central_state: next_account_info(accounts_iter)?,
50 | };
51 |
52 | // Check keys
53 |
54 | // Check ownership
55 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
56 | check_account_owner(
57 | accounts.stake_pool,
58 | program_id,
59 | AccessError::WrongStakePoolAccountOwner,
60 | )?;
61 |
62 | // Check signer
63 | check_signer(
64 | accounts.stake_pool_owner,
65 | AccessError::StakePoolOwnerMustSign,
66 | )?;
67 |
68 | Ok(accounts)
69 | }
70 | }
71 |
72 | pub fn process_change_pool_multiplier(
73 | program_id: &Pubkey,
74 | accounts: &[AccountInfo],
75 | params: Params,
76 | ) -> ProgramResult {
77 | let Params { new_multiplier } = params;
78 | let accounts = Accounts::parse(accounts, program_id)?;
79 | let central_state = CentralStateV2::from_account_info(accounts.central_state)?;
80 | central_state.assert_instruction_allowed(&ChangePoolMultiplier)?;
81 |
82 | let mut stake_pool = StakePool::get_checked(accounts.stake_pool, vec![Tag::StakePool])?;
83 |
84 | if new_multiplier > 100 {
85 | msg!("The pool multiplier is a percentage and needs to be smaller than 100 and greater than 0");
86 | return Err(ProgramError::InvalidArgument);
87 | }
88 |
89 | check_account_key(
90 | accounts.stake_pool_owner,
91 | &Pubkey::from(stake_pool.header.owner),
92 | AccessError::StakeAccountOwnerMismatch,
93 | )?;
94 |
95 | stake_pool.header.stakers_part = new_multiplier;
96 |
97 | Ok(())
98 | }
99 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/claim_bond_rewards.rs:
--------------------------------------------------------------------------------
1 | //! Claim bond rewards
2 | //! This Instruction allows bond owners to claim their staking rewards
3 | use std::convert::TryInto;
4 | use borsh::{BorshDeserialize, BorshSerialize};
5 | use solana_program::{
6 | account_info::{next_account_info, AccountInfo},
7 | entrypoint::ProgramResult,
8 | msg,
9 | program::invoke_signed,
10 | program_error::ProgramError,
11 | program_pack::Pack,
12 | pubkey::Pubkey,
13 | };
14 |
15 | use crate::state::{BondAccount, StakePool};
16 | use crate::{error::AccessError, state::Tag};
17 | use bonfida_utils::{BorshSize, InstructionsAccount};
18 | use spl_token::{instruction::mint_to, state::Account};
19 | use crate::instruction::ProgramInstruction::{ClaimBondRewards};
20 |
21 | use crate::utils::{
22 | assert_no_close_or_delegate, calc_reward_fp32, check_account_key, check_account_owner,
23 | check_signer,
24 | };
25 | use crate::state:: CentralStateV2;
26 |
27 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
28 | /// The required parameters for the `claim_bond_rewards` instruction
29 | pub struct Params {}
30 |
31 | #[derive(InstructionsAccount)]
32 | /// The required accounts for the `claim_bond_rewards` instruction
33 | pub struct Accounts<'a, T> {
34 | /// The stake pool account
35 | #[cons(writable)]
36 | pub stake_pool: &'a T,
37 |
38 | /// The bond account
39 | #[cons(writable)]
40 | pub bond_account: &'a T,
41 |
42 | /// The bond account owner
43 | #[cons(signer)]
44 | pub bond_owner: &'a T,
45 |
46 | /// The rewards destination
47 | #[cons(writable)]
48 | pub rewards_destination: &'a T,
49 |
50 | /// The central state account
51 | pub central_state: &'a T,
52 |
53 | /// The mint address of the ACCESS token
54 | #[cons(writable)]
55 | pub mint: &'a T,
56 |
57 | /// The SPL token program account
58 | pub spl_token_program: &'a T,
59 | }
60 |
61 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
62 | pub fn parse(
63 | accounts: &'a [AccountInfo<'b>],
64 | program_id: &Pubkey,
65 | ) -> Result {
66 | let accounts_iter = &mut accounts.iter();
67 | let accounts = Accounts {
68 | stake_pool: next_account_info(accounts_iter)?,
69 | bond_account: next_account_info(accounts_iter)?,
70 | bond_owner: next_account_info(accounts_iter)?,
71 | rewards_destination: next_account_info(accounts_iter)?,
72 | central_state: next_account_info(accounts_iter)?,
73 | mint: next_account_info(accounts_iter)?,
74 | spl_token_program: next_account_info(accounts_iter)?,
75 | };
76 |
77 | // Check keys
78 | check_account_key(
79 | accounts.spl_token_program,
80 | &spl_token::ID,
81 | AccessError::WrongSplTokenProgramId,
82 | )?;
83 |
84 | // Check ownership
85 | check_account_owner(
86 | accounts.stake_pool,
87 | program_id,
88 | AccessError::WrongStakePoolAccountOwner,
89 | )?;
90 | check_account_owner(
91 | accounts.bond_account,
92 | program_id,
93 | AccessError::WrongStakeAccountOwner,
94 | )?;
95 | check_account_owner(
96 | accounts.rewards_destination,
97 | &spl_token::ID,
98 | AccessError::WrongOwner,
99 | )?;
100 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
101 | check_account_owner(accounts.mint, &spl_token::ID, AccessError::WrongOwner)?;
102 |
103 | Ok(accounts)
104 | }
105 | }
106 |
107 | pub fn process_claim_bond_rewards(
108 | program_id: &Pubkey,
109 | accounts: &[AccountInfo],
110 | _params: Params,
111 | ) -> ProgramResult {
112 | // If you don't want to issue rewards to bond holders
113 | if cfg!(feature = "claim-bond-rewards-off") {
114 | return Err(AccessError::NoOp.into());
115 | }
116 |
117 | let accounts = Accounts::parse(accounts, program_id)?;
118 |
119 | let central_state = CentralStateV2::from_account_info(accounts.central_state)?;
120 | central_state.assert_instruction_allowed(&ClaimBondRewards)?;
121 | let stake_pool = StakePool::get_checked(accounts.stake_pool, vec![Tag::StakePool])?;
122 | let mut bond = BondAccount::from_account_info(accounts.bond_account, false)?;
123 |
124 | let destination_token_acc = Account::unpack(&accounts.rewards_destination.data.borrow())?;
125 | if destination_token_acc.mint != central_state.token_mint {
126 | msg!("Invalid ACCESS mint");
127 | #[cfg(not(feature = "no-mint-check"))]
128 | return Err(AccessError::WrongMint.into());
129 | }
130 |
131 | if destination_token_acc.owner != bond.owner {
132 | // If the destination does not belong to the bond owner he must sign
133 | check_signer(accounts.bond_owner, AccessError::BuyerMustSign)?;
134 | } else {
135 | assert_no_close_or_delegate(&destination_token_acc)?;
136 | }
137 |
138 | // Safety checks
139 | check_account_key(
140 | accounts.stake_pool,
141 | &bond.stake_pool,
142 | AccessError::WrongStakePool,
143 | )?;
144 | check_account_key(
145 | accounts.bond_owner,
146 | &bond.owner,
147 | AccessError::StakeAccountOwnerMismatch,
148 | )?;
149 | check_account_key(
150 | accounts.mint,
151 | ¢ral_state.token_mint,
152 | AccessError::WrongMint,
153 | )?;
154 |
155 | // Calculate the rewards (checks if the pool is cranked as well)
156 | let reward = calc_reward_fp32(
157 | central_state.last_snapshot_offset,
158 | bond.last_claimed_offset,
159 | &stake_pool,
160 | true,
161 | false,
162 | )?
163 | // Multiply by the staker shares of the total pool
164 | .checked_mul(bond.total_staked as u128)
165 | .map(|r| ((r >> 31) + 1) >> 1)
166 | .ok_or(AccessError::Overflow)?
167 | .try_into()
168 | .map_err(|_|AccessError::Overflow)?;
169 |
170 | msg!("Claiming bond rewards {}", reward);
171 | msg!("Total staked {}", bond.total_staked);
172 |
173 | // Transfer rewards
174 | let transfer_ix = mint_to(
175 | &spl_token::ID,
176 | accounts.mint.key,
177 | accounts.rewards_destination.key,
178 | accounts.central_state.key,
179 | &[],
180 | reward,
181 | )?;
182 | invoke_signed(
183 | &transfer_ix,
184 | &[
185 | accounts.spl_token_program.clone(),
186 | accounts.mint.clone(),
187 | accounts.central_state.clone(),
188 | accounts.rewards_destination.clone(),
189 | ],
190 | &[&[&program_id.to_bytes(), &[central_state.bump_seed]]],
191 | )?;
192 |
193 | // Update states
194 | bond.last_claimed_offset = central_state.last_snapshot_offset;
195 | bond.save(&mut accounts.bond_account.data.borrow_mut())?;
196 |
197 | Ok(())
198 | }
199 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/close_royalty_account.rs:
--------------------------------------------------------------------------------
1 | //! Close a royalty account
2 | //! This instruction can be used to close a royalty account. The laports will be sent to the original fee payer
3 | use borsh::{BorshDeserialize, BorshSerialize};
4 | use solana_program::{
5 | account_info::{next_account_info, AccountInfo},
6 | entrypoint::ProgramResult,
7 | program_error::ProgramError,
8 | pubkey::Pubkey,
9 | };
10 |
11 | use crate::utils::{
12 | check_account_key, check_account_owner, check_signer,
13 | };
14 | use bonfida_utils::BorshSize;
15 | use bonfida_utils::InstructionsAccount;
16 |
17 | use crate::error::AccessError;
18 | use crate::instruction::ProgramInstruction::CloseStakeAccount;
19 | use crate::state::{RoyaltyAccount};
20 | use crate::state:: CentralStateV2;
21 |
22 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
23 | /// The required parameters for the `close_royalty_account` instruction
24 | pub struct Params {}
25 |
26 | #[derive(InstructionsAccount)]
27 | /// The required accounts for the `close_royalty_account` instruction
28 | pub struct Accounts<'a, T> {
29 | /// The royalty account
30 | #[cons(writable)]
31 | pub royalty_account: &'a T,
32 |
33 | /// The royalty payer of the royalty account
34 | #[cons(signer)]
35 | pub royalty_payer: &'a T,
36 |
37 | /// The account where the funds should be returned
38 | #[cons(writable)]
39 | pub rent_destination: &'a T,
40 |
41 | /// The central state account
42 | pub central_state: &'a T,
43 | }
44 |
45 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
46 | pub fn parse(
47 | accounts: &'a [AccountInfo<'b>],
48 | program_id: &Pubkey,
49 | ) -> Result {
50 | let accounts_iter = &mut accounts.iter();
51 | let accounts = Accounts {
52 | royalty_account: next_account_info(accounts_iter)?,
53 | royalty_payer: next_account_info(accounts_iter)?,
54 | rent_destination: next_account_info(accounts_iter)?,
55 | central_state: next_account_info(accounts_iter)?,
56 | };
57 |
58 | // Check ownership
59 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
60 | check_account_owner(accounts.royalty_account, program_id, AccessError::WrongOwner)?;
61 |
62 | // Check signer
63 | check_signer(accounts.royalty_payer, AccessError::StakePoolOwnerMustSign)?;
64 |
65 | Ok(accounts)
66 | }
67 | }
68 |
69 | pub fn process_close_royalty_account(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
70 | let accounts = Accounts::parse(accounts, program_id)?;
71 |
72 | let central_state = CentralStateV2::from_account_info(accounts.central_state)?;
73 | central_state.assert_instruction_allowed(&CloseStakeAccount)?;
74 | let mut royalty_account = RoyaltyAccount::from_account_info(accounts.royalty_account)?;
75 |
76 | check_account_key(
77 | accounts.royalty_payer,
78 | &royalty_account.royalty_payer,
79 | AccessError::WrongOwner,
80 | )?;
81 |
82 | check_account_key(
83 | accounts.rent_destination,
84 | &royalty_account.rent_payer,
85 | AccessError::WrongQuoteDestination,
86 | )?;
87 |
88 | royalty_account.close();
89 | royalty_account.save(&mut accounts.royalty_account.data.borrow_mut())?;
90 |
91 | let mut account_lamports = accounts.royalty_account.lamports.borrow_mut();
92 | let mut destination_lamports = accounts.rent_destination.lamports.borrow_mut();
93 |
94 | **destination_lamports += **account_lamports;
95 | **account_lamports = 0;
96 |
97 | Ok(())
98 | }
99 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/close_stake_account.rs:
--------------------------------------------------------------------------------
1 | //! Close a stake account
2 | //! This instruction can be used to close an empty stake account and collect the lamports
3 | use borsh::{BorshDeserialize, BorshSerialize};
4 | use solana_program::{
5 | account_info::{next_account_info, AccountInfo},
6 | entrypoint::ProgramResult,
7 | program_error::ProgramError,
8 | pubkey::Pubkey,
9 | };
10 |
11 | use crate::utils::{
12 | assert_empty_stake_account, check_account_key, check_account_owner, check_signer,
13 | };
14 | use bonfida_utils::BorshSize;
15 | use bonfida_utils::InstructionsAccount;
16 |
17 | use crate::error::AccessError;
18 | use crate::instruction::ProgramInstruction::CloseStakeAccount;
19 | use crate::state::{StakeAccount, V1_INSTRUCTIONS_ALLOWED};
20 | use crate::state:: CentralStateV2;
21 |
22 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
23 | /// The required parameters for the `close_stake_account` instruction
24 | pub struct Params {}
25 |
26 | #[derive(InstructionsAccount)]
27 | /// The required accounts for the `close_stake_account` instruction
28 | pub struct Accounts<'a, T> {
29 | /// The stake account
30 | #[cons(writable)]
31 | pub stake_account: &'a T,
32 |
33 | /// The owner of the stake account
34 | #[cons(writable, signer)]
35 | pub owner: &'a T,
36 |
37 | /// The central state account
38 | pub central_state: &'a T,
39 | }
40 |
41 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
42 | pub fn parse(
43 | accounts: &'a [AccountInfo<'b>],
44 | program_id: &Pubkey,
45 | ) -> Result {
46 | let accounts_iter = &mut accounts.iter();
47 | let accounts = Accounts {
48 | stake_account: next_account_info(accounts_iter)?,
49 | owner: next_account_info(accounts_iter)?,
50 | central_state: next_account_info(accounts_iter)?,
51 | };
52 |
53 | // Check ownership
54 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
55 | check_account_owner(accounts.stake_account, program_id, AccessError::WrongOwner)?;
56 |
57 | // Check signer
58 | check_signer(accounts.owner, AccessError::StakePoolOwnerMustSign)?;
59 |
60 | Ok(accounts)
61 | }
62 | }
63 |
64 | pub fn process_close_stake_account(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
65 | if !V1_INSTRUCTIONS_ALLOWED {
66 | return Err(AccessError::DeprecatedInstruction.into());
67 | }
68 |
69 | let accounts = Accounts::parse(accounts, program_id)?;
70 |
71 | let central_state = CentralStateV2::from_account_info(accounts.central_state)?;
72 | central_state.assert_instruction_allowed(&CloseStakeAccount)?;
73 | let mut stake_account = StakeAccount::from_account_info(accounts.stake_account)?;
74 |
75 | check_account_key(
76 | accounts.owner,
77 | &stake_account.owner,
78 | AccessError::WrongStakePoolOwner,
79 | )?;
80 |
81 | assert_empty_stake_account(&stake_account)?;
82 |
83 | stake_account.close();
84 | stake_account.save(&mut accounts.stake_account.data.borrow_mut())?;
85 |
86 | let mut stake_lamports = accounts.stake_account.lamports.borrow_mut();
87 | let mut owner_lamports = accounts.owner.lamports.borrow_mut();
88 |
89 | **owner_lamports += **stake_lamports;
90 | **stake_lamports = 0;
91 |
92 | Ok(())
93 | }
94 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/close_stake_pool.rs:
--------------------------------------------------------------------------------
1 | //! Close a stake pool
2 | //! This instruction can be used to close an empty stake pool and collect the lamports
3 | use crate::{
4 | state::Tag,
5 | utils::{assert_empty_stake_pool, check_account_key, check_account_owner, check_signer},
6 | };
7 | use bonfida_utils::{BorshSize, InstructionsAccount};
8 | use borsh::{BorshDeserialize, BorshSerialize};
9 | use solana_program::{
10 | account_info::{next_account_info, AccountInfo},
11 | entrypoint::ProgramResult,
12 | msg,
13 | program_error::ProgramError,
14 | program_pack::Pack,
15 | pubkey::Pubkey,
16 | };
17 | use spl_token::state::Account;
18 | use crate::state:: CentralStateV2;
19 |
20 | use crate::error::AccessError;
21 | use crate::instruction::ProgramInstruction::CloseStakePool;
22 | use crate::state::{StakePool, V1_INSTRUCTIONS_ALLOWED};
23 |
24 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
25 | /// The required parameters for the `close_stake_pool` instruction
26 | pub struct Params {}
27 |
28 | #[derive(InstructionsAccount)]
29 | /// The required accounts for the `close_stake_pool` instruction
30 | pub struct Accounts<'a, T> {
31 | /// The account of the stake pool
32 | #[cons(writable)]
33 | pub stake_pool_account: &'a T,
34 |
35 | /// Pool vault
36 | pub pool_vault: &'a T,
37 |
38 | /// The owner of the stake pool
39 | #[cons(writable, signer)]
40 | pub owner: &'a T,
41 |
42 | /// The central state account
43 | pub central_state: &'a T,
44 | }
45 |
46 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
47 | pub fn parse(
48 | accounts: &'a [AccountInfo<'b>],
49 | program_id: &Pubkey,
50 | ) -> Result {
51 | let accounts_iter = &mut accounts.iter();
52 | let accounts = Accounts {
53 | stake_pool_account: next_account_info(accounts_iter)?,
54 | pool_vault: next_account_info(accounts_iter)?,
55 | owner: next_account_info(accounts_iter)?,
56 | central_state: next_account_info(accounts_iter)?,
57 | };
58 |
59 | // Check keys
60 |
61 | // Check ownership
62 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
63 | check_account_owner(
64 | accounts.stake_pool_account,
65 | program_id,
66 | AccessError::WrongOwner,
67 | )?;
68 | check_account_owner(accounts.pool_vault, &spl_token::ID, AccessError::WrongOwner)?;
69 |
70 | // Check signer
71 | check_signer(accounts.owner, AccessError::StakePoolOwnerMustSign)?;
72 |
73 | Ok(accounts)
74 | }
75 | }
76 |
77 | pub fn process_close_stake_pool(
78 | program_id: &Pubkey,
79 | accounts: &[AccountInfo],
80 | _params: Params,
81 | ) -> ProgramResult {
82 | if !V1_INSTRUCTIONS_ALLOWED {
83 | return Err(AccessError::DeprecatedInstruction.into());
84 | }
85 |
86 | let accounts = Accounts::parse(accounts, program_id)?;
87 |
88 | let central_state = CentralStateV2::from_account_info(accounts.central_state)?;
89 | central_state.assert_instruction_allowed(&CloseStakePool)?;
90 | let mut stake_pool = StakePool::get_checked(
91 | accounts.stake_pool_account,
92 | vec![Tag::InactiveStakePool, Tag::StakePool],
93 | )?;
94 |
95 | check_account_key(
96 | accounts.owner,
97 | &Pubkey::from(stake_pool.header.owner),
98 | AccessError::WrongStakePoolOwner,
99 | )?;
100 | check_account_key(
101 | accounts.pool_vault,
102 | &Pubkey::from(stake_pool.header.vault),
103 | AccessError::StakePoolVaultMismatch,
104 | )?;
105 |
106 | let vault = Account::unpack_from_slice(&accounts.pool_vault.data.borrow_mut())?;
107 |
108 | if vault.amount != 0 {
109 | msg!("Vault isn't empty, there are remaining unstake requests");
110 | return Err(AccessError::PendingUnstakeRequests.into());
111 | }
112 |
113 | assert_empty_stake_pool(&stake_pool)?;
114 |
115 | stake_pool.header.close();
116 |
117 | let mut stake_pool_lamports = accounts.stake_pool_account.lamports.borrow_mut();
118 | let mut owner_lamports = accounts.owner.lamports.borrow_mut();
119 |
120 | **owner_lamports += **stake_pool_lamports;
121 | **stake_pool_lamports = 0;
122 |
123 | Ok(())
124 | }
125 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/crank.rs:
--------------------------------------------------------------------------------
1 | //! Permissionless crank to update the stake pool rewards
2 | //! This instructions updates the circular buffer with the pool balances multiplied by the current inflation
3 |
4 | use bonfida_utils::{BorshSize, InstructionsAccount};
5 | use borsh::{BorshDeserialize, BorshSerialize};
6 | use solana_program::{
7 | account_info::{next_account_info, AccountInfo},
8 | entrypoint::ProgramResult,
9 | msg,
10 | program_error::ProgramError,
11 | pubkey::Pubkey,
12 | };
13 | use spl_math::precise_number::PreciseNumber;
14 |
15 | use crate::error::AccessError;
16 | use crate::instruction::ProgramInstruction::Crank;
17 | use crate::state::{RewardsTuple, StakePool, Tag};
18 | use crate::utils::check_account_owner;
19 | use crate::state:: CentralStateV2;
20 |
21 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
22 | /// The required parameters for the `crank` instruction
23 | pub struct Params {}
24 |
25 | #[derive(InstructionsAccount)]
26 | /// The required accounts for the `crank` instruction
27 | pub struct Accounts<'a, T> {
28 | /// The stake pool account
29 | #[cons(writable)]
30 | pub stake_pool: &'a T,
31 |
32 | /// The central state account
33 | #[cons(writable)]
34 | pub central_state: &'a T,
35 | }
36 |
37 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
38 | pub fn parse(
39 | accounts: &'a [AccountInfo<'b>],
40 | program_id: &Pubkey,
41 | ) -> Result {
42 | let accounts_iter = &mut accounts.iter();
43 | let accounts = Accounts {
44 | stake_pool: next_account_info(accounts_iter)?,
45 | central_state: next_account_info(accounts_iter)?,
46 | };
47 |
48 | // Check ownership
49 | check_account_owner(
50 | accounts.stake_pool,
51 | program_id,
52 | AccessError::WrongStakeAccountOwner,
53 | )?;
54 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
55 |
56 | Ok(accounts)
57 | }
58 | }
59 |
60 | pub fn process_crank(
61 | program_id: &Pubkey,
62 | accounts: &[AccountInfo],
63 | _params: Params,
64 | ) -> ProgramResult {
65 | let accounts = Accounts::parse(accounts, program_id)?;
66 |
67 | let mut stake_pool = StakePool::get_checked(accounts.stake_pool, vec![Tag::StakePool])?;
68 | let mut central_state = CentralStateV2::from_account_info(accounts.central_state)?;
69 | central_state.assert_instruction_allowed(&Crank)?;
70 |
71 | let current_offset = central_state.get_current_offset()?;
72 | // check if we need to do a system wide snapshot
73 | if central_state.last_snapshot_offset < current_offset {
74 | central_state.total_staked_snapshot = central_state.total_staked;
75 | central_state.last_snapshot_offset = current_offset;
76 | central_state.save(&mut accounts.central_state.data.borrow_mut())?;
77 | }
78 |
79 | if stake_pool.header.current_day_idx as u64 == central_state.last_snapshot_offset {
80 | #[cfg(not(any(feature = "days-to-sec-10s", feature = "days-to-sec-15m")))]
81 | return Err(AccessError::NoOp.into());
82 | }
83 | msg!("Total staked in pool {}", stake_pool.header.total_staked);
84 | msg!("Daily inflation {}", central_state.daily_inflation);
85 | msg!("Total staked {}", central_state.total_staked);
86 | msg!(
87 | "Total staked snapshot {}",
88 | central_state.total_staked_snapshot
89 | );
90 |
91 | // get the pool staked amount at the time of last system snapshot
92 | let total_staked_snapshot = stake_pool.header.total_staked as u128;
93 |
94 | let mut stakers_reward = 0;
95 | if total_staked_snapshot != 0 {
96 | // Stakers rewards per ACS staked
97 | stakers_reward = ((central_state.daily_inflation as u128) << 32)
98 | .checked_mul(stake_pool.header.stakers_part as u128)
99 | .ok_or(AccessError::Overflow)?
100 | .checked_div(100u128)
101 | .ok_or(AccessError::Overflow)?
102 | .checked_div(central_state.total_staked_snapshot as u128)
103 | .unwrap_or(0);
104 | };
105 |
106 | msg!("Stakers reward {}", stakers_reward);
107 |
108 | let precise_total_staked_snapshot = PreciseNumber::new(
109 | total_staked_snapshot
110 | .checked_shl(32)
111 | .ok_or(AccessError::Overflow)?,
112 | )
113 | .ok_or(AccessError::Overflow)?;
114 | let precise_daily_inflation =
115 | PreciseNumber::new(central_state.daily_inflation as u128).ok_or(AccessError::Overflow)?;
116 | let precise_system_staked_snapshot =
117 | PreciseNumber::new(central_state.total_staked_snapshot as u128)
118 | .ok_or(AccessError::Overflow)?;
119 |
120 | // Total pool reward
121 | let precise_pool_reward = (precise_total_staked_snapshot)
122 | .checked_mul(&precise_daily_inflation)
123 | .ok_or(AccessError::Overflow)?
124 | .checked_mul(
125 | &PreciseNumber::new(
126 | 100u64
127 | .checked_sub(stake_pool.header.stakers_part)
128 | .ok_or(AccessError::Overflow)? as u128,
129 | )
130 | .ok_or(AccessError::Overflow)?,
131 | )
132 | .ok_or(AccessError::Overflow)?
133 | .checked_div(&PreciseNumber::new(100u128).ok_or(AccessError::Overflow)?)
134 | .ok_or(AccessError::Overflow)?
135 | .checked_div(&precise_system_staked_snapshot)
136 | .unwrap_or(PreciseNumber::new(0).ok_or(AccessError::Overflow)?);
137 |
138 | let pool_reward = precise_pool_reward
139 | .to_imprecise()
140 | .ok_or(AccessError::Overflow)?;
141 |
142 | msg!("Pool reward {}", pool_reward);
143 |
144 | let total_claimable_rewards = (((pool_reward >> 31) + 1) >> 1)
145 | .checked_add(
146 | ((stakers_reward
147 | .checked_mul(total_staked_snapshot)
148 | .ok_or(AccessError::Overflow)?
149 | >> 31)
150 | + 1)
151 | >> 1,
152 | )
153 | .ok_or(AccessError::Overflow)?;
154 |
155 | msg!("Total claimable rewards {}", total_claimable_rewards);
156 |
157 | assert!(
158 | total_claimable_rewards
159 | <= (central_state.daily_inflation as u128)
160 | .checked_add(1_000_000)
161 | .ok_or(AccessError::Overflow)?
162 | );
163 |
164 | stake_pool.push_balances_buff(
165 | current_offset,
166 | RewardsTuple {
167 | pool_reward,
168 | stakers_reward,
169 | },
170 | )?;
171 | Ok(())
172 | }
173 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/create_bond.rs:
--------------------------------------------------------------------------------
1 | //! Create a bond
2 | //! This instruction can be used by authorized sellers to create a bond
3 | use borsh::{BorshDeserialize, BorshSerialize};
4 | use solana_program::{
5 | account_info::{next_account_info, AccountInfo},
6 | entrypoint::ProgramResult,
7 | program_error::ProgramError,
8 | pubkey::Pubkey,
9 | system_program,
10 | };
11 | use crate::state:: CentralStateV2;
12 |
13 | use crate::error::AccessError;
14 | use crate::state::{BondAccount, StakePool, BOND_SIGNER_THRESHOLD, V1_INSTRUCTIONS_ALLOWED};
15 | #[cfg(not(feature = "no-bond-signer"))]
16 | use crate::utils::assert_authorized_seller;
17 | use crate::utils::{assert_uninitialized, check_account_key, check_account_owner, check_signer};
18 | use crate::{cpi::Cpi, state::Tag};
19 | use bonfida_utils::{BorshSize, InstructionsAccount};
20 | use crate::instruction::ProgramInstruction::CreateBond;
21 |
22 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
23 | /// The required parameters for the `create_bond` instruction
24 | pub struct Params {
25 | /// Ultimate buyer of the bond
26 | pub buyer: Pubkey,
27 | /// Total amount of ACCESS tokens being sold
28 | pub total_amount_sold: u64,
29 | /// Total price of the bond
30 | pub total_quote_amount: u64,
31 | /// Mint of the token used to buy the bond
32 | pub quote_mint: Pubkey,
33 | /// The token account i.e where the sell proceeds go
34 | pub seller_token_account: Pubkey,
35 | /// The start date of the unlock
36 | pub unlock_start_date: i64,
37 | /// The time interval at which the tokens unlock
38 | pub unlock_period: i64,
39 | /// The amount of tokens that unlock at each `unlock_period`
40 | pub unlock_amount: u64,
41 | /// Index of the seller in the [`array`][`crate::state::AUTHORIZED_BOND_SELLERS`] of authorized sellers
42 | pub seller_index: u64,
43 | }
44 |
45 | #[derive(InstructionsAccount)]
46 | /// The required accounts for the `create_bond` instruction
47 | pub struct Accounts<'a, T> {
48 | /// The bond seller account
49 | #[cons(writable, signer)]
50 | pub seller: &'a T,
51 |
52 | /// The bond account
53 | #[cons(writable)]
54 | pub bond_account: &'a T,
55 |
56 | // The stake pool
57 | pub stake_pool: &'a T,
58 |
59 | /// The system program account
60 | pub system_program: &'a T,
61 |
62 | /// The fee account
63 | #[cons(writable, signer)]
64 | pub fee_payer: &'a T,
65 |
66 | /// The central state account
67 | pub central_state: &'a T,
68 | }
69 |
70 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
71 | pub fn parse(
72 | accounts: &'a [AccountInfo<'b>],
73 | program_id: &Pubkey,
74 | ) -> Result {
75 | let accounts_iter = &mut accounts.iter();
76 | let accounts = Accounts {
77 | seller: next_account_info(accounts_iter)?,
78 | bond_account: next_account_info(accounts_iter)?,
79 | stake_pool: next_account_info(accounts_iter)?,
80 | system_program: next_account_info(accounts_iter)?,
81 | fee_payer: next_account_info(accounts_iter)?,
82 | central_state: next_account_info(accounts_iter)?,
83 | };
84 |
85 | // Check keys
86 | check_account_key(
87 | accounts.system_program,
88 | &system_program::ID,
89 | AccessError::WrongSystemProgram,
90 | )?;
91 |
92 | // Check ownership
93 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
94 | check_account_owner(accounts.stake_pool, program_id, AccessError::WrongOwner)?;
95 |
96 | // Check signer
97 | check_signer(accounts.seller, AccessError::BondSellerMustSign)?;
98 |
99 | Ok(accounts)
100 | }
101 | }
102 |
103 | pub fn process_create_bond(
104 | program_id: &Pubkey,
105 | accounts: &[AccountInfo],
106 | params: Params,
107 | ) -> ProgramResult {
108 | if !V1_INSTRUCTIONS_ALLOWED {
109 | return Err(AccessError::DeprecatedInstruction.into());
110 | }
111 |
112 | let accounts = Accounts::parse(accounts, program_id)?;
113 | let central_state = CentralStateV2::from_account_info(accounts.central_state)?;
114 | central_state.assert_instruction_allowed(&CreateBond)?;
115 |
116 | let (derived_key, nonce) =
117 | BondAccount::create_key(¶ms.buyer, params.total_amount_sold, program_id);
118 |
119 | let stake_pool = StakePool::get_checked(accounts.stake_pool, vec![Tag::StakePool])?;
120 |
121 | check_account_key(
122 | accounts.bond_account,
123 | &derived_key,
124 | AccessError::AccountNotDeterministic,
125 | )?;
126 | assert_uninitialized(accounts.bond_account)?;
127 |
128 | #[cfg(not(feature = "no-bond-signer"))]
129 | assert_authorized_seller(accounts.seller, params.seller_index as usize)?;
130 |
131 | if params.unlock_period == 0 {
132 | return Err(AccessError::ForbiddenUnlockPeriodZero.into());
133 | }
134 |
135 | let bond = BondAccount::new(
136 | params.buyer,
137 | params.total_amount_sold,
138 | params.total_quote_amount,
139 | params.quote_mint,
140 | params.seller_token_account,
141 | params.unlock_start_date,
142 | params.unlock_period,
143 | params.unlock_amount,
144 | params.unlock_start_date,
145 | stake_pool.header.minimum_stake_amount,
146 | *accounts.stake_pool.key,
147 | *accounts.seller.key,
148 | );
149 |
150 | // Create bond account
151 | let seeds: &[&[u8]] = &[
152 | BondAccount::SEED,
153 | ¶ms.buyer.to_bytes(),
154 | ¶ms.total_amount_sold.to_le_bytes(),
155 | &[nonce],
156 | ];
157 |
158 | Cpi::create_account(
159 | program_id,
160 | accounts.system_program,
161 | accounts.fee_payer,
162 | accounts.bond_account,
163 | seeds,
164 | bond.borsh_len() + ((BOND_SIGNER_THRESHOLD - 1) * 32) as usize,
165 | )?;
166 |
167 | bond.save(&mut accounts.bond_account.data.borrow_mut())?;
168 |
169 | Ok(())
170 | }
171 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/create_bond_v2.rs:
--------------------------------------------------------------------------------
1 | //! Create a Bond V2
2 | use bonfida_utils::{BorshSize, InstructionsAccount};
3 | use borsh::{BorshDeserialize, BorshSerialize};
4 | use solana_program::{
5 | account_info::{AccountInfo, next_account_info},
6 | clock::Clock,
7 | entrypoint::ProgramResult,
8 | msg,
9 | program_error::ProgramError,
10 | pubkey::Pubkey,
11 | system_program,
12 | sysvar::Sysvar,
13 | };
14 |
15 |
16 |
17 |
18 | use crate::{cpi::Cpi, state::Tag};
19 | use crate::error::AccessError;
20 | use crate::instruction::ProgramInstruction::CreateBondV2;
21 | use crate::state::{BondV2Account, StakePool};
22 | use crate::state::CentralStateV2;
23 | use crate::utils::{
24 | check_account_key, check_account_owner,
25 | };
26 |
27 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
28 | /// The required parameters for the `create_bond_v2` instruction
29 | pub struct Params {
30 | /// The timestamp of the unlock, if any
31 | pub unlock_timestamp: Option,
32 | /// Owner of the bond account
33 | pub owner: Pubkey,
34 | }
35 |
36 | #[derive(InstructionsAccount)]
37 | /// The required accounts for the `create_bond_v2` instruction
38 | pub struct Accounts<'a, T> {
39 | /// The bond account
40 | #[cons(writable)]
41 | pub bond_v2_account: &'a T,
42 |
43 | /// The system program account
44 | pub system_program: &'a T,
45 |
46 | /// The pool account
47 | pub pool: &'a T,
48 |
49 | /// The fee account
50 | #[cons(writable, signer)]
51 | pub fee_payer: &'a T,
52 |
53 | /// Central state
54 | pub central_state: &'a T,
55 | }
56 |
57 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
58 | pub fn parse(
59 | accounts: &'a [AccountInfo<'b>],
60 | program_id: &Pubkey,
61 | ) -> Result {
62 | let accounts_iter = &mut accounts.iter();
63 | let accounts = Accounts {
64 | bond_v2_account: next_account_info(accounts_iter)?,
65 | system_program: next_account_info(accounts_iter)?,
66 | pool: next_account_info(accounts_iter)?,
67 | fee_payer: next_account_info(accounts_iter)?,
68 | central_state: next_account_info(accounts_iter)?,
69 | };
70 |
71 | // Check keys
72 | check_account_key(
73 | accounts.system_program,
74 | &system_program::ID,
75 | AccessError::WrongSystemProgram,
76 | )?;
77 |
78 | // Check ownership
79 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
80 | check_account_owner(
81 | accounts.bond_v2_account,
82 | &system_program::ID,
83 | AccessError::WrongOwner,
84 | )?;
85 | check_account_owner(
86 | accounts.pool,
87 | program_id,
88 | AccessError::WrongStakePoolAccountOwner,
89 | )?;
90 |
91 |
92 | Ok(accounts)
93 | }
94 | }
95 |
96 | pub fn process_create_bond_v2(
97 | program_id: &Pubkey,
98 | accounts: &[AccountInfo],
99 | params: Params,
100 | ) -> ProgramResult {
101 | let Params {
102 | unlock_timestamp,
103 | owner
104 | } = params;
105 | let accounts = Accounts::parse(accounts, program_id)?;
106 |
107 | let central_state = CentralStateV2::from_account_info(accounts.central_state)?;
108 | central_state.assert_instruction_allowed(&CreateBondV2)?;
109 | let pool = StakePool::get_checked(accounts.pool, vec![Tag::StakePool])?;
110 |
111 | let (derived_key, bump_seed) =
112 | BondV2Account::create_key(&owner, accounts.pool.key, unlock_timestamp, program_id);
113 |
114 | check_account_key(
115 | accounts.bond_v2_account,
116 | &derived_key,
117 | AccessError::AccountNotDeterministic,
118 | )?;
119 |
120 | let current_time = Clock::get()?.unix_timestamp;
121 | if unlock_timestamp.is_some() && current_time > unlock_timestamp.unwrap() {
122 | msg!("Cannot create a bond with an unlock timestamp in the past");
123 | return Err(ProgramError::InvalidArgument);
124 | }
125 |
126 | let bond = BondV2Account::new(
127 | owner,
128 | *accounts.pool.key,
129 | pool.header.minimum_stake_amount,
130 | unlock_timestamp,
131 | );
132 |
133 | // Create bond account
134 | let seeds: &[&[u8]] = &[
135 | BondV2Account::SEED,
136 | &owner.to_bytes(),
137 | &accounts.pool.key.to_bytes(),
138 | &unlock_timestamp.unwrap_or(0).to_le_bytes(),
139 | &[bump_seed],
140 | ];
141 |
142 | Cpi::create_account(
143 | program_id,
144 | accounts.system_program,
145 | accounts.fee_payer,
146 | accounts.bond_v2_account,
147 | seeds,
148 | bond.borsh_len(),
149 | )?;
150 |
151 | bond.save(&mut accounts.bond_v2_account.data.borrow_mut())?;
152 | Ok(())
153 | }
154 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/create_central_state.rs:
--------------------------------------------------------------------------------
1 | //! Create central state
2 | use bonfida_utils::{BorshSize, InstructionsAccount};
3 | use borsh::{BorshDeserialize, BorshSerialize};
4 | use solana_program::{
5 | account_info::{next_account_info, AccountInfo},
6 | entrypoint::ProgramResult,
7 | msg,
8 | program_error::ProgramError,
9 | pubkey::Pubkey,
10 | system_program,
11 | };
12 |
13 | use crate::state::CentralState;
14 | use crate::{cpi::Cpi, error::AccessError};
15 |
16 | use crate::utils::{check_account_key, check_account_owner};
17 |
18 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
19 | /// The required parameters for the `create_central_state` instruction
20 | pub struct Params {
21 | // Daily inflation in token amount
22 | pub daily_inflation: u64,
23 | // Authority
24 | pub authority: Pubkey,
25 | }
26 |
27 | #[derive(InstructionsAccount)]
28 | /// The required accounts for the `create_central_state` instruction
29 | pub struct Accounts<'a, T> {
30 | /// The central state account
31 | #[cons(writable)]
32 | pub central_state: &'a T,
33 |
34 | /// The system program account
35 | pub system_program: &'a T,
36 |
37 | /// The fee payer account
38 | #[cons(writable, signer)]
39 | pub fee_payer: &'a T,
40 |
41 | /// The mint of the ACCESS token
42 | pub mint: &'a T,
43 | }
44 |
45 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
46 | pub fn parse(accounts: &'a [AccountInfo<'b>]) -> Result {
47 | let accounts_iter = &mut accounts.iter();
48 | let accounts = Accounts {
49 | central_state: next_account_info(accounts_iter)?,
50 | system_program: next_account_info(accounts_iter)?,
51 | fee_payer: next_account_info(accounts_iter)?,
52 | mint: next_account_info(accounts_iter)?,
53 | };
54 |
55 | // Check keys
56 | check_account_key(
57 | accounts.system_program,
58 | &system_program::ID,
59 | AccessError::WrongSystemProgram,
60 | )?;
61 |
62 | // Check ownership
63 | check_account_owner(
64 | accounts.central_state,
65 | &system_program::ID,
66 | AccessError::WrongOwner,
67 | )?;
68 |
69 | Ok(accounts)
70 | }
71 | }
72 |
73 | pub fn process_create_central_state(
74 | program_id: &Pubkey,
75 | accounts: &[AccountInfo],
76 | params: Params,
77 | ) -> ProgramResult {
78 | let accounts = Accounts::parse(accounts)?;
79 | let (derived_state_key, nonce) = CentralState::find_key(program_id);
80 |
81 | check_account_key(
82 | accounts.central_state,
83 | &derived_state_key,
84 | AccessError::AccountNotDeterministic,
85 | )?;
86 |
87 | let state = CentralState::new(
88 | nonce,
89 | params.daily_inflation,
90 | *accounts.mint.key,
91 | params.authority,
92 | 0,
93 | )?;
94 |
95 | msg!("+ Creating central state");
96 | Cpi::create_account(
97 | program_id,
98 | accounts.system_program,
99 | accounts.fee_payer,
100 | accounts.central_state,
101 | &[&program_id.to_bytes(), &[nonce]],
102 | state.borsh_len(),
103 | )?;
104 |
105 | state.save(&mut accounts.central_state.data.borrow_mut())?;
106 |
107 | Ok(())
108 | }
109 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/create_royalty_account.rs:
--------------------------------------------------------------------------------
1 | //! Create royalty account
2 | use bonfida_utils::{BorshSize, InstructionsAccount};
3 | use borsh::{BorshDeserialize, BorshSerialize};
4 | use solana_program::{
5 | account_info::{AccountInfo, next_account_info},
6 | entrypoint::ProgramResult,
7 | program_error::ProgramError,
8 | pubkey::Pubkey,
9 | system_program,
10 | };
11 |
12 | use crate::{cpi::Cpi, error::AccessError};
13 | use crate::instruction::ProgramInstruction::CreateRoyaltyAccount;
14 | use crate::state::CentralStateV2;
15 | use crate::state::RoyaltyAccount;
16 | use crate::utils::{check_account_key, check_account_owner, check_signer};
17 |
18 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
19 | /// The required parameters for the `create_royalty_account` instruction
20 | pub struct Params {
21 | // Royalty basis points
22 | pub royalty_basis_points: u16,
23 | // Expiration date
24 | pub expiration_date: u64,
25 | // The ATA that should be getting the ACS rewards
26 | pub royalty_ata: Pubkey,
27 | }
28 |
29 | #[derive(InstructionsAccount)]
30 | /// The required parameters for the `create_royalty_account` instruction
31 | pub struct Accounts<'a, T> {
32 | /// The royalty account to be created
33 | #[cons(writable)]
34 | pub royalty_account: &'a T,
35 |
36 | /// The fee payer account
37 | #[cons(writable, signer)]
38 | pub fee_payer: &'a T,
39 |
40 | /// The royalty payer
41 | #[cons(signer)]
42 | pub royalty_payer: &'a T,
43 |
44 | /// The system program account
45 | pub system_program: &'a T,
46 |
47 | /// The central state account
48 | pub central_state: &'a T,
49 | }
50 |
51 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
52 | pub fn parse(
53 | accounts: &'a [AccountInfo<'b>],
54 | program_id: &Pubkey,
55 | ) -> Result {
56 | let accounts_iter = &mut accounts.iter();
57 | let accounts = Accounts {
58 | royalty_account: next_account_info(accounts_iter)?,
59 | fee_payer: next_account_info(accounts_iter)?,
60 | royalty_payer: next_account_info(accounts_iter)?,
61 | system_program: next_account_info(accounts_iter)?,
62 | central_state: next_account_info(accounts_iter)?,
63 | };
64 |
65 | // Check keys
66 | check_account_key(
67 | accounts.system_program,
68 | &system_program::ID,
69 | AccessError::WrongSystemProgram,
70 | )?;
71 |
72 | // Check ownership
73 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
74 | check_account_owner(
75 | accounts.royalty_account,
76 | &system_program::ID,
77 | AccessError::WrongOwner,
78 | )?;
79 |
80 | check_signer(accounts.royalty_payer, AccessError::OwnerMustSign)?;
81 |
82 | Ok(accounts)
83 | }
84 | }
85 |
86 | pub fn process_create_royalty_account(
87 | program_id: &Pubkey,
88 | accounts: &[AccountInfo],
89 | params: Params,
90 | ) -> ProgramResult {
91 | let accounts = Accounts::parse(accounts, program_id)?;
92 |
93 | if params.royalty_basis_points > 10000 {
94 | return Err(ProgramError::InvalidInstructionData);
95 | }
96 |
97 | let central_state = CentralStateV2::from_account_info(accounts.central_state)?;
98 | central_state.assert_instruction_allowed(&CreateRoyaltyAccount)?;
99 |
100 | let (derived_royalty_key, bump_seed) = RoyaltyAccount::create_key(
101 | accounts.royalty_payer.key,
102 | program_id,
103 | );
104 |
105 | check_account_key(
106 | accounts.royalty_account,
107 | &derived_royalty_key,
108 | AccessError::AccountNotDeterministic,
109 | )?;
110 |
111 | let royalty_account = RoyaltyAccount::new(
112 | *accounts.fee_payer.key,
113 | *accounts.royalty_payer.key,
114 | params.royalty_ata,
115 | params.expiration_date,
116 | params.royalty_basis_points,
117 | );
118 |
119 | Cpi::create_account(
120 | program_id,
121 | accounts.system_program,
122 | accounts.fee_payer,
123 | accounts.royalty_account,
124 | &[
125 | RoyaltyAccount::SEED,
126 | &accounts.royalty_payer.key.to_bytes(),
127 | &[bump_seed],
128 | ],
129 | royalty_account.borsh_len(),
130 | )?;
131 |
132 | royalty_account.save(&mut accounts.royalty_account.data.borrow_mut())?;
133 |
134 | Ok(())
135 | }
136 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/create_stake_account.rs:
--------------------------------------------------------------------------------
1 | //! Create stake account
2 | use borsh::{BorshDeserialize, BorshSerialize};
3 | use solana_program::{
4 | account_info::{next_account_info, AccountInfo},
5 | entrypoint::ProgramResult,
6 | program_error::ProgramError,
7 | pubkey::Pubkey,
8 | system_program,
9 | };
10 |
11 | use crate::state::{StakeAccount, StakePool, Tag};
12 | use crate::{cpi::Cpi, error::AccessError};
13 |
14 | use bonfida_utils::{BorshSize, InstructionsAccount};
15 | use crate::instruction::ProgramInstruction::CreateStakeAccount;
16 |
17 | use crate::utils::{check_account_key, check_account_owner};
18 | use crate::state:: CentralStateV2;
19 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
20 | /// The required parameters for the `create_stake_account` instruction
21 | pub struct Params {
22 | // The PDA nonce
23 | pub nonce: u8,
24 | // Owner of the stake account
25 | pub owner: Pubkey,
26 | }
27 |
28 | #[derive(InstructionsAccount)]
29 | /// The required parameters for the `create_stake_account` instruction
30 | pub struct Accounts<'a, T> {
31 | /// The stake account
32 | #[cons(writable)]
33 | pub stake_account: &'a T,
34 |
35 | /// The system program account
36 | pub system_program: &'a T,
37 |
38 | /// The stake pool account
39 | pub stake_pool: &'a T,
40 |
41 | /// The fee payer account
42 | #[cons(writable, signer)]
43 | pub fee_payer: &'a T,
44 |
45 | /// The central state account
46 | pub central_state: &'a T,
47 | }
48 |
49 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
50 | pub fn parse(
51 | accounts: &'a [AccountInfo<'b>],
52 | program_id: &Pubkey,
53 | ) -> Result {
54 | let accounts_iter = &mut accounts.iter();
55 | let accounts = Accounts {
56 | stake_account: next_account_info(accounts_iter)?,
57 | system_program: next_account_info(accounts_iter)?,
58 | stake_pool: next_account_info(accounts_iter)?,
59 | fee_payer: next_account_info(accounts_iter)?,
60 | central_state: next_account_info(accounts_iter)?,
61 | };
62 |
63 | // Check keys
64 | check_account_key(
65 | accounts.system_program,
66 | &system_program::ID,
67 | AccessError::WrongSystemProgram,
68 | )?;
69 |
70 | // Check ownership
71 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
72 | check_account_owner(
73 | accounts.stake_account,
74 | &system_program::ID,
75 | AccessError::WrongOwner,
76 | )?;
77 | check_account_owner(accounts.stake_pool, program_id, AccessError::WrongOwner)?;
78 |
79 | Ok(accounts)
80 | }
81 | }
82 |
83 | pub fn process_create_stake_account(
84 | program_id: &Pubkey,
85 | accounts: &[AccountInfo],
86 | params: Params,
87 | ) -> ProgramResult {
88 | let accounts = Accounts::parse(accounts, program_id)?;
89 |
90 | let central_state = CentralStateV2::from_account_info(accounts.central_state)?;
91 | central_state.assert_instruction_allowed(&CreateStakeAccount)?;
92 | let stake_pool = StakePool::get_checked(accounts.stake_pool, vec![Tag::StakePool])?;
93 |
94 | let derived_stake_key = StakeAccount::create_key(
95 | ¶ms.nonce,
96 | ¶ms.owner,
97 | accounts.stake_pool.key,
98 | program_id,
99 | )?;
100 |
101 | check_account_key(
102 | accounts.stake_account,
103 | &derived_stake_key,
104 | AccessError::AccountNotDeterministic,
105 | )?;
106 |
107 | let stake_account = StakeAccount::new(
108 | params.owner,
109 | *accounts.stake_pool.key,
110 | stake_pool.header.minimum_stake_amount,
111 | );
112 |
113 | Cpi::create_account(
114 | program_id,
115 | accounts.system_program,
116 | accounts.fee_payer,
117 | accounts.stake_account,
118 | &[
119 | StakeAccount::SEED,
120 | ¶ms.owner.to_bytes(),
121 | &accounts.stake_pool.key.to_bytes(),
122 | &[params.nonce],
123 | ],
124 | stake_account.borsh_len(),
125 | )?;
126 |
127 | stake_account.save(&mut accounts.stake_account.data.borrow_mut())?;
128 |
129 | Ok(())
130 | }
131 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/create_stake_pool.rs:
--------------------------------------------------------------------------------
1 | //! Create stake pool
2 | use std::mem::size_of;
3 |
4 | use borsh::{BorshDeserialize, BorshSerialize};
5 | use solana_program::{
6 | account_info::{next_account_info, AccountInfo},
7 | entrypoint::ProgramResult,
8 | program_error::ProgramError,
9 | pubkey::Pubkey,
10 | system_program,
11 | };
12 |
13 | use crate::{
14 | cpi::Cpi,
15 | error::AccessError,
16 | state::{RewardsTuple, StakePoolHeader, Tag, STAKE_BUFFER_LEN},
17 | };
18 | use crate::{state::StakePool, utils::assert_valid_vault};
19 | use bonfida_utils::{BorshSize, InstructionsAccount};
20 | use crate::instruction::ProgramInstruction::CreateStakePool;
21 |
22 | use crate::utils::{check_account_key, check_account_owner};
23 | use crate::state:: CentralStateV2;
24 |
25 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
26 | /// The required parameters for the `create_stake_pool` instruction
27 | pub struct Params {
28 | // Minimum amount to stake
29 | pub minimum_stake_amount: u64,
30 | }
31 |
32 | #[derive(InstructionsAccount)]
33 | /// The required accounts for the `create_stake_pool` instruction
34 | pub struct Accounts<'a, T> {
35 | /// The stake pool account
36 | #[cons(writable)]
37 | pub stake_pool_account: &'a T,
38 |
39 | /// The system program account
40 | pub system_program: &'a T,
41 |
42 | /// The pool owner
43 | #[cons(signer)]
44 | pub owner: &'a T,
45 |
46 | /// The fee payer account
47 | #[cons(writable, signer)]
48 | pub fee_payer: &'a T,
49 |
50 | /// The stake pool vault account
51 | pub vault: &'a T,
52 |
53 | /// The central state account
54 | pub central_state: &'a T,
55 | }
56 |
57 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
58 | pub fn parse(
59 | accounts: &'a [AccountInfo<'b>],
60 | program_id: &Pubkey,
61 | ) -> Result {
62 | let accounts_iter = &mut accounts.iter();
63 | let accounts = Accounts {
64 | stake_pool_account: next_account_info(accounts_iter)?,
65 | system_program: next_account_info(accounts_iter)?,
66 | owner: next_account_info(accounts_iter)?,
67 | fee_payer: next_account_info(accounts_iter)?,
68 | vault: next_account_info(accounts_iter)?,
69 | central_state: next_account_info(accounts_iter)?,
70 | };
71 |
72 | // Check keys
73 | check_account_key(
74 | accounts.system_program,
75 | &system_program::ID,
76 | AccessError::WrongSystemProgram,
77 | )?;
78 |
79 | // Check ownership
80 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
81 | check_account_owner(
82 | accounts.stake_pool_account,
83 | &system_program::ID,
84 | AccessError::WrongOwner,
85 | )?;
86 | check_account_owner(
87 | accounts.vault,
88 | &spl_token::ID,
89 | AccessError::WrongTokenAccountOwner,
90 | )?;
91 |
92 | Ok(accounts)
93 | }
94 | }
95 |
96 | pub fn process_create_stake_pool(
97 | program_id: &Pubkey,
98 | accounts: &[AccountInfo],
99 | params: Params,
100 | ) -> ProgramResult {
101 | let accounts = Accounts::parse(accounts, program_id)?;
102 | let central_state = CentralStateV2::from_account_info(accounts.central_state)?;
103 | central_state.assert_instruction_allowed(&CreateStakePool)?;
104 |
105 | let (derived_stake_key, nonce) = StakePool::find_key(&accounts.owner.key, program_id);
106 |
107 | check_account_key(
108 | accounts.stake_pool_account,
109 | &derived_stake_key,
110 | AccessError::AccountNotDeterministic,
111 | )?;
112 |
113 | assert_valid_vault(accounts.vault, &derived_stake_key)?;
114 |
115 | let stake_pool_header = StakePoolHeader::new(
116 | *accounts.owner.key,
117 | nonce,
118 | *accounts.vault.key,
119 | params.minimum_stake_amount,
120 | )?;
121 |
122 | Cpi::create_account(
123 | program_id,
124 | accounts.system_program,
125 | accounts.fee_payer,
126 | accounts.stake_pool_account,
127 | &[StakePoolHeader::SEED, &accounts.owner.key.to_bytes(), &[nonce]],
128 | stake_pool_header.borsh_len() + size_of::() * STAKE_BUFFER_LEN as usize,
129 | )?;
130 |
131 | let mut stake_pool =
132 | StakePool::get_checked(accounts.stake_pool_account, vec![Tag::Uninitialized])?;
133 |
134 | *stake_pool.header = stake_pool_header;
135 |
136 | Ok(())
137 | }
138 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/edit_metadata.rs:
--------------------------------------------------------------------------------
1 | //! Edit metadata
2 | use bonfida_utils::{BorshSize, InstructionsAccount};
3 | use borsh::{BorshDeserialize, BorshSerialize};
4 | use mpl_token_metadata::{
5 | instruction::update_metadata_accounts_v2, pda::find_metadata_account, state::DataV2,
6 | };
7 | use solana_program::{
8 | account_info::{AccountInfo, next_account_info},
9 | entrypoint::ProgramResult,
10 | program::invoke_signed,
11 | program_error::ProgramError,
12 | pubkey::Pubkey,
13 | };
14 |
15 | use crate::{error::AccessError};
16 | use crate::instruction::ProgramInstruction::EditMetadata;
17 | use crate::state::V1_INSTRUCTIONS_ALLOWED;
18 | use crate::utils::{check_account_key, check_account_owner, check_signer};
19 | use crate::state:: CentralStateV2;
20 |
21 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
22 | /// The required parameters for the `edit_metadata` instruction
23 | pub struct Params {
24 | // The name of the token
25 | pub name: String,
26 | // The symbol of the token
27 | pub symbol: String,
28 | // The URI of the token logo
29 | pub uri: String,
30 | }
31 |
32 | #[derive(InstructionsAccount)]
33 | /// The required accounts for the `change_inflation` instruction
34 | pub struct Accounts<'a, T> {
35 | /// The central state account
36 | pub central_state: &'a T,
37 |
38 | /// The central state account authority
39 | #[cons(signer)]
40 | pub authority: &'a T,
41 |
42 | /// The metadata account
43 | #[cons(writable)]
44 | pub metadata: &'a T,
45 |
46 | /// The metadata program account
47 | pub metadata_program: &'a T,
48 | }
49 |
50 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
51 | pub fn parse(
52 | accounts: &'a [AccountInfo<'b>],
53 | program_id: &Pubkey,
54 | ) -> Result {
55 | let accounts_iter = &mut accounts.iter();
56 | let accounts = Accounts {
57 | central_state: next_account_info(accounts_iter)?,
58 | authority: next_account_info(accounts_iter)?,
59 | metadata: next_account_info(accounts_iter)?,
60 | metadata_program: next_account_info(accounts_iter)?,
61 | };
62 |
63 | // Check keys
64 | check_account_key(
65 | accounts.metadata_program,
66 | &mpl_token_metadata::ID,
67 | AccessError::WrongMplProgram,
68 | )?;
69 |
70 | // Check ownership
71 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
72 | check_account_owner(
73 | accounts.metadata,
74 | &mpl_token_metadata::ID,
75 | AccessError::WrongOwner,
76 | )?;
77 |
78 | // Check signer
79 | check_signer(
80 | accounts.authority,
81 | AccessError::CentralStateAuthorityMustSign,
82 | )?;
83 |
84 | Ok(accounts)
85 | }
86 | }
87 |
88 | pub fn process_edit_metadata(
89 | program_id: &Pubkey,
90 | accounts: &[AccountInfo],
91 | params: Params,
92 | ) -> ProgramResult {
93 | if !V1_INSTRUCTIONS_ALLOWED {
94 | return Err(AccessError::DeprecatedInstruction.into());
95 | }
96 |
97 | let accounts = Accounts::parse(accounts, program_id)?;
98 |
99 | let central_state = CentralStateV2::from_account_info(accounts.central_state)?;
100 | central_state.assert_instruction_allowed(&EditMetadata)?;
101 | let (metadata_key, _) = find_metadata_account(¢ral_state.token_mint);
102 |
103 | check_account_key(
104 | accounts.authority,
105 | ¢ral_state.authority,
106 | AccessError::WrongCentralStateAuthority,
107 | )?;
108 | check_account_key(
109 | accounts.metadata,
110 | &metadata_key,
111 | AccessError::AccountNotDeterministic,
112 | )?;
113 |
114 | let data = DataV2 {
115 | name: params.name,
116 | uri: params.uri,
117 | symbol: params.symbol,
118 | seller_fee_basis_points: 0,
119 | creators: None,
120 | collection: None,
121 | uses: None,
122 | };
123 |
124 | let ix = update_metadata_accounts_v2(
125 | *accounts.metadata_program.key,
126 | *accounts.metadata.key,
127 | *accounts.central_state.key,
128 | None,
129 | Some(data),
130 | None,
131 | Some(true),
132 | );
133 | invoke_signed(
134 | &ix,
135 | &[accounts.metadata.clone(), accounts.central_state.clone()],
136 | &[&[&program_id.to_bytes(), &[central_state.bump_seed]]],
137 | )?;
138 |
139 | Ok(())
140 | }
141 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/migrate_central_state_v2.rs:
--------------------------------------------------------------------------------
1 | //! Migrate the central state to the v2 format
2 | use std::mem::size_of;
3 |
4 | use bonfida_utils::{BorshSize, InstructionsAccount};
5 | use borsh::{BorshDeserialize, BorshSerialize};
6 | use solana_program::{
7 | account_info::{AccountInfo, next_account_info},
8 | entrypoint::ProgramResult,
9 | program::invoke,
10 | program_error::ProgramError,
11 | pubkey::Pubkey,
12 | rent::Rent,
13 | system_instruction,
14 | system_program,
15 | };
16 | use solana_program::sysvar::Sysvar;
17 |
18 | use crate::error::AccessError;
19 | use crate::state::{CentralState, CentralStateV2, FeeRecipient, MAX_FEE_RECIPIENTS};
20 | use crate::utils::{check_account_key, check_account_owner};
21 |
22 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
23 | /// The required parameters for the `migrate_central_state_v2` instruction
24 | pub struct Params {}
25 |
26 | #[derive(InstructionsAccount)]
27 | /// The required accounts for the `migrate_central_state_v2` instruction
28 | pub struct Accounts<'a, T> {
29 | /// The central state account
30 | #[cons(writable)]
31 | pub central_state: &'a T,
32 |
33 | /// The system program account
34 | pub system_program: &'a T,
35 |
36 | /// The fee payer account
37 | #[cons(writable, signer)]
38 | pub fee_payer: &'a T,
39 | }
40 |
41 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
42 | pub fn parse(
43 | accounts: &'a [AccountInfo<'b>],
44 | program_id: &Pubkey,
45 | ) -> Result {
46 | let accounts_iter = &mut accounts.iter();
47 | let accounts = Accounts {
48 | central_state: next_account_info(accounts_iter)?,
49 | system_program: next_account_info(accounts_iter)?,
50 | fee_payer: next_account_info(accounts_iter)?,
51 | };
52 |
53 | // Check keys
54 | check_account_key(
55 | accounts.system_program,
56 | &system_program::ID,
57 | AccessError::WrongSystemProgram,
58 | )?;
59 |
60 | // Check ownership
61 | check_account_owner(
62 | accounts.central_state,
63 | program_id,
64 | AccessError::WrongOwner,
65 | )?;
66 |
67 | Ok(accounts)
68 | }
69 | }
70 |
71 | pub fn process_migrate_central_state_v2(
72 | program_id: &Pubkey,
73 | accounts: &[AccountInfo],
74 | _params: Params,
75 | ) -> ProgramResult {
76 | let accounts = Accounts::parse(accounts, program_id)?;
77 |
78 | let central_state = CentralState::from_account_info(accounts.central_state)?;
79 |
80 | // Migrate data
81 | let state_v2 = CentralStateV2::from_central_state(central_state)?;
82 |
83 | // Resize account
84 | let new_data_len = state_v2.borsh_len() + size_of::() * MAX_FEE_RECIPIENTS;
85 | let new_minimum_balance = Rent::get()?.minimum_balance(new_data_len);
86 | let lamports_diff = new_minimum_balance
87 | .checked_sub(accounts.central_state.lamports())
88 | .ok_or(AccessError::Overflow)?;
89 |
90 | invoke(
91 | &system_instruction::transfer(
92 | accounts.fee_payer.key,
93 | accounts.central_state.key,
94 | lamports_diff),
95 | &[
96 | accounts.fee_payer.clone(),
97 | accounts.central_state.clone(),
98 | accounts.system_program.clone(),
99 | ],
100 | )?;
101 | accounts.central_state.realloc(new_data_len, false)?;
102 |
103 | // Save new data
104 | state_v2.save(&mut accounts.central_state.data.borrow_mut())?;
105 |
106 | Ok(())
107 | }
108 |
--------------------------------------------------------------------------------
/smart-contract/program/src/processor/sign_bond.rs:
--------------------------------------------------------------------------------
1 | //! Sign a bond
2 | //! This instruction is used by authorized sellers to approve the creation of a bond
3 | use bonfida_utils::{BorshSize, InstructionsAccount};
4 | use borsh::{BorshDeserialize, BorshSerialize};
5 | use solana_program::{
6 | account_info::{AccountInfo, next_account_info},
7 | entrypoint::ProgramResult,
8 | msg,
9 | program_error::ProgramError,
10 | pubkey::Pubkey,
11 | };
12 | use crate::state:: CentralStateV2;
13 | use crate::error::AccessError;
14 | use crate::instruction::ProgramInstruction::SignBond;
15 | use crate::state::{BOND_SIGNER_THRESHOLD, BondAccount, V1_INSTRUCTIONS_ALLOWED};
16 | use crate::utils::{assert_authorized_seller, check_account_owner, check_signer};
17 |
18 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
19 | /// The required parameters for the `sign_bond` instruction
20 | pub struct Params {
21 | seller_index: u64,
22 | }
23 |
24 | #[derive(InstructionsAccount)]
25 | /// The required accounts for the `sign_bond` instruction
26 | pub struct Accounts<'a, T> {
27 | #[cons(signer)]
28 | seller: &'a T,
29 | #[cons(writable)]
30 | bond_account: &'a T,
31 | central_state: &'a T,
32 | }
33 |
34 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
35 | pub fn parse(
36 | accounts: &'a [AccountInfo<'b>],
37 | program_id: &Pubkey,
38 | ) -> Result {
39 | let accounts_iter = &mut accounts.iter();
40 | let accounts = Accounts {
41 | seller: next_account_info(accounts_iter)?,
42 | bond_account: next_account_info(accounts_iter)?,
43 | central_state: next_account_info(accounts_iter)?,
44 | };
45 |
46 | // Check ownership
47 | check_account_owner(accounts.central_state, program_id, AccessError::WrongOwner)?;
48 | check_account_owner(accounts.bond_account, program_id, AccessError::WrongOwner)?;
49 |
50 | // Check signer
51 | check_signer(accounts.seller, AccessError::BondSellerMustSign)?;
52 |
53 | Ok(accounts)
54 | }
55 | }
56 |
57 | pub fn process_sign_bond(
58 | program_id: &Pubkey,
59 | accounts: &[AccountInfo],
60 | params: Params,
61 | ) -> ProgramResult {
62 | if !V1_INSTRUCTIONS_ALLOWED {
63 | return Err(AccessError::DeprecatedInstruction.into());
64 | }
65 |
66 | let accounts = Accounts::parse(accounts, program_id)?;
67 |
68 | let central_state = CentralStateV2::from_account_info(accounts.central_state)?;
69 | central_state.assert_instruction_allowed(&SignBond)?;
70 | let mut bond = BondAccount::from_account_info(accounts.bond_account, true)?;
71 | assert_authorized_seller(accounts.seller, params.seller_index as usize)?;
72 |
73 | if bond.sellers.len() == BOND_SIGNER_THRESHOLD as usize {
74 | msg!("There are enough signers already");
75 | return Err(AccessError::NoOp.into());
76 | }
77 |
78 | #[cfg(not(feature = "no-bond-signer"))]
79 | for current_seller in &bond.sellers {
80 | if accounts.seller.key == current_seller {
81 | msg!("The seller has already signed");
82 | return Err(AccessError::BondSellerAlreadySigner.into());
83 | }
84 | }
85 |
86 | bond.sellers.push(*accounts.seller.key);
87 |
88 | bond.save(&mut accounts.bond_account.data.borrow_mut())?;
89 |
90 | Ok(())
91 | }
92 |
--------------------------------------------------------------------------------
/smart-contract/program/tests/basic_functionality.rs:
--------------------------------------------------------------------------------
1 | use solana_program::pubkey::Pubkey;
2 | use solana_sdk::signature::{Keypair, Signer};
3 | use solana_test_framework::*;
4 | use access_protocol::state::Tag;
5 |
6 | use crate::common::test_runner::{INITIAL_SUPPLY, TestRunner};
7 |
8 | pub mod common;
9 |
10 | #[tokio::test]
11 | async fn change_inflation() {
12 | // Setup the token + basic accounts
13 | let mut tr = TestRunner::new(1_000_000).await.unwrap();
14 |
15 | // Set daily inflation - should fail as it is over 100% per year
16 | tr.change_inflation(INITIAL_SUPPLY /365 + 2).await.unwrap_err();
17 | // Check the inflation
18 | let stats = tr.central_state_stats().await.unwrap();
19 | assert_eq!(stats.account.daily_inflation, 1_000_000);
20 |
21 | // increase supply
22 | tr.sleep(1).await.unwrap();
23 | // Set daily inflation - should succeed
24 | tr.change_inflation(INITIAL_SUPPLY / 365).await.unwrap();
25 | // Check the inflation
26 | let stats = tr.central_state_stats().await.unwrap();
27 | assert_eq!(stats.account.daily_inflation, INITIAL_SUPPLY / 365);
28 | }
29 |
30 |
31 | #[tokio::test]
32 | async fn change_authority() {
33 | // Setup the token + basic accounts
34 | let mut tr = TestRunner::new(1_000_000).await.unwrap();
35 | // Change the authority
36 | let new_authority = Keypair::new();
37 | let stats = tr.central_state_stats().await.unwrap();
38 | println!("old authority: {:?}", stats.account.authority);
39 | tr.change_central_state_authority(&new_authority)
40 | .await
41 | .unwrap();
42 | // Check the authority
43 | let stats = tr.central_state_stats().await.unwrap();
44 | assert_eq!(stats.account.authority, new_authority.pubkey());
45 | }
46 |
47 | #[tokio::test]
48 | async fn zero_inflation_start() {
49 | // Setup the token + basic accounts
50 | let mut tr = TestRunner::new(0).await.unwrap();
51 | // Sleep for 5 days
52 | tr.sleep(5 * 86400).await.unwrap();
53 | // Create users
54 | let stake_pool_owner = tr.create_user_with_ata().await.unwrap();
55 | // Create a pool
56 | tr.create_pool(&stake_pool_owner, 10000)
57 | .await
58 | .unwrap();
59 | // Check the pool
60 | let stats = tr.pool_stats(stake_pool_owner.pubkey()).await.unwrap();
61 | assert_eq!(
62 | Pubkey::from(stats.header.owner).to_string(),
63 | stake_pool_owner.pubkey().to_string()
64 | );
65 | assert_eq!(stats.header.tag, Tag::InactiveStakePool as u8);
66 | // Activate stake pool
67 | tr.activate_stake_pool(&stake_pool_owner.pubkey())
68 | .await
69 | .unwrap();
70 | // Crank
71 | tr.crank_pool(&stake_pool_owner.pubkey()).await.unwrap();
72 | // Check the central state
73 | let stats = tr.central_state_stats().await.unwrap();
74 | assert_eq!(stats.account.last_snapshot_offset, 5);
75 | assert_eq!(stats.account.total_staked, 0);
76 | assert_eq!(stats.account.total_staked_snapshot, 0);
77 | }
--------------------------------------------------------------------------------
/smart-contract/program/tests/bonds.rs:
--------------------------------------------------------------------------------
1 | use solana_sdk::signer::Signer;
2 | use solana_test_framework::*;
3 |
4 | use crate::common::test_runner::TestRunner;
5 |
6 | pub mod common;
7 |
8 | #[tokio::test]
9 | async fn permissionless_claim() {
10 | // Setup the token + basic accounts
11 | let mut tr = TestRunner::new(1_000_000).await.unwrap();
12 | // Create users
13 | let stake_pool_owner = tr.create_user_with_ata().await.unwrap();
14 | let _staker = tr.create_user_with_ata().await.unwrap();
15 | // Create stake pool
16 | tr.create_pool(&stake_pool_owner, 10000)
17 | .await
18 | .unwrap();
19 | // Activate stake pool
20 | tr.activate_stake_pool(&stake_pool_owner.pubkey())
21 | .await
22 | .unwrap();
23 | // Create bond
24 | // tr.create_bond(&stake_pool_owner.pubkey(), &staker.pubkey(), 10000, 1).await.unwrap();
25 | // // Claim bond
26 | // tr.claim_bond(&stake_pool_owner.pubkey(), &staker.pubkey()).await.unwrap();
27 | }
28 |
29 | #[tokio::test]
30 | async fn signed_claim() {
31 | // Setup the token + basic accounts
32 | let mut tr = TestRunner::new(1_000_000).await.unwrap();
33 | // Create users
34 | let stake_pool_owner = tr.create_user_with_ata().await.unwrap();
35 | let staker = tr.create_user_with_ata().await.unwrap();
36 | // Mint to staker
37 | tr.mint(&staker.pubkey(), 100_000_000_000).await.unwrap();
38 | // Create stake pool
39 | tr.create_pool(&stake_pool_owner, 10000)
40 | .await
41 | .unwrap();
42 | // Activate stake pool
43 | tr.activate_stake_pool(&stake_pool_owner.pubkey())
44 | .await
45 | .unwrap();
46 | // Create real bond with quote amount
47 | tr.create_bond_with_quote(&stake_pool_owner.pubkey(), &staker.pubkey(), 10000, 200, 1)
48 | .await
49 | .unwrap();
50 | // Claim bond without signature should fail
51 | assert!(tr
52 | .claim_bond(&stake_pool_owner.pubkey(), &staker.pubkey())
53 | .await
54 | .is_err());
55 | // Claim bond with signature should succeed
56 | tr.claim_bond_with_quote(&stake_pool_owner.pubkey(), &staker)
57 | .await
58 | .unwrap();
59 | }
--------------------------------------------------------------------------------
/smart-contract/program/tests/claim-before-crank.rs:
--------------------------------------------------------------------------------
1 | use solana_sdk::signer::Signer;
2 |
3 | use solana_test_framework::*;
4 |
5 | pub mod common;
6 | use crate::common::test_runner::TestRunner;
7 |
8 | #[tokio::test]
9 | async fn claim_before_crank() {
10 | // Setup the token + basic accounts
11 | let mut tr = TestRunner::new(1_000_000).await.unwrap();
12 |
13 | // Create users
14 | let stake_pool_owner = tr.create_user_with_ata().await.unwrap();
15 | let stake_pool2_owner = tr.create_user_with_ata().await.unwrap();
16 | let staker = tr.create_user_with_ata().await.unwrap();
17 |
18 | // Mint
19 | tr.mint(&staker.pubkey(), 20_400).await.unwrap();
20 |
21 | // Setup stake pool on day 1 12:00
22 | tr.create_pool(&stake_pool_owner, 1000).await.unwrap();
23 | tr.activate_stake_pool(&stake_pool_owner.pubkey()).await.unwrap();
24 | tr.create_stake_account(&stake_pool_owner.pubkey(), &staker.pubkey()).await.unwrap();
25 |
26 | // Wait 1 hour
27 | tr.sleep(3600).await.unwrap();
28 |
29 | // Setup stake pool 2 on day 1 13:00
30 | tr.create_pool(&stake_pool2_owner, 1000).await.unwrap();
31 | tr.activate_stake_pool(&stake_pool2_owner.pubkey()).await.unwrap();
32 | tr.create_stake_account(&stake_pool2_owner.pubkey(), &staker.pubkey()).await.unwrap();
33 |
34 | // Stake to pool 1 and 2
35 | let token_amount = 10_000;
36 | tr.stake(&stake_pool_owner.pubkey(), &staker, token_amount).await.unwrap();
37 | tr.stake(&stake_pool2_owner.pubkey(), &staker, token_amount).await.unwrap();
38 |
39 | // wait until day 2 12:15
40 | tr.sleep(86400 - 2700).await.unwrap();
41 |
42 | // Crank pool 1 (+ implicitly the whole system)
43 | tr.crank_pool(&stake_pool_owner.pubkey()).await.unwrap();
44 |
45 | // Claim pool 1 rewards
46 | tr.claim_pool_rewards(&stake_pool_owner).await.unwrap();
47 |
48 | // Claim staker rewards in pool 1
49 |
50 | tr.claim_staker_rewards(&stake_pool_owner.pubkey(), &staker)
51 | .await
52 | .unwrap();
53 |
54 | // Claiming staker rewards in pool2 - no rewards yet
55 | assert!(tr.claim_staker_rewards(&stake_pool2_owner.pubkey(), &staker)
56 | .await
57 | .is_err());
58 |
59 | // Stake to pool 2 should fail
60 | let result = tr
61 | .stake(&stake_pool2_owner.pubkey(), &staker, token_amount)
62 | .await;
63 | assert!(result.is_err());
64 | tr.sleep(1).await.unwrap();
65 |
66 | // Crank pool 2
67 | tr.crank_pool(&stake_pool2_owner.pubkey()).await.unwrap();
68 |
69 | // wait until day 3 12:15
70 | tr.sleep(86400).await.unwrap();
71 |
72 | // Crank pool 1 (+ implicitly the whole system)
73 | let pool_stats = tr.pool_stats(stake_pool_owner.pubkey()).await.unwrap();
74 | assert_eq!(pool_stats.header.current_day_idx, 1);
75 | tr.crank_pool(&stake_pool_owner.pubkey()).await.unwrap();
76 | let pool_stats = tr.pool_stats(stake_pool_owner.pubkey()).await.unwrap();
77 | assert_eq!(pool_stats.header.current_day_idx, 2);
78 |
79 | // Claim staker rewards in pool2 - should fail (one day)
80 | let stake_account_stats = tr.stake_account_stats(staker.pubkey(), stake_pool2_owner.pubkey()).await.unwrap();
81 | assert_eq!(stake_account_stats.last_claimed_offset, 0);
82 |
83 | tr.claim_staker_rewards(&stake_pool2_owner.pubkey(), &staker)
84 | .await
85 | .unwrap_err();
86 |
87 | let staker_stats = tr.staker_stats(staker.pubkey()).await.unwrap();
88 | assert_eq!(staker_stats.balance, 250_000);
89 | let stake_account_stats = tr.stake_account_stats(staker.pubkey(), stake_pool2_owner.pubkey()).await.unwrap();
90 | assert_eq!(stake_account_stats.last_claimed_offset, 0);
91 |
92 | // Crank pool 2
93 | let pool_stats = tr.pool_stats(stake_pool2_owner.pubkey()).await.unwrap();
94 | assert_eq!(pool_stats.header.current_day_idx, 1);
95 | tr.crank_pool(&stake_pool2_owner.pubkey()).await.unwrap();
96 | let pool_stats = tr.pool_stats(stake_pool2_owner.pubkey()).await.unwrap();
97 | assert_eq!(pool_stats.header.current_day_idx, 2);
98 |
99 | // Claim staker rewards in pool2 - should succeed (another day)
100 | tr.sleep(1).await.unwrap();
101 | tr.claim_staker_rewards(&stake_pool2_owner.pubkey(), &staker)
102 | .await
103 | .unwrap();
104 | let stake_account_stats = tr.stake_account_stats(staker.pubkey(), stake_pool2_owner.pubkey()).await.unwrap();
105 | assert_eq!(stake_account_stats.last_claimed_offset, 2);
106 | let staker_stats = tr.staker_stats(staker.pubkey()).await.unwrap();
107 | assert_eq!(staker_stats.balance, 750_000);
108 | }
109 |
--------------------------------------------------------------------------------
/smart-contract/program/tests/common/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod test_runner;
2 | pub mod utils;
3 |
--------------------------------------------------------------------------------
/smart-contract/program/tests/common/utils.rs:
--------------------------------------------------------------------------------
1 | use std::str::FromStr;
2 |
3 | use solana_program::instruction::Instruction;
4 | use solana_program::program_pack::Pack;
5 | use solana_program::pubkey::Pubkey;
6 | use solana_program_test::{BanksClientError, ProgramTest, ProgramTestContext};
7 | use solana_sdk::{signature::Keypair, transaction::Transaction};
8 | use solana_sdk::account::Account;
9 | use solana_sdk::signature::Signer;
10 |
11 | use spl_token::state::Mint;
12 |
13 | // Utils
14 | // todo maybe don't sign everything with an authority here
15 | pub async fn sign_send_instructions(
16 | ctx: &mut ProgramTestContext,
17 | instructions: Vec,
18 | signers: Vec<&Keypair>,
19 | ) -> Result<(), BanksClientError> {
20 | let mut transaction = Transaction::new_with_payer(&instructions, Some(&ctx.payer.pubkey()));
21 | let mut payer_signers = vec![&ctx.payer];
22 | for s in signers {
23 | payer_signers.push(s);
24 | }
25 | transaction.partial_sign(&payer_signers, ctx.last_blockhash);
26 | ctx.banks_client.process_transaction(transaction).await
27 | }
28 |
29 | pub async fn sign_send_instructions_without_authority(
30 | ctx: &mut ProgramTestContext,
31 | instructions: Vec,
32 | signers: Vec<&Keypair>,
33 | ) -> Result<(), BanksClientError> {
34 | let mut transaction = Transaction::new_with_payer(&instructions, Some(&signers[0].pubkey()));
35 | transaction.partial_sign(&signers, ctx.last_blockhash);
36 | ctx.banks_client.process_transaction(transaction).await
37 | }
38 |
39 | pub fn mint_bootstrap(
40 | address: Option<&str>,
41 | decimals: u8,
42 | program_test: &mut ProgramTest,
43 | mint_authority: &Pubkey,
44 | ) -> (Pubkey, Mint) {
45 | let address = address
46 | .map(|s| Pubkey::from_str(s).unwrap())
47 | .unwrap_or_else(Pubkey::new_unique);
48 | let mint_info = Mint {
49 | mint_authority: Some(*mint_authority).into(),
50 | supply: 0,
51 | decimals,
52 | is_initialized: true,
53 | freeze_authority: None.into(),
54 | };
55 | let mut data = [0; Mint::LEN];
56 | mint_info.pack_into_slice(&mut data);
57 | program_test.add_account(
58 | address,
59 | Account {
60 | lamports: u32::MAX.into(),
61 | data: data.into(),
62 | owner: spl_token::ID,
63 | executable: false,
64 | ..Account::default()
65 | },
66 | );
67 | (address, mint_info)
68 | }
69 |
--------------------------------------------------------------------------------
/smart-contract/program/tests/common_stake_limit.rs:
--------------------------------------------------------------------------------
1 | use solana_sdk::signer::Signer;
2 | use solana_test_framework::*;
3 |
4 | use crate::common::test_runner::TestRunner;
5 |
6 | pub mod common;
7 |
8 | #[tokio::test]
9 | async fn common_stake_limit() {
10 | // Setup the token + basic accounts
11 | let mut tr = TestRunner::new(1_000_000_000).await.unwrap();
12 |
13 | // Create users
14 | let stake_pool_owner = tr.create_user_with_ata().await.unwrap();
15 | let staker = tr.create_user_with_ata().await.unwrap();
16 |
17 | // Mint
18 | tr.mint(&staker.pubkey(), 10_200).await.unwrap();
19 |
20 | // Create stake pool on day 1 12:00
21 | tr.create_pool(&stake_pool_owner, 1000)
22 | .await
23 | .unwrap();
24 |
25 | // Activate stake pool
26 | tr.activate_stake_pool(&stake_pool_owner.pubkey())
27 | .await
28 | .unwrap();
29 |
30 | // Create stake account
31 | tr.create_stake_account(&stake_pool_owner.pubkey(), &staker.pubkey())
32 | .await
33 | .unwrap();
34 |
35 | // try staking to pool 1 under the stake limit
36 | tr.stake(&stake_pool_owner.pubkey(), &staker, 999).await.unwrap_err();
37 |
38 | // try staking to pool 1 on the stake limit
39 | tr.stake(&stake_pool_owner.pubkey(), &staker, 1000)
40 | .await
41 | .unwrap();
42 |
43 | // unstake
44 | tr.unstake(&stake_pool_owner.pubkey(), &staker, 1000)
45 | .await
46 | .unwrap();
47 |
48 | // Create bond account
49 |
50 | tr.create_bond(
51 | &stake_pool_owner.pubkey(),
52 | &staker.pubkey(),
53 | 10_000,
54 | 1,
55 | 1,
56 | 1,
57 | )
58 | .await
59 | .unwrap();
60 |
61 | // Claim bond
62 | tr.claim_bond(&stake_pool_owner.pubkey(), &staker.pubkey())
63 | .await
64 | .unwrap();
65 |
66 | // staking under the stake limit still shouldn't work
67 | tr.stake(&stake_pool_owner.pubkey(), &staker, 999)
68 | .await
69 | .unwrap_err();
70 | }
71 |
--------------------------------------------------------------------------------
/smart-contract/program/tests/common_unstake_limit.rs:
--------------------------------------------------------------------------------
1 | use solana_sdk::signer::Signer;
2 | use solana_test_framework::*;
3 | pub mod common;
4 |
5 | use crate::common::test_runner::TestRunner;
6 |
7 | #[tokio::test]
8 | async fn common_unstake_limit() {
9 | // Setup the token + basic accounts
10 | let mut tr = TestRunner::new(1_000_000_000).await.unwrap();
11 |
12 | // Create users
13 | let stake_pool_owner = tr.create_user_with_ata().await.unwrap();
14 | let staker = tr.create_user_with_ata().await.unwrap();
15 |
16 | // Mint
17 | tr.mint(&staker.pubkey(), 10_200).await.unwrap();
18 |
19 | // Create stake pool on day 1 12:00
20 | tr.create_pool(&stake_pool_owner, 1000)
21 | .await
22 | .unwrap();
23 |
24 | // Activate stake pool
25 | tr.activate_stake_pool(&stake_pool_owner.pubkey())
26 | .await
27 | .unwrap();
28 |
29 | // Create stake account
30 | tr.create_stake_account(&stake_pool_owner.pubkey(), &staker.pubkey())
31 | .await
32 | .unwrap();
33 |
34 | // Stake to pool 1 on the stake limit
35 | tr.stake(&stake_pool_owner.pubkey(), &staker, 1100)
36 | .await
37 | .unwrap();
38 |
39 | // unstake under the pool minimum should fail
40 | let result = tr.unstake(&stake_pool_owner.pubkey(), &staker, 200).await;
41 | assert!(result.is_err());
42 | let pool_stats = tr.pool_stats(stake_pool_owner.pubkey()).await.unwrap();
43 | assert_eq!(pool_stats.header.total_staked, 1100);
44 |
45 | tr.sleep(1).await.unwrap();
46 |
47 | // unstake above the pool minimum should work
48 | tr.unstake(&stake_pool_owner.pubkey(), &staker, 100)
49 | .await
50 | .unwrap();
51 | let pool_stats = tr.pool_stats(stake_pool_owner.pubkey()).await.unwrap();
52 | assert_eq!(pool_stats.header.total_staked, 1000);
53 |
54 | tr.sleep(1).await.unwrap();
55 |
56 | // full unstake should work
57 | tr.unstake(&stake_pool_owner.pubkey(), &staker, 1000)
58 | .await
59 | .unwrap();
60 | let pool_stats = tr.pool_stats(stake_pool_owner.pubkey()).await.unwrap();
61 | assert_eq!(pool_stats.header.total_staked, 0);
62 |
63 | // change the pool minimum
64 | tr.change_pool_minimum(&stake_pool_owner, 9000)
65 | .await
66 | .unwrap();
67 |
68 | // try staking under the pool minimum, but above the staker minimum
69 | let result = tr.stake(&stake_pool_owner.pubkey(), &staker, 8999).await;
70 | assert!(result.is_err());
71 |
72 | // stake above the pool minimum should work
73 | tr.stake(&stake_pool_owner.pubkey(), &staker, 9000)
74 | .await
75 | .unwrap();
76 |
77 | // Create bond account
78 | tr.create_bond(&stake_pool_owner.pubkey(), &staker.pubkey(), 5_000, 1, 1, 1)
79 | .await
80 | .unwrap();
81 |
82 | // Claim bond
83 | tr.claim_bond(&stake_pool_owner.pubkey(), &staker.pubkey())
84 | .await
85 | .unwrap();
86 |
87 | // unstake under the common pool minimum should fail
88 | let result = tr.unstake(&stake_pool_owner.pubkey(), &staker, 5001).await;
89 | assert!(result.is_err());
90 |
91 | // unstake above the common pool minimum should fail as well
92 | tr.unstake(&stake_pool_owner.pubkey(), &staker, 5000)
93 | .await
94 | .unwrap_err();
95 |
96 | // full unstake should still be possible
97 | tr.unstake(&stake_pool_owner.pubkey(), &staker, 9000)
98 | .await
99 | .unwrap();
100 | }
101 |
--------------------------------------------------------------------------------
/smart-contract/program/tests/freeze_and_renounce.rs:
--------------------------------------------------------------------------------
1 | use solana_sdk::signature::{Keypair, Signer};
2 | use access_protocol::instruction::ProgramInstruction::AdminProgramFreeze;
3 | use access_protocol::utils::{get_freeze_mask, get_unfreeze_mask};
4 | use crate::common::test_runner::TestRunner;
5 |
6 | mod common;
7 |
8 | #[tokio::test]
9 | async fn program_freeze() {
10 | // Setup the token + basic accounts
11 | let mut tr = TestRunner::new(1_000_000).await.unwrap();
12 |
13 | // Create a fee authority account
14 | let fee_authority = Keypair::new();
15 | tr.get_sol(&fee_authority.pubkey(), 100_000_000)
16 | .await
17 | .unwrap();
18 |
19 | // Freeze the program
20 | let freeze_mask = get_freeze_mask(vec![]);
21 | println!("freeze mask: {:0128b}", freeze_mask);
22 | let staker = tr.create_user_with_ata().await.unwrap();
23 | tr.freeze_program(freeze_mask, None).await.unwrap();
24 | tr.sleep(1).await.unwrap();
25 | tr.mint(&staker.pubkey(), 729_999_999_999).await.unwrap_err();
26 |
27 | // Unfreeze the program
28 | let unfreeze_mask = get_unfreeze_mask(vec![]);
29 | println!("unfreeze mask: {:0128b}", unfreeze_mask);
30 | tr.freeze_program(unfreeze_mask, None).await.unwrap();
31 | tr.sleep(1).await.unwrap();
32 | tr.mint(&staker.pubkey(), 729_999_999_999).await.unwrap();
33 |
34 | // Try freezing with different authority
35 | tr.freeze_program(freeze_mask, Some(&fee_authority)).await.unwrap_err();
36 |
37 | // Set the authority
38 | tr.change_freeze_authority(&fee_authority).await.unwrap();
39 | tr.sleep(1).await.unwrap();
40 |
41 | // Partial freeze with the new authority should fail
42 | tr.freeze_program(1, Some(&fee_authority)).await.unwrap_err();
43 |
44 | // Full freeze with the new authority should work
45 | tr.freeze_program(0, Some(&fee_authority)).await.unwrap();
46 |
47 | // Freeze a specific instruction
48 | let freeze_mask = get_freeze_mask(vec![access_protocol::instruction::ProgramInstruction::AdminMint]);
49 | println!("freeze mask: {:0128b}", freeze_mask);
50 | tr.freeze_program(freeze_mask, None).await.unwrap();
51 | tr.sleep(1).await.unwrap();
52 | tr.mint(&staker.pubkey(), 729_999_999_999).await.unwrap_err();
53 | tr.change_inflation(1_100_000_000).await.unwrap();
54 |
55 | // Unfreeze a specific instruction
56 | let unfreeze_mask = get_unfreeze_mask(vec![access_protocol::instruction::ProgramInstruction::AdminMint]);
57 | println!("unfreeze mask: {:0128b}", unfreeze_mask);
58 | tr.freeze_program(unfreeze_mask, None).await.unwrap();
59 | tr.sleep(1).await.unwrap();
60 | tr.mint(&staker.pubkey(), 729_999_999_999).await.unwrap();
61 | tr.change_inflation(1_100_000_000).await.unwrap_err();
62 |
63 | // Freeze a specific instruction
64 | let freeze_mask = get_freeze_mask(vec![access_protocol::instruction::ProgramInstruction::AdminMint]);
65 | println!("freeze mask: {:0128b}", freeze_mask);
66 | tr.freeze_program(freeze_mask, None).await.unwrap();
67 | tr.sleep(1).await.unwrap();
68 | tr.mint(&staker.pubkey(), 729_999_999_999).await.unwrap_err();
69 | tr.change_inflation(1_100_000_000).await.unwrap();
70 |
71 | // Renounce freeze functionality
72 | tr.renounce(AdminProgramFreeze).await.unwrap();
73 | tr.sleep(1).await.unwrap();
74 |
75 | // Unfreeze should not work anymore
76 | let unfreeze_mask = get_unfreeze_mask(vec![]);
77 | println!("unfreeze mask: {:0128b}", unfreeze_mask);
78 | tr.freeze_program(unfreeze_mask, None).await.unwrap_err();
79 | }
--------------------------------------------------------------------------------
/smart-contract/program/tests/later_creations.rs:
--------------------------------------------------------------------------------
1 | use solana_sdk::signer::Signer;
2 | use solana_test_framework::*;
3 |
4 | use crate::common::test_runner::TestRunner;
5 |
6 | pub mod common;
7 |
8 | #[tokio::test]
9 | async fn later_pool_creation() {
10 | // Setup the token + basic accounts
11 | let mut tr = TestRunner::new(1_000_000).await.unwrap();
12 |
13 | // Create users
14 | let stake_pool_owner = tr.create_user_with_ata().await.unwrap();
15 | let staker = tr.create_user_with_ata().await.unwrap();
16 |
17 | // Mint
18 | tr.mint(&staker.pubkey(), 20_400).await.unwrap();
19 |
20 | // Create stake pool on day 1
21 | tr.create_pool(&stake_pool_owner, 1000)
22 | .await
23 | .unwrap();
24 | tr.activate_stake_pool(&stake_pool_owner.pubkey())
25 | .await
26 | .unwrap();
27 |
28 | // Create stake account
29 | tr.create_stake_account(&stake_pool_owner.pubkey(), &staker.pubkey())
30 | .await
31 | .unwrap();
32 |
33 | // Stake to pool 1
34 | let token_amount = 10_000;
35 | tr.stake(&stake_pool_owner.pubkey(), &staker, token_amount)
36 | .await
37 | .unwrap();
38 | let central_state_stats = tr.central_state_stats().await.unwrap();
39 | assert_eq!(central_state_stats.account.total_staked, token_amount);
40 |
41 | // Crank 10 times
42 | for _ in 0..10 {
43 | tr.sleep(86400).await.unwrap();
44 | tr.crank_pool(&stake_pool_owner.pubkey()).await.unwrap();
45 | }
46 |
47 | // Create a second pool
48 | let stake_pool_owner2 = tr.create_user_with_ata().await.unwrap();
49 | tr.create_pool(&stake_pool_owner2, 1000)
50 | .await
51 | .unwrap();
52 | tr.activate_stake_pool(&stake_pool_owner2.pubkey())
53 | .await
54 | .unwrap();
55 |
56 | // Create stake account
57 | tr.create_stake_account(&stake_pool_owner2.pubkey(), &staker.pubkey())
58 | .await
59 | .unwrap();
60 |
61 | // Stake to pool 2
62 | let token_amount = 10_000;
63 | tr.stake(&stake_pool_owner2.pubkey(), &staker, token_amount)
64 | .await
65 | .unwrap();
66 |
67 | // Crank 10 times
68 | for _ in 0..10 {
69 | tr.sleep(86400).await.unwrap();
70 | tr.crank_pool(&stake_pool_owner.pubkey()).await.unwrap();
71 | tr.crank_pool(&stake_pool_owner2.pubkey()).await.unwrap();
72 | }
73 |
74 | // Create a second staker
75 | let staker2 = tr.create_user_with_ata().await.unwrap();
76 | tr.mint(&staker2.pubkey(), 20_400).await.unwrap();
77 |
78 | // Create stake account
79 | tr.create_stake_account(&stake_pool_owner2.pubkey(), &staker2.pubkey())
80 | .await
81 | .unwrap();
82 |
83 | // Try to claim rewards as staker2 from pool2
84 | tr.stake(&stake_pool_owner2.pubkey(), &staker2, 10_000)
85 | .await
86 | .unwrap();
87 | tr.claim_staker_rewards(&stake_pool_owner2.pubkey(), &staker2)
88 | .await
89 | .unwrap();
90 |
91 | // Check balance
92 | let staker_stats = tr.staker_stats(staker2.pubkey()).await.unwrap();
93 | assert_eq!(staker_stats.balance, 10_200);
94 | }
95 |
--------------------------------------------------------------------------------
/smart-contract/program/tests/overflow.rs:
--------------------------------------------------------------------------------
1 | use solana_sdk::signer::Signer;
2 | use solana_test_framework::*;
3 |
4 | use crate::common::test_runner::TestRunner;
5 |
6 | pub mod common;
7 |
8 | mod basic_functionality {
9 | use super::*;
10 |
11 | #[tokio::test]
12 | async fn overflow() {
13 | // Setup the token + basic accounts
14 | let mut tr = TestRunner::new(1_000_000).await.unwrap();
15 | // Set daily inflation
16 | tr.change_inflation(5_479_452_000_000_000).await.unwrap();
17 | // Create pools
18 | let pool_owner = tr.create_user_with_ata().await.unwrap();
19 | let pool_owner2 = tr.create_user_with_ata().await.unwrap();
20 | tr.create_pool(&pool_owner, 1_000_000_000)
21 | .await
22 | .unwrap();
23 | tr.create_pool(&pool_owner2, 1_000_000_000)
24 | .await
25 | .unwrap();
26 | tr.activate_stake_pool(&pool_owner.pubkey()).await.unwrap();
27 | tr.activate_stake_pool(&pool_owner2.pubkey()).await.unwrap();
28 | // Create a staker
29 | let staker = tr.create_user_with_ata().await.unwrap();
30 | tr.mint(&staker.pubkey(), 6_000_000_000_000_000_000)
31 | .await
32 | .unwrap();
33 | tr.create_stake_account(&pool_owner.pubkey(), &staker.pubkey())
34 | .await
35 | .unwrap();
36 | tr.create_stake_account(&pool_owner2.pubkey(), &staker.pubkey())
37 | .await
38 | .unwrap();
39 | // Stake
40 | tr.stake(&pool_owner.pubkey(), &staker, 530_959_347_000_000)
41 | .await
42 | .unwrap();
43 | tr.stake(&pool_owner2.pubkey(), &staker, 704_776_720_000_000)
44 | .await
45 | .unwrap();
46 | // Wait 1 day
47 | tr.sleep(86400).await.unwrap();
48 | // Crank
49 | tr.crank_pool(&pool_owner.pubkey()).await.unwrap();
50 | // Pool claim
51 | tr.claim_pool_rewards(&pool_owner).await.unwrap();
52 | // check pool owner balance
53 | let owner_stats = tr.staker_stats(pool_owner.pubkey()).await.unwrap();
54 | assert_eq!(owner_stats.balance, 1177179469601839)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/smart-contract/program/tests/pool_settings.rs:
--------------------------------------------------------------------------------
1 | use solana_sdk::signer::Signer;
2 | use solana_test_framework::*;
3 |
4 | use crate::common::test_runner::TestRunner;
5 |
6 | pub mod common;
7 |
8 | #[tokio::test]
9 | async fn can_change_minimum_stake_amount() {
10 | // Setup the token + basic accounts
11 | let mut tr = TestRunner::new(1_000_000).await.unwrap();
12 | // Create users
13 | let stake_pool_owner = tr.create_user_with_ata().await.unwrap();
14 | // Create stake pool
15 | tr.create_pool(&stake_pool_owner, 10000)
16 | .await
17 | .unwrap();
18 | // Activate stake pool
19 | tr.activate_stake_pool(&stake_pool_owner.pubkey())
20 | .await
21 | .unwrap();
22 | // Change the minimum stake amount
23 | tr.change_pool_minimum(&stake_pool_owner, 1000)
24 | .await
25 | .unwrap();
26 | // Check the pool
27 | let stats = tr.pool_stats(stake_pool_owner.pubkey()).await.unwrap();
28 | assert_eq!(stats.header.minimum_stake_amount, 1000);
29 | }
30 |
31 | #[tokio::test]
32 | async fn can_change_stakers_part() {
33 | // Setup the token + basic accounts
34 | let mut tr = TestRunner::new(1_000_000).await.unwrap();
35 | // Create users
36 | let stake_pool_owner = tr.create_user_with_ata().await.unwrap();
37 | // Create stake pool
38 | tr.create_pool(&stake_pool_owner, 10000)
39 | .await
40 | .unwrap();
41 | // Activate stake pool
42 | tr.activate_stake_pool(&stake_pool_owner.pubkey())
43 | .await
44 | .unwrap();
45 | // Change the stakers part
46 | tr.change_pool_multiplier(&stake_pool_owner, 20)
47 | .await
48 | .unwrap();
49 | // Check the pool
50 | let stats = tr.pool_stats(stake_pool_owner.pubkey()).await.unwrap();
51 | assert_eq!(stats.header.stakers_part, 20);
52 | }
--------------------------------------------------------------------------------
/smart-contract/program/tests/pool_setup.rs:
--------------------------------------------------------------------------------
1 | use solana_sdk::signer::Signer;
2 | use solana_test_framework::*;
3 | use crate::common::test_runner::TestRunner;
4 |
5 | pub mod common;
6 |
7 | use solana_program::pubkey::Pubkey;
8 | use access_protocol::state::Tag;
9 |
10 | #[tokio::test]
11 | async fn create_and_activate_pool() {
12 | // Setup the token + basic accounts
13 | let mut tr = TestRunner::new(1_000_000).await.unwrap();
14 | // Create users
15 | let stake_pool_owner = tr.create_user_with_ata().await.unwrap();
16 | // Create stake pool on day 1 12:00
17 | tr.create_pool(&stake_pool_owner, 10000)
18 | .await
19 | .unwrap();
20 | // Check the pool
21 | let stats = tr.pool_stats(stake_pool_owner.pubkey()).await.unwrap();
22 | assert_eq!(
23 | Pubkey::from(stats.header.owner).to_string(),
24 | stake_pool_owner.pubkey().to_string()
25 | );
26 | assert_eq!(stats.header.tag, Tag::InactiveStakePool as u8);
27 | // Activate stake pool
28 | tr.activate_stake_pool(&stake_pool_owner.pubkey())
29 | .await
30 | .unwrap();
31 | // Check the pool
32 | let stats = tr.pool_stats(stake_pool_owner.pubkey()).await.unwrap();
33 | assert_eq!(
34 | Pubkey::from(stats.header.owner).to_string(),
35 | stake_pool_owner.pubkey().to_string()
36 | );
37 | assert_eq!(stats.header.tag, Tag::StakePool as u8);
38 | }
39 |
40 | #[tokio::test]
41 | async fn cannot_change_stakers_part_to_invalid() {
42 | // Setup the token + basic accounts
43 | let mut tr = TestRunner::new(1_000_000).await.unwrap();
44 | // Create users
45 | let stake_pool_owner = tr.create_user_with_ata().await.unwrap();
46 | // Create stake pool
47 | tr.create_pool(&stake_pool_owner, 10000)
48 | .await
49 | .unwrap();
50 | // Activate stake pool
51 | tr.activate_stake_pool(&stake_pool_owner.pubkey())
52 | .await
53 | .unwrap();
54 | // Try to change the stakers part to 0
55 | tr.change_pool_multiplier(&stake_pool_owner, 0)
56 | .await
57 | .unwrap();
58 | // Try to change the stakers part to 101
59 | let result = tr.change_pool_multiplier(&stake_pool_owner, 10000).await;
60 | assert!(result.is_err());
61 | }
62 |
63 | #[tokio::test]
64 | async fn cannot_be_activated_if_not_created() {
65 | // Setup the token + basic accounts
66 | let mut tr = TestRunner::new(1_000_000).await.unwrap();
67 | // Create users
68 | let stake_pool_owner = tr.create_user_with_ata().await.unwrap();
69 | // Activate stake pool
70 | let result = tr.activate_stake_pool(&stake_pool_owner.pubkey()).await;
71 | assert!(result.is_err());
72 | }
73 |
74 | #[tokio::test]
75 | async fn cannot_be_created_twice() {
76 | // Setup the token + basic accounts
77 | let mut tr = TestRunner::new(1_000_000).await.unwrap();
78 | // Create users
79 | let stake_pool_owner = tr.create_user_with_ata().await.unwrap();
80 | // Create stake pool
81 | tr.create_pool(&stake_pool_owner, 10000)
82 | .await
83 | .unwrap();
84 | // Try to create stake pool again
85 | let result = tr.create_pool(&stake_pool_owner, 1000).await;
86 | assert!(result.is_err());
87 | }
88 |
89 | #[tokio::test]
90 | async fn cannot_be_activated_twice() {
91 | // Setup the token + basic accounts
92 | let mut tr = TestRunner::new(1_000_000).await.unwrap();
93 | // Create users
94 | let stake_pool_owner = tr.create_user_with_ata().await.unwrap();
95 | // Create stake pool
96 | tr.create_pool(&stake_pool_owner, 10000)
97 | .await
98 | .unwrap();
99 | // Activate stake pool
100 | tr.activate_stake_pool(&stake_pool_owner.pubkey())
101 | .await
102 | .unwrap();
103 | tr.sleep(1).await.unwrap();
104 | // Try to activate stake pool again
105 | let result = tr.activate_stake_pool(&stake_pool_owner.pubkey()).await;
106 | assert!(result.is_err());
107 | }
108 |
109 | #[tokio::test]
110 | async fn cannot_be_called_before_activation() {
111 | // Setup the token + basic accounts
112 | let mut tr = TestRunner::new(1_000_000).await.unwrap();
113 | // Create users
114 | let stake_pool_owner = tr.create_user_with_ata().await.unwrap();
115 | let staker = tr.create_user_with_ata().await.unwrap();
116 | // Mint to staker
117 | tr.mint(&staker.pubkey(), 100_000_000_000).await.unwrap();
118 | // Create stake pool
119 | tr.create_pool(&stake_pool_owner, 10000)
120 | .await
121 | .unwrap();
122 | // Try to stake
123 | let result = tr.stake(&stake_pool_owner.pubkey(), &staker, 100000).await;
124 | assert!(result.is_err());
125 | // Try to create a bond
126 | let result = tr
127 | .create_bond(&stake_pool_owner.pubkey(), &staker.pubkey(), 10000, 1, 1, 1)
128 | .await;
129 | assert!(result.is_err());
130 | }
131 |
--------------------------------------------------------------------------------
/smart-contract/program/tests/protocol_fee.rs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | use solana_sdk::signature::Signer;
5 |
6 | use access_protocol::state::FeeRecipient;
7 |
8 | use crate::common::test_runner::TestRunner;
9 |
10 | pub mod common;
11 |
12 | #[tokio::test]
13 | async fn change_protocol_fee() {
14 | // Setup the token + basic accounts
15 | let mut tr = TestRunner::new(1_000_000).await.unwrap();
16 | let staker = tr.create_user_with_ata().await.unwrap();
17 | tr.setup_fee_split(vec![FeeRecipient {
18 | owner: staker.pubkey(),
19 | percentage: 100,
20 | }]).await.unwrap();
21 |
22 | let pool_owner = tr.create_user_with_ata().await.unwrap();
23 | tr.create_pool(&pool_owner, 10_000_000)
24 | .await
25 | .unwrap();
26 | tr.activate_stake_pool(&pool_owner.pubkey()).await.unwrap();
27 |
28 | let staker = tr.create_user_with_ata().await.unwrap();
29 | tr.mint(&staker.pubkey(), 100_000_000).await.unwrap();
30 | tr.create_stake_account(&pool_owner.pubkey(), &staker.pubkey())
31 | .await
32 | .unwrap();
33 |
34 | // Check the fee basis points
35 | let stats = tr.central_state_stats().await.unwrap();
36 | assert_eq!(stats.account.fee_basis_points, 200);
37 | tr.stake(&pool_owner.pubkey(), &staker, 10_000_000)
38 | .await
39 | .unwrap();
40 |
41 | // Set the fee basis points - should fail as it is over 100%
42 | tr.change_protocol_fee(10_001).await.unwrap_err();
43 | let stats = tr.central_state_stats().await.unwrap();
44 | assert_eq!(stats.account.fee_basis_points, 200);
45 | assert_eq!(stats.balance, 200_000);
46 |
47 | // Set the fee basis points - should succeed
48 | tr.sleep(1).await.unwrap();
49 | tr.change_protocol_fee(3_000).await.unwrap();
50 |
51 | // Check the fee basis points
52 | tr.stake(&pool_owner.pubkey(), &staker, 10_000_000)
53 | .await
54 | .unwrap();
55 | let stats = tr.central_state_stats().await.unwrap();
56 | assert_eq!(stats.account.fee_basis_points, 3_000);
57 | assert_eq!(stats.balance, 3_200_000);
58 | }
--------------------------------------------------------------------------------
/smart-contract/program/tests/renounce.rs:
--------------------------------------------------------------------------------
1 | use solana_sdk::signer::Signer;
2 | use access_protocol::instruction::ProgramInstruction::{AdminMint, ClaimRewards};
3 | use access_protocol::utils::{get_freeze_mask};
4 | use crate::common::test_runner::TestRunner;
5 |
6 | pub mod common;
7 |
8 | #[tokio::test]
9 | async fn program_freeze() {
10 | // Setup the token + basic accounts
11 | let mut tr = TestRunner::new(1_000_000).await.unwrap();
12 |
13 | // Renounce a specific instruction
14 | let freeze_mask = get_freeze_mask(vec![access_protocol::instruction::ProgramInstruction::AdminMint]);
15 | println!("freeze mask: {:0128b}", freeze_mask);
16 | tr.renounce(AdminMint).await.unwrap();
17 | tr.sleep(1).await.unwrap();
18 | let staker = tr.create_user_with_ata().await.unwrap();
19 | tr.mint(&staker.pubkey(), 729_999_999_999).await.unwrap_err();
20 |
21 | // Renounce an instruction that is not renouncable
22 | let freeze_mask = get_freeze_mask(vec![access_protocol::instruction::ProgramInstruction::AdminMint]);
23 | println!("freeze mask: {:0128b}", freeze_mask);
24 | tr.renounce(ClaimRewards).await.unwrap_err();
25 | tr.sleep(1).await.unwrap();
26 |
27 | // Renounce a instruction again
28 | let freeze_mask = get_freeze_mask(vec![access_protocol::instruction::ProgramInstruction::AdminMint]);
29 | println!("freeze mask: {:0128b}", freeze_mask);
30 | tr.renounce(AdminMint).await.unwrap_err();
31 | }
--------------------------------------------------------------------------------
/smart-contract/program/tests/repeated_claim.rs:
--------------------------------------------------------------------------------
1 | use solana_sdk::signer::Signer;
2 |
3 | use solana_test_framework::*;
4 |
5 | pub mod common;
6 | use crate::common::test_runner::TestRunner;
7 |
8 | #[tokio::test]
9 | async fn repeated_claim() {
10 | // Setup the token + basic accounts
11 | let mut tr = TestRunner::new(1_000_000).await.unwrap();
12 |
13 | // Create users
14 | let stake_pool_owner = tr.create_user_with_ata().await.unwrap();
15 | let stake_pool2_owner = tr.create_user_with_ata().await.unwrap();
16 | let staker = tr.create_user_with_ata().await.unwrap();
17 |
18 | // Mint
19 | tr.mint(&staker.pubkey(), 10_200).await.unwrap();
20 |
21 | // Create stake pool on day 1 12:00
22 | tr.create_pool(&stake_pool_owner, 1000)
23 | .await
24 | .unwrap();
25 |
26 | // // Activate stake pool
27 | tr.activate_stake_pool(&stake_pool_owner.pubkey())
28 | .await
29 | .unwrap();
30 |
31 | // Create stake account
32 | tr.create_stake_account(&stake_pool_owner.pubkey(), &staker.pubkey())
33 | .await
34 | .unwrap();
35 |
36 | // Wait 1 hour
37 | tr.sleep(3600).await.unwrap();
38 |
39 | // Create stake pool 2 on day 1 13:00
40 | tr.create_pool(&stake_pool2_owner, 1000)
41 | .await
42 | .unwrap();
43 |
44 | // Activate stake pool 2
45 | tr.activate_stake_pool(&stake_pool2_owner.pubkey())
46 | .await
47 | .unwrap();
48 |
49 | // Create stake account 2
50 | tr.create_stake_account(&stake_pool2_owner.pubkey(), &staker.pubkey())
51 | .await
52 | .unwrap();
53 |
54 | // Stake to pool 1
55 | let token_amount = 10_000;
56 | tr.stake(&stake_pool_owner.pubkey(), &staker, token_amount)
57 | .await
58 | .unwrap();
59 |
60 | // wait until day 2 12:15
61 | tr.sleep(86400 - 2700).await.unwrap();
62 |
63 | // Crank pool 1 (+ implicitly the whole system)
64 | tr.crank_pool(&stake_pool_owner.pubkey()).await.unwrap();
65 |
66 | // Claim pool 1 rewards
67 | tr.claim_pool_rewards(&stake_pool_owner).await.unwrap();
68 |
69 | // Claim staker rewards in pool 1
70 |
71 | tr.claim_staker_rewards(&stake_pool_owner.pubkey(), &staker)
72 | .await
73 | .unwrap();
74 |
75 | // Unstake from pool 1
76 | tr.unstake(&stake_pool_owner.pubkey(), &staker, token_amount)
77 | .await
78 | .unwrap();
79 |
80 | // Stake to pool 2 should fail
81 | let result = tr
82 | .stake(&stake_pool2_owner.pubkey(), &staker, token_amount)
83 | .await;
84 | assert!(result.is_err());
85 | tr.sleep(1).await.unwrap();
86 |
87 | // Crank pool 2
88 | tr.crank_pool(&stake_pool2_owner.pubkey()).await.unwrap();
89 |
90 | // Stake to pool 2 should succeed
91 | tr.stake(&stake_pool2_owner.pubkey(), &staker, token_amount)
92 | .await
93 | .unwrap();
94 | tr.sleep(1).await.unwrap();
95 |
96 | // Claim stake pool rewards 2
97 | assert!(tr.claim_pool_rewards(&stake_pool2_owner).await.is_err());
98 |
99 | // Claim rewards 2
100 | tr.claim_staker_rewards(&stake_pool2_owner.pubkey(), &staker)
101 | .await
102 | .unwrap();
103 |
104 | // Check results
105 | let stats = tr.staker_stats(staker.pubkey()).await.unwrap();
106 | assert_eq!(stats.balance, 499_800);
107 | let pool_stats = tr.pool_stats(stake_pool_owner.pubkey()).await.unwrap();
108 | assert_eq!(pool_stats.balance, 500_000);
109 | assert_eq!(pool_stats.header.total_staked, 0);
110 | let pool_stats2 = tr.pool_stats(stake_pool2_owner.pubkey()).await.unwrap();
111 | assert_eq!(pool_stats2.balance, 0);
112 | assert_eq!(pool_stats2.header.total_staked, 10_000);
113 | }
114 |
--------------------------------------------------------------------------------
/smart-contract/program/tests/rewards_bonds.rs:
--------------------------------------------------------------------------------
1 | use solana_sdk::signer::Signer;
2 | use solana_test_framework::*;
3 |
4 | use crate::common::test_runner::TestRunner;
5 |
6 | pub mod common;
7 |
8 | #[tokio::test]
9 | async fn rewards_bonds() {
10 | // Setup the token + basic accounts
11 | let mut tr = TestRunner::new(1_000_000).await.unwrap();
12 |
13 | // Create users
14 | let stake_pool_owner = tr.create_user_with_ata().await.unwrap();
15 | let staker = tr.create_user_with_ata().await.unwrap();
16 |
17 | // Mint
18 | tr.mint(&staker.pubkey(), 10_200).await.unwrap();
19 |
20 | // Create stake pool on day 1 12:00
21 | tr.create_pool(&stake_pool_owner, 1000)
22 | .await
23 | .unwrap();
24 |
25 | // // Activate stake pool
26 | tr.activate_stake_pool(&stake_pool_owner.pubkey())
27 | .await
28 | .unwrap();
29 |
30 | // Create stake account
31 | tr.create_stake_account(&stake_pool_owner.pubkey(), &staker.pubkey())
32 | .await
33 | .unwrap();
34 |
35 | // Stake to pool 1
36 | let token_amount = 10_000;
37 | tr.stake(&stake_pool_owner.pubkey(), &staker, token_amount)
38 | .await
39 | .unwrap();
40 | let central_state_stats = tr.central_state_stats().await.unwrap();
41 | assert_eq!(central_state_stats.account.total_staked, token_amount);
42 |
43 | // Create bond account
44 | tr.create_bond(
45 | &stake_pool_owner.pubkey(),
46 | &staker.pubkey(),
47 | 10_000,
48 | 1,
49 | 1,
50 | 1,
51 | )
52 | .await
53 | .unwrap();
54 | let central_state_stats = tr.central_state_stats().await.unwrap();
55 | assert_eq!(central_state_stats.account.total_staked, token_amount);
56 |
57 | // Claim bond
58 | tr.claim_bond(&stake_pool_owner.pubkey(), &staker.pubkey())
59 | .await
60 | .unwrap();
61 | let central_state_stats = tr.central_state_stats().await.unwrap();
62 | assert_eq!(central_state_stats.account.total_staked, 20_000);
63 |
64 | // wait until day 2 12:00
65 | tr.sleep(86400).await.unwrap();
66 |
67 | // Crank pool 1 (+ implicitly the whole system)
68 | tr.crank_pool(&stake_pool_owner.pubkey()).await.unwrap();
69 |
70 | // Claim pool 1 rewards
71 | tr.claim_pool_rewards(&stake_pool_owner).await.unwrap();
72 | let pool_stats = tr.pool_stats(stake_pool_owner.pubkey()).await.unwrap();
73 | assert_eq!(pool_stats.balance, 500_000);
74 |
75 | // Claim staker rewards in pool 1
76 | tr.claim_staker_rewards(&stake_pool_owner.pubkey(), &staker)
77 | .await
78 | .unwrap();
79 | let stats = tr.staker_stats(staker.pubkey()).await.unwrap();
80 | assert_eq!(stats.balance, 250_000);
81 |
82 | // Claim bond rewards
83 | tr.claim_bond_rewards(&stake_pool_owner.pubkey(), &staker)
84 | .await
85 | .unwrap();
86 | let stats = tr.staker_stats(staker.pubkey()).await.unwrap();
87 | assert_eq!(stats.balance, 500_000);
88 |
89 | // 23 hours later
90 | tr.sleep(82800).await.unwrap();
91 |
92 | // Crank should fail
93 | let crank_result = tr.crank_pool(&stake_pool_owner.pubkey()).await;
94 | assert!(crank_result.is_err());
95 |
96 | // Try to claim rewards again
97 | let result = tr
98 | .claim_bond_rewards(&stake_pool_owner.pubkey(), &staker)
99 | .await;
100 | assert!(result.is_err());
101 | tr.claim_staker_rewards(&stake_pool_owner.pubkey(), &staker)
102 | .await
103 | .unwrap();
104 | let result = tr.claim_pool_rewards(&stake_pool_owner).await;
105 | assert!(result.is_err());
106 |
107 | // check balances
108 | let stats = tr.staker_stats(staker.pubkey()).await.unwrap();
109 | assert_eq!(stats.balance, 500_000);
110 | let pool_stats = tr.pool_stats(stake_pool_owner.pubkey()).await.unwrap();
111 | assert_eq!(pool_stats.balance, 500_000);
112 |
113 | // 1 hour later
114 | tr.sleep(3600).await.unwrap();
115 |
116 | // Crank should succeed
117 | tr.crank_pool(&stake_pool_owner.pubkey()).await.unwrap();
118 |
119 | // Claim rewards again
120 | tr.claim_bond_rewards(&stake_pool_owner.pubkey(), &staker)
121 | .await
122 | .unwrap();
123 | let stats = tr.staker_stats(staker.pubkey()).await.unwrap();
124 | assert_eq!(stats.balance, 750_000);
125 | tr.claim_staker_rewards(&stake_pool_owner.pubkey(), &staker)
126 | .await
127 | .unwrap();
128 | let stats = tr.staker_stats(staker.pubkey()).await.unwrap();
129 | assert_eq!(stats.balance, 1_000_000);
130 | tr.claim_pool_rewards(&stake_pool_owner).await.unwrap();
131 | let pool_stats = tr.pool_stats(stake_pool_owner.pubkey()).await.unwrap();
132 | assert_eq!(pool_stats.balance, 1_000_000);
133 | }
134 |
--------------------------------------------------------------------------------
/smart-contract/program/tests/royalties.rs:
--------------------------------------------------------------------------------
1 |
2 |
3 | use solana_sdk::signer::Signer;
4 |
5 | use crate::common::test_runner::TestRunner;
6 |
7 | pub mod common;
8 |
9 | #[tokio::test]
10 | async fn program_freeze() {
11 | // Setup the token + basic accounts
12 | let mut tr = TestRunner::new(1_000_000).await.unwrap();
13 |
14 | // Create users
15 | let stake_pool_owner = tr.create_user_with_ata().await.unwrap();
16 | let recommender = tr.create_user_with_ata().await.unwrap();
17 | let staker = tr.create_user_with_ata().await.unwrap();
18 |
19 | let start_time = tr.get_current_time().await;
20 |
21 | // Staker accepts the invitation
22 | let fee_payer_balance = tr.fee_payer_sol_balance().await.unwrap();
23 | println!("Fee payer balance: {}", fee_payer_balance);
24 | tr.create_royalty(
25 | &staker,
26 | &recommender.pubkey(),
27 | 1000, // 10 %
28 | (start_time + 3 * 86_400) as u64,
29 | )
30 | .await
31 | .unwrap();
32 | let create_royalty_fee = fee_payer_balance - tr.fee_payer_sol_balance().await.unwrap();
33 |
34 | // Pool owner accepts the invitation
35 | tr.create_royalty(
36 | &stake_pool_owner,
37 | &recommender.pubkey(),
38 | 2000, // 20 %
39 | (start_time + 2 * 86_400 + 100) as u64,
40 | )
41 | .await
42 | .unwrap();
43 |
44 | // Mint
45 | let fee_payer_balance = tr.fee_payer_sol_balance().await.unwrap();
46 | println!("Fee payer balance: {}", fee_payer_balance);
47 | tr.mint(&staker.pubkey(), 10_200).await.unwrap();
48 | let mint_fee = fee_payer_balance - tr.fee_payer_sol_balance().await.unwrap();
49 | println!("Mint fee: {}", mint_fee);
50 |
51 | // Create stake pool on day 1
52 | tr.create_pool(&stake_pool_owner, 10_000)
53 | .await
54 | .unwrap();
55 |
56 | // Activate stake pool
57 | tr.activate_stake_pool(&stake_pool_owner.pubkey())
58 | .await
59 | .unwrap();
60 |
61 | // Create stake account
62 | tr.create_stake_account(&stake_pool_owner.pubkey(), &staker.pubkey())
63 | .await
64 | .unwrap();
65 |
66 | // Stake to pool 1
67 | tr.stake(&stake_pool_owner.pubkey(), &staker, 10_000)
68 | .await
69 | .unwrap();
70 |
71 | // Wait for 1 day
72 | tr.sleep(86400).await.unwrap();
73 | tr.crank_pool(&stake_pool_owner.pubkey()).await.unwrap();
74 |
75 | // Claim rewards
76 | tr.claim_staker_rewards(&stake_pool_owner.pubkey(), &staker)
77 | .await
78 | .unwrap();
79 | let stats = tr.staker_stats(staker.pubkey()).await.unwrap();
80 | assert_eq!(stats.balance, 450_000);
81 | let stats = tr.staker_stats(recommender.pubkey()).await.unwrap();
82 | assert_eq!(stats.balance, 50_000);
83 |
84 | // Claim pool rewards
85 | tr.claim_pool_rewards(&stake_pool_owner).await.unwrap();
86 | let stats = tr.pool_stats(stake_pool_owner.pubkey()).await.unwrap();
87 | assert_eq!(stats.balance, 400_000);
88 | let stats = tr.staker_stats(recommender.pubkey()).await.unwrap();
89 | assert_eq!(stats.balance, 150_000);
90 |
91 | // Staker closes the royalty account
92 | let fee_payer_balance = tr.fee_payer_sol_balance().await.unwrap();
93 | println!("Fee payer balance: {}", fee_payer_balance);
94 | tr.close_royalty(&staker).await.unwrap();
95 | let close_royalty_fee = tr.fee_payer_sol_balance().await.unwrap() - fee_payer_balance;
96 | assert_eq!(create_royalty_fee - 10_000, close_royalty_fee + 10_000);
97 |
98 | // Wait for 1 day
99 | tr.sleep(86400).await.unwrap();
100 | tr.crank_pool(&stake_pool_owner.pubkey()).await.unwrap();
101 |
102 | // Claim rewards
103 | tr.claim_staker_rewards(&stake_pool_owner.pubkey(), &staker)
104 | .await
105 | .unwrap();
106 | let stats = tr.staker_stats(staker.pubkey()).await.unwrap();
107 | assert_eq!(stats.balance, 950_000);
108 | let stats = tr.staker_stats(recommender.pubkey()).await.unwrap();
109 | assert_eq!(stats.balance, 150_000);
110 |
111 | // Claim pool rewards
112 | tr.claim_pool_rewards(&stake_pool_owner).await.unwrap();
113 | let stats = tr.pool_stats(stake_pool_owner.pubkey()).await.unwrap();
114 | assert_eq!(stats.balance, 800_000);
115 | let stats = tr.staker_stats(recommender.pubkey()).await.unwrap();
116 | assert_eq!(stats.balance, 250_000);
117 |
118 | // Wait for 1 day
119 | tr.sleep(86400).await.unwrap();
120 | tr.crank_pool(&stake_pool_owner.pubkey()).await.unwrap();
121 |
122 | // Claim staker rewards with a different royalty account
123 | tr.claim_staker_rewards(&stake_pool_owner.pubkey(), &staker)
124 | .await
125 | .unwrap();
126 | let stats = tr.staker_stats(staker.pubkey()).await.unwrap();
127 | assert_eq!(stats.balance, 950_000 + 500_000);
128 | let stats = tr.staker_stats(recommender.pubkey()).await.unwrap();
129 | assert_eq!(stats.balance, 250_000);
130 |
131 | // Claim pool rewards - the royalty account should be expired already
132 | tr.claim_pool_rewards(&stake_pool_owner).await.unwrap();
133 | let stats = tr.pool_stats(stake_pool_owner.pubkey()).await.unwrap();
134 | assert_eq!(stats.balance, 800_000 + 500_000);
135 | let stats = tr.staker_stats(recommender.pubkey()).await.unwrap();
136 | assert_eq!(stats.balance, 250_000);
137 |
138 |
139 | // Create a new staker
140 | let staker2 = tr.create_user_with_ata().await.unwrap();
141 | tr.mint(&staker2.pubkey(), 10_000).await.unwrap();
142 |
143 | // Create a bond V2 account
144 | tr.create_bond_v2(
145 | &staker2.pubkey(),
146 | &stake_pool_owner.pubkey(),
147 | None,
148 | )
149 | .await
150 | .unwrap();
151 |
152 | // Add to the bond V2
153 | // add to bond
154 | tr.add_to_bond_v2(
155 | &staker2,
156 | &staker2.pubkey(),
157 | &stake_pool_owner.pubkey(),
158 | 10_000,
159 | None,
160 | )
161 | .await
162 | .unwrap();
163 |
164 | // Create a new royalty account
165 | tr.create_royalty(
166 | &staker2,
167 | &recommender.pubkey(),
168 | 4000, // 40 %
169 | (start_time + 1000 * 86_400) as u64,
170 | )
171 | .await
172 | .unwrap();
173 |
174 | // Wait for 10 days
175 | for _ in 0..10 {
176 | tr.sleep(86400).await.unwrap();
177 | tr.crank_pool(&stake_pool_owner.pubkey()).await.unwrap();
178 | }
179 |
180 | // Claim staker 2 rewards
181 | tr.claim_bond_v2_rewards(&staker2, &stake_pool_owner.pubkey(), None)
182 | .await
183 | .unwrap();
184 | let stats = tr.staker_stats(staker2.pubkey()).await.unwrap();
185 | assert_eq!(stats.balance, 150_000 * 10);
186 | let stats = tr.staker_stats(recommender.pubkey()).await.unwrap();
187 | assert_eq!(stats.balance, 250_000 + 100_000 * 10);
188 | }
189 |
--------------------------------------------------------------------------------
/smart-contract/program/tests/v1_bonds.rs:
--------------------------------------------------------------------------------
1 | use solana_program::clock::SECONDS_PER_DAY;
2 | use solana_sdk::signer::Signer;
3 |
4 | use access_protocol::state::Tag::BondAccount;
5 |
6 | use crate::common::test_runner::TestRunner;
7 |
8 | pub mod common;
9 |
10 | #[tokio::test]
11 | async fn v1_bonds() {
12 | // Setup the token + basic accounts
13 | let mut tr = TestRunner::new(1_000_000).await.unwrap();
14 | // ---------------------------------------------------------------------------------------------
15 | // Unlockable bond
16 | // ---------------------------------------------------------------------------------------------
17 | // Create users
18 | let pool_owner = tr.create_user_with_ata().await.unwrap();
19 | let bond_recipient = tr.create_user_with_ata().await.unwrap();
20 | // Create stake pool
21 | tr.create_pool(&pool_owner, 10_000)
22 | .await
23 | .unwrap();
24 | // Activate stake pool
25 | tr.activate_stake_pool(&pool_owner.pubkey()).await.unwrap();
26 |
27 | let current_time = tr.get_current_time().await;
28 | let bond_amount = 20_000;
29 |
30 | // Create bond
31 | tr.create_bond(
32 | &pool_owner.pubkey(),
33 | &bond_recipient.pubkey(),
34 | bond_amount,
35 | 1,
36 | 5 * SECONDS_PER_DAY as i64,
37 | 1,
38 | )
39 | .await
40 | .unwrap();
41 | tr.claim_bond(&pool_owner.pubkey(), &bond_recipient.pubkey())
42 | .await
43 | .unwrap();
44 |
45 | let pool_stats = tr.pool_stats(pool_owner.pubkey()).await.unwrap();
46 | assert_eq!(pool_stats.header.total_staked, bond_amount);
47 | assert_eq!(pool_stats.vault, bond_amount);
48 | let central_state_stats = tr.central_state_stats().await.unwrap();
49 | assert_eq!(central_state_stats.account.total_staked, bond_amount);
50 | let bond = tr
51 | .bond_stats(bond_recipient.pubkey(), pool_owner.pubkey(), bond_amount)
52 | .await
53 | .unwrap();
54 | assert_eq!(bond.tag, BondAccount);
55 | assert_eq!(
56 | bond.unlock_start_date,
57 | current_time + 5 * SECONDS_PER_DAY as i64
58 | );
59 | assert_eq!(bond.stake_pool, tr.get_pool_pda(&pool_owner.pubkey()));
60 | assert_eq!(bond.total_staked, bond_amount);
61 | assert_eq!(bond.owner, bond_recipient.pubkey());
62 | assert_eq!(bond.last_claimed_offset, 0);
63 | assert_eq!(bond.pool_minimum_at_creation, 10_000);
64 |
65 | // Claim zero rewards
66 | let recipient_stats = tr.staker_stats(bond_recipient.pubkey()).await.unwrap();
67 | assert_eq!(recipient_stats.balance, 0);
68 | tr.claim_bond_rewards(&pool_owner.pubkey(), &bond_recipient)
69 | .await
70 | .unwrap_err();
71 | let recipient_stats = tr.staker_stats(bond_recipient.pubkey()).await.unwrap();
72 | assert_eq!(recipient_stats.balance, 0);
73 |
74 | // Claim rewards
75 | _ = tr.sleep(SECONDS_PER_DAY).await;
76 | tr.crank_pool(&pool_owner.pubkey()).await.unwrap();
77 | tr.claim_bond_rewards(&pool_owner.pubkey(), &bond_recipient)
78 | .await
79 | .unwrap();
80 | let recipient_stats = tr.staker_stats(bond_recipient.pubkey()).await.unwrap();
81 | assert_eq!(recipient_stats.balance, 500_000);
82 |
83 | // Try unlocking - shouldn't be possible
84 | tr.unlock_bond(&pool_owner.pubkey(), &bond_recipient)
85 | .await
86 | .unwrap_err();
87 |
88 | // Move 5 days to the future
89 | _ = tr.sleep(5 * SECONDS_PER_DAY).await;
90 | tr.crank_pool(&pool_owner.pubkey()).await.unwrap();
91 | // Unlocking should not be possible before reward claim
92 | tr.unlock_bond(&pool_owner.pubkey(), &bond_recipient)
93 | .await
94 | .unwrap_err();
95 | _ = tr.sleep(1).await;
96 | // Claim rewards
97 | tr.claim_bond_rewards(&pool_owner.pubkey(), &bond_recipient)
98 | .await
99 | .unwrap();
100 | // Unlocking should be possible now
101 | tr.unlock_bond(&pool_owner.pubkey(), &bond_recipient)
102 | .await
103 | .unwrap();
104 | // Check all the stats
105 | let recipient_stats = tr.staker_stats(bond_recipient.pubkey()).await.unwrap();
106 | assert_eq!(recipient_stats.balance, 2 * 500_000 + bond_amount);
107 | let pool_stats = tr.pool_stats(pool_owner.pubkey()).await.unwrap();
108 | assert_eq!(pool_stats.header.total_staked, 0);
109 | assert_eq!(pool_stats.vault, 0);
110 | let central_state_stats = tr.central_state_stats().await.unwrap();
111 | assert_eq!(central_state_stats.account.total_staked, 0);
112 | let bond = tr
113 | .bond_stats(bond_recipient.pubkey(), pool_owner.pubkey(), bond_amount)
114 | .await
115 | .unwrap();
116 | assert_eq!(bond.tag, BondAccount);
117 | assert_eq!(
118 | bond.unlock_start_date,
119 | current_time + 5 * SECONDS_PER_DAY as i64
120 | );
121 | assert_eq!(bond.stake_pool, tr.get_pool_pda(&pool_owner.pubkey()));
122 | assert_eq!(bond.total_staked, 0);
123 | assert_eq!(bond.owner, bond_recipient.pubkey());
124 | assert_eq!(bond.last_claimed_offset, 6);
125 | assert_eq!(bond.pool_minimum_at_creation, 10_000);
126 |
127 | // Second unlock should not be possible
128 | // fixme but it is - what does it do?
129 | tr.unlock_bond(&pool_owner.pubkey(), &bond_recipient)
130 | .await
131 | .unwrap();
132 | let recipient_stats = tr.staker_stats(bond_recipient.pubkey()).await.unwrap();
133 | assert_eq!(recipient_stats.balance, 2 * 500_000 + bond_amount);
134 | let pool_stats = tr.pool_stats(pool_owner.pubkey()).await.unwrap();
135 | assert_eq!(pool_stats.header.total_staked, 0);
136 | assert_eq!(pool_stats.vault, 0);
137 | let central_state_stats = tr.central_state_stats().await.unwrap();
138 | assert_eq!(central_state_stats.account.total_staked, 0);
139 | let bond = tr
140 | .bond_stats(bond_recipient.pubkey(), pool_owner.pubkey(), bond_amount)
141 | .await
142 | .unwrap();
143 | assert_eq!(bond.tag, BondAccount);
144 | assert_eq!(
145 | bond.unlock_start_date,
146 | current_time + 5 * SECONDS_PER_DAY as i64
147 | );
148 | assert_eq!(bond.stake_pool, tr.get_pool_pda(&pool_owner.pubkey()));
149 | assert_eq!(bond.total_staked, 0);
150 | assert_eq!(bond.owner, bond_recipient.pubkey());
151 | assert_eq!(bond.last_claimed_offset, 6);
152 | assert_eq!(bond.pool_minimum_at_creation, 10_000);
153 |
154 | // Claim rewards in another 5 days
155 | _ = tr.sleep(5 * SECONDS_PER_DAY).await;
156 | tr.crank_pool(&pool_owner.pubkey()).await.unwrap();
157 | tr.claim_bond_rewards(&pool_owner.pubkey(), &bond_recipient)
158 | .await
159 | .unwrap_err();
160 | }
161 |
--------------------------------------------------------------------------------