import assert from "assert"; import { splStakePoolProgram } from "@coral-xyz/spl-stake-pool"; import { splTokenProgram } from "@coral-xyz/spl-token"; import { BN } from "@coral-xyz/anchor"; import { Keypair, LAMPORTS_PER_SOL, PublicKey, StakeProgram, STAKE_CONFIG_ID, SystemProgram, SYSVAR_CLOCK_PUBKEY, SYSVAR_RENT_PUBKEY, SYSVAR_STAKE_HISTORY_PUBKEY, VoteProgram, } from "@solana/web3.js"; import { SPL_STAKE_POOL_PROGRAM_ID, SPL_TOKEN_PROGRAM_ID } from "../constants"; import { createAta, createMint, createTokenAccount, getProvider, loadKp, sendAndConfirmTx, simulateTx, test, } from "../utils"; export async function stakePoolTests() { const provider = await getProvider(); const program = splStakePoolProgram({ provider, programId: SPL_STAKE_POOL_PROGRAM_ID, }); const tokenProgram = splTokenProgram({ provider, programId: SPL_TOKEN_PROGRAM_ID, }); const kp = await loadKp(); const VALIDATOR_LIST_COUNT = 10; const TRANSIENT_STAKE_SEED = 0; let stakePoolPk: PublicKey; let withdrawAuthorityPk: PublicKey; let poolMintPk: PublicKey; let managerPoolTokenAccountPk: PublicKey; let validatorListPk: PublicKey; let reserveStakePk: PublicKey; let stakeAccountPk: PublicKey; let voteAccountPk: PublicKey; let transientStakePk: PublicKey; let userPoolTokenAccountPk: PublicKey; async function initialize() { const stakePoolKp = new Keypair(); stakePoolPk = stakePoolKp.publicKey; const createStakePoolAccountIx = await program.account.stakePool.createInstruction(stakePoolKp, 5000); [withdrawAuthorityPk] = await PublicKey.findProgramAddress( [stakePoolPk.toBuffer(), Buffer.from("withdraw")], program.programId ); poolMintPk = await createMint(withdrawAuthorityPk); userPoolTokenAccountPk = await createAta(poolMintPk, kp.publicKey); // Fee account managerPoolTokenAccountPk = await createTokenAccount(poolMintPk); const validatorListKp = new Keypair(); validatorListPk = validatorListKp.publicKey; const createValidatorListAccountIx = await program.account.validatorList.createInstruction( validatorListKp, 5 + 4 + 73 * VALIDATOR_LIST_COUNT ); const reserveStakeKp = new Keypair(); reserveStakePk = reserveStakeKp.publicKey; const createReserveStakeAccountIxs = StakeProgram.createAccount({ authorized: { staker: withdrawAuthorityPk, withdrawer: withdrawAuthorityPk, }, fromPubkey: kp.publicKey, lamports: (await provider.connection.getMinimumBalanceForRentExemption( StakeProgram.space )) + LAMPORTS_PER_SOL * 11, stakePubkey: reserveStakePk, }).instructions; const initStakePoolIx = await program.methods .initialize( { denominator: new BN(10), numerator: new BN(1), }, { denominator: new BN(10), numerator: new BN(1), }, { denominator: new BN(10), numerator: new BN(1), }, 10, VALIDATOR_LIST_COUNT ) .accounts({ stakePool: stakePoolKp.publicKey, manager: kp.publicKey, staker: kp.publicKey, stakePoolWithdrawAuthority: withdrawAuthorityPk, validatorList: validatorListKp.publicKey, reserveStake: reserveStakePk, poolMint: poolMintPk, managerPoolAccount: managerPoolTokenAccountPk, tokenProgram: tokenProgram.programId, }) .instruction(); await sendAndConfirmTx( [ createStakePoolAccountIx, ...createReserveStakeAccountIxs, createValidatorListAccountIx, initStakePoolIx, ], [kp, stakePoolKp, validatorListKp, reserveStakeKp] ); } async function addValidatorToPool() { const voteAccountKp = new Keypair(); voteAccountPk = voteAccountKp.publicKey; const identityKp = new Keypair(); const createVoteAccountIxs = VoteProgram.createAccount({ fromPubkey: kp.publicKey, lamports: await provider.connection.getMinimumBalanceForRentExemption( VoteProgram.space ), voteInit: { authorizedVoter: kp.publicKey, authorizedWithdrawer: kp.publicKey, commission: 1, nodePubkey: identityKp.publicKey, }, votePubkey: voteAccountKp.publicKey, }).instructions; [stakeAccountPk] = await PublicKey.findProgramAddress( [voteAccountPk.toBuffer(), stakePoolPk.toBuffer()], program.programId ); [transientStakePk] = await PublicKey.findProgramAddress( [ Buffer.from("transient"), voteAccountPk.toBuffer(), stakePoolPk.toBuffer(), new BN(TRANSIENT_STAKE_SEED).toBuffer("le", 8), // Transient seed suffix start ], program.programId ); const addValidatorIx = await program.methods .addValidatorToPool() .accounts({ stakePool: stakePoolPk, staker: kp.publicKey, funder: kp.publicKey, stakePoolWithdraw: withdrawAuthorityPk, validatorList: validatorListPk, stake: stakeAccountPk, validator: voteAccountPk, rent: SYSVAR_RENT_PUBKEY, clock: SYSVAR_CLOCK_PUBKEY, sysvarStakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY, stakeConfig: STAKE_CONFIG_ID, systemProgram: SystemProgram.programId, stakeProgram: StakeProgram.programId, }) .instruction(); await sendAndConfirmTx( [...createVoteAccountIxs, addValidatorIx], [kp, voteAccountKp, identityKp] ); } async function removeValidatorFromPool() { const destinationStakeAccountKp = new Keypair(); const createDestinationStakeAccountIx = SystemProgram.createAccount({ fromPubkey: kp.publicKey, lamports: 0, newAccountPubkey: destinationStakeAccountKp.publicKey, programId: StakeProgram.programId, space: StakeProgram.space, }); const removeValidatorIx = await program.methods .removeValidatorFromPool() .accounts({ stakePool: stakePoolPk, staker: kp.publicKey, stakePoolWithdraw: withdrawAuthorityPk, newStakeAuthority: kp.publicKey, validatorList: validatorListPk, stakeAccount: stakeAccountPk, transientStakeAccount: transientStakePk, destinationStakeAccount: destinationStakeAccountKp.publicKey, clock: SYSVAR_CLOCK_PUBKEY, stakeProgram: StakeProgram.programId, }) .instruction(); await sendAndConfirmTx( [createDestinationStakeAccountIx, removeValidatorIx], [kp, destinationStakeAccountKp] ); } async function increaseValidatorStake() { await program.methods .increaseValidatorStake( new BN(LAMPORTS_PER_SOL), new BN(TRANSIENT_STAKE_SEED) ) .accounts({ stakePool: stakePoolPk, staker: kp.publicKey, stakePoolWithdrawAuthority: withdrawAuthorityPk, validatorList: validatorListPk, reserveStake: reserveStakePk, transientStake: transientStakePk, validatorStake: stakeAccountPk, validator: voteAccountPk, clock: SYSVAR_CLOCK_PUBKEY, rent: SYSVAR_RENT_PUBKEY, sysvarStakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY, stakeConfig: STAKE_CONFIG_ID, systemProgram: SystemProgram.programId, stakeProgram: StakeProgram.programId, }) .rpc(); } async function decreaseValidatorStake() { const decreaseValidatorStakeIx = await program.methods .decreaseValidatorStake( new BN(LAMPORTS_PER_SOL), new BN(TRANSIENT_STAKE_SEED + 1) ) .accounts({ stakePool: stakePoolPk, staker: kp.publicKey, stakePoolWithdrawAuthority: withdrawAuthorityPk, validatorList: validatorListPk, validatorStake: stakeAccountPk, transientStake: ( await PublicKey.findProgramAddress( [ Buffer.from("transient"), voteAccountPk.toBuffer(), stakePoolPk.toBuffer(), new BN(TRANSIENT_STAKE_SEED + 1).toBuffer("le", 8), ], program.programId ) )[0], clock: SYSVAR_CLOCK_PUBKEY, rent: SYSVAR_RENT_PUBKEY, systemProgram: SystemProgram.programId, stakeProgram: StakeProgram.programId, }) .instruction(); await simulateTx([decreaseValidatorStakeIx], [kp]); } async function setPreferredValidator() { await program.methods .setPreferredValidator({ deposit: {} }, null) .accounts({ stakePoolAddress: stakePoolPk, staker: kp.publicKey, validatorListAddress: validatorListPk, }) .rpc(); } async function updateValidatorListBalance() { await program.methods .updateValidatorListBalance(0, true) .accounts({ stakePool: stakePoolPk, stakePoolWithdrawAuthority: withdrawAuthorityPk, validatorListAddress: validatorListPk, reserveStake: reserveStakePk, clock: SYSVAR_CLOCK_PUBKEY, sysvarStakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY, stakeProgram: StakeProgram.programId, }) .rpc(); } async function updateStakePoolBalance() { await program.methods .updateStakePoolBalance() .accounts({ stakePool: stakePoolPk, withdrawAuthority: withdrawAuthorityPk, validatorListStorage: validatorListPk, reserveStake: reserveStakePk, managerFeeAccount: managerPoolTokenAccountPk, stakePoolMint: poolMintPk, tokenProgram: tokenProgram.programId, }) .rpc(); } async function cleanupRemovedValidatorEntries() { await program.methods .cleanupRemovedValidatorEntries() .accounts({ stakePool: stakePoolPk, validatorListStorage: validatorListPk, }) .rpc(); } async function depositStake() { const DEPOSIT_AMOUNT = LAMPORTS_PER_SOL; const [poolDepositAuthorityPk] = await PublicKey.findProgramAddress( [stakePoolPk.toBuffer(), Buffer.from("deposit")], program.programId ); const userStakeAccountKp = new Keypair(); const createUserStakeAccountIxs = StakeProgram.createAccount({ authorized: { staker: kp.publicKey, withdrawer: kp.publicKey }, fromPubkey: kp.publicKey, lamports: (await provider.connection.getMinimumBalanceForRentExemption( StakeProgram.space )) + DEPOSIT_AMOUNT, stakePubkey: userStakeAccountKp.publicKey, }).instructions; const delegateUserStakeAccountIxs = StakeProgram.delegate({ authorizedPubkey: kp.publicKey, stakePubkey: userStakeAccountKp.publicKey, votePubkey: voteAccountPk, }).instructions; const authorizeStakerIxs = StakeProgram.authorize({ authorizedPubkey: kp.publicKey, newAuthorizedPubkey: poolDepositAuthorityPk, stakeAuthorizationType: { index: 0 }, stakePubkey: userStakeAccountKp.publicKey, }).instructions; const authorizeWithdrawerIxs = StakeProgram.authorize({ authorizedPubkey: kp.publicKey, newAuthorizedPubkey: poolDepositAuthorityPk, stakeAuthorizationType: { index: 1 }, stakePubkey: userStakeAccountKp.publicKey, }).instructions; const depositStakeIx = await program.methods .depositStake() .accounts({ stakePool: stakePoolPk, validatorListStorage: validatorListPk, stakePoolDepositAuthority: poolDepositAuthorityPk, stakePoolWithdrawAuthority: withdrawAuthorityPk, depositStakeAddress: userStakeAccountKp.publicKey, validatorStakeAccount: stakeAccountPk, reserveStakeAccount: reserveStakePk, poolTokensTo: userPoolTokenAccountPk, managerFeeAccount: managerPoolTokenAccountPk, referrerPoolTokensAccount: managerPoolTokenAccountPk, poolMint: poolMintPk, clock: SYSVAR_CLOCK_PUBKEY, sysvarStakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY, tokenProgram: tokenProgram.programId, stakeProgram: StakeProgram.programId, }) .instruction(); await sendAndConfirmTx( [ ...createUserStakeAccountIxs, ...delegateUserStakeAccountIxs, ...authorizeStakerIxs, ...authorizeWithdrawerIxs, depositStakeIx, ], [kp, userStakeAccountKp] ); } async function withdrawStake() { const userStakeAccountKp = new Keypair(); const createUserStakeAccountIx = SystemProgram.createAccount({ fromPubkey: kp.publicKey, lamports: await provider.connection.getMinimumBalanceForRentExemption( StakeProgram.space ), newAccountPubkey: userStakeAccountKp.publicKey, programId: StakeProgram.programId, space: StakeProgram.space, }); const withdrawStakeIx = await program.methods .withdrawStake(new BN(1)) .accounts({ stakePool: stakePoolPk, validatorListStorage: validatorListPk, stakePoolWithdraw: withdrawAuthorityPk, stakeToSplit: stakeAccountPk, stakeToReceive: userStakeAccountKp.publicKey, userStakeAuthority: kp.publicKey, userTransferAuthority: kp.publicKey, userPoolTokenAccount: userPoolTokenAccountPk, managerFeeAccount: managerPoolTokenAccountPk, poolMint: poolMintPk, clock: SYSVAR_CLOCK_PUBKEY, tokenProgram: tokenProgram.programId, stakeProgram: StakeProgram.programId, }) .instruction(); await sendAndConfirmTx( [createUserStakeAccountIx, withdrawStakeIx], [kp, userStakeAccountKp] ); } async function setManager() { await program.methods .setManager() .accounts({ stakePool: stakePoolPk, manager: kp.publicKey, newManager: kp.publicKey, newFeeReceiver: managerPoolTokenAccountPk, }) .rpc(); } async function setFee() { await program.methods .setFee({ solReferral: [5] }) .accounts({ stakePool: stakePoolPk, manager: kp.publicKey, }) .rpc(); } async function setStaker() { await program.methods .setStaker() .accounts({ stakePool: stakePoolPk, setStakerAuthority: kp.publicKey, newStaker: kp.publicKey, }) .rpc(); } async function depositSol() { await program.methods .depositSol(new BN(1)) .accounts({ stakePool: stakePoolPk, stakePoolWithdrawAuthority: withdrawAuthorityPk, reserveStakeAccount: reserveStakePk, lamportsFrom: kp.publicKey, poolTokensTo: userPoolTokenAccountPk, managerFeeAccount: managerPoolTokenAccountPk, referrerPoolTokensAccount: managerPoolTokenAccountPk, poolMint: poolMintPk, systemProgram: SystemProgram.programId, tokenProgram: tokenProgram.programId, }) .rpc(); } async function setFundingAuthority() { await program.methods .setFundingAuthority({ stakeDeposit: {} }) .accounts({ stakePool: stakePoolPk, manager: kp.publicKey, }) .rpc(); } async function withdrawSol() { await program.methods .withdrawSol(new BN(1)) .accounts({ stakePool: stakePoolPk, stakePoolWithdrawAuthority: withdrawAuthorityPk, userTransferAuthority: kp.publicKey, poolTokensFrom: userPoolTokenAccountPk, reserveStakeAccount: reserveStakePk, lamportsTo: kp.publicKey, managerFeeAccount: managerPoolTokenAccountPk, poolMint: poolMintPk, clock: SYSVAR_CLOCK_PUBKEY, sysvarStakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY, stakeProgram: StakeProgram.programId, tokenProgram: tokenProgram.programId, }) .rpc(); } // TODO: this should work but it's not tested // async function createTokenMetadata() { // await program.methods.createTokenMetadata( // "acheron", // "ACH", // "https://github.com/acheroncrypto" // ).accounts({ // stakePool: stakePoolPk, // manager: kp.publicKey, // poolMint: poolMintPk, // payer: kp.publicKey, // tokenMetadata: , // mplTokenMetadata: , // systemProgram: SystemProgram.programId, // rent: SYSVAR_RENT_PUBKEY // }).rpc(); // } // TODO: this should work but it's not tested // async function updateTokenMetadata() { // await program.methods.updateTokenMetadata( // "acheron", // "ACH", // "https://twitter.com/acheroncrypto" // ).accounts({ // stakePool: stakePoolPk, // manager: kp.publicKey, // stakePoolWithdrawAuthority: withdrawAuthorityPk, // tokenMetadata: , // mplTokenMetadata: , // }).rpc(); // } async function fetchStakePool() { const stakePool = await program.account.stakePool.fetch(stakePoolPk); assert(stakePool.manager.equals(kp.publicKey)); } async function fetchValidatorList() { const validatorList = await program.account.validatorList.fetch( validatorListPk ); assert(validatorList.header.maxValidators === VALIDATOR_LIST_COUNT); } await test(initialize); await test(addValidatorToPool); await test(removeValidatorFromPool); // Re-adding validator for other tests to pass await test(addValidatorToPool); await test(increaseValidatorStake); await test(decreaseValidatorStake); await test(setPreferredValidator); await test(updateValidatorListBalance); await test(updateStakePoolBalance); await test(cleanupRemovedValidatorEntries); await test(depositStake); await test(withdrawStake); await test(setManager); await test(setFee); await test(setStaker); await test(depositSol); await test(setFundingAuthority); await test(withdrawSol); // await test(createTokenMetadata); // await test(updateTokenMetadata); await test(fetchStakePool); await test(fetchValidatorList); }