├── .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 | --------------------------------------------------------------------------------