From 02f4aefca5e3871eee0f054452873481b317cf62 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Sat, 15 Jun 2024 19:03:55 -0400 Subject: [PATCH 01/40] added restaking program with tests --- solana/restaking-v2/README.md | 140 +++++ solana/restaking-v2/migrations/deploy.ts | 12 + .../programs/restaking-v2/Cargo.toml | 31 + .../programs/restaking-v2/Xargo.toml | 2 + .../programs/restaking-v2/src/lib.rs | 436 ++++++++++++++ .../programs/restaking-v2/src/tests.rs | 275 +++++++++ solana/restaking-v2/restaking-flow.png | Bin 0 -> 216808 bytes solana/restaking-v2/tests/constants.ts | 7 + solana/restaking-v2/tests/helper.ts | 129 ++++ solana/restaking-v2/tests/instructions.ts | 347 +++++++++++ solana/restaking-v2/tests/restaking.ts | 560 ++++++++++++++++++ 11 files changed, 1939 insertions(+) create mode 100644 solana/restaking-v2/README.md create mode 100644 solana/restaking-v2/migrations/deploy.ts create mode 100644 solana/restaking-v2/programs/restaking-v2/Cargo.toml create mode 100644 solana/restaking-v2/programs/restaking-v2/Xargo.toml create mode 100644 solana/restaking-v2/programs/restaking-v2/src/lib.rs create mode 100644 solana/restaking-v2/programs/restaking-v2/src/tests.rs create mode 100644 solana/restaking-v2/restaking-flow.png create mode 100644 solana/restaking-v2/tests/constants.ts create mode 100644 solana/restaking-v2/tests/helper.ts create mode 100644 solana/restaking-v2/tests/instructions.ts create mode 100644 solana/restaking-v2/tests/restaking.ts diff --git a/solana/restaking-v2/README.md b/solana/restaking-v2/README.md new file mode 100644 index 00000000..44d57c84 --- /dev/null +++ b/solana/restaking-v2/README.md @@ -0,0 +1,140 @@ +# Restaking + +The high level flow of the program is given in the image below. + +![Flow of restaking](./restaking-flow.png) + +## Accounts + +- Vaults: The vaults are created for each whitelisted token. Vaults + are token accounts. The authority of the account is a PDA which + means the program controls the vault and any debit from the vault + has to go through the smart contract. + +- Receipt Token Mint: The receipt token mint is a NFT which is the + seed for the PDA storing information about stake amout, validator + and rewards. For more information, refer: + https://docs.composable.finance/technology/solana-restaking/vaults/#receipt-token + +- Staking Params: This is a PDA which stores the staking parameters + and also is the authority to `Receipt Token Mint` and `Vaults`. + +- Vault Params: PDA which stores the vault params which are stake time + and service for which it is staked along with when rewards were + claimed. + +## Instructions + +When the contract is deployed, the `initialize` method is called where +the whitelisted tokens, admin key and the rewards +token mint is set. Initially the `guest_chain_initialization` is set to +false. Any update to the staking paramters can only be +done by the admin key. A token account is also created for the +rewards token mint which would distribute the rewards. Since the +authority is PDA, any debit from the account will happen only through +the contract (only in `claim` method for now). After that the users +can start staking. + +- `Deposit`: User can stake any of the whitelisted token. The tokens + are stored in the vault and receipt tokens are minted for the user. + A CPI (cross program invocation) call is made to the guest chain + program where the stake is updated for the validator specified. + +- `Withdrawal Request`: Users can request for withdrawal and after the + unbonding period gets over, the tokens would be withdrawn. In this method, + the receipt NFT would be transferred to an escrow account and the receipt + NFT token account would be closed. All the pending rewards are transferred + in this method and users wont be eligible for rewards during the unbonding + period. + +- `Cancel Withdrawal Request`: Withdrawal request set by the user can be + cancelled as long at they are under unbonding period or if the withdraw + has not been executed yet. They would get back their receipt token and + withdrawal request would be cancelled. + +- `Withdraw`: Users can only withdraw their tokens after the unbonding + period ends. When user wants to withdraw the tokens, final stake amount + is fetched from the guest chain. The receipt token is burnt. A CPI call + is made to the guest chain to update the stake accordingly. + +- `Claim Rewards`: Users can claim rewards without withdrawing their + stake. They would have to have to own the non fungible receipt + token to be eligible for claiming rewards. + +- `Set Service`: Once the bridge is live, users who had deposited before + can call this method to delegate their stake to the validator. Users + cannot withdraw or claim any rewards until they delegate their stake + to the validator. But this method wont be needed after the bridge is + live and would panic if called otherwise. + +- `Update Guest chain Initialization`: The admin would call this method + when the bridge is up and running. This would set `guest_chain_program_id` + with the specified program ID which would allow to make CPI calls during + deposit and set stake to validator. + +- `Update token Whitelist`: The admin can update the token whitelist. + Only callable by admin set during `initialize` method. + +- `Withdraw Reward Funds`: This method is only callable by admin to + withdraw all the funds from the reward token account. This is a + safety measure so it should be called only during emergency. + +- `Change admin Proposal`: A proposal set by the current admin for + changing the admin. A new admin is proposed by the existing admin + and the until the new admin approves it in `accept_admin_change`, + the admin wont be changed. + +- `Accept admin change`: The new admin set by the existing admin is + exepected to call this method. When the new admin calls this method, + the admin is changed. + +- `Update Staking Cap`: Method which sets the staking cap which limits + how much total stake can be set in the contract. This method expects + the staking cap to be higher than previous to execute successfully. + +## Verifying the code + +First, compile the programs code from the `emulated-light-client` Github +repository to get its bytecode. + + git clone https://github.com/ComposableFi/emulated-light-client.git + anchor build + +Now, install the [Ellipsis Labs verifiable +build](https://crates.io/crates/solana-verify) crate. + + cargo install solana-verify + +Get the executable hash of the bytecode from the Restaking program that was +compiled + + solana-verify get-executable-hash target/deploy/restaking.so + +Get the hash from the bytecode of the on-chain restaking program that you want +to verify + + solana-verify get-program-hash -u \ + 8n3FHwYxFgQCQc2FNFkwDUf9mcqupxXcCvgfHbApMLv3 + +**Note for multisig members:** If you want to verify the upgrade program buffer, +then you need to get the bytecode from the buffer account using the below +command. You can get the buffer account address from the squads. + + solana-verify get-buffer-hash -u + +If the hash outputs of those two commands match, the code in the +repository matches the on-chain programs code. + +## Note + +- Since the rewards are not implemented yet on the Guest Chain, a nil value is + returned for now. + +- Oracle interface is yet to be added to fetch the current price of staked + tokens as well as the governance token in the Guest Chain. + +- Users who have deposited before the Guest Chain is initialized can choose the + validator in one of three ways(Yet to be implemented): + - choose a validator randomly, + - choose a validator from the list of top 10 validators chosen by us or + - choose a particular validator. diff --git a/solana/restaking-v2/migrations/deploy.ts b/solana/restaking-v2/migrations/deploy.ts new file mode 100644 index 00000000..82fb175f --- /dev/null +++ b/solana/restaking-v2/migrations/deploy.ts @@ -0,0 +1,12 @@ +// Migrations are an early feature. Currently, they're nothing more than this +// single deploy script that's invoked from the CLI, injecting a provider +// configured from the workspace's Anchor.toml. + +const anchor = require("@coral-xyz/anchor"); + +module.exports = async function (provider) { + // Configure client to use the provider. + anchor.setProvider(provider); + + // Add your deploy script here. +}; diff --git a/solana/restaking-v2/programs/restaking-v2/Cargo.toml b/solana/restaking-v2/programs/restaking-v2/Cargo.toml new file mode 100644 index 00000000..f303993d --- /dev/null +++ b/solana/restaking-v2/programs/restaking-v2/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "restaking_v2" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "restaking_v2" + +[features] +# added so that we can compile this along with `solana-ibc` with mocks features. Currently unused. +mocks = [] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = { workspace = true, features = ["init-if-needed"] } +anchor-spl = { workspace = true, features = ["metadata"] } +solana-ibc = { workspace = true, features = ["cpi"] } +solana-program.workspace = true +solana-signature-verifier.workspace = true + +[dev-dependencies] +anchor-client.workspace = true +anyhow.workspace = true +spl-associated-token-account.workspace = true +spl-token.workspace = true diff --git a/solana/restaking-v2/programs/restaking-v2/Xargo.toml b/solana/restaking-v2/programs/restaking-v2/Xargo.toml new file mode 100644 index 00000000..475fb71e --- /dev/null +++ b/solana/restaking-v2/programs/restaking-v2/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs new file mode 100644 index 00000000..c0f96b57 --- /dev/null +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -0,0 +1,436 @@ +use anchor_lang::prelude::*; +use anchor_spl::{token::{Mint, Token, TokenAccount}, associated_token::AssociatedToken}; +use solana_ibc::program::SolanaIbc; + +declare_id!("BtegF7pQSriyP7gSkDpAkPDMvTS8wfajHJSmvcVoC7kg"); + +pub const COMMON_SEED: &[u8] = b"common"; +pub const ESCROW_SEED: &[u8] = b"escrow"; +pub const RECEIPT_SEED: &[u8] = b"receipt"; + +pub const RECEIPT_TOKEN_DECIMALS: u8 = 9; + +#[cfg(test)] +mod tests; + +#[program] +pub mod restaking_v2 { + use anchor_spl::token::{Burn, MintTo, Transfer}; + + use super::*; + + pub fn initialize( + ctx: Context, + whitelisted_tokens: Vec, + initial_validators: Vec, + guest_chain_program_id: Pubkey, + ) -> Result<()> { + msg!("Initializng Restaking program"); + + let common_state = &mut ctx.accounts.common_state; + + common_state.admin = ctx.accounts.admin.key(); + common_state.whitelisted_tokens = whitelisted_tokens; + common_state.validators = initial_validators; + common_state.guest_chain_program_id = guest_chain_program_id; + + Ok(()) + } + + /// Deposit tokens in the escrow and mint receipt tokens to the staker while updating the + /// stake for the validators on the guest chain. + /// + /// Fails if + /// - token to be staked is not whitelisted + /// - staker does not have enough tokens + /// - accounts needed to call guest chain program are missing + pub fn deposit(ctx: Context, amount: u64) -> Result<()> { + let common_state = &mut ctx.accounts.common_state; + + let stake_token_mint = &ctx.accounts.token_mint.key(); + + if common_state + .whitelisted_tokens + .iter() + .find(|&x| x == stake_token_mint) + .is_none() + { + return Err(error!(ErrorCodes::InvalidTokenMint)); + } + + if ctx.accounts.staker_token_account.amount < amount { + return Err(error!(ErrorCodes::NotEnoughTokensToStake)); + } + + let bump = ctx.bumps.common_state; + let seeds = [COMMON_SEED, core::slice::from_ref(&bump)]; + let seeds = seeds.as_ref(); + let seeds = core::slice::from_ref(&seeds); + + let transfer_ix = Transfer { + from: ctx.accounts.staker_token_account.to_account_info(), + to: ctx.accounts.escrow_token_account.to_account_info(), + authority: ctx.accounts.staker.to_account_info(), + }; + + let cpi_ctx = CpiContext::new(ctx.accounts.token_program.to_account_info(), transfer_ix); + + anchor_spl::token::transfer(cpi_ctx, amount)?; + + let mint_to_ix = MintTo { + mint: ctx.accounts.receipt_token_mint.to_account_info(), + to: ctx.accounts.staker_receipt_token_account.to_account_info(), + authority: common_state.to_account_info(), + }; + + let cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.token_program.to_account_info(), + mint_to_ix, + seeds, + ); + + anchor_spl::token::mint_to(cpi_ctx, amount)?; + + // Call guest chain program to update the stake equally + let stake_per_validator = amount / common_state.validators.len() as u64; + + let set_stake_ix = solana_ibc::cpi::accounts::SetStake { + sender: ctx.accounts.staker.to_account_info(), + chain: ctx.accounts.chain.to_account_info(), + trie: ctx.accounts.trie.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + instruction: ctx.accounts.instruction.to_account_info(), + }; + + let cpi_ctx = CpiContext::new( + ctx.accounts.guest_chain_program.to_account_info(), + set_stake_ix, + ); + + let set_stake_arg = common_state + .validators + .iter() + .map(|validator| { + ( + sigverify::ed25519::PubKey::from(validator.clone()), + stake_per_validator as i128, + ) + }) + .collect::>(); + + solana_ibc::cpi::update_stake(cpi_ctx, set_stake_arg)?; + + Ok(()) + } + + /// Withdraw tokens from the escrow and burn receipt tokens while updating the + /// stake for the validators on the guest chain. + /// + /// Fails if + /// - staker does not have enough receipt tokens to burn + /// - accounts needed to call guest chain program are missing + pub fn withdraw(ctx: Context, amount: u64) -> Result<()> { + let common_state = &mut ctx.accounts.common_state; + + let bump = ctx.bumps.common_state; + let seeds = [COMMON_SEED, core::slice::from_ref(&bump)]; + let seeds = seeds.as_ref(); + let seeds = core::slice::from_ref(&seeds); + + // Check if balance is enough + let staker_receipt_token_account = &ctx.accounts.staker_receipt_token_account; + + if staker_receipt_token_account.amount < amount { + return Err(error!(ErrorCodes::NotEnoughReceiptTokensToWithdraw)); + } + + let transfer_ix = Transfer { + from: ctx.accounts.escrow_token_account.to_account_info(), + to: ctx.accounts.staker_token_account.to_account_info(), + authority: common_state.to_account_info(), + }; + + let cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.token_program.to_account_info(), + transfer_ix, + seeds, + ); + + anchor_spl::token::transfer(cpi_ctx, amount)?; + + let burn_ix = Burn { + mint: ctx.accounts.receipt_token_mint.to_account_info(), + from: ctx.accounts.staker_receipt_token_account.to_account_info(), + authority: ctx.accounts.staker.to_account_info(), + }; + + let cpi_ctx = CpiContext::new(ctx.accounts.token_program.to_account_info(), burn_ix); + + anchor_spl::token::burn(cpi_ctx, amount)?; + + // Call guest chain program to update the stake equally + let stake_per_validator = (amount / common_state.validators.len() as u64) as i128; + + let set_stake_ix = solana_ibc::cpi::accounts::SetStake { + sender: ctx.accounts.staker.to_account_info(), + chain: ctx.accounts.chain.to_account_info(), + trie: ctx.accounts.trie.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + instruction: ctx.accounts.instruction.to_account_info(), + }; + + let cpi_ctx = CpiContext::new( + ctx.accounts.guest_chain_program.to_account_info(), + set_stake_ix, + ); + + let set_stake_arg = common_state + .validators + .iter() + .map(|validator| { + ( + sigverify::ed25519::PubKey::from(validator.clone()), + -stake_per_validator, + ) + }) + .collect::>(); + + solana_ibc::cpi::update_stake(cpi_ctx, set_stake_arg)?; + + Ok(()) + } + + /// Updating admin proposal created by the existing admin. Admin would only be changed + /// if the new admin accepts it in `accept_admin_change` instruction. + pub fn change_admin_proposal( + ctx: Context, + new_admin: Pubkey, + ) -> Result<()> { + let common_state = &mut ctx.accounts.common_state; + msg!( + "Proposal for changing Admin from {} to {}", + common_state.admin, + new_admin + ); + + common_state.new_admin_proposal = Some(new_admin); + Ok(()) + } + + /// Accepting new admin change signed by the proposed admin. Admin would be changed if the + /// proposed admin calls the method. Would fail if there is no proposed admin and if the + /// signer is not the proposed admin. + pub fn accept_admin_change(ctx: Context) -> Result<()> { + let common_state = &mut ctx.accounts.common_state; + let new_admin = common_state + .new_admin_proposal + .ok_or(ErrorCodes::NoProposedAdmin)?; + if new_admin != ctx.accounts.new_admin.key() { + return Err(error!(ErrorCode::ConstraintSigner)); + } + + msg!( + "Changing Admin from {} to {}", + common_state.admin, + common_state.new_admin_proposal.unwrap() + ); + common_state.admin = new_admin; + + Ok(()) + } + + /// Whitelists new tokens + /// + /// This method checks if any of the new token mints which are to be whitelisted + /// are already whitelisted. If they are the method fails to update the + /// whitelisted token list. + pub fn update_token_whitelist( + ctx: Context, + new_token_mints: Vec, + ) -> Result<()> { + let staking_params = &mut ctx.accounts.common_state; + + let contains_mint = new_token_mints + .iter() + .any(|token_mint| staking_params.whitelisted_tokens.contains(token_mint)); + + if contains_mint { + return Err(error!(ErrorCodes::TokenAlreadyWhitelisted)); + } + + staking_params + .whitelisted_tokens + .append(&mut new_token_mints.as_slice().to_vec()); + + Ok(()) + } + + /// Adds new validator who are part of social consensus + /// + /// This method checks if any of the new validators to be added are already part of + /// the set and if so, the method fails. + pub fn update_validator_list( + ctx: Context, + new_validators: Vec, + ) -> Result<()> { + let staking_params = &mut ctx.accounts.common_state; + + let contains_validator = new_validators + .iter() + .any(|validator| staking_params.validators.contains(validator)); + + if contains_validator { + return Err(error!(ErrorCodes::ValidatorAlreadyAdded)); + } + + staking_params + .validators + .append(&mut new_validators.as_slice().to_vec()); + + Ok(()) + } +} + +#[derive(Accounts)] +pub struct Initialize<'info> { + #[account(mut)] + pub admin: Signer<'info>, + + #[account(init, payer = admin, seeds = [COMMON_SEED], bump, space = 1024)] + pub common_state: Account<'info, CommonState>, + + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct Deposit<'info> { + #[account(mut)] + pub staker: Signer<'info>, + + #[account(mut, seeds = [COMMON_SEED], bump)] + pub common_state: Account<'info, CommonState>, + + pub token_mint: Account<'info, Mint>, + #[account(mut, token::authority = staker, token::mint = token_mint)] + pub staker_token_account: Account<'info, TokenAccount>, + + #[account(init_if_needed, payer = staker, seeds = [ESCROW_SEED, &token_mint.key().to_bytes()], bump, token::mint = token_mint, token::authority = common_state)] + pub escrow_token_account: Account<'info, TokenAccount>, + + #[account(init_if_needed, payer = staker, seeds = [RECEIPT_SEED, &token_mint.key().to_bytes()], bump, mint::authority = common_state, mint::decimals = RECEIPT_TOKEN_DECIMALS)] + pub receipt_token_mint: Account<'info, Mint>, + #[account(init_if_needed, payer = staker, associated_token::authority = staker, associated_token::mint = receipt_token_mint)] + pub staker_receipt_token_account: Account<'info, TokenAccount>, + + pub token_program: Program<'info, Token>, + pub associated_token_program: Program<'info, AssociatedToken>, + + pub system_program: Program<'info, System>, + + #[account(mut, seeds = [solana_ibc::CHAIN_SEED], bump, seeds::program = guest_chain_program)] + /// CHECK: + pub chain: UncheckedAccount<'info>, + + #[account(mut, seeds = [solana_ibc::TRIE_SEED], bump, seeds::program = guest_chain_program)] + /// CHECK: + pub trie: UncheckedAccount<'info>, + + pub guest_chain_program: Program<'info, SolanaIbc>, + + /// The Instructions sysvar. + /// + /// CHECK: The account is passed on during CPI and destination contract + /// performs the validation so this is safe even if we don’t check the + /// address. Nonetheless, the account is checked at each use. + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + /// CHECK: + pub instruction: UncheckedAccount<'info>, +} + +#[derive(Accounts)] +pub struct Withdraw<'info> { + #[account(mut)] + pub staker: Signer<'info>, + + #[account(mut, seeds = [COMMON_SEED], bump)] + pub common_state: Account<'info, CommonState>, + + pub token_mint: Account<'info, Mint>, + #[account(mut, token::authority = staker, token::mint = token_mint)] + pub staker_token_account: Account<'info, TokenAccount>, + + #[account(mut, seeds = [ESCROW_SEED, &token_mint.key().to_bytes()], bump, token::mint = token_mint, token::authority = common_state)] + pub escrow_token_account: Account<'info, TokenAccount>, + + #[account(mut, seeds = [RECEIPT_SEED, &token_mint.key().to_bytes()], bump, mint::authority = common_state, mint::decimals = RECEIPT_TOKEN_DECIMALS)] + pub receipt_token_mint: Account<'info, Mint>, + #[account(mut, token::authority = staker, token::mint = receipt_token_mint)] + pub staker_receipt_token_account: Account<'info, TokenAccount>, + + pub token_program: Program<'info, Token>, + + pub system_program: Program<'info, System>, + + #[account(mut, seeds = [solana_ibc::CHAIN_SEED], bump, seeds::program = guest_chain_program)] + /// CHECK: + pub chain: UncheckedAccount<'info>, + + #[account(mut, seeds = [solana_ibc::TRIE_SEED], bump, seeds::program = guest_chain_program)] + /// CHECK: + pub trie: UncheckedAccount<'info>, + + pub guest_chain_program: Program<'info, SolanaIbc>, + + /// The Instructions sysvar. + /// + /// CHECK: The account is passed on during CPI and destination contract + /// performs the validation so this is safe even if we don’t check the + /// address. Nonetheless, the account is checked at each use. + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub instruction: UncheckedAccount<'info>, +} + +#[derive(Accounts)] +pub struct UpdateStakingParams<'info> { + #[account(mut)] + pub admin: Signer<'info>, + + #[account(mut, seeds = [COMMON_SEED], bump, has_one = admin)] + pub common_state: Account<'info, CommonState>, +} + +#[derive(Accounts)] +pub struct UpdateAdmin<'info> { + #[account(mut)] + pub new_admin: Signer<'info>, + + #[account(mut, seeds = [COMMON_SEED], bump)] + pub common_state: Account<'info, CommonState>, +} + +#[account] +pub struct CommonState { + pub admin: Pubkey, + pub whitelisted_tokens: Vec, + pub validators: Vec, + pub guest_chain_program_id: Pubkey, + pub new_admin_proposal: Option, +} + +#[error_code] +pub enum ErrorCodes { + #[msg("No proposed admin")] + NoProposedAdmin, + #[msg("Signer is not the proposed admin")] + ConstraintSigner, + #[msg("Only whitelisted tokens can be minted")] + InvalidTokenMint, + #[msg("Not enough receipt token to withdraw")] + NotEnoughReceiptTokensToWithdraw, + #[msg("Not enough tokens to stake")] + NotEnoughTokensToStake, + #[msg("Token is already whitelisted")] + TokenAlreadyWhitelisted, + #[msg("Validator is already added")] + ValidatorAlreadyAdded, +} diff --git a/solana/restaking-v2/programs/restaking-v2/src/tests.rs b/solana/restaking-v2/programs/restaking-v2/src/tests.rs new file mode 100644 index 00000000..aa56b2bd --- /dev/null +++ b/solana/restaking-v2/programs/restaking-v2/src/tests.rs @@ -0,0 +1,275 @@ +use std::rc::Rc; +use std::thread::sleep; +use std::time::Duration; + +use anchor_client::solana_client::rpc_client::RpcClient; +use anchor_client::solana_client::rpc_config::RpcSendTransactionConfig; +use anchor_client::solana_sdk::commitment_config::CommitmentConfig; +use anchor_client::solana_sdk::pubkey::Pubkey; +use anchor_client::solana_sdk::signature::{Keypair, Signature, Signer}; +use anchor_client::{Client, Cluster}; +use anchor_lang::solana_program::system_instruction::create_account; +use anchor_spl::associated_token::get_associated_token_address; +use anyhow::Result; +use spl_token::instruction::initialize_mint2; + +const MINT_AMOUNT: u64 = 1000000000000; +const STAKE_AMOUNT: u64 = 100000; + +fn airdrop(client: &RpcClient, account: Pubkey, lamports: u64) -> Signature { + let balance_before = client.get_balance(&account).unwrap(); + println!("This is balance before {}", balance_before); + let airdrop_signature = client.request_airdrop(&account, lamports).unwrap(); + sleep(Duration::from_secs(2)); + println!("This is airdrop signature {}", airdrop_signature); + + let balance_after = client.get_balance(&account).unwrap(); + println!("This is balance after {}", balance_after); + assert_eq!(balance_before + lamports, balance_after); + airdrop_signature +} + +#[test] +#[ignore = "Requires local validator to run"] +fn restaking_test_deliver() -> Result<()> { + let authority = Rc::new(Keypair::new()); + println!("This is pubkey {}", authority.pubkey().to_string()); + let lamports = 2_000_000_000; + + let client = Client::new_with_options( + Cluster::Localnet, + authority.clone(), + CommitmentConfig::processed(), + ); + let program = client.program(crate::ID).unwrap(); + + let sol_rpc_client = program.rpc(); + let _airdrop_signature = + airdrop(&sol_rpc_client, authority.pubkey(), lamports); + + let common_state = + Pubkey::find_program_address(&[crate::COMMON_SEED], &crate::ID).0; + + /* + * Creating Token Mint + */ + println!("\nCreating a token mint"); + + let token_mint = Keypair::new(); + let token_mint_key = token_mint.pubkey(); + + let create_account_ix = create_account( + &authority.pubkey(), + &token_mint_key, + sol_rpc_client.get_minimum_balance_for_rent_exemption(82).unwrap(), + 82, + &anchor_spl::token::ID, + ); + + let create_mint_ix = initialize_mint2( + &anchor_spl::token::ID, + &token_mint_key, + &authority.pubkey(), + Some(&authority.pubkey()), + 9, + ) + .expect("invalid mint instruction"); + + let create_token_acc_ix = spl_associated_token_account::instruction::create_associated_token_account(&authority.pubkey(), &authority.pubkey(), &token_mint_key, &anchor_spl::token::ID); + let associated_token_addr = + get_associated_token_address(&authority.pubkey(), &token_mint_key); + let mint_ix = spl_token::instruction::mint_to( + &anchor_spl::token::ID, + &token_mint_key, + &associated_token_addr, + &authority.pubkey(), + &[&authority.pubkey()], + MINT_AMOUNT, + ) + .unwrap(); + + let tx = program + .request() + .instruction(create_account_ix) + .instruction(create_mint_ix) + .instruction(create_token_acc_ix) + .instruction(mint_ix) + .payer(authority.clone()) + .signer(&*authority) + .signer(&token_mint) + .send_with_spinner_and_config(RpcSendTransactionConfig { + skip_preflight: true, + ..RpcSendTransactionConfig::default() + })?; + + println!(" Signature: {}", tx); + + /* + * Initializing the program + */ + println!("\nInitializing the program"); + + let tx = program + .request() + .accounts(crate::accounts::Initialize { + admin: authority.pubkey(), + common_state, + system_program: solana_program::system_program::ID, + }) + .args(crate::instruction::Initialize { + whitelisted_tokens: vec![token_mint_key], + initial_validators: vec![authority.pubkey()], + guest_chain_program_id: solana_ibc::ID, + }) + .payer(authority.clone()) + .signer(&*authority) + .send_with_spinner_and_config(RpcSendTransactionConfig { + skip_preflight: true, + ..Default::default() + })?; + + println!(" Signature: {}", tx); + + let escrow_token_account = Pubkey::find_program_address( + &[crate::ESCROW_SEED, &token_mint_key.to_bytes()], + &crate::ID, + ) + .0; + let receipt_token_mint = Pubkey::find_program_address( + &[crate::RECEIPT_SEED, &token_mint_key.to_bytes()], + &crate::ID, + ) + .0; + + let staker_receipt_token_account = + spl_associated_token_account::get_associated_token_address( + &authority.pubkey(), + &receipt_token_mint, + ); + + let trie = + Pubkey::find_program_address(&[solana_ibc::TRIE_SEED], &solana_ibc::ID) + .0; + let chain = Pubkey::find_program_address( + &[solana_ibc::CHAIN_SEED], + &solana_ibc::ID, + ) + .0; + + /* + * Depositing to multiple validators + */ + println!("\nDepositing to multiple validators"); + + let staker_token_acc_balance_before = sol_rpc_client + .get_token_account_balance(&associated_token_addr) + .unwrap(); + + let tx = program + .request() + .accounts(crate::accounts::Deposit { + common_state, + system_program: solana_program::system_program::ID, + staker: authority.pubkey(), + token_mint: token_mint_key, + staker_token_account: associated_token_addr, + escrow_token_account, + receipt_token_mint, + staker_receipt_token_account, + token_program: anchor_spl::token::ID, + associated_token_program: anchor_spl::associated_token::ID, + chain, + trie, + guest_chain_program: solana_ibc::ID, + instruction: solana_program::sysvar::instructions::ID, + }) + .args(crate::instruction::Deposit { amount: STAKE_AMOUNT }) + .payer(authority.clone()) + .signer(&*authority) + .send_with_spinner_and_config(RpcSendTransactionConfig { + skip_preflight: true, + ..Default::default() + })?; + + let staker_token_acc_balance_after = sol_rpc_client + .get_token_account_balance(&associated_token_addr) + .unwrap(); + let staker_receipt_token_acc_balance_after = sol_rpc_client + .get_token_account_balance(&staker_receipt_token_account) + .unwrap(); + + assert_eq!( + (staker_receipt_token_acc_balance_after.ui_amount.unwrap() + * 10_f64.powf(crate::RECEIPT_TOKEN_DECIMALS.into())) as u64, + STAKE_AMOUNT + ); + assert_eq!( + ((staker_token_acc_balance_before.ui_amount.unwrap() + - staker_token_acc_balance_after.ui_amount.unwrap()) + * 10_f64.powf(crate::RECEIPT_TOKEN_DECIMALS.into())).round() as u64, + STAKE_AMOUNT + ); + + println!(" Signature: {}", tx); + + /* + * Withdrawing the stake + */ + println!("\nWithdrawing the stake"); + + let staker_token_acc_balance_before = sol_rpc_client + .get_token_account_balance(&associated_token_addr) + .unwrap(); + let staker_receipt_token_acc_balance_before = sol_rpc_client + .get_token_account_balance(&staker_receipt_token_account) + .unwrap(); + + let tx = program + .request() + .accounts(crate::accounts::Withdraw { + common_state, + system_program: solana_program::system_program::ID, + staker: authority.pubkey(), + token_mint: token_mint_key, + staker_token_account: associated_token_addr, + escrow_token_account, + receipt_token_mint, + staker_receipt_token_account, + token_program: anchor_spl::token::ID, + chain, + trie, + guest_chain_program: solana_ibc::ID, + instruction: solana_program::sysvar::instructions::ID, + }) + .args(crate::instruction::Withdraw { amount: STAKE_AMOUNT }) + .payer(authority.clone()) + .signer(&*authority) + .send_with_spinner_and_config(RpcSendTransactionConfig { + skip_preflight: true, + ..Default::default() + })?; + + let staker_token_acc_balance_after = sol_rpc_client + .get_token_account_balance(&associated_token_addr) + .unwrap(); + let staker_receipt_token_acc_balance_after = sol_rpc_client + .get_token_account_balance(&staker_receipt_token_account) + .unwrap(); + + assert_eq!( + ((staker_receipt_token_acc_balance_before.ui_amount.unwrap() + - staker_receipt_token_acc_balance_after.ui_amount.unwrap()) + * 10_f64.powf(crate::RECEIPT_TOKEN_DECIMALS.into())).round() as u64, + STAKE_AMOUNT + ); + assert_eq!( + ((staker_token_acc_balance_after.ui_amount.unwrap() + - staker_token_acc_balance_before.ui_amount.unwrap()) + * 10_f64.powf(crate::RECEIPT_TOKEN_DECIMALS.into())).round() as u64, + STAKE_AMOUNT + ); + + println!(" Signature: {}", tx); + + Ok(()) +} diff --git a/solana/restaking-v2/restaking-flow.png b/solana/restaking-v2/restaking-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..1d29853c5657d3b785fded2843c1177bbb4a74ee GIT binary patch literal 216808 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

fy4RCfHnIUWDX_-Dg6x%H1mq&(E^l?0&9N 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 RJFfuWS5L#Jst&dOLb3 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 { + const [stakingParamsPDA, stakingParamsBump] = + anchor.web3.PublicKey.findProgramAddressSync( + [Buffer.from("staking_params"), Buffer.from(testSeed)], + restakingProgramID + ); + return { stakingParamsPDA, stakingParamsBump }; +}; + +export const getRewardsTokenAccountPDA = () => { + const [rewardsTokenAccountPDA, rewardsTokenAccountBump] = + anchor.web3.PublicKey.findProgramAddressSync( + [Buffer.from("rewards"), Buffer.from(testSeed)], + restakingProgramID + ); + return { rewardsTokenAccountPDA, rewardsTokenAccountBump }; +}; + +export const getVaultParamsPDA = (receipt_mint: anchor.web3.PublicKey) => { + const [vaultParamsPDA, vaultParamsBump] = + anchor.web3.PublicKey.findProgramAddressSync( + [Buffer.from("vault_params"), receipt_mint.toBuffer()], + restakingProgramID + ); + return { vaultParamsPDA, vaultParamsBump }; +}; + +export const getVaultTokenAccountPDA = (token_mint: anchor.web3.PublicKey) => { + const [vaultTokenAccountPDA, vaultTokenAccountBump] = + anchor.web3.PublicKey.findProgramAddressSync( + [Buffer.from("vault"), token_mint.toBuffer()], + restakingProgramID + ); + return { vaultTokenAccountPDA, vaultTokenAccountBump }; +}; + +export const getReceiptTokenMintPDA = (token_mint: anchor.web3.PublicKey) => { + const [receiptTokenMintPDA, receiptTokenMintBump] = + anchor.web3.PublicKey.findProgramAddressSync( + [Buffer.from("receipt"), token_mint.toBuffer()], + restakingProgramID + ); + return { receiptTokenMintPDA, receiptTokenMintBump }; +}; + +export const getMasterEditionPDA = (token_mint: anchor.web3.PublicKey) => { + const [masterEditionPDA, masterEditionBump] = + anchor.web3.PublicKey.findProgramAddressSync( + [ + Buffer.from("metadata"), + new anchor.web3.PublicKey(mpl.MPL_TOKEN_METADATA_PROGRAM_ID).toBuffer(), + token_mint.toBuffer(), + Buffer.from("edition"), + ], + new anchor.web3.PublicKey(mpl.MPL_TOKEN_METADATA_PROGRAM_ID) + ); + return { masterEditionPDA, masterEditionBump }; +}; + +export const getNftMetadataPDA = (token_mint: anchor.web3.PublicKey) => { + const [nftMetadataPDA, nftMetadataBump] = + anchor.web3.PublicKey.findProgramAddressSync( + [ + Buffer.from("metadata"), + new anchor.web3.PublicKey(mpl.MPL_TOKEN_METADATA_PROGRAM_ID).toBuffer(), + token_mint.toBuffer(), + ], + new anchor.web3.PublicKey(mpl.MPL_TOKEN_METADATA_PROGRAM_ID) + ); + return { nftMetadataPDA, nftMetadataBump }; +}; + +export const getEscrowReceiptTokenPDA = (token_mint: anchor.web3.PublicKey) => { + const [escrowReceiptTokenPDA, escrowReceiptTokenBump] = + anchor.web3.PublicKey.findProgramAddressSync( + [Buffer.from("escrow_receipt"), token_mint.toBuffer()], + restakingProgramID + ); + return { escrowReceiptTokenPDA, escrowReceiptTokenBump }; +} + +export const getGuestChainAccounts = () => { + const [guestChainPDA, guestChainBump] = + anchor.web3.PublicKey.findProgramAddressSync( + [Buffer.from("chain")], + guestChainProgramID + ); + + const [triePDA, trieBump] = anchor.web3.PublicKey.findProgramAddressSync( + [Buffer.from("trie")], + guestChainProgramID + ); + + const [ibcStoragePDA, ibcStorageBump] = + anchor.web3.PublicKey.findProgramAddressSync( + [Buffer.from("private")], + guestChainProgramID + ); + + return { guestChainPDA, triePDA, ibcStoragePDA }; +}; + +/// Queries for staking parameters data +/// +/// Contains the whitelisted token list, rewards token mint, bounding period along +/// with the admin +export const getStakingParameters = async(program: anchor.Program) => { + const { stakingParamsPDA } = getStakingParamsPDA(); + const stakingParams = await program.account.stakingParams.fetch(stakingParamsPDA); + return stakingParams +} + +/// Queries for vault parameters data. Requires the NFT mint +/// +/// Contains the staked token amount, staked token mint, stake time, +/// the height at which the rewards were previously claimed at. +export const getVaultParameters = async(program: anchor.Program, tokenMint: anchor.web3.PublicKey) => { + const { vaultParamsPDA } = getVaultParamsPDA(tokenMint); + const vaultParams = await program.account.vault.fetch(vaultParamsPDA); + return vaultParams +} \ No newline at end of file diff --git a/solana/restaking-v2/tests/instructions.ts b/solana/restaking-v2/tests/instructions.ts new file mode 100644 index 00000000..788a7740 --- /dev/null +++ b/solana/restaking-v2/tests/instructions.ts @@ -0,0 +1,347 @@ +import * as anchor from "@coral-xyz/anchor"; +import * as mpl from "@metaplex-foundation/mpl-token-metadata"; +import * as spl from "@solana/spl-token"; +import { Restaking } from "../../../target/types/restaking"; +import { + getEscrowReceiptTokenPDA, + getGuestChainAccounts, + getMasterEditionPDA, + getNftMetadataPDA, + getReceiptTokenMintPDA, + getRewardsTokenAccountPDA, + getStakingParamsPDA, + getVaultParamsPDA, + getVaultTokenAccountPDA, + guestChainProgramID, + restakingProgramID, +} from "./helper"; + +export const depositInstruction = async ( + program: anchor.Program, + stakeTokenMint: anchor.web3.PublicKey, + staker: anchor.web3.PublicKey, + stakeAmount: number, + receiptTokenKeypair?: anchor.web3.Keypair | undefined +) => { + if (!receiptTokenKeypair) { + receiptTokenKeypair = anchor.web3.Keypair.generate(); + } + const receiptTokenPublicKey = receiptTokenKeypair.publicKey; + + const { vaultParamsPDA } = getVaultParamsPDA(receiptTokenPublicKey); + const { stakingParamsPDA } = getStakingParamsPDA(); + const { guestChainPDA, triePDA, ibcStoragePDA } = getGuestChainAccounts(); + const { vaultTokenAccountPDA } = getVaultTokenAccountPDA(stakeTokenMint); + const { masterEditionPDA } = getMasterEditionPDA(receiptTokenPublicKey); + const { nftMetadataPDA } = getNftMetadataPDA(receiptTokenPublicKey); + + const receiptTokenAccount = await spl.getAssociatedTokenAddress( + receiptTokenPublicKey, + staker + ); + + const stakerTokenAccount = await spl.getAssociatedTokenAddress( + stakeTokenMint, + staker + ); + + const ix = await program.methods + .deposit( + { guestChain: { validator: staker } }, + new anchor.BN(stakeAmount) // amount how much they are staking + ) + .preInstructions([ + anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({ + units: 1000000, + }), + ]) + .accounts({ + depositor: staker, // staker + vaultParams: vaultParamsPDA, + stakingParams: stakingParamsPDA, + tokenMint: stakeTokenMint, // token which they are staking + depositorTokenAccount: stakerTokenAccount, + vaultTokenAccount: vaultTokenAccountPDA, + receiptTokenMint: receiptTokenPublicKey, // NFT + receiptTokenAccount, + tokenProgram: spl.TOKEN_PROGRAM_ID, + associatedTokenProgram: spl.ASSOCIATED_TOKEN_PROGRAM_ID, + systemProgram: anchor.web3.SystemProgram.programId, + masterEditionAccount: masterEditionPDA, + nftMetadata: nftMetadataPDA, + instruction: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, + metadataProgram: new anchor.web3.PublicKey( + mpl.MPL_TOKEN_METADATA_PROGRAM_ID + ), + }) + .remainingAccounts([ + { pubkey: guestChainPDA, isSigner: false, isWritable: true }, + { pubkey: triePDA, isSigner: false, isWritable: true }, + { pubkey: guestChainProgramID, isSigner: false, isWritable: true }, + ]) + .transaction(); + + return ix; +}; + +export const claimRewardsInstruction = async ( + program: anchor.Program, + claimer: anchor.web3.PublicKey, + receiptTokenMint: anchor.web3.PublicKey +) => { + const { vaultParamsPDA } = getVaultParamsPDA(receiptTokenMint); + const { stakingParamsPDA } = getStakingParamsPDA(); + const { guestChainPDA } = getGuestChainAccounts(); + const { rewardsTokenAccountPDA } = getRewardsTokenAccountPDA(); + + const stakingParams = await program.account.stakingParams.fetch( + stakingParamsPDA + ); + + const { rewardsTokenMint } = stakingParams; + + const receiptTokenAccount = await spl.getAssociatedTokenAddress( + receiptTokenMint, + claimer + ); + + const claimerRewardsTokenAccount = await spl.getAssociatedTokenAddress( + rewardsTokenMint, + claimer + ); + + const tx = await program.methods + .claimRewards() + .preInstructions([ + anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({ + units: 1000000, + }), + ]) + .accounts({ + claimer: claimer, + vaultParams: vaultParamsPDA, + stakingParams: stakingParamsPDA, + guestChain: guestChainPDA, + rewardsTokenMint, + depositorRewardsTokenAccount: claimerRewardsTokenAccount, + platformRewardsTokenAccount: rewardsTokenAccountPDA, + receiptTokenMint, + receiptTokenAccount, + guestChainProgram: guestChainProgramID, + tokenProgram: spl.TOKEN_PROGRAM_ID, + associatedTokenProgram: spl.ASSOCIATED_TOKEN_PROGRAM_ID, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .transaction(); + + return tx; +}; + +export const withdrawInstruction = async ( + program: anchor.Program, + withdrawer: anchor.web3.PublicKey, + receiptTokenMint: anchor.web3.PublicKey +) => { + const { vaultParamsPDA } = getVaultParamsPDA(receiptTokenMint); + const { stakingParamsPDA } = getStakingParamsPDA(); + const { guestChainPDA, triePDA } = getGuestChainAccounts(); + + const vaultParams = await program.account.vault.fetch(vaultParamsPDA); + const stakedTokenMint = vaultParams.stakeMint; + + const { vaultTokenAccountPDA } = getVaultTokenAccountPDA(stakedTokenMint); + const { masterEditionPDA } = getMasterEditionPDA(receiptTokenMint); + const { nftMetadataPDA } = getNftMetadataPDA(receiptTokenMint); + const { escrowReceiptTokenPDA } = getEscrowReceiptTokenPDA(receiptTokenMint); + + const withdrawerStakedTokenAccount = await spl.getAssociatedTokenAddress( + stakedTokenMint, + withdrawer + ); + + const tx = await program.methods + .withdraw() + .preInstructions([ + anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({ + units: 1000000, + }), + ]) + .accounts({ + signer: withdrawer, + withdrawer, + vaultParams: vaultParamsPDA, + stakingParams: stakingParamsPDA, + guestChain: guestChainPDA, + trie: triePDA, + tokenMint: stakedTokenMint, + withdrawerTokenAccount: withdrawerStakedTokenAccount, + vaultTokenAccount: vaultTokenAccountPDA, + receiptTokenMint, + escrowReceiptTokenAccount: escrowReceiptTokenPDA, + guestChainProgram: guestChainProgramID, + tokenProgram: spl.TOKEN_PROGRAM_ID, + masterEditionAccount: masterEditionPDA, + nftMetadata: nftMetadataPDA, + systemProgram: anchor.web3.SystemProgram.programId, + metadataProgram: new anchor.web3.PublicKey( + mpl.MPL_TOKEN_METADATA_PROGRAM_ID + ), + instruction: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, + }) + .transaction(); + + return tx; +}; + +export const withdrawalRequestInstruction = async ( + program: anchor.Program, + withdrawer: anchor.web3.PublicKey, + receiptTokenMint: anchor.web3.PublicKey +) => { + const { vaultParamsPDA } = getVaultParamsPDA(receiptTokenMint); + const { stakingParamsPDA } = getStakingParamsPDA(); + const { guestChainPDA, triePDA } = getGuestChainAccounts(); + + const vaultParams = await program.account.vault.fetch(vaultParamsPDA); + const stakedTokenMint = vaultParams.stakeMint; + + const { vaultTokenAccountPDA } = getVaultTokenAccountPDA(stakedTokenMint); + const { masterEditionPDA } = getMasterEditionPDA(receiptTokenMint); + const { escrowReceiptTokenPDA } = getEscrowReceiptTokenPDA(receiptTokenMint); + + const withdrawerStakedTokenAccount = await spl.getAssociatedTokenAddress( + stakedTokenMint, + withdrawer + ); + + const receiptTokenAccount = await spl.getAssociatedTokenAddress( + receiptTokenMint, + withdrawer + ); + + const { rewardsTokenAccountPDA } = getRewardsTokenAccountPDA(); + + const stakingParams = await program.account.stakingParams.fetch( + stakingParamsPDA + ); + + const { rewardsTokenMint } = stakingParams; + + const withdrawerRewardsTokenAccount = await spl.getAssociatedTokenAddress( + rewardsTokenMint, + withdrawer + ); + + const tx = await program.methods + .withdrawalRequest() + .preInstructions([ + anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({ + units: 1000000, + }), + ]) + .accounts({ + withdrawer, + vaultParams: vaultParamsPDA, + stakingParams: stakingParamsPDA, + guestChain: guestChainPDA, + trie: triePDA, + tokenMint: stakedTokenMint, + withdrawerTokenAccount: withdrawerStakedTokenAccount, + vaultTokenAccount: vaultTokenAccountPDA, + receiptTokenMint, + receiptTokenAccount, + rewardsTokenMint, + depositorRewardsTokenAccount: withdrawerRewardsTokenAccount, + platformRewardsTokenAccount: rewardsTokenAccountPDA, + escrowReceiptTokenAccount: escrowReceiptTokenPDA, + guestChainProgram: guestChainProgramID, + tokenProgram: spl.TOKEN_PROGRAM_ID, + masterEditionAccount: masterEditionPDA, + systemProgram: anchor.web3.SystemProgram.programId, + metadataProgram: new anchor.web3.PublicKey( + mpl.MPL_TOKEN_METADATA_PROGRAM_ID + ), + }) + .transaction(); + + return tx; +}; + +export const cancelWithdrawalRequestInstruction = async ( + program: anchor.Program, + withdrawer: anchor.web3.PublicKey, + receiptTokenMint: anchor.web3.PublicKey +) => { + const { vaultParamsPDA } = getVaultParamsPDA(receiptTokenMint); + const { stakingParamsPDA } = getStakingParamsPDA(); + + const { masterEditionPDA } = getMasterEditionPDA(receiptTokenMint); + const { escrowReceiptTokenPDA } = getEscrowReceiptTokenPDA(receiptTokenMint); + + const receiptTokenAccount = await spl.getAssociatedTokenAddress( + receiptTokenMint, + withdrawer + ); + + const tx = await program.methods + .cancelWithdrawalRequest() + .preInstructions([ + anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({ + units: 1000000, + }), + ]) + .accounts({ + withdrawer, + vaultParams: vaultParamsPDA, + stakingParams: stakingParamsPDA, + receiptTokenMint, + receiptTokenAccount, + escrowReceiptTokenAccount: escrowReceiptTokenPDA, + tokenProgram: spl.TOKEN_PROGRAM_ID, + masterEditionAccount: masterEditionPDA, + systemProgram: anchor.web3.SystemProgram.programId, + metadataProgram: new anchor.web3.PublicKey( + mpl.MPL_TOKEN_METADATA_PROGRAM_ID + ), + }) + .transaction(); + + return tx; +}; + +export const setServiceInstruction = async ( + program: anchor.Program, + depositor: anchor.web3.PublicKey, + validator: anchor.web3.PublicKey, + receiptTokenMint: anchor.web3.PublicKey, + /// Token which is staked + stakeTokenMint: anchor.web3.PublicKey, +) => { + const { vaultParamsPDA } = getVaultParamsPDA(receiptTokenMint); + const { stakingParamsPDA } = getStakingParamsPDA(); + + const receiptTokenAccount = await spl.getAssociatedTokenAddress( + receiptTokenMint, + depositor + ); + const { guestChainPDA, triePDA } = getGuestChainAccounts(); + const tx = await program.methods + .setService({ guestChain: { validator: validator } }) + .accounts({ + depositor: depositor, + vaultParams: vaultParamsPDA, + stakingParams: stakingParamsPDA, + receiptTokenMint, + receiptTokenAccount, + stakeMint: stakeTokenMint, + instruction: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .remainingAccounts([ + { pubkey: guestChainPDA, isSigner: false, isWritable: true }, + { pubkey: triePDA, isSigner: false, isWritable: true }, + { pubkey: guestChainProgramID, isSigner: false, isWritable: true }, + ]) + .transaction(); + return tx; +}; diff --git a/solana/restaking-v2/tests/restaking.ts b/solana/restaking-v2/tests/restaking.ts new file mode 100644 index 00000000..d5784976 --- /dev/null +++ b/solana/restaking-v2/tests/restaking.ts @@ -0,0 +1,560 @@ +import * as anchor from "@coral-xyz/anchor"; +import * as spl from "@solana/spl-token"; +import * as mpl from "@metaplex-foundation/mpl-token-metadata"; +import { Program } from "@coral-xyz/anchor"; +import { IDL } from "../../../target/types/restaking"; +import assert from "assert"; +import bs58 from "bs58"; +import { + guestChainProgramID, + getGuestChainAccounts, + getRewardsTokenAccountPDA, + getStakingParameters, + getStakingParamsPDA, + getVaultParamsPDA, +} from "./helper"; +import { restakingProgramId } from "./constants"; +import { + cancelWithdrawalRequestInstruction, + claimRewardsInstruction, + depositInstruction, + setServiceInstruction, + withdrawInstruction, + withdrawalRequestInstruction, +} from "./instructions"; + +async function expectException(callback: any, message: string) { + try { + await callback(); + } catch (e) { + return; + } + console.log("Expected exception not thrown"); + throw Error(message); +} + +describe("restaking", () => { + // Configure the client to use the local cluster. + const provider = anchor.AnchorProvider.env(); + anchor.setProvider(provider); + + const program = new Program(IDL, restakingProgramId, provider); + + let depositor: anchor.web3.Keypair; // Just another Keypair + let admin: anchor.web3.Keypair; // This is the authority which is responsible for setting up the staking parameters + + let wSolMint: anchor.web3.PublicKey; // token which would be staked + let rewardsTokenMint: anchor.web3.PublicKey; // token which would be given as rewards + + let depositorWSolTokenAccount: any; // depositor wSol token account + + let initialMintAmount = 100000000; + let stakingCap = 30000; + let newStakingCap = 60000; + const depositAmount = 4000; + + let tokenMintKeypair = anchor.web3.Keypair.generate(); + let tokenMint = tokenMintKeypair.publicKey; + + const sleep = async (ms: number) => new Promise((r) => setTimeout(r, ms)); + + console.log(provider.connection.rpcEndpoint); + + if (provider.connection.rpcEndpoint.endsWith("8899")) { + depositor = anchor.web3.Keypair.generate(); + admin = anchor.web3.Keypair.generate(); + + it("Funds all users", async () => { + await provider.connection.confirmTransaction( + await provider.connection.requestAirdrop( + depositor.publicKey, + 10000000000 + ), + "confirmed" + ); + await provider.connection.confirmTransaction( + await provider.connection.requestAirdrop(admin.publicKey, 10000000000), + "confirmed" + ); + + const depositorUserBalance = await provider.connection.getBalance( + depositor.publicKey + ); + const adminUserBalance = await provider.connection.getBalance( + admin.publicKey + ); + + assert.strictEqual(10000000000, depositorUserBalance); + assert.strictEqual(10000000000, adminUserBalance); + }); + + it("create project and stable mint and mint some tokens to stakeholders", async () => { + wSolMint = await spl.createMint( + provider.connection, + admin, + admin.publicKey, + null, + 9 + ); + + rewardsTokenMint = await spl.createMint( + provider.connection, + admin, + admin.publicKey, + null, + 6 + ); + + depositorWSolTokenAccount = await spl.createAccount( + provider.connection, + depositor, + wSolMint, + depositor.publicKey + ); + + await spl.mintTo( + provider.connection, + depositor, + wSolMint, + depositorWSolTokenAccount, + admin.publicKey, + initialMintAmount, + [admin] + ); + + let depositorWSolTokenAccountUpdated = await spl.getAccount( + provider.connection, + depositorWSolTokenAccount + ); + + assert.equal(initialMintAmount, depositorWSolTokenAccountUpdated.amount); + }); + } else { + // These are the private keys of accounts which i have created and have deposited some SOL in it. + // Since we cannot airdrop much SOL on devnet (fails most of the time), i have previously airdropped some SOL so that these accounts + // can be used for testing on devnet. + // We can have them in another file and import them. But these are only for testing and has 0 balance on mainnet. + const depositorPrivate = + "472ZS33Lftn7wdM31QauCkmpgFKFvgBRg6Z6NGtA6JgeRi1NfeZFRNvNi3b3sh5jvrQWrgiTimr8giVs9oq4UM5g"; + const adminPrivate = + "2HKjYz8yfQxxhRS5f17FRCx9kDp7ATF5R4esLnKA4VaUsMA5zquP5XkQmvv9J5ZUD6wAjD4iBPYXDzQDNZmQ1eki"; + + depositor = anchor.web3.Keypair.fromSecretKey( + new Uint8Array(bs58.decode(depositorPrivate)) + ); + admin = anchor.web3.Keypair.fromSecretKey( + new Uint8Array(bs58.decode(adminPrivate)) + ); + + wSolMint = new anchor.web3.PublicKey( + "CAb5AhUMS4EbKp1rEoNJqXGy94Abha4Tg4FrHz7zZDZ3" + ); + + it("Get the associated token account and mint tokens", async () => { + try { + await provider.connection.confirmTransaction( + await provider.connection.requestAirdrop( + depositor.publicKey, + 100000000 + ), + "confirmed" + ); + } catch (error) { + console.log("Airdrop failed"); + } + + const TempdepositorWSolTokenAccount = + await spl.getOrCreateAssociatedTokenAccount( + provider.connection, + depositor, + wSolMint, + depositor.publicKey, + false + ); + + depositorWSolTokenAccount = TempdepositorWSolTokenAccount.address; + + const _depositorWSolTokenAccountBefore = await spl.getAccount( + provider.connection, + depositorWSolTokenAccount + ); + + await spl.mintTo( + provider.connection, + depositor, + wSolMint, + depositorWSolTokenAccount, + admin.publicKey, + initialMintAmount, + [admin] + ); + + const _depositorWSolTokenAccountAfter = await spl.getAccount( + provider.connection, + depositorWSolTokenAccount + ); + + assert.equal( + initialMintAmount, + _depositorWSolTokenAccountAfter.amount - + _depositorWSolTokenAccountBefore.amount + ); + }); + } + + it("Is Initialized", async () => { + const whitelistedTokens = [wSolMint]; + const { stakingParamsPDA } = getStakingParamsPDA(); + const { rewardsTokenAccountPDA } = getRewardsTokenAccountPDA(); + try { + const tx = await program.methods + .initialize(whitelistedTokens, new anchor.BN(stakingCap)) + .accounts({ + admin: admin.publicKey, + stakingParams: stakingParamsPDA, + systemProgram: anchor.web3.SystemProgram.programId, + rewardsTokenMint, + tokenProgram: spl.TOKEN_PROGRAM_ID, + rewardsTokenAccount: rewardsTokenAccountPDA, + }) + .signers([admin]) + .rpc(); + console.log(" Signature for Initializing: ", tx); + } catch (error) { + console.log(error); + // throw error; + } + }); + + it("Deposit tokens before chain is initialized", async () => { + const receiptTokenAccount = await spl.getAssociatedTokenAddress( + tokenMint, + depositor.publicKey + ); + + const depositorBalanceBefore = await spl.getAccount( + provider.connection, + depositorWSolTokenAccount + ); + + const tx = await depositInstruction( + program, + wSolMint, + depositor.publicKey, + depositAmount, + tokenMintKeypair + ); + + try { + tx.feePayer = depositor.publicKey; + const sig = await anchor.web3.sendAndConfirmTransaction( + provider.connection, + tx, + [depositor, tokenMintKeypair] + ); + + console.log(" Signature for Depositing: ", sig); + + const depositorBalanceAfter = await spl.getAccount( + provider.connection, + depositorWSolTokenAccount + ); + const depositorReceiptTokenBalanceAfter = await spl.getAccount( + provider.connection, + receiptTokenAccount + ); + + assert.equal( + depositorBalanceBefore.amount - depositorBalanceAfter.amount, + depositAmount + ); + assert.equal(depositorReceiptTokenBalanceAfter.amount, 1); + } catch (error) { + console.log(error); + throw error; + } + }); + + it("Update guest chain initialization with its program ID", async () => { + const { stakingParamsPDA } = getStakingParamsPDA(); + try { + const tx = await program.methods + .updateGuestChainInitialization(guestChainProgramID) + .accounts({ + admin: admin.publicKey, + stakingParams: stakingParamsPDA, + }) + .signers([admin]) + .rpc(); + console.log(" Signature for Updating Guest chain Initialization: ", tx); + } catch (error) { + console.log(error); + throw error; + } + }); + + it("Set service after guest chain is initialized", async () => { + const tx = await setServiceInstruction( + program, + depositor.publicKey, + depositor.publicKey, + tokenMintKeypair.publicKey, + wSolMint + ); + try { + tx.feePayer = depositor.publicKey; + const sig = await anchor.web3.sendAndConfirmTransaction( + provider.connection, + tx, + [depositor] + ); + console.log(" Signature for Updating Guest chain Initialization: ", sig); + } catch (error) { + console.log(error); + throw error; + } + }); + + it("Claim rewards", async () => { + const depositorRewardsTokenAccount = await spl.getAssociatedTokenAddress( + rewardsTokenMint, + depositor.publicKey + ); + + const tx = await claimRewardsInstruction( + program, + depositor.publicKey, + tokenMintKeypair.publicKey + ); + + try { + tx.feePayer = depositor.publicKey; + const sig = await anchor.web3.sendAndConfirmTransaction( + provider.connection, + tx, + [depositor] + ); + + console.log(" Signature for Claiming rewards: ", sig); + + const depositorBalanceAfter = await spl.getAccount( + provider.connection, + depositorRewardsTokenAccount + ); + + assert.equal(depositorBalanceAfter.amount, 0); // Rewards is 0 for now. + } catch (error) { + console.log(error); + throw error; + } + }); + + it("Withdrawal request", async () => { + const receiptTokenAccount = await spl.getAssociatedTokenAddress( + tokenMint, + depositor.publicKey + ); + + const depositorReceiptTokenBalanceBefore = await spl.getAccount( + provider.connection, + receiptTokenAccount + ); + + const tx = await withdrawalRequestInstruction( + program, + depositor.publicKey, + tokenMint + ); + + try { + tx.feePayer = depositor.publicKey; + const sig = await anchor.web3.sendAndConfirmTransaction( + provider.connection, + tx, + [depositor] + ); + + console.log(" Signature for Withdrawal request: ", sig); + + // Since receipt NFT token account is closed, getting spl account + // should fail + await expectException(async () => { + const _depositorReceiptTokenBalanceAfter = await spl.getAccount( + provider.connection, + receiptTokenAccount + ); + console.log("this is depositor account balance", _depositorReceiptTokenBalanceAfter); + }, "Receipt NFT token account is not closed"); + } catch (error) { + console.log(error); + throw error; + } + }); + + it("Cancel withdraw request", async () => { + const receiptTokenAccount = await spl.getAssociatedTokenAddress( + tokenMint, + depositor.publicKey + ); + + // Since receipt NFT token account is closed, getting spl account + // should fail + await expectException(async () => { + const _depositorReceiptTokenBalanceBefore = await spl.getAccount( + provider.connection, + receiptTokenAccount + ); + }, "Receipt NFT token account is not closed"); + const tx = await cancelWithdrawalRequestInstruction( + program, + depositor.publicKey, + tokenMint + ); + + try { + tx.feePayer = depositor.publicKey; + const sig = await anchor.web3.sendAndConfirmTransaction( + provider.connection, + tx, + [depositor] + ); + + console.log(" Signature for Cancelling Withdrawal: ", sig); + + const depositorReceiptTokenBalance = await spl.getAccount( + provider.connection, + receiptTokenAccount + ); + + assert.equal(depositorReceiptTokenBalance.amount, 1); + } catch (error) { + console.log(error); + throw error; + } + }); + + it("Request withdrawal and Withdraw tokens", async () => { + const receiptTokenAccount = await spl.getAssociatedTokenAddress( + tokenMint, + depositor.publicKey + ); + + const depositorReceiptTokenBalanceBefore = await spl.getAccount( + provider.connection, + receiptTokenAccount + ); + + let tx = await withdrawalRequestInstruction( + program, + depositor.publicKey, + tokenMint + ); + + try { + tx.feePayer = depositor.publicKey; + const sig = await anchor.web3.sendAndConfirmTransaction( + provider.connection, + tx, + [depositor] + ); + + console.log(" Signature for Withdrawal request: ", sig); + + // Since receipt NFT token account is closed, getting spl account + // should fail + await expectException(async () => { + const _depositorReceiptTokenBalanceAfter = await spl.getAccount( + provider.connection, + receiptTokenAccount + ); + }, "Receipt NFT token account is not closed"); + // Once withdraw request is complete, we can withdraw + // sleeping for unbonding period to end + await sleep(2000); + const depositorBalanceBefore = await spl.getAccount( + provider.connection, + depositorWSolTokenAccount + ); + tx = await withdrawInstruction(program, depositor.publicKey, tokenMint); + + try { + tx.feePayer = depositor.publicKey; + const sig = await anchor.web3.sendAndConfirmTransaction( + provider.connection, + tx, + [depositor] + ); + + console.log(" Signature for Withdrawing: ", sig); + + const depositorBalanceAfter = await spl.getAccount( + provider.connection, + depositorWSolTokenAccount + ); + + assert.equal( + depositorBalanceAfter.amount - depositorBalanceBefore.amount, + depositAmount + ); + } catch (error) { + console.log(error); + throw error; + } + } catch (error) { + console.log(error); + throw error; + } + }); + + it("Update admin", async () => { + const { stakingParamsPDA } = getStakingParamsPDA(); + try { + let tx = await program.methods + .changeAdminProposal(depositor.publicKey) + .accounts({ + admin: admin.publicKey, + stakingParams: stakingParamsPDA, + }) + .signers([admin]) + .rpc(); + console.log(" Signature for Updating Admin Proposal: ", tx); + tx = await program.methods + .acceptAdminChange() + .accounts({ + newAdmin: depositor.publicKey, + stakingParams: stakingParamsPDA, + }) + .signers([depositor]) + .rpc(); + console.log(" Signature for Accepting Admin Proposal: ", tx); + const stakingParameters = await getStakingParameters(program); + assert.equal( + stakingParameters.admin.toBase58(), + depositor.publicKey.toBase58() + ); + } catch (error) { + console.log(error); + throw error; + } + }); + + it("Update staking cap after updating admin", async () => { + const { stakingParamsPDA } = getStakingParamsPDA(); + try { + const tx = await program.methods + .updateStakingCap(new anchor.BN(newStakingCap)) + .accounts({ + admin: depositor.publicKey, + stakingParams: stakingParamsPDA, + }) + .signers([depositor]) + .rpc(); + console.log(" Signature for Updating staking cap: ", tx); + const stakingParameters = await getStakingParameters(program); + assert.equal(stakingParameters.stakingCap.toNumber(), newStakingCap); + } catch (error) { + console.log(error); + throw error; + } + }); +}); From be2e588c3b8ce85e9983f1873e8bc2962cc3629d Mon Sep 17 00:00:00 2001 From: dhruvja Date: Sun, 16 Jun 2024 15:57:43 -0400 Subject: [PATCH 02/40] added restaking-v2 program to config --- solana/restaking-v2/tests/constants.ts | 7 - solana/restaking-v2/tests/helper.ts | 129 ----- solana/restaking-v2/tests/instructions.ts | 347 -------------- solana/restaking-v2/tests/restaking.ts | 560 ---------------------- 4 files changed, 1043 deletions(-) delete mode 100644 solana/restaking-v2/tests/constants.ts delete mode 100644 solana/restaking-v2/tests/helper.ts delete mode 100644 solana/restaking-v2/tests/instructions.ts delete mode 100644 solana/restaking-v2/tests/restaking.ts diff --git a/solana/restaking-v2/tests/constants.ts b/solana/restaking-v2/tests/constants.ts deleted file mode 100644 index b8a4765a..00000000 --- a/solana/restaking-v2/tests/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const restakingProgramId = "8n3FHwYxFgQCQc2FNFkwDUf9mcqupxXcCvgfHbApMLv3"; -export const guestChainProgramId = - "2HLLVco5HvwWriNbUhmVwA2pCetRkpgrqwnjcsZdyTKT"; -export const initialMintAmount = 100000000; -export const depositAmount = 4000; -export const boundingPeriod = 5; // seconds -export const testSeed = "abcdefg2"; diff --git a/solana/restaking-v2/tests/helper.ts b/solana/restaking-v2/tests/helper.ts deleted file mode 100644 index 56553952..00000000 --- a/solana/restaking-v2/tests/helper.ts +++ /dev/null @@ -1,129 +0,0 @@ -import * as anchor from "@coral-xyz/anchor"; -import * as mpl from "@metaplex-foundation/mpl-token-metadata"; -import { guestChainProgramId, restakingProgramId, testSeed } from "./constants"; -import { Restaking } from "../../../target/types/restaking"; - -export const guestChainProgramID = new anchor.web3.PublicKey(guestChainProgramId); -export const restakingProgramID = new anchor.web3.PublicKey(restakingProgramId); - -export const getStakingParamsPDA = () => { - const [stakingParamsPDA, stakingParamsBump] = - anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("staking_params"), Buffer.from(testSeed)], - restakingProgramID - ); - return { stakingParamsPDA, stakingParamsBump }; -}; - -export const getRewardsTokenAccountPDA = () => { - const [rewardsTokenAccountPDA, rewardsTokenAccountBump] = - anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("rewards"), Buffer.from(testSeed)], - restakingProgramID - ); - return { rewardsTokenAccountPDA, rewardsTokenAccountBump }; -}; - -export const getVaultParamsPDA = (receipt_mint: anchor.web3.PublicKey) => { - const [vaultParamsPDA, vaultParamsBump] = - anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("vault_params"), receipt_mint.toBuffer()], - restakingProgramID - ); - return { vaultParamsPDA, vaultParamsBump }; -}; - -export const getVaultTokenAccountPDA = (token_mint: anchor.web3.PublicKey) => { - const [vaultTokenAccountPDA, vaultTokenAccountBump] = - anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("vault"), token_mint.toBuffer()], - restakingProgramID - ); - return { vaultTokenAccountPDA, vaultTokenAccountBump }; -}; - -export const getReceiptTokenMintPDA = (token_mint: anchor.web3.PublicKey) => { - const [receiptTokenMintPDA, receiptTokenMintBump] = - anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("receipt"), token_mint.toBuffer()], - restakingProgramID - ); - return { receiptTokenMintPDA, receiptTokenMintBump }; -}; - -export const getMasterEditionPDA = (token_mint: anchor.web3.PublicKey) => { - const [masterEditionPDA, masterEditionBump] = - anchor.web3.PublicKey.findProgramAddressSync( - [ - Buffer.from("metadata"), - new anchor.web3.PublicKey(mpl.MPL_TOKEN_METADATA_PROGRAM_ID).toBuffer(), - token_mint.toBuffer(), - Buffer.from("edition"), - ], - new anchor.web3.PublicKey(mpl.MPL_TOKEN_METADATA_PROGRAM_ID) - ); - return { masterEditionPDA, masterEditionBump }; -}; - -export const getNftMetadataPDA = (token_mint: anchor.web3.PublicKey) => { - const [nftMetadataPDA, nftMetadataBump] = - anchor.web3.PublicKey.findProgramAddressSync( - [ - Buffer.from("metadata"), - new anchor.web3.PublicKey(mpl.MPL_TOKEN_METADATA_PROGRAM_ID).toBuffer(), - token_mint.toBuffer(), - ], - new anchor.web3.PublicKey(mpl.MPL_TOKEN_METADATA_PROGRAM_ID) - ); - return { nftMetadataPDA, nftMetadataBump }; -}; - -export const getEscrowReceiptTokenPDA = (token_mint: anchor.web3.PublicKey) => { - const [escrowReceiptTokenPDA, escrowReceiptTokenBump] = - anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("escrow_receipt"), token_mint.toBuffer()], - restakingProgramID - ); - return { escrowReceiptTokenPDA, escrowReceiptTokenBump }; -} - -export const getGuestChainAccounts = () => { - const [guestChainPDA, guestChainBump] = - anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("chain")], - guestChainProgramID - ); - - const [triePDA, trieBump] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("trie")], - guestChainProgramID - ); - - const [ibcStoragePDA, ibcStorageBump] = - anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("private")], - guestChainProgramID - ); - - return { guestChainPDA, triePDA, ibcStoragePDA }; -}; - -/// Queries for staking parameters data -/// -/// Contains the whitelisted token list, rewards token mint, bounding period along -/// with the admin -export const getStakingParameters = async(program: anchor.Program) => { - const { stakingParamsPDA } = getStakingParamsPDA(); - const stakingParams = await program.account.stakingParams.fetch(stakingParamsPDA); - return stakingParams -} - -/// Queries for vault parameters data. Requires the NFT mint -/// -/// Contains the staked token amount, staked token mint, stake time, -/// the height at which the rewards were previously claimed at. -export const getVaultParameters = async(program: anchor.Program, tokenMint: anchor.web3.PublicKey) => { - const { vaultParamsPDA } = getVaultParamsPDA(tokenMint); - const vaultParams = await program.account.vault.fetch(vaultParamsPDA); - return vaultParams -} \ No newline at end of file diff --git a/solana/restaking-v2/tests/instructions.ts b/solana/restaking-v2/tests/instructions.ts deleted file mode 100644 index 788a7740..00000000 --- a/solana/restaking-v2/tests/instructions.ts +++ /dev/null @@ -1,347 +0,0 @@ -import * as anchor from "@coral-xyz/anchor"; -import * as mpl from "@metaplex-foundation/mpl-token-metadata"; -import * as spl from "@solana/spl-token"; -import { Restaking } from "../../../target/types/restaking"; -import { - getEscrowReceiptTokenPDA, - getGuestChainAccounts, - getMasterEditionPDA, - getNftMetadataPDA, - getReceiptTokenMintPDA, - getRewardsTokenAccountPDA, - getStakingParamsPDA, - getVaultParamsPDA, - getVaultTokenAccountPDA, - guestChainProgramID, - restakingProgramID, -} from "./helper"; - -export const depositInstruction = async ( - program: anchor.Program, - stakeTokenMint: anchor.web3.PublicKey, - staker: anchor.web3.PublicKey, - stakeAmount: number, - receiptTokenKeypair?: anchor.web3.Keypair | undefined -) => { - if (!receiptTokenKeypair) { - receiptTokenKeypair = anchor.web3.Keypair.generate(); - } - const receiptTokenPublicKey = receiptTokenKeypair.publicKey; - - const { vaultParamsPDA } = getVaultParamsPDA(receiptTokenPublicKey); - const { stakingParamsPDA } = getStakingParamsPDA(); - const { guestChainPDA, triePDA, ibcStoragePDA } = getGuestChainAccounts(); - const { vaultTokenAccountPDA } = getVaultTokenAccountPDA(stakeTokenMint); - const { masterEditionPDA } = getMasterEditionPDA(receiptTokenPublicKey); - const { nftMetadataPDA } = getNftMetadataPDA(receiptTokenPublicKey); - - const receiptTokenAccount = await spl.getAssociatedTokenAddress( - receiptTokenPublicKey, - staker - ); - - const stakerTokenAccount = await spl.getAssociatedTokenAddress( - stakeTokenMint, - staker - ); - - const ix = await program.methods - .deposit( - { guestChain: { validator: staker } }, - new anchor.BN(stakeAmount) // amount how much they are staking - ) - .preInstructions([ - anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({ - units: 1000000, - }), - ]) - .accounts({ - depositor: staker, // staker - vaultParams: vaultParamsPDA, - stakingParams: stakingParamsPDA, - tokenMint: stakeTokenMint, // token which they are staking - depositorTokenAccount: stakerTokenAccount, - vaultTokenAccount: vaultTokenAccountPDA, - receiptTokenMint: receiptTokenPublicKey, // NFT - receiptTokenAccount, - tokenProgram: spl.TOKEN_PROGRAM_ID, - associatedTokenProgram: spl.ASSOCIATED_TOKEN_PROGRAM_ID, - systemProgram: anchor.web3.SystemProgram.programId, - masterEditionAccount: masterEditionPDA, - nftMetadata: nftMetadataPDA, - instruction: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, - metadataProgram: new anchor.web3.PublicKey( - mpl.MPL_TOKEN_METADATA_PROGRAM_ID - ), - }) - .remainingAccounts([ - { pubkey: guestChainPDA, isSigner: false, isWritable: true }, - { pubkey: triePDA, isSigner: false, isWritable: true }, - { pubkey: guestChainProgramID, isSigner: false, isWritable: true }, - ]) - .transaction(); - - return ix; -}; - -export const claimRewardsInstruction = async ( - program: anchor.Program, - claimer: anchor.web3.PublicKey, - receiptTokenMint: anchor.web3.PublicKey -) => { - const { vaultParamsPDA } = getVaultParamsPDA(receiptTokenMint); - const { stakingParamsPDA } = getStakingParamsPDA(); - const { guestChainPDA } = getGuestChainAccounts(); - const { rewardsTokenAccountPDA } = getRewardsTokenAccountPDA(); - - const stakingParams = await program.account.stakingParams.fetch( - stakingParamsPDA - ); - - const { rewardsTokenMint } = stakingParams; - - const receiptTokenAccount = await spl.getAssociatedTokenAddress( - receiptTokenMint, - claimer - ); - - const claimerRewardsTokenAccount = await spl.getAssociatedTokenAddress( - rewardsTokenMint, - claimer - ); - - const tx = await program.methods - .claimRewards() - .preInstructions([ - anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({ - units: 1000000, - }), - ]) - .accounts({ - claimer: claimer, - vaultParams: vaultParamsPDA, - stakingParams: stakingParamsPDA, - guestChain: guestChainPDA, - rewardsTokenMint, - depositorRewardsTokenAccount: claimerRewardsTokenAccount, - platformRewardsTokenAccount: rewardsTokenAccountPDA, - receiptTokenMint, - receiptTokenAccount, - guestChainProgram: guestChainProgramID, - tokenProgram: spl.TOKEN_PROGRAM_ID, - associatedTokenProgram: spl.ASSOCIATED_TOKEN_PROGRAM_ID, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .transaction(); - - return tx; -}; - -export const withdrawInstruction = async ( - program: anchor.Program, - withdrawer: anchor.web3.PublicKey, - receiptTokenMint: anchor.web3.PublicKey -) => { - const { vaultParamsPDA } = getVaultParamsPDA(receiptTokenMint); - const { stakingParamsPDA } = getStakingParamsPDA(); - const { guestChainPDA, triePDA } = getGuestChainAccounts(); - - const vaultParams = await program.account.vault.fetch(vaultParamsPDA); - const stakedTokenMint = vaultParams.stakeMint; - - const { vaultTokenAccountPDA } = getVaultTokenAccountPDA(stakedTokenMint); - const { masterEditionPDA } = getMasterEditionPDA(receiptTokenMint); - const { nftMetadataPDA } = getNftMetadataPDA(receiptTokenMint); - const { escrowReceiptTokenPDA } = getEscrowReceiptTokenPDA(receiptTokenMint); - - const withdrawerStakedTokenAccount = await spl.getAssociatedTokenAddress( - stakedTokenMint, - withdrawer - ); - - const tx = await program.methods - .withdraw() - .preInstructions([ - anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({ - units: 1000000, - }), - ]) - .accounts({ - signer: withdrawer, - withdrawer, - vaultParams: vaultParamsPDA, - stakingParams: stakingParamsPDA, - guestChain: guestChainPDA, - trie: triePDA, - tokenMint: stakedTokenMint, - withdrawerTokenAccount: withdrawerStakedTokenAccount, - vaultTokenAccount: vaultTokenAccountPDA, - receiptTokenMint, - escrowReceiptTokenAccount: escrowReceiptTokenPDA, - guestChainProgram: guestChainProgramID, - tokenProgram: spl.TOKEN_PROGRAM_ID, - masterEditionAccount: masterEditionPDA, - nftMetadata: nftMetadataPDA, - systemProgram: anchor.web3.SystemProgram.programId, - metadataProgram: new anchor.web3.PublicKey( - mpl.MPL_TOKEN_METADATA_PROGRAM_ID - ), - instruction: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, - }) - .transaction(); - - return tx; -}; - -export const withdrawalRequestInstruction = async ( - program: anchor.Program, - withdrawer: anchor.web3.PublicKey, - receiptTokenMint: anchor.web3.PublicKey -) => { - const { vaultParamsPDA } = getVaultParamsPDA(receiptTokenMint); - const { stakingParamsPDA } = getStakingParamsPDA(); - const { guestChainPDA, triePDA } = getGuestChainAccounts(); - - const vaultParams = await program.account.vault.fetch(vaultParamsPDA); - const stakedTokenMint = vaultParams.stakeMint; - - const { vaultTokenAccountPDA } = getVaultTokenAccountPDA(stakedTokenMint); - const { masterEditionPDA } = getMasterEditionPDA(receiptTokenMint); - const { escrowReceiptTokenPDA } = getEscrowReceiptTokenPDA(receiptTokenMint); - - const withdrawerStakedTokenAccount = await spl.getAssociatedTokenAddress( - stakedTokenMint, - withdrawer - ); - - const receiptTokenAccount = await spl.getAssociatedTokenAddress( - receiptTokenMint, - withdrawer - ); - - const { rewardsTokenAccountPDA } = getRewardsTokenAccountPDA(); - - const stakingParams = await program.account.stakingParams.fetch( - stakingParamsPDA - ); - - const { rewardsTokenMint } = stakingParams; - - const withdrawerRewardsTokenAccount = await spl.getAssociatedTokenAddress( - rewardsTokenMint, - withdrawer - ); - - const tx = await program.methods - .withdrawalRequest() - .preInstructions([ - anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({ - units: 1000000, - }), - ]) - .accounts({ - withdrawer, - vaultParams: vaultParamsPDA, - stakingParams: stakingParamsPDA, - guestChain: guestChainPDA, - trie: triePDA, - tokenMint: stakedTokenMint, - withdrawerTokenAccount: withdrawerStakedTokenAccount, - vaultTokenAccount: vaultTokenAccountPDA, - receiptTokenMint, - receiptTokenAccount, - rewardsTokenMint, - depositorRewardsTokenAccount: withdrawerRewardsTokenAccount, - platformRewardsTokenAccount: rewardsTokenAccountPDA, - escrowReceiptTokenAccount: escrowReceiptTokenPDA, - guestChainProgram: guestChainProgramID, - tokenProgram: spl.TOKEN_PROGRAM_ID, - masterEditionAccount: masterEditionPDA, - systemProgram: anchor.web3.SystemProgram.programId, - metadataProgram: new anchor.web3.PublicKey( - mpl.MPL_TOKEN_METADATA_PROGRAM_ID - ), - }) - .transaction(); - - return tx; -}; - -export const cancelWithdrawalRequestInstruction = async ( - program: anchor.Program, - withdrawer: anchor.web3.PublicKey, - receiptTokenMint: anchor.web3.PublicKey -) => { - const { vaultParamsPDA } = getVaultParamsPDA(receiptTokenMint); - const { stakingParamsPDA } = getStakingParamsPDA(); - - const { masterEditionPDA } = getMasterEditionPDA(receiptTokenMint); - const { escrowReceiptTokenPDA } = getEscrowReceiptTokenPDA(receiptTokenMint); - - const receiptTokenAccount = await spl.getAssociatedTokenAddress( - receiptTokenMint, - withdrawer - ); - - const tx = await program.methods - .cancelWithdrawalRequest() - .preInstructions([ - anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({ - units: 1000000, - }), - ]) - .accounts({ - withdrawer, - vaultParams: vaultParamsPDA, - stakingParams: stakingParamsPDA, - receiptTokenMint, - receiptTokenAccount, - escrowReceiptTokenAccount: escrowReceiptTokenPDA, - tokenProgram: spl.TOKEN_PROGRAM_ID, - masterEditionAccount: masterEditionPDA, - systemProgram: anchor.web3.SystemProgram.programId, - metadataProgram: new anchor.web3.PublicKey( - mpl.MPL_TOKEN_METADATA_PROGRAM_ID - ), - }) - .transaction(); - - return tx; -}; - -export const setServiceInstruction = async ( - program: anchor.Program, - depositor: anchor.web3.PublicKey, - validator: anchor.web3.PublicKey, - receiptTokenMint: anchor.web3.PublicKey, - /// Token which is staked - stakeTokenMint: anchor.web3.PublicKey, -) => { - const { vaultParamsPDA } = getVaultParamsPDA(receiptTokenMint); - const { stakingParamsPDA } = getStakingParamsPDA(); - - const receiptTokenAccount = await spl.getAssociatedTokenAddress( - receiptTokenMint, - depositor - ); - const { guestChainPDA, triePDA } = getGuestChainAccounts(); - const tx = await program.methods - .setService({ guestChain: { validator: validator } }) - .accounts({ - depositor: depositor, - vaultParams: vaultParamsPDA, - stakingParams: stakingParamsPDA, - receiptTokenMint, - receiptTokenAccount, - stakeMint: stakeTokenMint, - instruction: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .remainingAccounts([ - { pubkey: guestChainPDA, isSigner: false, isWritable: true }, - { pubkey: triePDA, isSigner: false, isWritable: true }, - { pubkey: guestChainProgramID, isSigner: false, isWritable: true }, - ]) - .transaction(); - return tx; -}; diff --git a/solana/restaking-v2/tests/restaking.ts b/solana/restaking-v2/tests/restaking.ts deleted file mode 100644 index d5784976..00000000 --- a/solana/restaking-v2/tests/restaking.ts +++ /dev/null @@ -1,560 +0,0 @@ -import * as anchor from "@coral-xyz/anchor"; -import * as spl from "@solana/spl-token"; -import * as mpl from "@metaplex-foundation/mpl-token-metadata"; -import { Program } from "@coral-xyz/anchor"; -import { IDL } from "../../../target/types/restaking"; -import assert from "assert"; -import bs58 from "bs58"; -import { - guestChainProgramID, - getGuestChainAccounts, - getRewardsTokenAccountPDA, - getStakingParameters, - getStakingParamsPDA, - getVaultParamsPDA, -} from "./helper"; -import { restakingProgramId } from "./constants"; -import { - cancelWithdrawalRequestInstruction, - claimRewardsInstruction, - depositInstruction, - setServiceInstruction, - withdrawInstruction, - withdrawalRequestInstruction, -} from "./instructions"; - -async function expectException(callback: any, message: string) { - try { - await callback(); - } catch (e) { - return; - } - console.log("Expected exception not thrown"); - throw Error(message); -} - -describe("restaking", () => { - // Configure the client to use the local cluster. - const provider = anchor.AnchorProvider.env(); - anchor.setProvider(provider); - - const program = new Program(IDL, restakingProgramId, provider); - - let depositor: anchor.web3.Keypair; // Just another Keypair - let admin: anchor.web3.Keypair; // This is the authority which is responsible for setting up the staking parameters - - let wSolMint: anchor.web3.PublicKey; // token which would be staked - let rewardsTokenMint: anchor.web3.PublicKey; // token which would be given as rewards - - let depositorWSolTokenAccount: any; // depositor wSol token account - - let initialMintAmount = 100000000; - let stakingCap = 30000; - let newStakingCap = 60000; - const depositAmount = 4000; - - let tokenMintKeypair = anchor.web3.Keypair.generate(); - let tokenMint = tokenMintKeypair.publicKey; - - const sleep = async (ms: number) => new Promise((r) => setTimeout(r, ms)); - - console.log(provider.connection.rpcEndpoint); - - if (provider.connection.rpcEndpoint.endsWith("8899")) { - depositor = anchor.web3.Keypair.generate(); - admin = anchor.web3.Keypair.generate(); - - it("Funds all users", async () => { - await provider.connection.confirmTransaction( - await provider.connection.requestAirdrop( - depositor.publicKey, - 10000000000 - ), - "confirmed" - ); - await provider.connection.confirmTransaction( - await provider.connection.requestAirdrop(admin.publicKey, 10000000000), - "confirmed" - ); - - const depositorUserBalance = await provider.connection.getBalance( - depositor.publicKey - ); - const adminUserBalance = await provider.connection.getBalance( - admin.publicKey - ); - - assert.strictEqual(10000000000, depositorUserBalance); - assert.strictEqual(10000000000, adminUserBalance); - }); - - it("create project and stable mint and mint some tokens to stakeholders", async () => { - wSolMint = await spl.createMint( - provider.connection, - admin, - admin.publicKey, - null, - 9 - ); - - rewardsTokenMint = await spl.createMint( - provider.connection, - admin, - admin.publicKey, - null, - 6 - ); - - depositorWSolTokenAccount = await spl.createAccount( - provider.connection, - depositor, - wSolMint, - depositor.publicKey - ); - - await spl.mintTo( - provider.connection, - depositor, - wSolMint, - depositorWSolTokenAccount, - admin.publicKey, - initialMintAmount, - [admin] - ); - - let depositorWSolTokenAccountUpdated = await spl.getAccount( - provider.connection, - depositorWSolTokenAccount - ); - - assert.equal(initialMintAmount, depositorWSolTokenAccountUpdated.amount); - }); - } else { - // These are the private keys of accounts which i have created and have deposited some SOL in it. - // Since we cannot airdrop much SOL on devnet (fails most of the time), i have previously airdropped some SOL so that these accounts - // can be used for testing on devnet. - // We can have them in another file and import them. But these are only for testing and has 0 balance on mainnet. - const depositorPrivate = - "472ZS33Lftn7wdM31QauCkmpgFKFvgBRg6Z6NGtA6JgeRi1NfeZFRNvNi3b3sh5jvrQWrgiTimr8giVs9oq4UM5g"; - const adminPrivate = - "2HKjYz8yfQxxhRS5f17FRCx9kDp7ATF5R4esLnKA4VaUsMA5zquP5XkQmvv9J5ZUD6wAjD4iBPYXDzQDNZmQ1eki"; - - depositor = anchor.web3.Keypair.fromSecretKey( - new Uint8Array(bs58.decode(depositorPrivate)) - ); - admin = anchor.web3.Keypair.fromSecretKey( - new Uint8Array(bs58.decode(adminPrivate)) - ); - - wSolMint = new anchor.web3.PublicKey( - "CAb5AhUMS4EbKp1rEoNJqXGy94Abha4Tg4FrHz7zZDZ3" - ); - - it("Get the associated token account and mint tokens", async () => { - try { - await provider.connection.confirmTransaction( - await provider.connection.requestAirdrop( - depositor.publicKey, - 100000000 - ), - "confirmed" - ); - } catch (error) { - console.log("Airdrop failed"); - } - - const TempdepositorWSolTokenAccount = - await spl.getOrCreateAssociatedTokenAccount( - provider.connection, - depositor, - wSolMint, - depositor.publicKey, - false - ); - - depositorWSolTokenAccount = TempdepositorWSolTokenAccount.address; - - const _depositorWSolTokenAccountBefore = await spl.getAccount( - provider.connection, - depositorWSolTokenAccount - ); - - await spl.mintTo( - provider.connection, - depositor, - wSolMint, - depositorWSolTokenAccount, - admin.publicKey, - initialMintAmount, - [admin] - ); - - const _depositorWSolTokenAccountAfter = await spl.getAccount( - provider.connection, - depositorWSolTokenAccount - ); - - assert.equal( - initialMintAmount, - _depositorWSolTokenAccountAfter.amount - - _depositorWSolTokenAccountBefore.amount - ); - }); - } - - it("Is Initialized", async () => { - const whitelistedTokens = [wSolMint]; - const { stakingParamsPDA } = getStakingParamsPDA(); - const { rewardsTokenAccountPDA } = getRewardsTokenAccountPDA(); - try { - const tx = await program.methods - .initialize(whitelistedTokens, new anchor.BN(stakingCap)) - .accounts({ - admin: admin.publicKey, - stakingParams: stakingParamsPDA, - systemProgram: anchor.web3.SystemProgram.programId, - rewardsTokenMint, - tokenProgram: spl.TOKEN_PROGRAM_ID, - rewardsTokenAccount: rewardsTokenAccountPDA, - }) - .signers([admin]) - .rpc(); - console.log(" Signature for Initializing: ", tx); - } catch (error) { - console.log(error); - // throw error; - } - }); - - it("Deposit tokens before chain is initialized", async () => { - const receiptTokenAccount = await spl.getAssociatedTokenAddress( - tokenMint, - depositor.publicKey - ); - - const depositorBalanceBefore = await spl.getAccount( - provider.connection, - depositorWSolTokenAccount - ); - - const tx = await depositInstruction( - program, - wSolMint, - depositor.publicKey, - depositAmount, - tokenMintKeypair - ); - - try { - tx.feePayer = depositor.publicKey; - const sig = await anchor.web3.sendAndConfirmTransaction( - provider.connection, - tx, - [depositor, tokenMintKeypair] - ); - - console.log(" Signature for Depositing: ", sig); - - const depositorBalanceAfter = await spl.getAccount( - provider.connection, - depositorWSolTokenAccount - ); - const depositorReceiptTokenBalanceAfter = await spl.getAccount( - provider.connection, - receiptTokenAccount - ); - - assert.equal( - depositorBalanceBefore.amount - depositorBalanceAfter.amount, - depositAmount - ); - assert.equal(depositorReceiptTokenBalanceAfter.amount, 1); - } catch (error) { - console.log(error); - throw error; - } - }); - - it("Update guest chain initialization with its program ID", async () => { - const { stakingParamsPDA } = getStakingParamsPDA(); - try { - const tx = await program.methods - .updateGuestChainInitialization(guestChainProgramID) - .accounts({ - admin: admin.publicKey, - stakingParams: stakingParamsPDA, - }) - .signers([admin]) - .rpc(); - console.log(" Signature for Updating Guest chain Initialization: ", tx); - } catch (error) { - console.log(error); - throw error; - } - }); - - it("Set service after guest chain is initialized", async () => { - const tx = await setServiceInstruction( - program, - depositor.publicKey, - depositor.publicKey, - tokenMintKeypair.publicKey, - wSolMint - ); - try { - tx.feePayer = depositor.publicKey; - const sig = await anchor.web3.sendAndConfirmTransaction( - provider.connection, - tx, - [depositor] - ); - console.log(" Signature for Updating Guest chain Initialization: ", sig); - } catch (error) { - console.log(error); - throw error; - } - }); - - it("Claim rewards", async () => { - const depositorRewardsTokenAccount = await spl.getAssociatedTokenAddress( - rewardsTokenMint, - depositor.publicKey - ); - - const tx = await claimRewardsInstruction( - program, - depositor.publicKey, - tokenMintKeypair.publicKey - ); - - try { - tx.feePayer = depositor.publicKey; - const sig = await anchor.web3.sendAndConfirmTransaction( - provider.connection, - tx, - [depositor] - ); - - console.log(" Signature for Claiming rewards: ", sig); - - const depositorBalanceAfter = await spl.getAccount( - provider.connection, - depositorRewardsTokenAccount - ); - - assert.equal(depositorBalanceAfter.amount, 0); // Rewards is 0 for now. - } catch (error) { - console.log(error); - throw error; - } - }); - - it("Withdrawal request", async () => { - const receiptTokenAccount = await spl.getAssociatedTokenAddress( - tokenMint, - depositor.publicKey - ); - - const depositorReceiptTokenBalanceBefore = await spl.getAccount( - provider.connection, - receiptTokenAccount - ); - - const tx = await withdrawalRequestInstruction( - program, - depositor.publicKey, - tokenMint - ); - - try { - tx.feePayer = depositor.publicKey; - const sig = await anchor.web3.sendAndConfirmTransaction( - provider.connection, - tx, - [depositor] - ); - - console.log(" Signature for Withdrawal request: ", sig); - - // Since receipt NFT token account is closed, getting spl account - // should fail - await expectException(async () => { - const _depositorReceiptTokenBalanceAfter = await spl.getAccount( - provider.connection, - receiptTokenAccount - ); - console.log("this is depositor account balance", _depositorReceiptTokenBalanceAfter); - }, "Receipt NFT token account is not closed"); - } catch (error) { - console.log(error); - throw error; - } - }); - - it("Cancel withdraw request", async () => { - const receiptTokenAccount = await spl.getAssociatedTokenAddress( - tokenMint, - depositor.publicKey - ); - - // Since receipt NFT token account is closed, getting spl account - // should fail - await expectException(async () => { - const _depositorReceiptTokenBalanceBefore = await spl.getAccount( - provider.connection, - receiptTokenAccount - ); - }, "Receipt NFT token account is not closed"); - const tx = await cancelWithdrawalRequestInstruction( - program, - depositor.publicKey, - tokenMint - ); - - try { - tx.feePayer = depositor.publicKey; - const sig = await anchor.web3.sendAndConfirmTransaction( - provider.connection, - tx, - [depositor] - ); - - console.log(" Signature for Cancelling Withdrawal: ", sig); - - const depositorReceiptTokenBalance = await spl.getAccount( - provider.connection, - receiptTokenAccount - ); - - assert.equal(depositorReceiptTokenBalance.amount, 1); - } catch (error) { - console.log(error); - throw error; - } - }); - - it("Request withdrawal and Withdraw tokens", async () => { - const receiptTokenAccount = await spl.getAssociatedTokenAddress( - tokenMint, - depositor.publicKey - ); - - const depositorReceiptTokenBalanceBefore = await spl.getAccount( - provider.connection, - receiptTokenAccount - ); - - let tx = await withdrawalRequestInstruction( - program, - depositor.publicKey, - tokenMint - ); - - try { - tx.feePayer = depositor.publicKey; - const sig = await anchor.web3.sendAndConfirmTransaction( - provider.connection, - tx, - [depositor] - ); - - console.log(" Signature for Withdrawal request: ", sig); - - // Since receipt NFT token account is closed, getting spl account - // should fail - await expectException(async () => { - const _depositorReceiptTokenBalanceAfter = await spl.getAccount( - provider.connection, - receiptTokenAccount - ); - }, "Receipt NFT token account is not closed"); - // Once withdraw request is complete, we can withdraw - // sleeping for unbonding period to end - await sleep(2000); - const depositorBalanceBefore = await spl.getAccount( - provider.connection, - depositorWSolTokenAccount - ); - tx = await withdrawInstruction(program, depositor.publicKey, tokenMint); - - try { - tx.feePayer = depositor.publicKey; - const sig = await anchor.web3.sendAndConfirmTransaction( - provider.connection, - tx, - [depositor] - ); - - console.log(" Signature for Withdrawing: ", sig); - - const depositorBalanceAfter = await spl.getAccount( - provider.connection, - depositorWSolTokenAccount - ); - - assert.equal( - depositorBalanceAfter.amount - depositorBalanceBefore.amount, - depositAmount - ); - } catch (error) { - console.log(error); - throw error; - } - } catch (error) { - console.log(error); - throw error; - } - }); - - it("Update admin", async () => { - const { stakingParamsPDA } = getStakingParamsPDA(); - try { - let tx = await program.methods - .changeAdminProposal(depositor.publicKey) - .accounts({ - admin: admin.publicKey, - stakingParams: stakingParamsPDA, - }) - .signers([admin]) - .rpc(); - console.log(" Signature for Updating Admin Proposal: ", tx); - tx = await program.methods - .acceptAdminChange() - .accounts({ - newAdmin: depositor.publicKey, - stakingParams: stakingParamsPDA, - }) - .signers([depositor]) - .rpc(); - console.log(" Signature for Accepting Admin Proposal: ", tx); - const stakingParameters = await getStakingParameters(program); - assert.equal( - stakingParameters.admin.toBase58(), - depositor.publicKey.toBase58() - ); - } catch (error) { - console.log(error); - throw error; - } - }); - - it("Update staking cap after updating admin", async () => { - const { stakingParamsPDA } = getStakingParamsPDA(); - try { - const tx = await program.methods - .updateStakingCap(new anchor.BN(newStakingCap)) - .accounts({ - admin: depositor.publicKey, - stakingParams: stakingParamsPDA, - }) - .signers([depositor]) - .rpc(); - console.log(" Signature for Updating staking cap: ", tx); - const stakingParameters = await getStakingParameters(program); - assert.equal(stakingParameters.stakingCap.toNumber(), newStakingCap); - } catch (error) { - console.log(error); - throw error; - } - }); -}); From 6f16a24b9844634008e9f9c7e117b224adf27268 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Sun, 16 Jun 2024 15:58:12 -0400 Subject: [PATCH 03/40] added restaking-v2 program to config --- Anchor.toml | 5 ++++- Cargo.lock | 15 +++++++++++++++ Cargo.toml | 1 + solana-test.sh | 1 + 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Anchor.toml b/Anchor.toml index b5c355d7..ab852a6e 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -5,10 +5,12 @@ skip-lint = false [programs.devnet] restaking = "8n3FHwYxFgQCQc2FNFkwDUf9mcqupxXcCvgfHbApMLv3" solana_ibc = "2HLLVco5HvwWriNbUhmVwA2pCetRkpgrqwnjcsZdyTKT" +restaking_v2 = "BtegF7pQSriyP7gSkDpAkPDMvTS8wfajHJSmvcVoC7kg" [programs.localnet] restaking = "8n3FHwYxFgQCQc2FNFkwDUf9mcqupxXcCvgfHbApMLv3" solana_ibc = "2HLLVco5HvwWriNbUhmVwA2pCetRkpgrqwnjcsZdyTKT" +restaking_v2 = "BtegF7pQSriyP7gSkDpAkPDMvTS8wfajHJSmvcVoC7kg" [registry] url = "https://api.apr.dev" @@ -20,7 +22,8 @@ wallet = "~/.config/solana/id.json" [workspace] members = [ "solana/restaking/programs/restaking", - "solana/solana-ibc/programs/solana-ibc" + "solana/solana-ibc/programs/solana-ibc", + "solana/restaking-v2/programs/restaking-v2" ] [scripts] diff --git a/Cargo.lock b/Cargo.lock index 6485cf0d..68b4ec09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4252,6 +4252,21 @@ dependencies = [ "solana-program", ] +[[package]] +name = "restaking_v2" +version = "0.1.0" +dependencies = [ + "anchor-client", + "anchor-lang", + "anchor-spl", + "anyhow", + "solana-ibc", + "solana-program", + "solana-signature-verifier", + "spl-associated-token-account", + "spl-token", +] + [[package]] name = "ring" version = "0.16.20" diff --git a/Cargo.toml b/Cargo.toml index 9d4bab9f..a710bffc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "common/*", "solana/allocator", "solana/restaking/programs/*", + "solana/restaking-v2/programs/*", "solana/signature-verifier", "solana/solana-ibc/programs/*", "solana/trie", diff --git a/solana-test.sh b/solana-test.sh index 5d37e362..acf6dd6a 100755 --- a/solana-test.sh +++ b/solana-test.sh @@ -10,5 +10,6 @@ cd ../.. solana program deploy target/deploy/write.so solana program deploy target/deploy/sigverify.so cargo test --lib -- --nocapture --include-ignored ::anchor +cargo test --lib -- --nocapture --include-ignored ::restaking find solana/restaking/tests/ -name '*.ts' \ -exec yarn run ts-mocha -p ./tsconfig.json -t 1000000 {} + From c5fd07c06130508096e5b837ccc2b10e843c6740 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Tue, 18 Jun 2024 17:18:55 -0400 Subject: [PATCH 04/40] add fee payer account for initializing accounts --- solana/restaking-v2/programs/restaking-v2/src/lib.rs | 10 ++++++---- solana/restaking-v2/programs/restaking-v2/src/tests.rs | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index c0f96b57..c41072b8 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -304,9 +304,11 @@ pub struct Initialize<'info> { #[derive(Accounts)] pub struct Deposit<'info> { - #[account(mut)] pub staker: Signer<'info>, + #[account(mut)] + pub fee_payer: Signer<'info>, + #[account(mut, seeds = [COMMON_SEED], bump)] pub common_state: Account<'info, CommonState>, @@ -314,12 +316,12 @@ pub struct Deposit<'info> { #[account(mut, token::authority = staker, token::mint = token_mint)] pub staker_token_account: Account<'info, TokenAccount>, - #[account(init_if_needed, payer = staker, seeds = [ESCROW_SEED, &token_mint.key().to_bytes()], bump, token::mint = token_mint, token::authority = common_state)] + #[account(init_if_needed, payer = fee_payer, seeds = [ESCROW_SEED, &token_mint.key().to_bytes()], bump, token::mint = token_mint, token::authority = common_state)] pub escrow_token_account: Account<'info, TokenAccount>, - #[account(init_if_needed, payer = staker, seeds = [RECEIPT_SEED, &token_mint.key().to_bytes()], bump, mint::authority = common_state, mint::decimals = RECEIPT_TOKEN_DECIMALS)] + #[account(init_if_needed, payer = fee_payer, seeds = [RECEIPT_SEED, &token_mint.key().to_bytes()], bump, mint::authority = common_state, mint::decimals = RECEIPT_TOKEN_DECIMALS)] pub receipt_token_mint: Account<'info, Mint>, - #[account(init_if_needed, payer = staker, associated_token::authority = staker, associated_token::mint = receipt_token_mint)] + #[account(init_if_needed, payer = fee_payer, associated_token::authority = staker, associated_token::mint = receipt_token_mint)] pub staker_receipt_token_account: Account<'info, TokenAccount>, pub token_program: Program<'info, Token>, diff --git a/solana/restaking-v2/programs/restaking-v2/src/tests.rs b/solana/restaking-v2/programs/restaking-v2/src/tests.rs index aa56b2bd..6dd29cb9 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/tests.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/tests.rs @@ -169,6 +169,7 @@ fn restaking_test_deliver() -> Result<()> { .request() .accounts(crate::accounts::Deposit { common_state, + fee_payer: authority.pubkey(), system_program: solana_program::system_program::ID, staker: authority.pubkey(), token_mint: token_mint_key, From 1abae32815e3e39f84f16d1b4869445164fa69a7 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Tue, 18 Jun 2024 17:19:08 -0400 Subject: [PATCH 05/40] fmt --- .../programs/restaking-v2/src/lib.rs | 25 +++++++++++++------ .../programs/restaking-v2/src/tests.rs | 25 +++++++++++-------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index c41072b8..74221242 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -1,5 +1,6 @@ use anchor_lang::prelude::*; -use anchor_spl::{token::{Mint, Token, TokenAccount}, associated_token::AssociatedToken}; +use anchor_spl::associated_token::AssociatedToken; +use anchor_spl::token::{Mint, Token, TokenAccount}; use solana_ibc::program::SolanaIbc; declare_id!("BtegF7pQSriyP7gSkDpAkPDMvTS8wfajHJSmvcVoC7kg"); @@ -73,7 +74,10 @@ pub mod restaking_v2 { authority: ctx.accounts.staker.to_account_info(), }; - let cpi_ctx = CpiContext::new(ctx.accounts.token_program.to_account_info(), transfer_ix); + let cpi_ctx = CpiContext::new( + ctx.accounts.token_program.to_account_info(), + transfer_ix, + ); anchor_spl::token::transfer(cpi_ctx, amount)?; @@ -138,7 +142,8 @@ pub mod restaking_v2 { let seeds = core::slice::from_ref(&seeds); // Check if balance is enough - let staker_receipt_token_account = &ctx.accounts.staker_receipt_token_account; + let staker_receipt_token_account = + &ctx.accounts.staker_receipt_token_account; if staker_receipt_token_account.amount < amount { return Err(error!(ErrorCodes::NotEnoughReceiptTokensToWithdraw)); @@ -164,12 +169,16 @@ pub mod restaking_v2 { authority: ctx.accounts.staker.to_account_info(), }; - let cpi_ctx = CpiContext::new(ctx.accounts.token_program.to_account_info(), burn_ix); + let cpi_ctx = CpiContext::new( + ctx.accounts.token_program.to_account_info(), + burn_ix, + ); anchor_spl::token::burn(cpi_ctx, amount)?; // Call guest chain program to update the stake equally - let stake_per_validator = (amount / common_state.validators.len() as u64) as i128; + let stake_per_validator = + (amount / common_state.validators.len() as u64) as i128; let set_stake_ix = solana_ibc::cpi::accounts::SetStake { sender: ctx.accounts.staker.to_account_info(), @@ -250,9 +259,9 @@ pub mod restaking_v2 { ) -> Result<()> { let staking_params = &mut ctx.accounts.common_state; - let contains_mint = new_token_mints - .iter() - .any(|token_mint| staking_params.whitelisted_tokens.contains(token_mint)); + let contains_mint = new_token_mints.iter().any(|token_mint| { + staking_params.whitelisted_tokens.contains(token_mint) + }); if contains_mint { return Err(error!(ErrorCodes::TokenAlreadyWhitelisted)); diff --git a/solana/restaking-v2/programs/restaking-v2/src/tests.rs b/solana/restaking-v2/programs/restaking-v2/src/tests.rs index 6dd29cb9..d869d7cf 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/tests.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/tests.rs @@ -200,14 +200,15 @@ fn restaking_test_deliver() -> Result<()> { .unwrap(); assert_eq!( - (staker_receipt_token_acc_balance_after.ui_amount.unwrap() - * 10_f64.powf(crate::RECEIPT_TOKEN_DECIMALS.into())) as u64, + (staker_receipt_token_acc_balance_after.ui_amount.unwrap() * + 10_f64.powf(crate::RECEIPT_TOKEN_DECIMALS.into())) as u64, STAKE_AMOUNT ); assert_eq!( - ((staker_token_acc_balance_before.ui_amount.unwrap() - - staker_token_acc_balance_after.ui_amount.unwrap()) - * 10_f64.powf(crate::RECEIPT_TOKEN_DECIMALS.into())).round() as u64, + ((staker_token_acc_balance_before.ui_amount.unwrap() - + staker_token_acc_balance_after.ui_amount.unwrap()) * + 10_f64.powf(crate::RECEIPT_TOKEN_DECIMALS.into())) + .round() as u64, STAKE_AMOUNT ); @@ -258,15 +259,17 @@ fn restaking_test_deliver() -> Result<()> { .unwrap(); assert_eq!( - ((staker_receipt_token_acc_balance_before.ui_amount.unwrap() - - staker_receipt_token_acc_balance_after.ui_amount.unwrap()) - * 10_f64.powf(crate::RECEIPT_TOKEN_DECIMALS.into())).round() as u64, + ((staker_receipt_token_acc_balance_before.ui_amount.unwrap() - + staker_receipt_token_acc_balance_after.ui_amount.unwrap()) * + 10_f64.powf(crate::RECEIPT_TOKEN_DECIMALS.into())) + .round() as u64, STAKE_AMOUNT ); assert_eq!( - ((staker_token_acc_balance_after.ui_amount.unwrap() - - staker_token_acc_balance_before.ui_amount.unwrap()) - * 10_f64.powf(crate::RECEIPT_TOKEN_DECIMALS.into())).round() as u64, + ((staker_token_acc_balance_after.ui_amount.unwrap() - + staker_token_acc_balance_before.ui_amount.unwrap()) * + 10_f64.powf(crate::RECEIPT_TOKEN_DECIMALS.into())) + .round() as u64, STAKE_AMOUNT ); From b7b1a5d096b22fe27db97048c40b2fd3d130806b Mon Sep 17 00:00:00 2001 From: dhruvja Date: Wed, 19 Jun 2024 11:37:33 -0400 Subject: [PATCH 06/40] set remainder to the first validator --- .../programs/restaking-v2/src/lib.rs | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index 74221242..22e784a1 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -96,7 +96,11 @@ pub mod restaking_v2 { anchor_spl::token::mint_to(cpi_ctx, amount)?; // Call guest chain program to update the stake equally - let stake_per_validator = amount / common_state.validators.len() as u64; + + let validators_len = common_state.validators.len() as u64; + + let stake_per_validator = amount / validators_len; + let stake_remainder = amount % validators_len; let set_stake_ix = solana_ibc::cpi::accounts::SetStake { sender: ctx.accounts.staker.to_account_info(), @@ -114,10 +118,15 @@ pub mod restaking_v2 { let set_stake_arg = common_state .validators .iter() - .map(|validator| { + .enumerate() + .map(|(index, validator)| { ( sigverify::ed25519::PubKey::from(validator.clone()), - stake_per_validator as i128, + if index == 0 { + (stake_per_validator + stake_remainder) as i128 + } else { + stake_per_validator as i128 + }, ) }) .collect::>(); @@ -177,8 +186,10 @@ pub mod restaking_v2 { anchor_spl::token::burn(cpi_ctx, amount)?; // Call guest chain program to update the stake equally + let validators_len = common_state.validators.len() as u64; let stake_per_validator = (amount / common_state.validators.len() as u64) as i128; + let stake_remainder = (amount % validators_len) as i128; let set_stake_ix = solana_ibc::cpi::accounts::SetStake { sender: ctx.accounts.staker.to_account_info(), @@ -196,10 +207,15 @@ pub mod restaking_v2 { let set_stake_arg = common_state .validators .iter() - .map(|validator| { + .enumerate() + .map(|(index, validator)| { ( sigverify::ed25519::PubKey::from(validator.clone()), - -stake_per_validator, + if index == 0 { + -(stake_per_validator + stake_remainder) + } else { + -stake_per_validator + }, ) }) .collect::>(); From 142110b5a0206d957ed9becedb12b5e7da5b7c60 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Wed, 19 Jun 2024 11:39:38 -0400 Subject: [PATCH 07/40] use extend instead of append --- solana/restaking-v2/programs/restaking-v2/src/lib.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index 22e784a1..8ba88deb 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -285,7 +285,7 @@ pub mod restaking_v2 { staking_params .whitelisted_tokens - .append(&mut new_token_mints.as_slice().to_vec()); + .extend_from_slice(new_token_mints.as_slice()); Ok(()) } @@ -308,9 +308,7 @@ pub mod restaking_v2 { return Err(error!(ErrorCodes::ValidatorAlreadyAdded)); } - staking_params - .validators - .append(&mut new_validators.as_slice().to_vec()); + staking_params.validators.extend_from_slice(new_validators.as_slice()); Ok(()) } From a1edae5b9f1bd1e095ee56f79ccb0d940f72c01f Mon Sep 17 00:00:00 2001 From: dhruvja Date: Tue, 2 Jul 2024 10:10:48 +0100 Subject: [PATCH 08/40] add docs --- solana/restaking-v2/README.md | 148 +++------------------------------- 1 file changed, 13 insertions(+), 135 deletions(-) diff --git a/solana/restaking-v2/README.md b/solana/restaking-v2/README.md index 44d57c84..298953e8 100644 --- a/solana/restaking-v2/README.md +++ b/solana/restaking-v2/README.md @@ -1,140 +1,18 @@ -# Restaking +# Restaking V2 -The high level flow of the program is given in the image below. +This program was built to make it easier to keep track of deposits to the restaking vaults and make use of fungible receipt tokens as opposed to using NFTs in previous version which had a few drawbacks for it to be used as an yield for the rollup. -![Flow of restaking](./restaking-flow.png) +The drawbacks of the previous restaking program +- The depositors were given an NFT as a receipt token which meant that there were no partial withdrawals. +- Since the users could choose the validator to which they deposit to, the token deposited to one validator is different from the token deposited to another validator. Which means if the token is restaked and bridged to the rollup, then there would be a token for each validator even if they are same token. -## Accounts +For example: JitoSOL deposited to validator A and B would be different on rollup even though it is the same token on Solana. -- Vaults: The vaults are created for each whitelisted token. Vaults - are token accounts. The authority of the account is a PDA which - means the program controls the vault and any debit from the vault - has to go through the smart contract. +So the new restaking program was built specifically to support restaking of tokens before bridging to rollup and use fungible receipt tokens. +These are changes which were introduced in the new version. +- Users cannot choose which validator they delegate their stake to since their stake is equally divided among the validators specified in the program. +- If one of the validator gets slashed, the amount is slashed equally among the validators. +- A fungible receipt token is issued instead of a non fungible one. +- There is no unbonding period since all the validators get slashed equally. -- Receipt Token Mint: The receipt token mint is a NFT which is the - seed for the PDA storing information about stake amout, validator - and rewards. For more information, refer: - https://docs.composable.finance/technology/solana-restaking/vaults/#receipt-token - -- Staking Params: This is a PDA which stores the staking parameters - and also is the authority to `Receipt Token Mint` and `Vaults`. - -- Vault Params: PDA which stores the vault params which are stake time - and service for which it is staked along with when rewards were - claimed. - -## Instructions - -When the contract is deployed, the `initialize` method is called where -the whitelisted tokens, admin key and the rewards -token mint is set. Initially the `guest_chain_initialization` is set to -false. Any update to the staking paramters can only be -done by the admin key. A token account is also created for the -rewards token mint which would distribute the rewards. Since the -authority is PDA, any debit from the account will happen only through -the contract (only in `claim` method for now). After that the users -can start staking. - -- `Deposit`: User can stake any of the whitelisted token. The tokens - are stored in the vault and receipt tokens are minted for the user. - A CPI (cross program invocation) call is made to the guest chain - program where the stake is updated for the validator specified. - -- `Withdrawal Request`: Users can request for withdrawal and after the - unbonding period gets over, the tokens would be withdrawn. In this method, - the receipt NFT would be transferred to an escrow account and the receipt - NFT token account would be closed. All the pending rewards are transferred - in this method and users wont be eligible for rewards during the unbonding - period. - -- `Cancel Withdrawal Request`: Withdrawal request set by the user can be - cancelled as long at they are under unbonding period or if the withdraw - has not been executed yet. They would get back their receipt token and - withdrawal request would be cancelled. - -- `Withdraw`: Users can only withdraw their tokens after the unbonding - period ends. When user wants to withdraw the tokens, final stake amount - is fetched from the guest chain. The receipt token is burnt. A CPI call - is made to the guest chain to update the stake accordingly. - -- `Claim Rewards`: Users can claim rewards without withdrawing their - stake. They would have to have to own the non fungible receipt - token to be eligible for claiming rewards. - -- `Set Service`: Once the bridge is live, users who had deposited before - can call this method to delegate their stake to the validator. Users - cannot withdraw or claim any rewards until they delegate their stake - to the validator. But this method wont be needed after the bridge is - live and would panic if called otherwise. - -- `Update Guest chain Initialization`: The admin would call this method - when the bridge is up and running. This would set `guest_chain_program_id` - with the specified program ID which would allow to make CPI calls during - deposit and set stake to validator. - -- `Update token Whitelist`: The admin can update the token whitelist. - Only callable by admin set during `initialize` method. - -- `Withdraw Reward Funds`: This method is only callable by admin to - withdraw all the funds from the reward token account. This is a - safety measure so it should be called only during emergency. - -- `Change admin Proposal`: A proposal set by the current admin for - changing the admin. A new admin is proposed by the existing admin - and the until the new admin approves it in `accept_admin_change`, - the admin wont be changed. - -- `Accept admin change`: The new admin set by the existing admin is - exepected to call this method. When the new admin calls this method, - the admin is changed. - -- `Update Staking Cap`: Method which sets the staking cap which limits - how much total stake can be set in the contract. This method expects - the staking cap to be higher than previous to execute successfully. - -## Verifying the code - -First, compile the programs code from the `emulated-light-client` Github -repository to get its bytecode. - - git clone https://github.com/ComposableFi/emulated-light-client.git - anchor build - -Now, install the [Ellipsis Labs verifiable -build](https://crates.io/crates/solana-verify) crate. - - cargo install solana-verify - -Get the executable hash of the bytecode from the Restaking program that was -compiled - - solana-verify get-executable-hash target/deploy/restaking.so - -Get the hash from the bytecode of the on-chain restaking program that you want -to verify - - solana-verify get-program-hash -u \ - 8n3FHwYxFgQCQc2FNFkwDUf9mcqupxXcCvgfHbApMLv3 - -**Note for multisig members:** If you want to verify the upgrade program buffer, -then you need to get the bytecode from the buffer account using the below -command. You can get the buffer account address from the squads. - - solana-verify get-buffer-hash -u - -If the hash outputs of those two commands match, the code in the -repository matches the on-chain programs code. - -## Note - -- Since the rewards are not implemented yet on the Guest Chain, a nil value is - returned for now. - -- Oracle interface is yet to be added to fetch the current price of staked - tokens as well as the governance token in the Guest Chain. - -- Users who have deposited before the Guest Chain is initialized can choose the - validator in one of three ways(Yet to be implemented): - - choose a validator randomly, - - choose a validator from the list of top 10 validators chosen by us or - - choose a particular validator. +This program can only be called by the bridge contract. If people just want to restake directly and dont want to bridge, they can do it via restaking-v1 program. From 59c57e5777fd7baf39f4dd70abca14f18fdcc61e Mon Sep 17 00:00:00 2001 From: dhruvja Date: Mon, 8 Jul 2024 11:32:38 +0100 Subject: [PATCH 09/40] add support for oracles --- Anchor.toml | 6 + Cargo.lock | 59 + Cargo.toml | 1 + solana/restaking-v2/README.md | 5 + .../programs/restaking-v2/Cargo.toml | 1 + .../programs/restaking-v2/src/lib.rs | 290 ++++- .../programs/restaking-v2/src/tests.rs | 71 +- solana/restaking/tests/restaking.ts | 1006 +++++++++-------- 8 files changed, 934 insertions(+), 505 deletions(-) diff --git a/Anchor.toml b/Anchor.toml index ab852a6e..01ef2480 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -42,3 +42,9 @@ rpc_port = 8899 [[test.validator.clone]] address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" + +[[test.validator.clone]] +address = "Dpw1EAVrSB1ibxiDQyTAW6Zip3J4Btk2x4SgApQCeFbX" + +[[test.validator.clone]] +address = "7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE" diff --git a/Cargo.lock b/Cargo.lock index 68b4ec09..1cdd5134 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1709,6 +1709,15 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "fast-math" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2465292146cdfc2011350fe3b1c616ac83cf0faeedb33463ba1c332ed8948d66" +dependencies = [ + "ieee754", +] + [[package]] name = "fastrand" version = "2.0.1" @@ -2103,6 +2112,9 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hex-literal" @@ -2879,6 +2891,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "ieee754" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9007da9cacbd3e6343da136e98b0d2df013f553d35bdec8b518f07bea768e19c" + [[package]] name = "im" version = "15.1.0" @@ -3906,6 +3924,37 @@ dependencies = [ "prost", ] +[[package]] +name = "pyth-solana-receiver-sdk" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "937c8595148fb2a9a90439daf6a371a5b3c9fcd9b636f26d36ae31d6846d4339" +dependencies = [ + "anchor-lang", + "hex", + "pythnet-sdk", + "solana-program", +] + +[[package]] +name = "pythnet-sdk" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bbbc0456f9f27c9ad16b6c3bf1b2a7fea61eebf900f4d024a0468b9a84fe0c1" +dependencies = [ + "bincode", + "borsh 0.10.3", + "bytemuck", + "byteorder", + "fast-math", + "hex", + "rustc_version", + "serde", + "sha3 0.10.8", + "slow_primes", + "thiserror", +] + [[package]] name = "qstring" version = "0.7.2" @@ -4260,6 +4309,7 @@ dependencies = [ "anchor-lang", "anchor-spl", "anyhow", + "pyth-solana-receiver-sdk", "solana-ibc", "solana-program", "solana-signature-verifier", @@ -4816,6 +4866,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slow_primes" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58267dd2fbaa6dceecba9e3e106d2d90a2b02497c0e8b01b8759beccf5113938" +dependencies = [ + "num", +] + [[package]] name = "smallvec" version = "1.13.2" diff --git a/Cargo.toml b/Cargo.toml index a710bffc..331ec2fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ pretty_assertions = "1.4.0" primitive-types = "0.12.2" prost = { version = "0.12.3", default-features = false } prost-build = { version = "0.12.3", default-features = false } +pyth-solana-receiver-sdk ="0.1.0" rand = { version = "0.8.5" } reqwest = "0.12.3" serde = "1" diff --git a/solana/restaking-v2/README.md b/solana/restaking-v2/README.md index 298953e8..ef8cbb5f 100644 --- a/solana/restaking-v2/README.md +++ b/solana/restaking-v2/README.md @@ -14,5 +14,10 @@ These are changes which were introduced in the new version. - If one of the validator gets slashed, the amount is slashed equally among the validators. - A fungible receipt token is issued instead of a non fungible one. - There is no unbonding period since all the validators get slashed equally. +- Allow deposits of tokens other than LSTs with the use of oracles. This program can only be called by the bridge contract. If people just want to restake directly and dont want to bridge, they can do it via restaking-v1 program. + +## Oracles + +For tokens others than LSTs which have a different value than SOL, we need to use oracle to get their current price and then update the stake. But since the stake is recorded in lamports in the guest chain, everytime the price changes we would need to update the stake. To do this, we store the delegations of the particular token locally and get the price on every interval set during initialization. Every time the price is updated, we calculate the difference and update the price on the guest chain. This would make sure that the stake on guest chain is always up to date. Whenever new deposits are made, it would use the stored price rather than from the oracle. But if the price is stale then the deposit would be rejected. This is a better way of updating the stake using oracle where we need to update it once for all the deposits for a particular token during an interval. \ No newline at end of file diff --git a/solana/restaking-v2/programs/restaking-v2/Cargo.toml b/solana/restaking-v2/programs/restaking-v2/Cargo.toml index f303993d..4ef97435 100644 --- a/solana/restaking-v2/programs/restaking-v2/Cargo.toml +++ b/solana/restaking-v2/programs/restaking-v2/Cargo.toml @@ -23,6 +23,7 @@ anchor-spl = { workspace = true, features = ["metadata"] } solana-ibc = { workspace = true, features = ["cpi"] } solana-program.workspace = true solana-signature-verifier.workspace = true +pyth-solana-receiver-sdk.workspace = true [dev-dependencies] anchor-client.workspace = true diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index 8ba88deb..7785ea5a 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -1,6 +1,7 @@ use anchor_lang::prelude::*; use anchor_spl::associated_token::AssociatedToken; use anchor_spl::token::{Mint, Token, TokenAccount}; +use pyth_solana_receiver_sdk::price_update::PriceUpdateV2; use solana_ibc::program::SolanaIbc; declare_id!("BtegF7pQSriyP7gSkDpAkPDMvTS8wfajHJSmvcVoC7kg"); @@ -10,6 +11,10 @@ pub const ESCROW_SEED: &[u8] = b"escrow"; pub const RECEIPT_SEED: &[u8] = b"receipt"; pub const RECEIPT_TOKEN_DECIMALS: u8 = 9; +pub const SOL_DECIMALS: u8 = 9; + +pub const SOL_PRICE_FEED_ID: &str = + "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; #[cfg(test)] mod tests; @@ -17,12 +22,13 @@ mod tests; #[program] pub mod restaking_v2 { use anchor_spl::token::{Burn, MintTo, Transfer}; + use pyth_solana_receiver_sdk::price_update::get_feed_id_from_hex; use super::*; pub fn initialize( ctx: Context, - whitelisted_tokens: Vec, + whitelisted_tokens: Vec, initial_validators: Vec, guest_chain_program_id: Pubkey, ) -> Result<()> { @@ -31,7 +37,10 @@ pub mod restaking_v2 { let common_state = &mut ctx.accounts.common_state; common_state.admin = ctx.accounts.admin.key(); - common_state.whitelisted_tokens = whitelisted_tokens; + common_state.whitelisted_tokens = whitelisted_tokens + .iter() + .map(|token_mint| token_mint.into()) + .collect::>(); common_state.validators = initial_validators; common_state.guest_chain_program_id = guest_chain_program_id; @@ -50,14 +59,11 @@ pub mod restaking_v2 { let stake_token_mint = &ctx.accounts.token_mint.key(); - if common_state + let whitelisted_token = common_state .whitelisted_tokens .iter() - .find(|&x| x == stake_token_mint) - .is_none() - { - return Err(error!(ErrorCodes::InvalidTokenMint)); - } + .find(|&x| &x.address == stake_token_mint) + .ok_or(ErrorCodes::InvalidTokenMint)?; if ctx.accounts.staker_token_account.amount < amount { return Err(error!(ErrorCodes::NotEnoughTokensToStake)); @@ -99,6 +105,29 @@ pub mod restaking_v2 { let validators_len = common_state.validators.len() as u64; + let amount = if let Some(_) = &whitelisted_token.oracle_address { + // Check if the price is stale + let current_time = Clock::get()?.unix_timestamp as u64; + + if (current_time - whitelisted_token.last_updated_in_sec) > + whitelisted_token.max_update_time_in_sec + { + return Err(error!(ErrorCodes::PriceTooStale)); + } + + // let token_decimals = ctx.accounts.token_mint.decimals; + + // let amount_in_sol_decimals = (1_u64 + // * 10u64.pow(SOL_DECIMALS as u32)) + // / 10u64.pow(token_decimals as u32); + + whitelisted_token.latest_price * (amount as u64) + + // update the validator with the stake he deposited + } else { + amount + }; + let stake_per_validator = amount / validators_len; let stake_remainder = amount % validators_len; @@ -145,6 +174,14 @@ pub mod restaking_v2 { pub fn withdraw(ctx: Context, amount: u64) -> Result<()> { let common_state = &mut ctx.accounts.common_state; + let stake_token_mint = &ctx.accounts.token_mint.key(); + + let whitelisted_token = common_state + .whitelisted_tokens + .iter() + .find(|&x| &x.address == stake_token_mint) + .ok_or(ErrorCodes::InvalidTokenMint)?; + let bump = ctx.bumps.common_state; let seeds = [COMMON_SEED, core::slice::from_ref(&bump)]; let seeds = seeds.as_ref(); @@ -185,6 +222,20 @@ pub mod restaking_v2 { anchor_spl::token::burn(cpi_ctx, amount)?; + let amount = if let Some(_) = &whitelisted_token.oracle_address { + // Check if the price is stale + let current_time = Clock::get()?.unix_timestamp as u64; + + if (current_time - whitelisted_token.last_updated_in_sec) > + whitelisted_token.max_update_time_in_sec + { + return Err(error!(ErrorCodes::PriceTooStale)); + } + whitelisted_token.latest_price * (amount as u64) + } else { + amount + }; + // Call guest chain program to update the stake equally let validators_len = common_state.validators.len() as u64; let stake_per_validator = @@ -271,18 +322,29 @@ pub mod restaking_v2 { /// whitelisted token list. pub fn update_token_whitelist( ctx: Context, - new_token_mints: Vec, + new_token_mints: Vec, ) -> Result<()> { let staking_params = &mut ctx.accounts.common_state; let contains_mint = new_token_mints.iter().any(|token_mint| { - staking_params.whitelisted_tokens.contains(token_mint) + staking_params + .whitelisted_tokens + .iter() + .find(|whitelisted_token_mint| { + whitelisted_token_mint.address == token_mint.address + }) + .is_some() }); if contains_mint { return Err(error!(ErrorCodes::TokenAlreadyWhitelisted)); } + let new_token_mints = new_token_mints + .iter() + .map(|token_mint| token_mint.into()) + .collect::>(); + staking_params .whitelisted_tokens .extend_from_slice(new_token_mints.as_slice()); @@ -312,6 +374,124 @@ pub mod restaking_v2 { Ok(()) } + + pub fn update_token_price(ctx: Context) -> Result<()> { + let common_state = &mut ctx.accounts.common_state; + + let token_price_feed = &ctx.accounts.token_price_feed; + let sol_price_feed = &ctx.accounts.sol_price_feed; + + let token_mint = ctx.accounts.token_mint.key(); + + let validators = common_state.validators.clone(); + + let staked_token = common_state + .whitelisted_tokens + .iter_mut() + .find(|whitelisted_token| whitelisted_token.address == token_mint); + + if let Some(staked_token) = staked_token { + if let Some(token_feed_id) = staked_token.oracle_address.as_ref() { + let (token_price, sol_price) = if cfg!(feature = "mocks") { + let feed_id: [u8; 32] = + get_feed_id_from_hex(token_feed_id)?; + let sol_price = sol_price_feed.get_price_unchecked( + &get_feed_id_from_hex(SOL_PRICE_FEED_ID)?, + )?; + let token_price = + token_price_feed.get_price_unchecked(&feed_id)?; + (token_price, sol_price) + } else { + let maximum_age_in_sec: u64 = 30; + let feed_id: [u8; 32] = + get_feed_id_from_hex(token_feed_id)?; + let sol_price = sol_price_feed.get_price_no_older_than( + &Clock::get()?, + maximum_age_in_sec, + &get_feed_id_from_hex(SOL_PRICE_FEED_ID)?, + )?; + let token_price = token_price_feed + .get_price_no_older_than( + &Clock::get()?, + maximum_age_in_sec, + &feed_id, + )?; + (token_price, sol_price) + }; + + let token_decimals = ctx.accounts.token_mint.decimals; + + let amount_in_sol_decimals = (1_u64 * + 10u64.pow(SOL_DECIMALS as u32)) / + 10u64.pow(token_decimals as u32); + + let final_amount_in_sol = + ((token_price.price * (amount_in_sol_decimals as i64)) / + sol_price.price) as u64; + + msg!( + "The price of solana is ({} ± {}) * 10^{} and final price \ + {}\n + The price of solana is ({} ± {}) * 10^{} and amount in \ + sol decimals {}", + sol_price.price, + sol_price.conf, + sol_price.exponent, + final_amount_in_sol, + token_price.price, + token_price.conf, + token_price.exponent, + amount_in_sol_decimals + ); + + let previous_price = staked_token.latest_price; + + let set_stake_arg = staked_token + .delegations + .iter() + .map(|&(validator_idx, amount)| { + let amount = amount as i128; + let validator = validators[validator_idx as usize]; + let change_in_stake = (previous_price as i128 - + final_amount_in_sol as i128) * + amount; + ( + sigverify::ed25519::PubKey::from(validator.clone()), + change_in_stake as i128, + ) + }) + .collect(); + + let set_stake_ix = solana_ibc::cpi::accounts::SetStake { + sender: ctx.accounts.signer.to_account_info(), + chain: ctx.accounts.chain.to_account_info(), + trie: ctx.accounts.trie.to_account_info(), + system_program: ctx + .accounts + .system_program + .to_account_info(), + instruction: ctx.accounts.instruction.to_account_info(), + }; + + let cpi_ctx = CpiContext::new( + ctx.accounts.guest_chain_program.to_account_info(), + set_stake_ix, + ); + + solana_ibc::cpi::update_stake(cpi_ctx, set_stake_arg)?; + + staked_token.latest_price = final_amount_in_sol; + staked_token.last_updated_in_sec = + Clock::get()?.unix_timestamp as u64; + } else { + return Err(error!(ErrorCodes::OracleAddressNotFound)); + } + } else { + return Err(error!(ErrorCodes::InvalidTokenMint)); + } + + Ok(()) + } } #[derive(Accounts)] @@ -415,6 +595,40 @@ pub struct Withdraw<'info> { pub instruction: UncheckedAccount<'info>, } +#[derive(Accounts)] +pub struct UpdateTokenPrice<'info> { + #[account(mut)] + pub signer: Signer<'info>, + + #[account(mut, seeds = [COMMON_SEED], bump)] + pub common_state: Account<'info, CommonState>, + + pub token_mint: Account<'info, Mint>, + + pub token_price_feed: Account<'info, PriceUpdateV2>, + pub sol_price_feed: Account<'info, PriceUpdateV2>, + + pub system_program: Program<'info, System>, + + #[account(mut, seeds = [solana_ibc::CHAIN_SEED], bump, seeds::program = guest_chain_program)] + /// CHECK: + pub chain: UncheckedAccount<'info>, + + #[account(mut, seeds = [solana_ibc::TRIE_SEED], bump, seeds::program = guest_chain_program)] + /// CHECK: + pub trie: UncheckedAccount<'info>, + + pub guest_chain_program: Program<'info, SolanaIbc>, + + /// The Instructions sysvar. + /// + /// CHECK: The account is passed on during CPI and destination contract + /// performs the validation so this is safe even if we don’t check the + /// address. Nonetheless, the account is checked at each use. + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub instruction: UncheckedAccount<'info>, +} + #[derive(Accounts)] pub struct UpdateStakingParams<'info> { #[account(mut)] @@ -433,10 +647,56 @@ pub struct UpdateAdmin<'info> { pub common_state: Account<'info, CommonState>, } +#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] +pub struct NewTokenPayload { + pub address: Pubkey, + pub oracle_address: Option, + pub max_update_time_in_sec: u64, + pub update_frequency_in_sec: u64, +} + +/// Struct which stores the token address and price information. The price +/// is updated based on the frequency. It also stores the amount which has been +/// delegated to the validators which is then recalculated with the new price and +/// updated. +/// +/// If the price of the token increased by 10%, then the delegations +/// would be increased by 10% and then `update_stake` method would be called. +#[derive(AnchorDeserialize, AnchorSerialize, Debug, Clone)] +pub struct StakeToken { + pub address: Pubkey, // 32 + pub oracle_address: Option, + /// Latest price of token wrt to SOL fetched from the oracle. + pub latest_price: u64, // 8 + /// Time at which the price was updated. Used to check if the price is stale. + pub last_updated_in_sec: u64, // 8 + /// If the price is not updated after the `max_update_time` below, + /// the above price should be considered invalid. + pub max_update_time_in_sec: u64, // 8 + /// The frequency at which the price should be updated. + pub update_frequency_in_sec: u64, // 8 + /// mapping of the validator index with their stake in the above token + pub delegations: Vec<(u8, u128)>, // n * (1 + 16) +} + +impl From<&NewTokenPayload> for StakeToken { + fn from(payload: &NewTokenPayload) -> Self { + StakeToken { + address: payload.address, + oracle_address: payload.oracle_address.clone(), + latest_price: 0, + last_updated_in_sec: 0, + max_update_time_in_sec: payload.max_update_time_in_sec, + update_frequency_in_sec: payload.update_frequency_in_sec, + delegations: vec![], + } + } +} + #[account] pub struct CommonState { pub admin: Pubkey, - pub whitelisted_tokens: Vec, + pub whitelisted_tokens: Vec, pub validators: Vec, pub guest_chain_program_id: Pubkey, pub new_admin_proposal: Option, @@ -448,7 +708,7 @@ pub enum ErrorCodes { NoProposedAdmin, #[msg("Signer is not the proposed admin")] ConstraintSigner, - #[msg("Only whitelisted tokens can be minted")] + #[msg("Only whitelisted tokens can be deposited")] InvalidTokenMint, #[msg("Not enough receipt token to withdraw")] NotEnoughReceiptTokensToWithdraw, @@ -458,4 +718,10 @@ pub enum ErrorCodes { TokenAlreadyWhitelisted, #[msg("Validator is already added")] ValidatorAlreadyAdded, + #[msg( + "Oracle address not found. Maybe its price doesnt need to be updated?" + )] + OracleAddressNotFound, + #[msg("The oracle price has not been updated yet")] + PriceTooStale, } diff --git a/solana/restaking-v2/programs/restaking-v2/src/tests.rs b/solana/restaking-v2/programs/restaking-v2/src/tests.rs index d869d7cf..c160c846 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/tests.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/tests.rs @@ -1,4 +1,5 @@ use std::rc::Rc; +use std::str::FromStr; use std::thread::sleep; use std::time::Duration; @@ -11,11 +12,21 @@ use anchor_client::{Client, Cluster}; use anchor_lang::solana_program::system_instruction::create_account; use anchor_spl::associated_token::get_associated_token_address; use anyhow::Result; +use pyth_solana_receiver_sdk::price_update::get_feed_id_from_hex; use spl_token::instruction::initialize_mint2; +use crate::{NewTokenPayload, SOL_PRICE_FEED_ID}; + +const PYTH_PROGRAM_ID: &str = "pythWSnswVUd12oZpeFP8e9CVaEqJg25g1Vtc2biRsT"; + +const STAKE_TOKEN_MINT_DECIMALS: u8 = 6; + const MINT_AMOUNT: u64 = 1000000000000; const STAKE_AMOUNT: u64 = 100000; +const TOKEN_FEED_ID: &str = + "0xeaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a"; + fn airdrop(client: &RpcClient, account: Pubkey, lamports: u64) -> Signature { let balance_before = client.get_balance(&account).unwrap(); println!("This is balance before {}", balance_before); @@ -71,7 +82,7 @@ fn restaking_test_deliver() -> Result<()> { &token_mint_key, &authority.pubkey(), Some(&authority.pubkey()), - 9, + STAKE_TOKEN_MINT_DECIMALS, ) .expect("invalid mint instruction"); @@ -109,6 +120,13 @@ fn restaking_test_deliver() -> Result<()> { */ println!("\nInitializing the program"); + let new_token_mint = NewTokenPayload { + address: token_mint_key, + oracle_address: Some(TOKEN_FEED_ID.to_string()), + max_update_time_in_sec: 0, + update_frequency_in_sec: 0, + }; + let tx = program .request() .accounts(crate::accounts::Initialize { @@ -117,7 +135,7 @@ fn restaking_test_deliver() -> Result<()> { system_program: solana_program::system_program::ID, }) .args(crate::instruction::Initialize { - whitelisted_tokens: vec![token_mint_key], + whitelisted_tokens: vec![new_token_mint], initial_validators: vec![authority.pubkey()], guest_chain_program_id: solana_ibc::ID, }) @@ -156,6 +174,51 @@ fn restaking_test_deliver() -> Result<()> { ) .0; + /* + Update the token price + */ + println!("\nUpdating the token price"); + + let token_feed_id = get_feed_id_from_hex(TOKEN_FEED_ID).unwrap(); + let sol_feed_id = get_feed_id_from_hex(SOL_PRICE_FEED_ID).unwrap(); + let shard_buffer = 0_u16.to_le_bytes(); + + let token_price_acc = Pubkey::find_program_address( + &[&shard_buffer, &token_feed_id], + &Pubkey::from_str(PYTH_PROGRAM_ID).unwrap(), + ) + .0; + + let sol_price_acc = Pubkey::find_program_address( + &[&shard_buffer, &sol_feed_id], + &Pubkey::from_str(PYTH_PROGRAM_ID).unwrap(), + ) + .0; + + let tx = program + .request() + .accounts(crate::accounts::UpdateTokenPrice { + signer: authority.pubkey(), + common_state, + token_mint: token_mint_key, + token_price_feed: token_price_acc, + sol_price_feed: sol_price_acc, + system_program: solana_program::system_program::ID, + chain, + trie, + guest_chain_program: solana_ibc::ID, + instruction: solana_program::sysvar::instructions::ID, + }) + .args(crate::instruction::UpdateTokenPrice {}) + .payer(authority.clone()) + .signer(&*authority) + .send_with_spinner_and_config(RpcSendTransactionConfig { + skip_preflight: true, + ..Default::default() + })?; + + println!(" Signature: {}", tx); + /* * Depositing to multiple validators */ @@ -207,7 +270,7 @@ fn restaking_test_deliver() -> Result<()> { assert_eq!( ((staker_token_acc_balance_before.ui_amount.unwrap() - staker_token_acc_balance_after.ui_amount.unwrap()) * - 10_f64.powf(crate::RECEIPT_TOKEN_DECIMALS.into())) + 10_f64.powf(STAKE_TOKEN_MINT_DECIMALS.into())) .round() as u64, STAKE_AMOUNT ); @@ -268,7 +331,7 @@ fn restaking_test_deliver() -> Result<()> { assert_eq!( ((staker_token_acc_balance_after.ui_amount.unwrap() - staker_token_acc_balance_before.ui_amount.unwrap()) * - 10_f64.powf(crate::RECEIPT_TOKEN_DECIMALS.into())) + 10_f64.powf(STAKE_TOKEN_MINT_DECIMALS.into())) .round() as u64, STAKE_AMOUNT ); diff --git a/solana/restaking/tests/restaking.ts b/solana/restaking/tests/restaking.ts index d5784976..6221f8dd 100644 --- a/solana/restaking/tests/restaking.ts +++ b/solana/restaking/tests/restaking.ts @@ -22,6 +22,7 @@ import { withdrawInstruction, withdrawalRequestInstruction, } from "./instructions"; +import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver"; async function expectException(callback: any, message: string) { try { @@ -60,501 +61,528 @@ describe("restaking", () => { console.log(provider.connection.rpcEndpoint); - if (provider.connection.rpcEndpoint.endsWith("8899")) { - depositor = anchor.web3.Keypair.generate(); - admin = anchor.web3.Keypair.generate(); - - it("Funds all users", async () => { - await provider.connection.confirmTransaction( - await provider.connection.requestAirdrop( - depositor.publicKey, - 10000000000 - ), - "confirmed" - ); - await provider.connection.confirmTransaction( - await provider.connection.requestAirdrop(admin.publicKey, 10000000000), - "confirmed" - ); - - const depositorUserBalance = await provider.connection.getBalance( - depositor.publicKey - ); - const adminUserBalance = await provider.connection.getBalance( - admin.publicKey - ); - - assert.strictEqual(10000000000, depositorUserBalance); - assert.strictEqual(10000000000, adminUserBalance); - }); - - it("create project and stable mint and mint some tokens to stakeholders", async () => { - wSolMint = await spl.createMint( - provider.connection, - admin, - admin.publicKey, - null, - 9 - ); - - rewardsTokenMint = await spl.createMint( - provider.connection, - admin, - admin.publicKey, - null, - 6 - ); - - depositorWSolTokenAccount = await spl.createAccount( - provider.connection, - depositor, - wSolMint, - depositor.publicKey - ); - - await spl.mintTo( - provider.connection, - depositor, - wSolMint, - depositorWSolTokenAccount, - admin.publicKey, - initialMintAmount, - [admin] - ); - - let depositorWSolTokenAccountUpdated = await spl.getAccount( - provider.connection, - depositorWSolTokenAccount - ); - - assert.equal(initialMintAmount, depositorWSolTokenAccountUpdated.amount); - }); - } else { - // These are the private keys of accounts which i have created and have deposited some SOL in it. - // Since we cannot airdrop much SOL on devnet (fails most of the time), i have previously airdropped some SOL so that these accounts - // can be used for testing on devnet. - // We can have them in another file and import them. But these are only for testing and has 0 balance on mainnet. - const depositorPrivate = - "472ZS33Lftn7wdM31QauCkmpgFKFvgBRg6Z6NGtA6JgeRi1NfeZFRNvNi3b3sh5jvrQWrgiTimr8giVs9oq4UM5g"; - const adminPrivate = - "2HKjYz8yfQxxhRS5f17FRCx9kDp7ATF5R4esLnKA4VaUsMA5zquP5XkQmvv9J5ZUD6wAjD4iBPYXDzQDNZmQ1eki"; - - depositor = anchor.web3.Keypair.fromSecretKey( - new Uint8Array(bs58.decode(depositorPrivate)) - ); - admin = anchor.web3.Keypair.fromSecretKey( - new Uint8Array(bs58.decode(adminPrivate)) - ); - - wSolMint = new anchor.web3.PublicKey( - "CAb5AhUMS4EbKp1rEoNJqXGy94Abha4Tg4FrHz7zZDZ3" - ); - - it("Get the associated token account and mint tokens", async () => { - try { - await provider.connection.confirmTransaction( - await provider.connection.requestAirdrop( - depositor.publicKey, - 100000000 - ), - "confirmed" - ); - } catch (error) { - console.log("Airdrop failed"); - } - - const TempdepositorWSolTokenAccount = - await spl.getOrCreateAssociatedTokenAccount( - provider.connection, - depositor, - wSolMint, - depositor.publicKey, - false - ); - - depositorWSolTokenAccount = TempdepositorWSolTokenAccount.address; - - const _depositorWSolTokenAccountBefore = await spl.getAccount( - provider.connection, - depositorWSolTokenAccount - ); - - await spl.mintTo( - provider.connection, - depositor, - wSolMint, - depositorWSolTokenAccount, - admin.publicKey, - initialMintAmount, - [admin] - ); - - const _depositorWSolTokenAccountAfter = await spl.getAccount( - provider.connection, - depositorWSolTokenAccount - ); - - assert.equal( - initialMintAmount, - _depositorWSolTokenAccountAfter.amount - - _depositorWSolTokenAccountBefore.amount - ); - }); - } + const solTokenId = "ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; + const priceFeedId = Buffer.from(solTokenId, "hex"); - it("Is Initialized", async () => { - const whitelistedTokens = [wSolMint]; - const { stakingParamsPDA } = getStakingParamsPDA(); - const { rewardsTokenAccountPDA } = getRewardsTokenAccountPDA(); - try { - const tx = await program.methods - .initialize(whitelistedTokens, new anchor.BN(stakingCap)) - .accounts({ - admin: admin.publicKey, - stakingParams: stakingParamsPDA, - systemProgram: anchor.web3.SystemProgram.programId, - rewardsTokenMint, - tokenProgram: spl.TOKEN_PROGRAM_ID, - rewardsTokenAccount: rewardsTokenAccountPDA, - }) - .signers([admin]) - .rpc(); - console.log(" Signature for Initializing: ", tx); - } catch (error) { - console.log(error); - // throw error; - } - }); + const shardBuffer = Buffer.alloc(2); + shardBuffer.writeUint16LE(0, 0); - it("Deposit tokens before chain is initialized", async () => { - const receiptTokenAccount = await spl.getAssociatedTokenAddress( - tokenMint, - depositor.publicKey - ); - - const depositorBalanceBefore = await spl.getAccount( - provider.connection, - depositorWSolTokenAccount - ); - - const tx = await depositInstruction( - program, - wSolMint, - depositor.publicKey, - depositAmount, - tokenMintKeypair - ); - - try { - tx.feePayer = depositor.publicKey; - const sig = await anchor.web3.sendAndConfirmTransaction( - provider.connection, - tx, - [depositor, tokenMintKeypair] - ); - - console.log(" Signature for Depositing: ", sig); - - const depositorBalanceAfter = await spl.getAccount( - provider.connection, - depositorWSolTokenAccount - ); - const depositorReceiptTokenBalanceAfter = await spl.getAccount( - provider.connection, - receiptTokenAccount - ); - - assert.equal( - depositorBalanceBefore.amount - depositorBalanceAfter.amount, - depositAmount - ); - assert.equal(depositorReceiptTokenBalanceAfter.amount, 1); - } catch (error) { - console.log(error); - throw error; - } - }); + console.log("Price Feed ID: ", priceFeedId); + console.log("Shard Buffer: ", shardBuffer.toString("hex")); - it("Update guest chain initialization with its program ID", async () => { - const { stakingParamsPDA } = getStakingParamsPDA(); - try { - const tx = await program.methods - .updateGuestChainInitialization(guestChainProgramID) - .accounts({ - admin: admin.publicKey, - stakingParams: stakingParamsPDA, - }) - .signers([admin]) - .rpc(); - console.log(" Signature for Updating Guest chain Initialization: ", tx); - } catch (error) { - console.log(error); - throw error; - } - }); + const pythProgramId = new anchor.web3.PublicKey("pythWSnswVUd12oZpeFP8e9CVaEqJg25g1Vtc2biRsT"); - it("Set service after guest chain is initialized", async () => { - const tx = await setServiceInstruction( - program, - depositor.publicKey, - depositor.publicKey, - tokenMintKeypair.publicKey, - wSolMint - ); - try { - tx.feePayer = depositor.publicKey; - const sig = await anchor.web3.sendAndConfirmTransaction( - provider.connection, - tx, - [depositor] - ); - console.log(" Signature for Updating Guest chain Initialization: ", sig); - } catch (error) { - console.log(error); - throw error; - } - }); + const [priceFeedPDA, priceFeedBump] = anchor.web3.PublicKey.findProgramAddressSync( + [shardBuffer, priceFeedId], + pythProgramId + ); - it("Claim rewards", async () => { - const depositorRewardsTokenAccount = await spl.getAssociatedTokenAddress( - rewardsTokenMint, - depositor.publicKey - ); - - const tx = await claimRewardsInstruction( - program, - depositor.publicKey, - tokenMintKeypair.publicKey - ); - - try { - tx.feePayer = depositor.publicKey; - const sig = await anchor.web3.sendAndConfirmTransaction( - provider.connection, - tx, - [depositor] - ); - - console.log(" Signature for Claiming rewards: ", sig); - - const depositorBalanceAfter = await spl.getAccount( - provider.connection, - depositorRewardsTokenAccount - ); - - assert.equal(depositorBalanceAfter.amount, 0); // Rewards is 0 for now. - } catch (error) { - console.log(error); - throw error; - } - }); + console.log("Price Feed PDA: ", priceFeedPDA.toBase58()); - it("Withdrawal request", async () => { - const receiptTokenAccount = await spl.getAssociatedTokenAddress( - tokenMint, - depositor.publicKey - ); - - const depositorReceiptTokenBalanceBefore = await spl.getAccount( - provider.connection, - receiptTokenAccount - ); - - const tx = await withdrawalRequestInstruction( - program, - depositor.publicKey, - tokenMint - ); - - try { - tx.feePayer = depositor.publicKey; - const sig = await anchor.web3.sendAndConfirmTransaction( - provider.connection, - tx, - [depositor] - ); - - console.log(" Signature for Withdrawal request: ", sig); - - // Since receipt NFT token account is closed, getting spl account - // should fail - await expectException(async () => { - const _depositorReceiptTokenBalanceAfter = await spl.getAccount( - provider.connection, - receiptTokenAccount - ); - console.log("this is depositor account balance", _depositorReceiptTokenBalanceAfter); - }, "Receipt NFT token account is not closed"); - } catch (error) { - console.log(error); - throw error; - } + const pythSolanaReceiver = new PythSolanaReceiver({ + connection: provider.connection, + wallet: provider.wallet as anchor.Wallet, }); - it("Cancel withdraw request", async () => { - const receiptTokenAccount = await spl.getAssociatedTokenAddress( - tokenMint, - depositor.publicKey - ); - - // Since receipt NFT token account is closed, getting spl account - // should fail - await expectException(async () => { - const _depositorReceiptTokenBalanceBefore = await spl.getAccount( - provider.connection, - receiptTokenAccount - ); - }, "Receipt NFT token account is not closed"); - const tx = await cancelWithdrawalRequestInstruction( - program, - depositor.publicKey, - tokenMint - ); - - try { - tx.feePayer = depositor.publicKey; - const sig = await anchor.web3.sendAndConfirmTransaction( - provider.connection, - tx, - [depositor] - ); - - console.log(" Signature for Cancelling Withdrawal: ", sig); - - const depositorReceiptTokenBalance = await spl.getAccount( - provider.connection, - receiptTokenAccount - ); - - assert.equal(depositorReceiptTokenBalance.amount, 1); - } catch (error) { - console.log(error); - throw error; - } - }); - - it("Request withdrawal and Withdraw tokens", async () => { - const receiptTokenAccount = await spl.getAssociatedTokenAddress( - tokenMint, - depositor.publicKey - ); - - const depositorReceiptTokenBalanceBefore = await spl.getAccount( - provider.connection, - receiptTokenAccount - ); - - let tx = await withdrawalRequestInstruction( - program, - depositor.publicKey, - tokenMint - ); - - try { - tx.feePayer = depositor.publicKey; - const sig = await anchor.web3.sendAndConfirmTransaction( - provider.connection, - tx, - [depositor] - ); - - console.log(" Signature for Withdrawal request: ", sig); - - // Since receipt NFT token account is closed, getting spl account - // should fail - await expectException(async () => { - const _depositorReceiptTokenBalanceAfter = await spl.getAccount( - provider.connection, - receiptTokenAccount - ); - }, "Receipt NFT token account is not closed"); - // Once withdraw request is complete, we can withdraw - // sleeping for unbonding period to end - await sleep(2000); - const depositorBalanceBefore = await spl.getAccount( - provider.connection, - depositorWSolTokenAccount - ); - tx = await withdrawInstruction(program, depositor.publicKey, tokenMint); - - try { - tx.feePayer = depositor.publicKey; - const sig = await anchor.web3.sendAndConfirmTransaction( - provider.connection, - tx, - [depositor] - ); - - console.log(" Signature for Withdrawing: ", sig); - - const depositorBalanceAfter = await spl.getAccount( - provider.connection, - depositorWSolTokenAccount - ); - - assert.equal( - depositorBalanceAfter.amount - depositorBalanceBefore.amount, - depositAmount - ); - } catch (error) { - console.log(error); - throw error; - } - } catch (error) { - console.log(error); - throw error; - } - }); - - it("Update admin", async () => { - const { stakingParamsPDA } = getStakingParamsPDA(); - try { - let tx = await program.methods - .changeAdminProposal(depositor.publicKey) - .accounts({ - admin: admin.publicKey, - stakingParams: stakingParamsPDA, - }) - .signers([admin]) - .rpc(); - console.log(" Signature for Updating Admin Proposal: ", tx); - tx = await program.methods - .acceptAdminChange() - .accounts({ - newAdmin: depositor.publicKey, - stakingParams: stakingParamsPDA, - }) - .signers([depositor]) - .rpc(); - console.log(" Signature for Accepting Admin Proposal: ", tx); - const stakingParameters = await getStakingParameters(program); - assert.equal( - stakingParameters.admin.toBase58(), - depositor.publicKey.toBase58() - ); - } catch (error) { - console.log(error); - throw error; - } - }); - - it("Update staking cap after updating admin", async () => { - const { stakingParamsPDA } = getStakingParamsPDA(); - try { - const tx = await program.methods - .updateStakingCap(new anchor.BN(newStakingCap)) - .accounts({ - admin: depositor.publicKey, - stakingParams: stakingParamsPDA, - }) - .signers([depositor]) - .rpc(); - console.log(" Signature for Updating staking cap: ", tx); - const stakingParameters = await getStakingParameters(program); - assert.equal(stakingParameters.stakingCap.toNumber(), newStakingCap); - } catch (error) { - console.log(error); - throw error; - } - }); + const priceFeed = pythSolanaReceiver.getPriceFeedAccountAddress(0, solTokenId); + + console.log("Correct Price Feed PDA: ", priceFeed.toBase58()); + + // if (provider.connection.rpcEndpoint.endsWith("8899")) { + // depositor = anchor.web3.Keypair.generate(); + // admin = anchor.web3.Keypair.generate(); + + // it("Funds all users", async () => { + // await provider.connection.confirmTransaction( + // await provider.connection.requestAirdrop( + // depositor.publicKey, + // 10000000000 + // ), + // "confirmed" + // ); + // await provider.connection.confirmTransaction( + // await provider.connection.requestAirdrop(admin.publicKey, 10000000000), + // "confirmed" + // ); + + // const depositorUserBalance = await provider.connection.getBalance( + // depositor.publicKey + // ); + // const adminUserBalance = await provider.connection.getBalance( + // admin.publicKey + // ); + + // assert.strictEqual(10000000000, depositorUserBalance); + // assert.strictEqual(10000000000, adminUserBalance); + // }); + + // it("create project and stable mint and mint some tokens to stakeholders", async () => { + // wSolMint = await spl.createMint( + // provider.connection, + // admin, + // admin.publicKey, + // null, + // 9 + // ); + + // rewardsTokenMint = await spl.createMint( + // provider.connection, + // admin, + // admin.publicKey, + // null, + // 6 + // ); + + // depositorWSolTokenAccount = await spl.createAccount( + // provider.connection, + // depositor, + // wSolMint, + // depositor.publicKey + // ); + + // await spl.mintTo( + // provider.connection, + // depositor, + // wSolMint, + // depositorWSolTokenAccount, + // admin.publicKey, + // initialMintAmount, + // [admin] + // ); + + // let depositorWSolTokenAccountUpdated = await spl.getAccount( + // provider.connection, + // depositorWSolTokenAccount + // ); + + // assert.equal(initialMintAmount, depositorWSolTokenAccountUpdated.amount); + // }); + // } else { + // // These are the private keys of accounts which i have created and have deposited some SOL in it. + // // Since we cannot airdrop much SOL on devnet (fails most of the time), i have previously airdropped some SOL so that these accounts + // // can be used for testing on devnet. + // // We can have them in another file and import them. But these are only for testing and has 0 balance on mainnet. + // const depositorPrivate = + // "472ZS33Lftn7wdM31QauCkmpgFKFvgBRg6Z6NGtA6JgeRi1NfeZFRNvNi3b3sh5jvrQWrgiTimr8giVs9oq4UM5g"; + // const adminPrivate = + // "2HKjYz8yfQxxhRS5f17FRCx9kDp7ATF5R4esLnKA4VaUsMA5zquP5XkQmvv9J5ZUD6wAjD4iBPYXDzQDNZmQ1eki"; + + // depositor = anchor.web3.Keypair.fromSecretKey( + // new Uint8Array(bs58.decode(depositorPrivate)) + // ); + // admin = anchor.web3.Keypair.fromSecretKey( + // new Uint8Array(bs58.decode(adminPrivate)) + // ); + + // wSolMint = new anchor.web3.PublicKey( + // "CAb5AhUMS4EbKp1rEoNJqXGy94Abha4Tg4FrHz7zZDZ3" + // ); + + // it("Get the associated token account and mint tokens", async () => { + // try { + // await provider.connection.confirmTransaction( + // await provider.connection.requestAirdrop( + // depositor.publicKey, + // 100000000 + // ), + // "confirmed" + // ); + // } catch (error) { + // console.log("Airdrop failed"); + // } + + // const TempdepositorWSolTokenAccount = + // await spl.getOrCreateAssociatedTokenAccount( + // provider.connection, + // depositor, + // wSolMint, + // depositor.publicKey, + // false + // ); + + // depositorWSolTokenAccount = TempdepositorWSolTokenAccount.address; + + // const _depositorWSolTokenAccountBefore = await spl.getAccount( + // provider.connection, + // depositorWSolTokenAccount + // ); + + // await spl.mintTo( + // provider.connection, + // depositor, + // wSolMint, + // depositorWSolTokenAccount, + // admin.publicKey, + // initialMintAmount, + // [admin] + // ); + + // const _depositorWSolTokenAccountAfter = await spl.getAccount( + // provider.connection, + // depositorWSolTokenAccount + // ); + + // assert.equal( + // initialMintAmount, + // _depositorWSolTokenAccountAfter.amount - + // _depositorWSolTokenAccountBefore.amount + // ); + // }); + // } + + // it("Is Initialized", async () => { + // const whitelistedTokens = [wSolMint]; + // const { stakingParamsPDA } = getStakingParamsPDA(); + // const { rewardsTokenAccountPDA } = getRewardsTokenAccountPDA(); + // try { + // const tx = await program.methods + // .initialize(whitelistedTokens, new anchor.BN(stakingCap)) + // .accounts({ + // admin: admin.publicKey, + // stakingParams: stakingParamsPDA, + // systemProgram: anchor.web3.SystemProgram.programId, + // rewardsTokenMint, + // tokenProgram: spl.TOKEN_PROGRAM_ID, + // rewardsTokenAccount: rewardsTokenAccountPDA, + // }) + // .signers([admin]) + // .rpc(); + // console.log(" Signature for Initializing: ", tx); + // } catch (error) { + // console.log(error); + // // throw error; + // } + // }); + + // it("Deposit tokens before chain is initialized", async () => { + // const receiptTokenAccount = await spl.getAssociatedTokenAddress( + // tokenMint, + // depositor.publicKey + // ); + + // const depositorBalanceBefore = await spl.getAccount( + // provider.connection, + // depositorWSolTokenAccount + // ); + + // const tx = await depositInstruction( + // program, + // wSolMint, + // depositor.publicKey, + // depositAmount, + // tokenMintKeypair + // ); + + // try { + // tx.feePayer = depositor.publicKey; + // const sig = await anchor.web3.sendAndConfirmTransaction( + // provider.connection, + // tx, + // [depositor, tokenMintKeypair] + // ); + + // console.log(" Signature for Depositing: ", sig); + + // const depositorBalanceAfter = await spl.getAccount( + // provider.connection, + // depositorWSolTokenAccount + // ); + // const depositorReceiptTokenBalanceAfter = await spl.getAccount( + // provider.connection, + // receiptTokenAccount + // ); + + // assert.equal( + // depositorBalanceBefore.amount - depositorBalanceAfter.amount, + // depositAmount + // ); + // assert.equal(depositorReceiptTokenBalanceAfter.amount, 1); + // } catch (error) { + // console.log(error); + // throw error; + // } + // }); + + // it("Update guest chain initialization with its program ID", async () => { + // const { stakingParamsPDA } = getStakingParamsPDA(); + // try { + // const tx = await program.methods + // .updateGuestChainInitialization(guestChainProgramID) + // .accounts({ + // admin: admin.publicKey, + // stakingParams: stakingParamsPDA, + // }) + // .signers([admin]) + // .rpc(); + // console.log(" Signature for Updating Guest chain Initialization: ", tx); + // } catch (error) { + // console.log(error); + // throw error; + // } + // }); + + // it("Set service after guest chain is initialized", async () => { + // const tx = await setServiceInstruction( + // program, + // depositor.publicKey, + // depositor.publicKey, + // tokenMintKeypair.publicKey, + // wSolMint + // ); + // try { + // tx.feePayer = depositor.publicKey; + // const sig = await anchor.web3.sendAndConfirmTransaction( + // provider.connection, + // tx, + // [depositor] + // ); + // console.log(" Signature for Updating Guest chain Initialization: ", sig); + // } catch (error) { + // console.log(error); + // throw error; + // } + // }); + + // it("Claim rewards", async () => { + // const depositorRewardsTokenAccount = await spl.getAssociatedTokenAddress( + // rewardsTokenMint, + // depositor.publicKey + // ); + + // const tx = await claimRewardsInstruction( + // program, + // depositor.publicKey, + // tokenMintKeypair.publicKey + // ); + + // try { + // tx.feePayer = depositor.publicKey; + // const sig = await anchor.web3.sendAndConfirmTransaction( + // provider.connection, + // tx, + // [depositor] + // ); + + // console.log(" Signature for Claiming rewards: ", sig); + + // const depositorBalanceAfter = await spl.getAccount( + // provider.connection, + // depositorRewardsTokenAccount + // ); + + // assert.equal(depositorBalanceAfter.amount, 0); // Rewards is 0 for now. + // } catch (error) { + // console.log(error); + // throw error; + // } + // }); + + // it("Withdrawal request", async () => { + // const receiptTokenAccount = await spl.getAssociatedTokenAddress( + // tokenMint, + // depositor.publicKey + // ); + + // const depositorReceiptTokenBalanceBefore = await spl.getAccount( + // provider.connection, + // receiptTokenAccount + // ); + + // const tx = await withdrawalRequestInstruction( + // program, + // depositor.publicKey, + // tokenMint + // ); + + // try { + // tx.feePayer = depositor.publicKey; + // const sig = await anchor.web3.sendAndConfirmTransaction( + // provider.connection, + // tx, + // [depositor] + // ); + + // console.log(" Signature for Withdrawal request: ", sig); + + // // Since receipt NFT token account is closed, getting spl account + // // should fail + // await expectException(async () => { + // const _depositorReceiptTokenBalanceAfter = await spl.getAccount( + // provider.connection, + // receiptTokenAccount + // ); + // console.log("this is depositor account balance", _depositorReceiptTokenBalanceAfter); + // }, "Receipt NFT token account is not closed"); + // } catch (error) { + // console.log(error); + // throw error; + // } + // }); + + // it("Cancel withdraw request", async () => { + // const receiptTokenAccount = await spl.getAssociatedTokenAddress( + // tokenMint, + // depositor.publicKey + // ); + + // // Since receipt NFT token account is closed, getting spl account + // // should fail + // await expectException(async () => { + // const _depositorReceiptTokenBalanceBefore = await spl.getAccount( + // provider.connection, + // receiptTokenAccount + // ); + // }, "Receipt NFT token account is not closed"); + // const tx = await cancelWithdrawalRequestInstruction( + // program, + // depositor.publicKey, + // tokenMint + // ); + + // try { + // tx.feePayer = depositor.publicKey; + // const sig = await anchor.web3.sendAndConfirmTransaction( + // provider.connection, + // tx, + // [depositor] + // ); + + // console.log(" Signature for Cancelling Withdrawal: ", sig); + + // const depositorReceiptTokenBalance = await spl.getAccount( + // provider.connection, + // receiptTokenAccount + // ); + + // assert.equal(depositorReceiptTokenBalance.amount, 1); + // } catch (error) { + // console.log(error); + // throw error; + // } + // }); + + // it("Request withdrawal and Withdraw tokens", async () => { + // const receiptTokenAccount = await spl.getAssociatedTokenAddress( + // tokenMint, + // depositor.publicKey + // ); + + // const depositorReceiptTokenBalanceBefore = await spl.getAccount( + // provider.connection, + // receiptTokenAccount + // ); + + // let tx = await withdrawalRequestInstruction( + // program, + // depositor.publicKey, + // tokenMint + // ); + + // try { + // tx.feePayer = depositor.publicKey; + // const sig = await anchor.web3.sendAndConfirmTransaction( + // provider.connection, + // tx, + // [depositor] + // ); + + // console.log(" Signature for Withdrawal request: ", sig); + + // // Since receipt NFT token account is closed, getting spl account + // // should fail + // await expectException(async () => { + // const _depositorReceiptTokenBalanceAfter = await spl.getAccount( + // provider.connection, + // receiptTokenAccount + // ); + // }, "Receipt NFT token account is not closed"); + // // Once withdraw request is complete, we can withdraw + // // sleeping for unbonding period to end + // await sleep(2000); + // const depositorBalanceBefore = await spl.getAccount( + // provider.connection, + // depositorWSolTokenAccount + // ); + // tx = await withdrawInstruction(program, depositor.publicKey, tokenMint); + + // try { + // tx.feePayer = depositor.publicKey; + // const sig = await anchor.web3.sendAndConfirmTransaction( + // provider.connection, + // tx, + // [depositor] + // ); + + // console.log(" Signature for Withdrawing: ", sig); + + // const depositorBalanceAfter = await spl.getAccount( + // provider.connection, + // depositorWSolTokenAccount + // ); + + // assert.equal( + // depositorBalanceAfter.amount - depositorBalanceBefore.amount, + // depositAmount + // ); + // } catch (error) { + // console.log(error); + // throw error; + // } + // } catch (error) { + // console.log(error); + // throw error; + // } + // }); + + // it("Update admin", async () => { + // const { stakingParamsPDA } = getStakingParamsPDA(); + // try { + // let tx = await program.methods + // .changeAdminProposal(depositor.publicKey) + // .accounts({ + // admin: admin.publicKey, + // stakingParams: stakingParamsPDA, + // }) + // .signers([admin]) + // .rpc(); + // console.log(" Signature for Updating Admin Proposal: ", tx); + // tx = await program.methods + // .acceptAdminChange() + // .accounts({ + // newAdmin: depositor.publicKey, + // stakingParams: stakingParamsPDA, + // }) + // .signers([depositor]) + // .rpc(); + // console.log(" Signature for Accepting Admin Proposal: ", tx); + // const stakingParameters = await getStakingParameters(program); + // assert.equal( + // stakingParameters.admin.toBase58(), + // depositor.publicKey.toBase58() + // ); + // } catch (error) { + // console.log(error); + // throw error; + // } + // }); + + // it("Update staking cap after updating admin", async () => { + // const { stakingParamsPDA } = getStakingParamsPDA(); + // try { + // const tx = await program.methods + // .updateStakingCap(new anchor.BN(newStakingCap)) + // .accounts({ + // admin: depositor.publicKey, + // stakingParams: stakingParamsPDA, + // }) + // .signers([depositor]) + // .rpc(); + // console.log(" Signature for Updating staking cap: ", tx); + // const stakingParameters = await getStakingParameters(program); + // assert.equal(stakingParameters.stakingCap.toNumber(), newStakingCap); + // } catch (error) { + // console.log(error); + // throw error; + // } + // }); }); From c0fc9ef56cc7d5a35003547a525880917b5d83d6 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Mon, 8 Jul 2024 11:36:42 +0100 Subject: [PATCH 10/40] deref instead of clone --- solana/restaking-v2/programs/restaking-v2/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index 7785ea5a..3178d55e 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -150,7 +150,7 @@ pub mod restaking_v2 { .enumerate() .map(|(index, validator)| { ( - sigverify::ed25519::PubKey::from(validator.clone()), + sigverify::ed25519::PubKey::from(*validator), if index == 0 { (stake_per_validator + stake_remainder) as i128 } else { @@ -261,7 +261,7 @@ pub mod restaking_v2 { .enumerate() .map(|(index, validator)| { ( - sigverify::ed25519::PubKey::from(validator.clone()), + sigverify::ed25519::PubKey::from(*validator), if index == 0 { -(stake_per_validator + stake_remainder) } else { From a2d2c3c3f7c97340217f6ca341c2c9de358ad8bf Mon Sep 17 00:00:00 2001 From: dhruvja Date: Mon, 8 Jul 2024 11:43:47 +0100 Subject: [PATCH 11/40] fix clippy --- .../programs/restaking-v2/src/lib.rs | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index 3178d55e..d7af02ce 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -105,12 +105,12 @@ pub mod restaking_v2 { let validators_len = common_state.validators.len() as u64; - let amount = if let Some(_) = &whitelisted_token.oracle_address { + let amount = if whitelisted_token.oracle_address.is_some() { // Check if the price is stale let current_time = Clock::get()?.unix_timestamp as u64; - if (current_time - whitelisted_token.last_updated_in_sec) > - whitelisted_token.max_update_time_in_sec + if (current_time - whitelisted_token.last_updated_in_sec) + > whitelisted_token.max_update_time_in_sec { return Err(error!(ErrorCodes::PriceTooStale)); } @@ -121,7 +121,7 @@ pub mod restaking_v2 { // * 10u64.pow(SOL_DECIMALS as u32)) // / 10u64.pow(token_decimals as u32); - whitelisted_token.latest_price * (amount as u64) + whitelisted_token.latest_price * amount // update the validator with the stake he deposited } else { @@ -226,12 +226,12 @@ pub mod restaking_v2 { // Check if the price is stale let current_time = Clock::get()?.unix_timestamp as u64; - if (current_time - whitelisted_token.last_updated_in_sec) > - whitelisted_token.max_update_time_in_sec + if (current_time - whitelisted_token.last_updated_in_sec) + > whitelisted_token.max_update_time_in_sec { return Err(error!(ErrorCodes::PriceTooStale)); } - whitelisted_token.latest_price * (amount as u64) + whitelisted_token.latest_price * amount } else { amount }; @@ -330,10 +330,9 @@ pub mod restaking_v2 { staking_params .whitelisted_tokens .iter() - .find(|whitelisted_token_mint| { + .any(|whitelisted_token_mint| { whitelisted_token_mint.address == token_mint.address }) - .is_some() }); if contains_mint { @@ -421,13 +420,12 @@ pub mod restaking_v2 { let token_decimals = ctx.accounts.token_mint.decimals; - let amount_in_sol_decimals = (1_u64 * - 10u64.pow(SOL_DECIMALS as u32)) / - 10u64.pow(token_decimals as u32); + let amount_in_sol_decimals = 10u64.pow(SOL_DECIMALS as u32) + / 10u64.pow(token_decimals as u32); let final_amount_in_sol = - ((token_price.price * (amount_in_sol_decimals as i64)) / - sol_price.price) as u64; + ((token_price.price * (amount_in_sol_decimals as i64)) + / sol_price.price) as u64; msg!( "The price of solana is ({} ± {}) * 10^{} and final price \ @@ -452,12 +450,12 @@ pub mod restaking_v2 { .map(|&(validator_idx, amount)| { let amount = amount as i128; let validator = validators[validator_idx as usize]; - let change_in_stake = (previous_price as i128 - - final_amount_in_sol as i128) * - amount; + let change_in_stake = (previous_price as i128 + - final_amount_in_sol as i128) + * amount; ( sigverify::ed25519::PubKey::from(validator.clone()), - change_in_stake as i128, + change_in_stake, ) }) .collect(); From 7077206da115f528a9c9ee7ae3b322e8e33a7455 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Mon, 8 Jul 2024 11:44:30 +0100 Subject: [PATCH 12/40] fmt --- .../programs/restaking-v2/src/lib.rs | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index d7af02ce..d2725711 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -109,8 +109,8 @@ pub mod restaking_v2 { // Check if the price is stale let current_time = Clock::get()?.unix_timestamp as u64; - if (current_time - whitelisted_token.last_updated_in_sec) - > whitelisted_token.max_update_time_in_sec + if (current_time - whitelisted_token.last_updated_in_sec) > + whitelisted_token.max_update_time_in_sec { return Err(error!(ErrorCodes::PriceTooStale)); } @@ -226,8 +226,8 @@ pub mod restaking_v2 { // Check if the price is stale let current_time = Clock::get()?.unix_timestamp as u64; - if (current_time - whitelisted_token.last_updated_in_sec) - > whitelisted_token.max_update_time_in_sec + if (current_time - whitelisted_token.last_updated_in_sec) > + whitelisted_token.max_update_time_in_sec { return Err(error!(ErrorCodes::PriceTooStale)); } @@ -327,12 +327,11 @@ pub mod restaking_v2 { let staking_params = &mut ctx.accounts.common_state; let contains_mint = new_token_mints.iter().any(|token_mint| { - staking_params - .whitelisted_tokens - .iter() - .any(|whitelisted_token_mint| { + staking_params.whitelisted_tokens.iter().any( + |whitelisted_token_mint| { whitelisted_token_mint.address == token_mint.address - }) + }, + ) }); if contains_mint { @@ -420,12 +419,12 @@ pub mod restaking_v2 { let token_decimals = ctx.accounts.token_mint.decimals; - let amount_in_sol_decimals = 10u64.pow(SOL_DECIMALS as u32) - / 10u64.pow(token_decimals as u32); + let amount_in_sol_decimals = 10u64.pow(SOL_DECIMALS as u32) / + 10u64.pow(token_decimals as u32); let final_amount_in_sol = - ((token_price.price * (amount_in_sol_decimals as i64)) - / sol_price.price) as u64; + ((token_price.price * (amount_in_sol_decimals as i64)) / + sol_price.price) as u64; msg!( "The price of solana is ({} ± {}) * 10^{} and final price \ @@ -450,9 +449,9 @@ pub mod restaking_v2 { .map(|&(validator_idx, amount)| { let amount = amount as i128; let validator = validators[validator_idx as usize]; - let change_in_stake = (previous_price as i128 - - final_amount_in_sol as i128) - * amount; + let change_in_stake = (previous_price as i128 - + final_amount_in_sol as i128) * + amount; ( sigverify::ed25519::PubKey::from(validator.clone()), change_in_stake, From b263140b61882fc36f79192f540bc02fc038ecde Mon Sep 17 00:00:00 2001 From: dhruvja Date: Mon, 8 Jul 2024 11:48:42 +0100 Subject: [PATCH 13/40] fix clippy --- solana/restaking-v2/programs/restaking-v2/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index d2725711..4430e392 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -222,7 +222,7 @@ pub mod restaking_v2 { anchor_spl::token::burn(cpi_ctx, amount)?; - let amount = if let Some(_) = &whitelisted_token.oracle_address { + let amount = if whitelisted_token.oracle_address.is_some() { // Check if the price is stale let current_time = Clock::get()?.unix_timestamp as u64; @@ -453,7 +453,7 @@ pub mod restaking_v2 { final_amount_in_sol as i128) * amount; ( - sigverify::ed25519::PubKey::from(validator.clone()), + sigverify::ed25519::PubKey::from(validator), change_in_stake, ) }) From 1f951b3b3361259b1e7d2ff8ef48af07565f77a4 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Wed, 10 Jul 2024 10:35:20 +0100 Subject: [PATCH 14/40] use into iter --- .../restaking-v2/programs/restaking-v2/src/lib.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index 4430e392..86010733 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -38,9 +38,9 @@ pub mod restaking_v2 { common_state.admin = ctx.accounts.admin.key(); common_state.whitelisted_tokens = whitelisted_tokens - .iter() - .map(|token_mint| token_mint.into()) - .collect::>(); + .into_iter() + .map(StakeToken::from) + .collect(); common_state.validators = initial_validators; common_state.guest_chain_program_id = guest_chain_program_id; @@ -339,8 +339,8 @@ pub mod restaking_v2 { } let new_token_mints = new_token_mints - .iter() - .map(|token_mint| token_mint.into()) + .into_iter() + .map(StakeToken::from) .collect::>(); staking_params @@ -676,8 +676,8 @@ pub struct StakeToken { pub delegations: Vec<(u8, u128)>, // n * (1 + 16) } -impl From<&NewTokenPayload> for StakeToken { - fn from(payload: &NewTokenPayload) -> Self { +impl From for StakeToken { + fn from(payload: NewTokenPayload) -> Self { StakeToken { address: payload.address, oracle_address: payload.oracle_address.clone(), From b894ff2dfd7684d636d953447af550afd1606133 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Wed, 10 Jul 2024 10:36:13 +0100 Subject: [PATCH 15/40] not use ref in find --- solana/restaking-v2/programs/restaking-v2/src/lib.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index 86010733..558dc0f3 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -37,10 +37,8 @@ pub mod restaking_v2 { let common_state = &mut ctx.accounts.common_state; common_state.admin = ctx.accounts.admin.key(); - common_state.whitelisted_tokens = whitelisted_tokens - .into_iter() - .map(StakeToken::from) - .collect(); + common_state.whitelisted_tokens = + whitelisted_tokens.into_iter().map(StakeToken::from).collect(); common_state.validators = initial_validators; common_state.guest_chain_program_id = guest_chain_program_id; @@ -62,7 +60,7 @@ pub mod restaking_v2 { let whitelisted_token = common_state .whitelisted_tokens .iter() - .find(|&x| &x.address == stake_token_mint) + .find(|x| &x.address == stake_token_mint) .ok_or(ErrorCodes::InvalidTokenMint)?; if ctx.accounts.staker_token_account.amount < amount { @@ -179,7 +177,7 @@ pub mod restaking_v2 { let whitelisted_token = common_state .whitelisted_tokens .iter() - .find(|&x| &x.address == stake_token_mint) + .find(|x| &x.address == stake_token_mint) .ok_or(ErrorCodes::InvalidTokenMint)?; let bump = ctx.bumps.common_state; From 8ad8b6266e608f180ececceb8283eca3a81af06a Mon Sep 17 00:00:00 2001 From: dhruvja Date: Wed, 10 Jul 2024 10:40:47 +0100 Subject: [PATCH 16/40] throw err without if else branching --- .../programs/restaking-v2/src/lib.rs | 181 ++++++++---------- 1 file changed, 84 insertions(+), 97 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index 558dc0f3..18bbe36d 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -386,104 +386,91 @@ pub mod restaking_v2 { .iter_mut() .find(|whitelisted_token| whitelisted_token.address == token_mint); - if let Some(staked_token) = staked_token { - if let Some(token_feed_id) = staked_token.oracle_address.as_ref() { - let (token_price, sol_price) = if cfg!(feature = "mocks") { - let feed_id: [u8; 32] = - get_feed_id_from_hex(token_feed_id)?; - let sol_price = sol_price_feed.get_price_unchecked( - &get_feed_id_from_hex(SOL_PRICE_FEED_ID)?, - )?; - let token_price = - token_price_feed.get_price_unchecked(&feed_id)?; - (token_price, sol_price) - } else { - let maximum_age_in_sec: u64 = 30; - let feed_id: [u8; 32] = - get_feed_id_from_hex(token_feed_id)?; - let sol_price = sol_price_feed.get_price_no_older_than( - &Clock::get()?, - maximum_age_in_sec, - &get_feed_id_from_hex(SOL_PRICE_FEED_ID)?, - )?; - let token_price = token_price_feed - .get_price_no_older_than( - &Clock::get()?, - maximum_age_in_sec, - &feed_id, - )?; - (token_price, sol_price) - }; - - let token_decimals = ctx.accounts.token_mint.decimals; - - let amount_in_sol_decimals = 10u64.pow(SOL_DECIMALS as u32) / - 10u64.pow(token_decimals as u32); - - let final_amount_in_sol = - ((token_price.price * (amount_in_sol_decimals as i64)) / - sol_price.price) as u64; - - msg!( - "The price of solana is ({} ± {}) * 10^{} and final price \ - {}\n - The price of solana is ({} ± {}) * 10^{} and amount in \ - sol decimals {}", - sol_price.price, - sol_price.conf, - sol_price.exponent, - final_amount_in_sol, - token_price.price, - token_price.conf, - token_price.exponent, - amount_in_sol_decimals - ); - - let previous_price = staked_token.latest_price; - - let set_stake_arg = staked_token - .delegations - .iter() - .map(|&(validator_idx, amount)| { - let amount = amount as i128; - let validator = validators[validator_idx as usize]; - let change_in_stake = (previous_price as i128 - - final_amount_in_sol as i128) * - amount; - ( - sigverify::ed25519::PubKey::from(validator), - change_in_stake, - ) - }) - .collect(); - - let set_stake_ix = solana_ibc::cpi::accounts::SetStake { - sender: ctx.accounts.signer.to_account_info(), - chain: ctx.accounts.chain.to_account_info(), - trie: ctx.accounts.trie.to_account_info(), - system_program: ctx - .accounts - .system_program - .to_account_info(), - instruction: ctx.accounts.instruction.to_account_info(), - }; - - let cpi_ctx = CpiContext::new( - ctx.accounts.guest_chain_program.to_account_info(), - set_stake_ix, - ); - - solana_ibc::cpi::update_stake(cpi_ctx, set_stake_arg)?; - - staked_token.latest_price = final_amount_in_sol; - staked_token.last_updated_in_sec = - Clock::get()?.unix_timestamp as u64; - } else { - return Err(error!(ErrorCodes::OracleAddressNotFound)); - } + let staked_token = + staked_token.ok_or_else(|| error!(ErrorCodes::InvalidTokenMint))?; + + let token_feed_id = staked_token + .oracle_address + .as_ref() + .ok_or_else(|| error!(ErrorCodes::OracleAddressNotFound))?; + let (token_price, sol_price) = if cfg!(feature = "mocks") { + let feed_id: [u8; 32] = get_feed_id_from_hex(token_feed_id)?; + let sol_price = sol_price_feed.get_price_unchecked( + &get_feed_id_from_hex(SOL_PRICE_FEED_ID)?, + )?; + let token_price = token_price_feed.get_price_unchecked(&feed_id)?; + (token_price, sol_price) } else { - return Err(error!(ErrorCodes::InvalidTokenMint)); - } + let maximum_age_in_sec: u64 = 30; + let feed_id: [u8; 32] = get_feed_id_from_hex(token_feed_id)?; + let sol_price = sol_price_feed.get_price_no_older_than( + &Clock::get()?, + maximum_age_in_sec, + &get_feed_id_from_hex(SOL_PRICE_FEED_ID)?, + )?; + let token_price = token_price_feed.get_price_no_older_than( + &Clock::get()?, + maximum_age_in_sec, + &feed_id, + )?; + (token_price, sol_price) + }; + + let token_decimals = ctx.accounts.token_mint.decimals; + + let amount_in_sol_decimals = + 10u64.pow(SOL_DECIMALS as u32) / 10u64.pow(token_decimals as u32); + + let final_amount_in_sol = ((token_price.price * + (amount_in_sol_decimals as i64)) / + sol_price.price) as u64; + + msg!( + "The price of solana is ({} ± {}) * 10^{} and final price {}\n + The price of solana is ({} ± {}) * 10^{} and amount in \ + sol decimals {}", + sol_price.price, + sol_price.conf, + sol_price.exponent, + final_amount_in_sol, + token_price.price, + token_price.conf, + token_price.exponent, + amount_in_sol_decimals + ); + + let previous_price = staked_token.latest_price; + + let set_stake_arg = staked_token + .delegations + .iter() + .map(|&(validator_idx, amount)| { + let amount = amount as i128; + let validator = validators[validator_idx as usize]; + let change_in_stake = (previous_price as i128 - + final_amount_in_sol as i128) * + amount; + (sigverify::ed25519::PubKey::from(validator), change_in_stake) + }) + .collect(); + + let set_stake_ix = solana_ibc::cpi::accounts::SetStake { + sender: ctx.accounts.signer.to_account_info(), + chain: ctx.accounts.chain.to_account_info(), + trie: ctx.accounts.trie.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + instruction: ctx.accounts.instruction.to_account_info(), + }; + + let cpi_ctx = CpiContext::new( + ctx.accounts.guest_chain_program.to_account_info(), + set_stake_ix, + ); + + solana_ibc::cpi::update_stake(cpi_ctx, set_stake_arg)?; + + staked_token.latest_price = final_amount_in_sol; + staked_token.last_updated_in_sec = Clock::get()?.unix_timestamp as u64; Ok(()) } From 66d22dfbe6580bbdc5b3b7cf6fc22da136f7273b Mon Sep 17 00:00:00 2001 From: dhruvja Date: Wed, 10 Jul 2024 10:44:54 +0100 Subject: [PATCH 17/40] use decimals in the eq instead of calculating before --- .../programs/restaking-v2/src/lib.rs | 34 +++++++------------ 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index 18bbe36d..933e240c 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -107,18 +107,12 @@ pub mod restaking_v2 { // Check if the price is stale let current_time = Clock::get()?.unix_timestamp as u64; - if (current_time - whitelisted_token.last_updated_in_sec) > - whitelisted_token.max_update_time_in_sec + if (current_time - whitelisted_token.last_updated_in_sec) + > whitelisted_token.max_update_time_in_sec { return Err(error!(ErrorCodes::PriceTooStale)); } - // let token_decimals = ctx.accounts.token_mint.decimals; - - // let amount_in_sol_decimals = (1_u64 - // * 10u64.pow(SOL_DECIMALS as u32)) - // / 10u64.pow(token_decimals as u32); - whitelisted_token.latest_price * amount // update the validator with the stake he deposited @@ -224,8 +218,8 @@ pub mod restaking_v2 { // Check if the price is stale let current_time = Clock::get()?.unix_timestamp as u64; - if (current_time - whitelisted_token.last_updated_in_sec) > - whitelisted_token.max_update_time_in_sec + if (current_time - whitelisted_token.last_updated_in_sec) + > whitelisted_token.max_update_time_in_sec { return Err(error!(ErrorCodes::PriceTooStale)); } @@ -418,17 +412,14 @@ pub mod restaking_v2 { let token_decimals = ctx.accounts.token_mint.decimals; - let amount_in_sol_decimals = - 10u64.pow(SOL_DECIMALS as u32) / 10u64.pow(token_decimals as u32); - - let final_amount_in_sol = ((token_price.price * - (amount_in_sol_decimals as i64)) / - sol_price.price) as u64; + let final_amount_in_sol = (token_price.price + * 10i64.pow(SOL_DECIMALS as u32) + / (sol_price.price * 10i64.pow(token_decimals as u32))) + as u64; msg!( "The price of solana is ({} ± {}) * 10^{} and final price {}\n - The price of solana is ({} ± {}) * 10^{} and amount in \ - sol decimals {}", + The price of solana is ({} ± {}) * 10^{}", sol_price.price, sol_price.conf, sol_price.exponent, @@ -436,7 +427,6 @@ pub mod restaking_v2 { token_price.price, token_price.conf, token_price.exponent, - amount_in_sol_decimals ); let previous_price = staked_token.latest_price; @@ -447,9 +437,9 @@ pub mod restaking_v2 { .map(|&(validator_idx, amount)| { let amount = amount as i128; let validator = validators[validator_idx as usize]; - let change_in_stake = (previous_price as i128 - - final_amount_in_sol as i128) * - amount; + let change_in_stake = (previous_price as i128 + - final_amount_in_sol as i128) + * amount; (sigverify::ed25519::PubKey::from(validator), change_in_stake) }) .collect(); From ccfc142d7593e3fb4ebafbb5fbfb3a33fc85299d Mon Sep 17 00:00:00 2001 From: dhruvja Date: Wed, 10 Jul 2024 10:45:18 +0100 Subject: [PATCH 18/40] fmt --- .../programs/restaking-v2/src/lib.rs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index 933e240c..0641161a 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -107,8 +107,8 @@ pub mod restaking_v2 { // Check if the price is stale let current_time = Clock::get()?.unix_timestamp as u64; - if (current_time - whitelisted_token.last_updated_in_sec) - > whitelisted_token.max_update_time_in_sec + if (current_time - whitelisted_token.last_updated_in_sec) > + whitelisted_token.max_update_time_in_sec { return Err(error!(ErrorCodes::PriceTooStale)); } @@ -218,8 +218,8 @@ pub mod restaking_v2 { // Check if the price is stale let current_time = Clock::get()?.unix_timestamp as u64; - if (current_time - whitelisted_token.last_updated_in_sec) - > whitelisted_token.max_update_time_in_sec + if (current_time - whitelisted_token.last_updated_in_sec) > + whitelisted_token.max_update_time_in_sec { return Err(error!(ErrorCodes::PriceTooStale)); } @@ -412,9 +412,9 @@ pub mod restaking_v2 { let token_decimals = ctx.accounts.token_mint.decimals; - let final_amount_in_sol = (token_price.price - * 10i64.pow(SOL_DECIMALS as u32) - / (sol_price.price * 10i64.pow(token_decimals as u32))) + let final_amount_in_sol = (token_price.price * + 10i64.pow(SOL_DECIMALS as u32) / + (sol_price.price * 10i64.pow(token_decimals as u32))) as u64; msg!( @@ -437,9 +437,9 @@ pub mod restaking_v2 { .map(|&(validator_idx, amount)| { let amount = amount as i128; let validator = validators[validator_idx as usize]; - let change_in_stake = (previous_price as i128 - - final_amount_in_sol as i128) - * amount; + let change_in_stake = (previous_price as i128 - + final_amount_in_sol as i128) * + amount; (sigverify::ed25519::PubKey::from(validator), change_in_stake) }) .collect(); From 33101f900a9a858fb9583baac130aba7744c2ca1 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Mon, 15 Jul 2024 11:50:35 +0100 Subject: [PATCH 19/40] fmt --- solana/restaking-v2/programs/restaking-v2/src/lib.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index 0641161a..2fdd0ba5 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -412,9 +412,15 @@ pub mod restaking_v2 { let token_decimals = ctx.accounts.token_mint.decimals; - let final_amount_in_sol = (token_price.price * - 10i64.pow(SOL_DECIMALS as u32) / - (sol_price.price * 10i64.pow(token_decimals as u32))) + // since the exponents are predominanlty negative, we switch the exponents and convert + // them to absolute value. + let final_amount_in_sol = (token_price.price as i128 * + 10_i128.pow(sol_price.exponent.abs().try_into().unwrap()) * + 10i128.pow(SOL_DECIMALS as u32) / + (sol_price.price as i128 * + 10_i128 + .pow(token_price.exponent.abs().try_into().unwrap()) * + 10i128.pow(token_decimals as u32))) as u64; msg!( From 48483d7a47c10a35efe3106f350b44d103d05f1e Mon Sep 17 00:00:00 2001 From: dhruvja Date: Mon, 15 Jul 2024 11:53:21 +0100 Subject: [PATCH 20/40] use lazy evaluation --- solana/restaking-v2/programs/restaking-v2/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index 2fdd0ba5..90fdd0e8 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -61,7 +61,7 @@ pub mod restaking_v2 { .whitelisted_tokens .iter() .find(|x| &x.address == stake_token_mint) - .ok_or(ErrorCodes::InvalidTokenMint)?; + .ok_or_else(|| error!(ErrorCodes::InvalidTokenMint))?; if ctx.accounts.staker_token_account.amount < amount { return Err(error!(ErrorCodes::NotEnoughTokensToStake)); @@ -172,7 +172,7 @@ pub mod restaking_v2 { .whitelisted_tokens .iter() .find(|x| &x.address == stake_token_mint) - .ok_or(ErrorCodes::InvalidTokenMint)?; + .ok_or_else(|| error!(ErrorCodes::InvalidTokenMint))?; let bump = ctx.bumps.common_state; let seeds = [COMMON_SEED, core::slice::from_ref(&bump)]; @@ -292,7 +292,7 @@ pub mod restaking_v2 { let common_state = &mut ctx.accounts.common_state; let new_admin = common_state .new_admin_proposal - .ok_or(ErrorCodes::NoProposedAdmin)?; + .ok_or_else(|| error!(ErrorCodes::NoProposedAdmin))?; if new_admin != ctx.accounts.new_admin.key() { return Err(error!(ErrorCode::ConstraintSigner)); } From f11a0658f216c71a16f82f9e109afc541d877b87 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Mon, 15 Jul 2024 12:50:02 +0100 Subject: [PATCH 21/40] store price with exponent --- .../programs/restaking-v2/src/lib.rs | 32 +++++++++++-------- .../programs/restaking-v2/src/tests.rs | 4 +-- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index 90fdd0e8..6d48496b 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -113,9 +113,7 @@ pub mod restaking_v2 { return Err(error!(ErrorCodes::PriceTooStale)); } - whitelisted_token.latest_price * amount - - // update the validator with the stake he deposited + (whitelisted_token.latest_price * amount) / 10u64.pow(SOL_DECIMALS as u32) } else { amount }; @@ -223,7 +221,7 @@ pub mod restaking_v2 { { return Err(error!(ErrorCodes::PriceTooStale)); } - whitelisted_token.latest_price * amount + (whitelisted_token.latest_price * amount) / 10u64.pow(SOL_DECIMALS as u32) } else { amount }; @@ -414,22 +412,27 @@ pub mod restaking_v2 { // since the exponents are predominanlty negative, we switch the exponents and convert // them to absolute value. - let final_amount_in_sol = (token_price.price as i128 * + let final_amount_in_sol = ((token_price.price as i128 * 10_i128.pow(sol_price.exponent.abs().try_into().unwrap()) * - 10i128.pow(SOL_DECIMALS as u32) / + 10i128.pow(SOL_DECIMALS as u32)) + as f64 / (sol_price.price as i128 * - 10_i128 - .pow(token_price.exponent.abs().try_into().unwrap()) * - 10i128.pow(token_decimals as u32))) - as u64; + 10_i128.pow(token_price.exponent.abs().try_into().unwrap()) * + 10i128.pow(token_decimals as u32)) as f64) + as f64; + + let multipled_price = final_amount_in_sol * 10f64.powi(SOL_DECIMALS as i32); + let final_amount_in_sol = multipled_price.round() as u64; msg!( - "The price of solana is ({} ± {}) * 10^{} and final price {}\n + "The price of solana is ({} ± {}) * 10^{} and final price in dec \ + {} and int {} \n The price of solana is ({} ± {}) * 10^{}", sol_price.price, sol_price.conf, sol_price.exponent, final_amount_in_sol, + final_amount_in_sol as u64, token_price.price, token_price.conf, token_price.exponent, @@ -465,7 +468,7 @@ pub mod restaking_v2 { solana_ibc::cpi::update_stake(cpi_ctx, set_stake_arg)?; - staked_token.latest_price = final_amount_in_sol; + staked_token.latest_price = final_amount_in_sol as u64; staked_token.last_updated_in_sec = Clock::get()?.unix_timestamp as u64; Ok(()) @@ -644,7 +647,10 @@ pub struct NewTokenPayload { pub struct StakeToken { pub address: Pubkey, // 32 pub oracle_address: Option, - /// Latest price of token wrt to SOL fetched from the oracle. + /// Latest price of token wrt to lamports fetched from the oracle. + /// + /// The value is always `latest_price * 10^9` so whenever we need the original price, + /// we need to divide by 10^9 pub latest_price: u64, // 8 /// Time at which the price was updated. Used to check if the price is stale. pub last_updated_in_sec: u64, // 8 diff --git a/solana/restaking-v2/programs/restaking-v2/src/tests.rs b/solana/restaking-v2/programs/restaking-v2/src/tests.rs index c160c846..e957df46 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/tests.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/tests.rs @@ -123,8 +123,8 @@ fn restaking_test_deliver() -> Result<()> { let new_token_mint = NewTokenPayload { address: token_mint_key, oracle_address: Some(TOKEN_FEED_ID.to_string()), - max_update_time_in_sec: 0, - update_frequency_in_sec: 0, + max_update_time_in_sec: 60, + update_frequency_in_sec: 60, }; let tx = program From f200f48402a063d6a9963267b4859813ee3d44e8 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Tue, 16 Jul 2024 12:48:30 +0100 Subject: [PATCH 22/40] fmt --- .../restaking-v2/programs/restaking-v2/src/lib.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index 6d48496b..f2e7dfe8 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -113,7 +113,8 @@ pub mod restaking_v2 { return Err(error!(ErrorCodes::PriceTooStale)); } - (whitelisted_token.latest_price * amount) / 10u64.pow(SOL_DECIMALS as u32) + (whitelisted_token.latest_price * amount) / + 10u64.pow(SOL_DECIMALS as u32) } else { amount }; @@ -221,7 +222,8 @@ pub mod restaking_v2 { { return Err(error!(ErrorCodes::PriceTooStale)); } - (whitelisted_token.latest_price * amount) / 10u64.pow(SOL_DECIMALS as u32) + (whitelisted_token.latest_price * amount) / + 10u64.pow(SOL_DECIMALS as u32) } else { amount }; @@ -420,8 +422,9 @@ pub mod restaking_v2 { 10_i128.pow(token_price.exponent.abs().try_into().unwrap()) * 10i128.pow(token_decimals as u32)) as f64) as f64; - - let multipled_price = final_amount_in_sol * 10f64.powi(SOL_DECIMALS as i32); + + let multipled_price = + final_amount_in_sol * 10f64.powi(SOL_DECIMALS as i32); let final_amount_in_sol = multipled_price.round() as u64; msg!( @@ -648,7 +651,7 @@ pub struct StakeToken { pub address: Pubkey, // 32 pub oracle_address: Option, /// Latest price of token wrt to lamports fetched from the oracle. - /// + /// /// The value is always `latest_price * 10^9` so whenever we need the original price, /// we need to divide by 10^9 pub latest_price: u64, // 8 From b1969e410a0ef8aa9c5e2086665bceb84f2206b7 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Tue, 16 Jul 2024 12:50:14 +0100 Subject: [PATCH 23/40] revert change to restaking.ts file --- solana/restaking/tests/restaking.ts | 1008 +++++++++++++-------------- 1 file changed, 490 insertions(+), 518 deletions(-) diff --git a/solana/restaking/tests/restaking.ts b/solana/restaking/tests/restaking.ts index 6221f8dd..e3a69a78 100644 --- a/solana/restaking/tests/restaking.ts +++ b/solana/restaking/tests/restaking.ts @@ -22,7 +22,6 @@ import { withdrawInstruction, withdrawalRequestInstruction, } from "./instructions"; -import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver"; async function expectException(callback: any, message: string) { try { @@ -61,528 +60,501 @@ describe("restaking", () => { console.log(provider.connection.rpcEndpoint); - const solTokenId = "ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; - const priceFeedId = Buffer.from(solTokenId, "hex"); + if (provider.connection.rpcEndpoint.endsWith("8899")) { + depositor = anchor.web3.Keypair.generate(); + admin = anchor.web3.Keypair.generate(); + + it("Funds all users", async () => { + await provider.connection.confirmTransaction( + await provider.connection.requestAirdrop( + depositor.publicKey, + 10000000000 + ), + "confirmed" + ); + await provider.connection.confirmTransaction( + await provider.connection.requestAirdrop(admin.publicKey, 10000000000), + "confirmed" + ); + + const depositorUserBalance = await provider.connection.getBalance( + depositor.publicKey + ); + const adminUserBalance = await provider.connection.getBalance( + admin.publicKey + ); + + assert.strictEqual(10000000000, depositorUserBalance); + assert.strictEqual(10000000000, adminUserBalance); + }); + + it("create project and stable mint and mint some tokens to stakeholders", async () => { + wSolMint = await spl.createMint( + provider.connection, + admin, + admin.publicKey, + null, + 9 + ); + + rewardsTokenMint = await spl.createMint( + provider.connection, + admin, + admin.publicKey, + null, + 6 + ); + + depositorWSolTokenAccount = await spl.createAccount( + provider.connection, + depositor, + wSolMint, + depositor.publicKey + ); + + await spl.mintTo( + provider.connection, + depositor, + wSolMint, + depositorWSolTokenAccount, + admin.publicKey, + initialMintAmount, + [admin] + ); + + let depositorWSolTokenAccountUpdated = await spl.getAccount( + provider.connection, + depositorWSolTokenAccount + ); + + assert.equal(initialMintAmount, depositorWSolTokenAccountUpdated.amount); + }); + } else { + // These are the private keys of accounts which i have created and have deposited some SOL in it. + // Since we cannot airdrop much SOL on devnet (fails most of the time), i have previously airdropped some SOL so that these accounts + // can be used for testing on devnet. + // We can have them in another file and import them. But these are only for testing and has 0 balance on mainnet. + const depositorPrivate = + "472ZS33Lftn7wdM31QauCkmpgFKFvgBRg6Z6NGtA6JgeRi1NfeZFRNvNi3b3sh5jvrQWrgiTimr8giVs9oq4UM5g"; + const adminPrivate = + "2HKjYz8yfQxxhRS5f17FRCx9kDp7ATF5R4esLnKA4VaUsMA5zquP5XkQmvv9J5ZUD6wAjD4iBPYXDzQDNZmQ1eki"; + + depositor = anchor.web3.Keypair.fromSecretKey( + new Uint8Array(bs58.decode(depositorPrivate)) + ); + admin = anchor.web3.Keypair.fromSecretKey( + new Uint8Array(bs58.decode(adminPrivate)) + ); + + wSolMint = new anchor.web3.PublicKey( + "CAb5AhUMS4EbKp1rEoNJqXGy94Abha4Tg4FrHz7zZDZ3" + ); + + it("Get the associated token account and mint tokens", async () => { + try { + await provider.connection.confirmTransaction( + await provider.connection.requestAirdrop( + depositor.publicKey, + 100000000 + ), + "confirmed" + ); + } catch (error) { + console.log("Airdrop failed"); + } + + const TempdepositorWSolTokenAccount = + await spl.getOrCreateAssociatedTokenAccount( + provider.connection, + depositor, + wSolMint, + depositor.publicKey, + false + ); + + depositorWSolTokenAccount = TempdepositorWSolTokenAccount.address; + + const _depositorWSolTokenAccountBefore = await spl.getAccount( + provider.connection, + depositorWSolTokenAccount + ); + + await spl.mintTo( + provider.connection, + depositor, + wSolMint, + depositorWSolTokenAccount, + admin.publicKey, + initialMintAmount, + [admin] + ); + + const _depositorWSolTokenAccountAfter = await spl.getAccount( + provider.connection, + depositorWSolTokenAccount + ); + + assert.equal( + initialMintAmount, + _depositorWSolTokenAccountAfter.amount - + _depositorWSolTokenAccountBefore.amount + ); + }); + } - const shardBuffer = Buffer.alloc(2); - shardBuffer.writeUint16LE(0, 0); + it("Is Initialized", async () => { + const whitelistedTokens = [wSolMint]; + const { stakingParamsPDA } = getStakingParamsPDA(); + const { rewardsTokenAccountPDA } = getRewardsTokenAccountPDA(); + try { + const tx = await program.methods + .initialize(whitelistedTokens, new anchor.BN(stakingCap)) + .accounts({ + admin: admin.publicKey, + stakingParams: stakingParamsPDA, + systemProgram: anchor.web3.SystemProgram.programId, + rewardsTokenMint, + tokenProgram: spl.TOKEN_PROGRAM_ID, + rewardsTokenAccount: rewardsTokenAccountPDA, + }) + .signers([admin]) + .rpc(); + console.log(" Signature for Initializing: ", tx); + } catch (error) { + console.log(error); + // throw error; + } + }); - console.log("Price Feed ID: ", priceFeedId); - console.log("Shard Buffer: ", shardBuffer.toString("hex")); + it("Deposit tokens before chain is initialized", async () => { + const receiptTokenAccount = await spl.getAssociatedTokenAddress( + tokenMint, + depositor.publicKey + ); + + const depositorBalanceBefore = await spl.getAccount( + provider.connection, + depositorWSolTokenAccount + ); + + const tx = await depositInstruction( + program, + wSolMint, + depositor.publicKey, + depositAmount, + tokenMintKeypair + ); + + try { + tx.feePayer = depositor.publicKey; + const sig = await anchor.web3.sendAndConfirmTransaction( + provider.connection, + tx, + [depositor, tokenMintKeypair] + ); + + console.log(" Signature for Depositing: ", sig); + + const depositorBalanceAfter = await spl.getAccount( + provider.connection, + depositorWSolTokenAccount + ); + const depositorReceiptTokenBalanceAfter = await spl.getAccount( + provider.connection, + receiptTokenAccount + ); + + assert.equal( + depositorBalanceBefore.amount - depositorBalanceAfter.amount, + depositAmount + ); + assert.equal(depositorReceiptTokenBalanceAfter.amount, 1); + } catch (error) { + console.log(error); + throw error; + } + }); - const pythProgramId = new anchor.web3.PublicKey("pythWSnswVUd12oZpeFP8e9CVaEqJg25g1Vtc2biRsT"); + it("Update guest chain initialization with its program ID", async () => { + const { stakingParamsPDA } = getStakingParamsPDA(); + try { + const tx = await program.methods + .updateGuestChainInitialization(guestChainProgramID) + .accounts({ + admin: admin.publicKey, + stakingParams: stakingParamsPDA, + }) + .signers([admin]) + .rpc(); + console.log(" Signature for Updating Guest chain Initialization: ", tx); + } catch (error) { + console.log(error); + throw error; + } + }); - const [priceFeedPDA, priceFeedBump] = anchor.web3.PublicKey.findProgramAddressSync( - [shardBuffer, priceFeedId], - pythProgramId - ); + it("Set service after guest chain is initialized", async () => { + const tx = await setServiceInstruction( + program, + depositor.publicKey, + depositor.publicKey, + tokenMintKeypair.publicKey, + wSolMint + ); + try { + tx.feePayer = depositor.publicKey; + const sig = await anchor.web3.sendAndConfirmTransaction( + provider.connection, + tx, + [depositor] + ); + console.log(" Signature for Updating Guest chain Initialization: ", sig); + } catch (error) { + console.log(error); + throw error; + } + }); - console.log("Price Feed PDA: ", priceFeedPDA.toBase58()); + it("Claim rewards", async () => { + const depositorRewardsTokenAccount = await spl.getAssociatedTokenAddress( + rewardsTokenMint, + depositor.publicKey + ); + + const tx = await claimRewardsInstruction( + program, + depositor.publicKey, + tokenMintKeypair.publicKey + ); + + try { + tx.feePayer = depositor.publicKey; + const sig = await anchor.web3.sendAndConfirmTransaction( + provider.connection, + tx, + [depositor] + ); + + console.log(" Signature for Claiming rewards: ", sig); + + const depositorBalanceAfter = await spl.getAccount( + provider.connection, + depositorRewardsTokenAccount + ); + + assert.equal(depositorBalanceAfter.amount, 0); // Rewards is 0 for now. + } catch (error) { + console.log(error); + throw error; + } + }); - const pythSolanaReceiver = new PythSolanaReceiver({ - connection: provider.connection, - wallet: provider.wallet as anchor.Wallet, + it("Withdrawal request", async () => { + const receiptTokenAccount = await spl.getAssociatedTokenAddress( + tokenMint, + depositor.publicKey + ); + + const depositorReceiptTokenBalanceBefore = await spl.getAccount( + provider.connection, + receiptTokenAccount + ); + + const tx = await withdrawalRequestInstruction( + program, + depositor.publicKey, + tokenMint + ); + + try { + tx.feePayer = depositor.publicKey; + const sig = await anchor.web3.sendAndConfirmTransaction( + provider.connection, + tx, + [depositor] + ); + + console.log(" Signature for Withdrawal request: ", sig); + + // Since receipt NFT token account is closed, getting spl account + // should fail + await expectException(async () => { + const _depositorReceiptTokenBalanceAfter = await spl.getAccount( + provider.connection, + receiptTokenAccount + ); + console.log("this is depositor account balance", _depositorReceiptTokenBalanceAfter); + }, "Receipt NFT token account is not closed"); + } catch (error) { + console.log(error); + throw error; + } }); - const priceFeed = pythSolanaReceiver.getPriceFeedAccountAddress(0, solTokenId); - - console.log("Correct Price Feed PDA: ", priceFeed.toBase58()); - - // if (provider.connection.rpcEndpoint.endsWith("8899")) { - // depositor = anchor.web3.Keypair.generate(); - // admin = anchor.web3.Keypair.generate(); - - // it("Funds all users", async () => { - // await provider.connection.confirmTransaction( - // await provider.connection.requestAirdrop( - // depositor.publicKey, - // 10000000000 - // ), - // "confirmed" - // ); - // await provider.connection.confirmTransaction( - // await provider.connection.requestAirdrop(admin.publicKey, 10000000000), - // "confirmed" - // ); - - // const depositorUserBalance = await provider.connection.getBalance( - // depositor.publicKey - // ); - // const adminUserBalance = await provider.connection.getBalance( - // admin.publicKey - // ); - - // assert.strictEqual(10000000000, depositorUserBalance); - // assert.strictEqual(10000000000, adminUserBalance); - // }); - - // it("create project and stable mint and mint some tokens to stakeholders", async () => { - // wSolMint = await spl.createMint( - // provider.connection, - // admin, - // admin.publicKey, - // null, - // 9 - // ); - - // rewardsTokenMint = await spl.createMint( - // provider.connection, - // admin, - // admin.publicKey, - // null, - // 6 - // ); - - // depositorWSolTokenAccount = await spl.createAccount( - // provider.connection, - // depositor, - // wSolMint, - // depositor.publicKey - // ); - - // await spl.mintTo( - // provider.connection, - // depositor, - // wSolMint, - // depositorWSolTokenAccount, - // admin.publicKey, - // initialMintAmount, - // [admin] - // ); - - // let depositorWSolTokenAccountUpdated = await spl.getAccount( - // provider.connection, - // depositorWSolTokenAccount - // ); - - // assert.equal(initialMintAmount, depositorWSolTokenAccountUpdated.amount); - // }); - // } else { - // // These are the private keys of accounts which i have created and have deposited some SOL in it. - // // Since we cannot airdrop much SOL on devnet (fails most of the time), i have previously airdropped some SOL so that these accounts - // // can be used for testing on devnet. - // // We can have them in another file and import them. But these are only for testing and has 0 balance on mainnet. - // const depositorPrivate = - // "472ZS33Lftn7wdM31QauCkmpgFKFvgBRg6Z6NGtA6JgeRi1NfeZFRNvNi3b3sh5jvrQWrgiTimr8giVs9oq4UM5g"; - // const adminPrivate = - // "2HKjYz8yfQxxhRS5f17FRCx9kDp7ATF5R4esLnKA4VaUsMA5zquP5XkQmvv9J5ZUD6wAjD4iBPYXDzQDNZmQ1eki"; - - // depositor = anchor.web3.Keypair.fromSecretKey( - // new Uint8Array(bs58.decode(depositorPrivate)) - // ); - // admin = anchor.web3.Keypair.fromSecretKey( - // new Uint8Array(bs58.decode(adminPrivate)) - // ); - - // wSolMint = new anchor.web3.PublicKey( - // "CAb5AhUMS4EbKp1rEoNJqXGy94Abha4Tg4FrHz7zZDZ3" - // ); - - // it("Get the associated token account and mint tokens", async () => { - // try { - // await provider.connection.confirmTransaction( - // await provider.connection.requestAirdrop( - // depositor.publicKey, - // 100000000 - // ), - // "confirmed" - // ); - // } catch (error) { - // console.log("Airdrop failed"); - // } - - // const TempdepositorWSolTokenAccount = - // await spl.getOrCreateAssociatedTokenAccount( - // provider.connection, - // depositor, - // wSolMint, - // depositor.publicKey, - // false - // ); - - // depositorWSolTokenAccount = TempdepositorWSolTokenAccount.address; - - // const _depositorWSolTokenAccountBefore = await spl.getAccount( - // provider.connection, - // depositorWSolTokenAccount - // ); - - // await spl.mintTo( - // provider.connection, - // depositor, - // wSolMint, - // depositorWSolTokenAccount, - // admin.publicKey, - // initialMintAmount, - // [admin] - // ); - - // const _depositorWSolTokenAccountAfter = await spl.getAccount( - // provider.connection, - // depositorWSolTokenAccount - // ); - - // assert.equal( - // initialMintAmount, - // _depositorWSolTokenAccountAfter.amount - - // _depositorWSolTokenAccountBefore.amount - // ); - // }); - // } - - // it("Is Initialized", async () => { - // const whitelistedTokens = [wSolMint]; - // const { stakingParamsPDA } = getStakingParamsPDA(); - // const { rewardsTokenAccountPDA } = getRewardsTokenAccountPDA(); - // try { - // const tx = await program.methods - // .initialize(whitelistedTokens, new anchor.BN(stakingCap)) - // .accounts({ - // admin: admin.publicKey, - // stakingParams: stakingParamsPDA, - // systemProgram: anchor.web3.SystemProgram.programId, - // rewardsTokenMint, - // tokenProgram: spl.TOKEN_PROGRAM_ID, - // rewardsTokenAccount: rewardsTokenAccountPDA, - // }) - // .signers([admin]) - // .rpc(); - // console.log(" Signature for Initializing: ", tx); - // } catch (error) { - // console.log(error); - // // throw error; - // } - // }); - - // it("Deposit tokens before chain is initialized", async () => { - // const receiptTokenAccount = await spl.getAssociatedTokenAddress( - // tokenMint, - // depositor.publicKey - // ); - - // const depositorBalanceBefore = await spl.getAccount( - // provider.connection, - // depositorWSolTokenAccount - // ); - - // const tx = await depositInstruction( - // program, - // wSolMint, - // depositor.publicKey, - // depositAmount, - // tokenMintKeypair - // ); - - // try { - // tx.feePayer = depositor.publicKey; - // const sig = await anchor.web3.sendAndConfirmTransaction( - // provider.connection, - // tx, - // [depositor, tokenMintKeypair] - // ); - - // console.log(" Signature for Depositing: ", sig); - - // const depositorBalanceAfter = await spl.getAccount( - // provider.connection, - // depositorWSolTokenAccount - // ); - // const depositorReceiptTokenBalanceAfter = await spl.getAccount( - // provider.connection, - // receiptTokenAccount - // ); - - // assert.equal( - // depositorBalanceBefore.amount - depositorBalanceAfter.amount, - // depositAmount - // ); - // assert.equal(depositorReceiptTokenBalanceAfter.amount, 1); - // } catch (error) { - // console.log(error); - // throw error; - // } - // }); - - // it("Update guest chain initialization with its program ID", async () => { - // const { stakingParamsPDA } = getStakingParamsPDA(); - // try { - // const tx = await program.methods - // .updateGuestChainInitialization(guestChainProgramID) - // .accounts({ - // admin: admin.publicKey, - // stakingParams: stakingParamsPDA, - // }) - // .signers([admin]) - // .rpc(); - // console.log(" Signature for Updating Guest chain Initialization: ", tx); - // } catch (error) { - // console.log(error); - // throw error; - // } - // }); - - // it("Set service after guest chain is initialized", async () => { - // const tx = await setServiceInstruction( - // program, - // depositor.publicKey, - // depositor.publicKey, - // tokenMintKeypair.publicKey, - // wSolMint - // ); - // try { - // tx.feePayer = depositor.publicKey; - // const sig = await anchor.web3.sendAndConfirmTransaction( - // provider.connection, - // tx, - // [depositor] - // ); - // console.log(" Signature for Updating Guest chain Initialization: ", sig); - // } catch (error) { - // console.log(error); - // throw error; - // } - // }); - - // it("Claim rewards", async () => { - // const depositorRewardsTokenAccount = await spl.getAssociatedTokenAddress( - // rewardsTokenMint, - // depositor.publicKey - // ); - - // const tx = await claimRewardsInstruction( - // program, - // depositor.publicKey, - // tokenMintKeypair.publicKey - // ); - - // try { - // tx.feePayer = depositor.publicKey; - // const sig = await anchor.web3.sendAndConfirmTransaction( - // provider.connection, - // tx, - // [depositor] - // ); - - // console.log(" Signature for Claiming rewards: ", sig); - - // const depositorBalanceAfter = await spl.getAccount( - // provider.connection, - // depositorRewardsTokenAccount - // ); - - // assert.equal(depositorBalanceAfter.amount, 0); // Rewards is 0 for now. - // } catch (error) { - // console.log(error); - // throw error; - // } - // }); - - // it("Withdrawal request", async () => { - // const receiptTokenAccount = await spl.getAssociatedTokenAddress( - // tokenMint, - // depositor.publicKey - // ); - - // const depositorReceiptTokenBalanceBefore = await spl.getAccount( - // provider.connection, - // receiptTokenAccount - // ); - - // const tx = await withdrawalRequestInstruction( - // program, - // depositor.publicKey, - // tokenMint - // ); - - // try { - // tx.feePayer = depositor.publicKey; - // const sig = await anchor.web3.sendAndConfirmTransaction( - // provider.connection, - // tx, - // [depositor] - // ); - - // console.log(" Signature for Withdrawal request: ", sig); - - // // Since receipt NFT token account is closed, getting spl account - // // should fail - // await expectException(async () => { - // const _depositorReceiptTokenBalanceAfter = await spl.getAccount( - // provider.connection, - // receiptTokenAccount - // ); - // console.log("this is depositor account balance", _depositorReceiptTokenBalanceAfter); - // }, "Receipt NFT token account is not closed"); - // } catch (error) { - // console.log(error); - // throw error; - // } - // }); - - // it("Cancel withdraw request", async () => { - // const receiptTokenAccount = await spl.getAssociatedTokenAddress( - // tokenMint, - // depositor.publicKey - // ); - - // // Since receipt NFT token account is closed, getting spl account - // // should fail - // await expectException(async () => { - // const _depositorReceiptTokenBalanceBefore = await spl.getAccount( - // provider.connection, - // receiptTokenAccount - // ); - // }, "Receipt NFT token account is not closed"); - // const tx = await cancelWithdrawalRequestInstruction( - // program, - // depositor.publicKey, - // tokenMint - // ); - - // try { - // tx.feePayer = depositor.publicKey; - // const sig = await anchor.web3.sendAndConfirmTransaction( - // provider.connection, - // tx, - // [depositor] - // ); - - // console.log(" Signature for Cancelling Withdrawal: ", sig); - - // const depositorReceiptTokenBalance = await spl.getAccount( - // provider.connection, - // receiptTokenAccount - // ); - - // assert.equal(depositorReceiptTokenBalance.amount, 1); - // } catch (error) { - // console.log(error); - // throw error; - // } - // }); - - // it("Request withdrawal and Withdraw tokens", async () => { - // const receiptTokenAccount = await spl.getAssociatedTokenAddress( - // tokenMint, - // depositor.publicKey - // ); - - // const depositorReceiptTokenBalanceBefore = await spl.getAccount( - // provider.connection, - // receiptTokenAccount - // ); - - // let tx = await withdrawalRequestInstruction( - // program, - // depositor.publicKey, - // tokenMint - // ); - - // try { - // tx.feePayer = depositor.publicKey; - // const sig = await anchor.web3.sendAndConfirmTransaction( - // provider.connection, - // tx, - // [depositor] - // ); - - // console.log(" Signature for Withdrawal request: ", sig); - - // // Since receipt NFT token account is closed, getting spl account - // // should fail - // await expectException(async () => { - // const _depositorReceiptTokenBalanceAfter = await spl.getAccount( - // provider.connection, - // receiptTokenAccount - // ); - // }, "Receipt NFT token account is not closed"); - // // Once withdraw request is complete, we can withdraw - // // sleeping for unbonding period to end - // await sleep(2000); - // const depositorBalanceBefore = await spl.getAccount( - // provider.connection, - // depositorWSolTokenAccount - // ); - // tx = await withdrawInstruction(program, depositor.publicKey, tokenMint); - - // try { - // tx.feePayer = depositor.publicKey; - // const sig = await anchor.web3.sendAndConfirmTransaction( - // provider.connection, - // tx, - // [depositor] - // ); - - // console.log(" Signature for Withdrawing: ", sig); - - // const depositorBalanceAfter = await spl.getAccount( - // provider.connection, - // depositorWSolTokenAccount - // ); - - // assert.equal( - // depositorBalanceAfter.amount - depositorBalanceBefore.amount, - // depositAmount - // ); - // } catch (error) { - // console.log(error); - // throw error; - // } - // } catch (error) { - // console.log(error); - // throw error; - // } - // }); - - // it("Update admin", async () => { - // const { stakingParamsPDA } = getStakingParamsPDA(); - // try { - // let tx = await program.methods - // .changeAdminProposal(depositor.publicKey) - // .accounts({ - // admin: admin.publicKey, - // stakingParams: stakingParamsPDA, - // }) - // .signers([admin]) - // .rpc(); - // console.log(" Signature for Updating Admin Proposal: ", tx); - // tx = await program.methods - // .acceptAdminChange() - // .accounts({ - // newAdmin: depositor.publicKey, - // stakingParams: stakingParamsPDA, - // }) - // .signers([depositor]) - // .rpc(); - // console.log(" Signature for Accepting Admin Proposal: ", tx); - // const stakingParameters = await getStakingParameters(program); - // assert.equal( - // stakingParameters.admin.toBase58(), - // depositor.publicKey.toBase58() - // ); - // } catch (error) { - // console.log(error); - // throw error; - // } - // }); - - // it("Update staking cap after updating admin", async () => { - // const { stakingParamsPDA } = getStakingParamsPDA(); - // try { - // const tx = await program.methods - // .updateStakingCap(new anchor.BN(newStakingCap)) - // .accounts({ - // admin: depositor.publicKey, - // stakingParams: stakingParamsPDA, - // }) - // .signers([depositor]) - // .rpc(); - // console.log(" Signature for Updating staking cap: ", tx); - // const stakingParameters = await getStakingParameters(program); - // assert.equal(stakingParameters.stakingCap.toNumber(), newStakingCap); - // } catch (error) { - // console.log(error); - // throw error; - // } - // }); -}); + it("Cancel withdraw request", async () => { + const receiptTokenAccount = await spl.getAssociatedTokenAddress( + tokenMint, + depositor.publicKey + ); + + // Since receipt NFT token account is closed, getting spl account + // should fail + await expectException(async () => { + const _depositorReceiptTokenBalanceBefore = await spl.getAccount( + provider.connection, + receiptTokenAccount + ); + }, "Receipt NFT token account is not closed"); + const tx = await cancelWithdrawalRequestInstruction( + program, + depositor.publicKey, + tokenMint + ); + + try { + tx.feePayer = depositor.publicKey; + const sig = await anchor.web3.sendAndConfirmTransaction( + provider.connection, + tx, + [depositor] + ); + + console.log(" Signature for Cancelling Withdrawal: ", sig); + + const depositorReceiptTokenBalance = await spl.getAccount( + provider.connection, + receiptTokenAccount + ); + + assert.equal(depositorReceiptTokenBalance.amount, 1); + } catch (error) { + console.log(error); + throw error; + } + }); + + it("Request withdrawal and Withdraw tokens", async () => { + const receiptTokenAccount = await spl.getAssociatedTokenAddress( + tokenMint, + depositor.publicKey + ); + + const depositorReceiptTokenBalanceBefore = await spl.getAccount( + provider.connection, + receiptTokenAccount + ); + + let tx = await withdrawalRequestInstruction( + program, + depositor.publicKey, + tokenMint + ); + + try { + tx.feePayer = depositor.publicKey; + const sig = await anchor.web3.sendAndConfirmTransaction( + provider.connection, + tx, + [depositor] + ); + + console.log(" Signature for Withdrawal request: ", sig); + + // Since receipt NFT token account is closed, getting spl account + // should fail + await expectException(async () => { + const _depositorReceiptTokenBalanceAfter = await spl.getAccount( + provider.connection, + receiptTokenAccount + ); + }, "Receipt NFT token account is not closed"); + // Once withdraw request is complete, we can withdraw + // sleeping for unbonding period to end + await sleep(2000); + const depositorBalanceBefore = await spl.getAccount( + provider.connection, + depositorWSolTokenAccount + ); + tx = await withdrawInstruction(program, depositor.publicKey, tokenMint); + + try { + tx.feePayer = depositor.publicKey; + const sig = await anchor.web3.sendAndConfirmTransaction( + provider.connection, + tx, + [depositor] + ); + + console.log(" Signature for Withdrawing: ", sig); + + const depositorBalanceAfter = await spl.getAccount( + provider.connection, + depositorWSolTokenAccount + ); + + assert.equal( + depositorBalanceAfter.amount - depositorBalanceBefore.amount, + depositAmount + ); + } catch (error) { + console.log(error); + throw error; + } + } catch (error) { + console.log(error); + throw error; + } + }); + + it("Update admin", async () => { + const { stakingParamsPDA } = getStakingParamsPDA(); + try { + let tx = await program.methods + .changeAdminProposal(depositor.publicKey) + .accounts({ + admin: admin.publicKey, + stakingParams: stakingParamsPDA, + }) + .signers([admin]) + .rpc(); + console.log(" Signature for Updating Admin Proposal: ", tx); + tx = await program.methods + .acceptAdminChange() + .accounts({ + newAdmin: depositor.publicKey, + stakingParams: stakingParamsPDA, + }) + .signers([depositor]) + .rpc(); + console.log(" Signature for Accepting Admin Proposal: ", tx); + const stakingParameters = await getStakingParameters(program); + assert.equal( + stakingParameters.admin.toBase58(), + depositor.publicKey.toBase58() + ); + } catch (error) { + console.log(error); + throw error; + } + }); + + it("Update staking cap after updating admin", async () => { + const { stakingParamsPDA } = getStakingParamsPDA(); + try { + const tx = await program.methods + .updateStakingCap(new anchor.BN(newStakingCap)) + .accounts({ + admin: depositor.publicKey, + stakingParams: stakingParamsPDA, + }) + .signers([depositor]) + .rpc(); + console.log(" Signature for Updating staking cap: ", tx); + const stakingParameters = await getStakingParameters(program); + assert.equal(stakingParameters.stakingCap.toNumber(), newStakingCap); + } catch (error) { + console.log(error); + throw error; + } + }); +}); \ No newline at end of file From dd164a0cd7021aa086113123c32dc7aa184f82ca Mon Sep 17 00:00:00 2001 From: dhruvja Date: Tue, 16 Jul 2024 13:55:19 +0100 Subject: [PATCH 24/40] fix clippy --- solana/restaking-v2/programs/restaking-v2/src/lib.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index f2e7dfe8..8c1af149 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -414,14 +414,13 @@ pub mod restaking_v2 { // since the exponents are predominanlty negative, we switch the exponents and convert // them to absolute value. - let final_amount_in_sol = ((token_price.price as i128 * + let final_amount_in_sol = (token_price.price as i128 * 10_i128.pow(sol_price.exponent.abs().try_into().unwrap()) * 10i128.pow(SOL_DECIMALS as u32)) as f64 / (sol_price.price as i128 * 10_i128.pow(token_price.exponent.abs().try_into().unwrap()) * - 10i128.pow(token_decimals as u32)) as f64) - as f64; + 10i128.pow(token_decimals as u32)) as f64; let multipled_price = final_amount_in_sol * 10f64.powi(SOL_DECIMALS as i32); @@ -429,13 +428,12 @@ pub mod restaking_v2 { msg!( "The price of solana is ({} ± {}) * 10^{} and final price in dec \ - {} and int {} \n + {} \n The price of solana is ({} ± {}) * 10^{}", sol_price.price, sol_price.conf, sol_price.exponent, final_amount_in_sol, - final_amount_in_sol as u64, token_price.price, token_price.conf, token_price.exponent, @@ -471,7 +469,7 @@ pub mod restaking_v2 { solana_ibc::cpi::update_stake(cpi_ctx, set_stake_arg)?; - staked_token.latest_price = final_amount_in_sol as u64; + staked_token.latest_price = final_amount_in_sol; staked_token.last_updated_in_sec = Clock::get()?.unix_timestamp as u64; Ok(()) From eb3b581fef3e60deb6f758c20bb8fb16130bc926 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Tue, 23 Jul 2024 12:50:04 -0400 Subject: [PATCH 25/40] update delegations during deposit/withdraw --- .../programs/restaking-v2/src/lib.rs | 81 +++++++++++++------ .../programs/restaking-v2/src/tests.rs | 6 +- 2 files changed, 60 insertions(+), 27 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index 8c1af149..e9274f70 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -57,12 +57,15 @@ pub mod restaking_v2 { let stake_token_mint = &ctx.accounts.token_mint.key(); - let whitelisted_token = common_state + let whitelisted_token_idx = common_state .whitelisted_tokens .iter() - .find(|x| &x.address == stake_token_mint) + .position(|x| &x.address == stake_token_mint) .ok_or_else(|| error!(ErrorCodes::InvalidTokenMint))?; + let whitelisted_token = + &common_state.whitelisted_tokens[whitelisted_token_idx]; + if ctx.accounts.staker_token_account.amount < amount { return Err(error!(ErrorCodes::NotEnoughTokensToStake)); } @@ -107,14 +110,14 @@ pub mod restaking_v2 { // Check if the price is stale let current_time = Clock::get()?.unix_timestamp as u64; - if (current_time - whitelisted_token.last_updated_in_sec) > - whitelisted_token.max_update_time_in_sec + if (current_time - whitelisted_token.last_updated_in_sec) + > whitelisted_token.max_update_time_in_sec { return Err(error!(ErrorCodes::PriceTooStale)); } - (whitelisted_token.latest_price * amount) / - 10u64.pow(SOL_DECIMALS as u32) + (whitelisted_token.latest_price * amount) + / 10u64.pow(SOL_DECIMALS as u32) } else { amount }; @@ -151,6 +154,22 @@ pub mod restaking_v2 { }) .collect::>(); + let delegations_len = common_state.whitelisted_tokens + [whitelisted_token_idx] + .delegations + .len(); + + set_stake_arg.iter().enumerate().for_each(|(index, validator)| { + if delegations_len <= index { + common_state.whitelisted_tokens[whitelisted_token_idx] + .delegations + .push(validator.1 as u128) + } else { + common_state.whitelisted_tokens[whitelisted_token_idx] + .delegations[index] += validator.1 as u128 + } + }); + solana_ibc::cpi::update_stake(cpi_ctx, set_stake_arg)?; Ok(()) @@ -167,12 +186,15 @@ pub mod restaking_v2 { let stake_token_mint = &ctx.accounts.token_mint.key(); - let whitelisted_token = common_state + let whitelisted_token_idx = common_state .whitelisted_tokens .iter() - .find(|x| &x.address == stake_token_mint) + .position(|x| &x.address == stake_token_mint) .ok_or_else(|| error!(ErrorCodes::InvalidTokenMint))?; + let whitelisted_token = + &common_state.whitelisted_tokens[whitelisted_token_idx]; + let bump = ctx.bumps.common_state; let seeds = [COMMON_SEED, core::slice::from_ref(&bump)]; let seeds = seeds.as_ref(); @@ -217,13 +239,13 @@ pub mod restaking_v2 { // Check if the price is stale let current_time = Clock::get()?.unix_timestamp as u64; - if (current_time - whitelisted_token.last_updated_in_sec) > - whitelisted_token.max_update_time_in_sec + if (current_time - whitelisted_token.last_updated_in_sec) + > whitelisted_token.max_update_time_in_sec { return Err(error!(ErrorCodes::PriceTooStale)); } - (whitelisted_token.latest_price * amount) / - 10u64.pow(SOL_DECIMALS as u32) + (whitelisted_token.latest_price * amount) + / 10u64.pow(SOL_DECIMALS as u32) } else { amount }; @@ -263,6 +285,11 @@ pub mod restaking_v2 { }) .collect::>(); + set_stake_arg.iter().enumerate().for_each(|(index, validator)| { + common_state.whitelisted_tokens[whitelisted_token_idx] + .delegations[index] -= validator.1.abs() as u128; + }); + solana_ibc::cpi::update_stake(cpi_ctx, set_stake_arg)?; Ok(()) @@ -414,13 +441,13 @@ pub mod restaking_v2 { // since the exponents are predominanlty negative, we switch the exponents and convert // them to absolute value. - let final_amount_in_sol = (token_price.price as i128 * - 10_i128.pow(sol_price.exponent.abs().try_into().unwrap()) * - 10i128.pow(SOL_DECIMALS as u32)) - as f64 / - (sol_price.price as i128 * - 10_i128.pow(token_price.exponent.abs().try_into().unwrap()) * - 10i128.pow(token_decimals as u32)) as f64; + let final_amount_in_sol = (token_price.price as i128 + * 10_i128.pow(sol_price.exponent.abs().try_into().unwrap()) + * 10i128.pow(SOL_DECIMALS as u32)) + as f64 + / (sol_price.price as i128 + * 10_i128.pow(token_price.exponent.abs().try_into().unwrap()) + * 10i128.pow(token_decimals as u32)) as f64; let multipled_price = final_amount_in_sol * 10f64.powi(SOL_DECIMALS as i32); @@ -444,12 +471,13 @@ pub mod restaking_v2 { let set_stake_arg = staked_token .delegations .iter() - .map(|&(validator_idx, amount)| { - let amount = amount as i128; - let validator = validators[validator_idx as usize]; - let change_in_stake = (previous_price as i128 - - final_amount_in_sol as i128) * - amount; + .enumerate() + .map(|(validator_idx, amount)| { + let amount = *amount as i128; + let validator = validators[validator_idx]; + let change_in_stake = (previous_price as i128 + - final_amount_in_sol as i128) + * amount; (sigverify::ed25519::PubKey::from(validator), change_in_stake) }) .collect(); @@ -661,7 +689,7 @@ pub struct StakeToken { /// The frequency at which the price should be updated. pub update_frequency_in_sec: u64, // 8 /// mapping of the validator index with their stake in the above token - pub delegations: Vec<(u8, u128)>, // n * (1 + 16) + pub delegations: Vec, // n * 16 } impl From for StakeToken { @@ -679,6 +707,7 @@ impl From for StakeToken { } #[account] +#[derive(Debug)] pub struct CommonState { pub admin: Pubkey, pub whitelisted_tokens: Vec, diff --git a/solana/restaking-v2/programs/restaking-v2/src/tests.rs b/solana/restaking-v2/programs/restaking-v2/src/tests.rs index e957df46..db953e66 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/tests.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/tests.rs @@ -15,7 +15,7 @@ use anyhow::Result; use pyth_solana_receiver_sdk::price_update::get_feed_id_from_hex; use spl_token::instruction::initialize_mint2; -use crate::{NewTokenPayload, SOL_PRICE_FEED_ID}; +use crate::{CommonState, NewTokenPayload, SOL_PRICE_FEED_ID}; const PYTH_PROGRAM_ID: &str = "pythWSnswVUd12oZpeFP8e9CVaEqJg25g1Vtc2biRsT"; @@ -254,6 +254,10 @@ fn restaking_test_deliver() -> Result<()> { skip_preflight: true, ..Default::default() })?; + + let common_state_data: CommonState = program.account(common_state).unwrap(); + + println!("\n{:?}\n", common_state_data); let staker_token_acc_balance_after = sol_rpc_client .get_token_account_balance(&associated_token_addr) From 20547b94c5df8df35c4c65d547ab2d91fbbc62df Mon Sep 17 00:00:00 2001 From: dhruvja Date: Tue, 23 Jul 2024 13:12:30 -0400 Subject: [PATCH 26/40] check for duplicates in the ix payload --- .../programs/restaking-v2/src/lib.rs | 76 ++++++++++++++----- .../programs/restaking-v2/src/tests.rs | 2 +- 2 files changed, 59 insertions(+), 19 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index e9274f70..21d8868c 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -21,6 +21,8 @@ mod tests; #[program] pub mod restaking_v2 { + use std::collections::BTreeSet; + use anchor_spl::token::{Burn, MintTo, Transfer}; use pyth_solana_receiver_sdk::price_update::get_feed_id_from_hex; @@ -36,6 +38,23 @@ pub mod restaking_v2 { let common_state = &mut ctx.accounts.common_state; + let mut address_set = BTreeSet::new(); + let is_token_list_unique = whitelisted_tokens + .iter() + .all(|token_payload| address_set.insert(token_payload.address)); + + if !is_token_list_unique { + return Err(error!(ErrorCodes::TokenListContainDuplicates)); + } + + address_set = BTreeSet::new(); + let is_validator_list_unique = initial_validators + .iter() + .all(|validator| address_set.insert(*validator)); + if !is_validator_list_unique { + return Err(error!(ErrorCodes::ValidatorListContainDuplicates)); + } + common_state.admin = ctx.accounts.admin.key(); common_state.whitelisted_tokens = whitelisted_tokens.into_iter().map(StakeToken::from).collect(); @@ -110,14 +129,14 @@ pub mod restaking_v2 { // Check if the price is stale let current_time = Clock::get()?.unix_timestamp as u64; - if (current_time - whitelisted_token.last_updated_in_sec) - > whitelisted_token.max_update_time_in_sec + if (current_time - whitelisted_token.last_updated_in_sec) > + whitelisted_token.max_update_time_in_sec { return Err(error!(ErrorCodes::PriceTooStale)); } - (whitelisted_token.latest_price * amount) - / 10u64.pow(SOL_DECIMALS as u32) + (whitelisted_token.latest_price * amount) / + 10u64.pow(SOL_DECIMALS as u32) } else { amount }; @@ -239,13 +258,13 @@ pub mod restaking_v2 { // Check if the price is stale let current_time = Clock::get()?.unix_timestamp as u64; - if (current_time - whitelisted_token.last_updated_in_sec) - > whitelisted_token.max_update_time_in_sec + if (current_time - whitelisted_token.last_updated_in_sec) > + whitelisted_token.max_update_time_in_sec { return Err(error!(ErrorCodes::PriceTooStale)); } - (whitelisted_token.latest_price * amount) - / 10u64.pow(SOL_DECIMALS as u32) + (whitelisted_token.latest_price * amount) / + 10u64.pow(SOL_DECIMALS as u32) } else { amount }; @@ -345,6 +364,15 @@ pub mod restaking_v2 { ) -> Result<()> { let staking_params = &mut ctx.accounts.common_state; + let mut token_address_set = BTreeSet::new(); + let is_token_list_unique = new_token_mints + .iter() + .all(|token_mint| token_address_set.insert(token_mint.address)); + + if !is_token_list_unique { + return Err(error!(ErrorCodes::TokenListContainDuplicates)); + } + let contains_mint = new_token_mints.iter().any(|token_mint| { staking_params.whitelisted_tokens.iter().any( |whitelisted_token_mint| { @@ -379,6 +407,14 @@ pub mod restaking_v2 { ) -> Result<()> { let staking_params = &mut ctx.accounts.common_state; + let mut address_set = BTreeSet::new(); + let is_validator_list_unique = new_validators + .iter() + .all(|validator| address_set.insert(*validator)); + if !is_validator_list_unique { + return Err(error!(ErrorCodes::ValidatorListContainDuplicates)); + } + let contains_validator = new_validators .iter() .any(|validator| staking_params.validators.contains(validator)); @@ -441,13 +477,13 @@ pub mod restaking_v2 { // since the exponents are predominanlty negative, we switch the exponents and convert // them to absolute value. - let final_amount_in_sol = (token_price.price as i128 - * 10_i128.pow(sol_price.exponent.abs().try_into().unwrap()) - * 10i128.pow(SOL_DECIMALS as u32)) - as f64 - / (sol_price.price as i128 - * 10_i128.pow(token_price.exponent.abs().try_into().unwrap()) - * 10i128.pow(token_decimals as u32)) as f64; + let final_amount_in_sol = (token_price.price as i128 * + 10_i128.pow(sol_price.exponent.abs().try_into().unwrap()) * + 10i128.pow(SOL_DECIMALS as u32)) + as f64 / + (sol_price.price as i128 * + 10_i128.pow(token_price.exponent.abs().try_into().unwrap()) * + 10i128.pow(token_decimals as u32)) as f64; let multipled_price = final_amount_in_sol * 10f64.powi(SOL_DECIMALS as i32); @@ -475,9 +511,9 @@ pub mod restaking_v2 { .map(|(validator_idx, amount)| { let amount = *amount as i128; let validator = validators[validator_idx]; - let change_in_stake = (previous_price as i128 - - final_amount_in_sol as i128) - * amount; + let change_in_stake = (previous_price as i128 - + final_amount_in_sol as i128) * + amount; (sigverify::ed25519::PubKey::from(validator), change_in_stake) }) .collect(); @@ -738,4 +774,8 @@ pub enum ErrorCodes { OracleAddressNotFound, #[msg("The oracle price has not been updated yet")] PriceTooStale, + #[msg("The token list in the instruction argument contain duplicates")] + TokenListContainDuplicates, + #[msg("The validator list in the instruction argument contain duplicates")] + ValidatorListContainDuplicates, } diff --git a/solana/restaking-v2/programs/restaking-v2/src/tests.rs b/solana/restaking-v2/programs/restaking-v2/src/tests.rs index db953e66..7b2255db 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/tests.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/tests.rs @@ -254,7 +254,7 @@ fn restaking_test_deliver() -> Result<()> { skip_preflight: true, ..Default::default() })?; - + let common_state_data: CommonState = program.account(common_state).unwrap(); println!("\n{:?}\n", common_state_data); From cfbd6e8b6a0bf3b97239099a3f50d35b08ca33cc Mon Sep 17 00:00:00 2001 From: dhruvja Date: Tue, 23 Jul 2024 14:38:45 -0400 Subject: [PATCH 27/40] proper calculation when updating the stake --- .../programs/restaking-v2/src/lib.rs | 43 +++++++++++++---- .../programs/restaking-v2/src/tests.rs | 47 ++++++++++++++++++- 2 files changed, 80 insertions(+), 10 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index 21d8868c..f1fb0755 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -125,6 +125,8 @@ pub mod restaking_v2 { let validators_len = common_state.validators.len() as u64; + let original_amount = amount; + let amount = if whitelisted_token.oracle_address.is_some() { // Check if the price is stale let current_time = Clock::get()?.unix_timestamp as u64; @@ -178,17 +180,19 @@ pub mod restaking_v2 { .delegations .len(); - set_stake_arg.iter().enumerate().for_each(|(index, validator)| { + set_stake_arg.iter().enumerate().for_each(|(index, _validator)| { if delegations_len <= index { common_state.whitelisted_tokens[whitelisted_token_idx] .delegations - .push(validator.1 as u128) + .push(original_amount as u128) } else { common_state.whitelisted_tokens[whitelisted_token_idx] - .delegations[index] += validator.1 as u128 + .delegations[index] += original_amount as u128 } }); + msg!("Depositing {}", amount); + solana_ibc::cpi::update_stake(cpi_ctx, set_stake_arg)?; Ok(()) @@ -254,6 +258,8 @@ pub mod restaking_v2 { anchor_spl::token::burn(cpi_ctx, amount)?; + let original_amount = amount; + let amount = if whitelisted_token.oracle_address.is_some() { // Check if the price is stale let current_time = Clock::get()?.unix_timestamp as u64; @@ -304,11 +310,13 @@ pub mod restaking_v2 { }) .collect::>(); - set_stake_arg.iter().enumerate().for_each(|(index, validator)| { + set_stake_arg.iter().enumerate().for_each(|(index, _validator)| { common_state.whitelisted_tokens[whitelisted_token_idx] - .delegations[index] -= validator.1.abs() as u128; + .delegations[index] -= original_amount as u128; }); + msg!("Withdrawing {}", amount); + solana_ibc::cpi::update_stake(cpi_ctx, set_stake_arg)?; Ok(()) @@ -452,10 +460,18 @@ pub mod restaking_v2 { .ok_or_else(|| error!(ErrorCodes::OracleAddressNotFound))?; let (token_price, sol_price) = if cfg!(feature = "mocks") { let feed_id: [u8; 32] = get_feed_id_from_hex(token_feed_id)?; - let sol_price = sol_price_feed.get_price_unchecked( + let mut sol_price = sol_price_feed.get_price_unchecked( &get_feed_id_from_hex(SOL_PRICE_FEED_ID)?, )?; let token_price = token_price_feed.get_price_unchecked(&feed_id)?; + + // Using a random value since the price doesnt change when running locally since + // the accounts are cloned during genesis and remain unchanged. + let mut random_value = Clock::get()?.unix_timestamp % 10; + random_value = + if random_value == 0 { random_value + 1 } else { random_value }; + msg!("Random value {}", random_value); + sol_price.price = sol_price.price * random_value; (token_price, sol_price) } else { let maximum_age_in_sec: u64 = 30; @@ -504,6 +520,8 @@ pub mod restaking_v2 { let previous_price = staked_token.latest_price; + msg!("This is staked token {:?}", staked_token); + let set_stake_arg = staked_token .delegations .iter() @@ -511,9 +529,16 @@ pub mod restaking_v2 { .map(|(validator_idx, amount)| { let amount = *amount as i128; let validator = validators[validator_idx]; - let change_in_stake = (previous_price as i128 - - final_amount_in_sol as i128) * - amount; + let diff = final_amount_in_sol as i128 - previous_price as i128; + msg!( + "final amount in sol {} and previous price {} and diff {}", + final_amount_in_sol, + previous_price, + diff + ); + let change_in_stake = + (diff * amount) / 10_i128.pow(SOL_DECIMALS as u32); + msg!("This is change in stake {}", change_in_stake); (sigverify::ed25519::PubKey::from(validator), change_in_stake) }) .collect(); diff --git a/solana/restaking-v2/programs/restaking-v2/src/tests.rs b/solana/restaking-v2/programs/restaking-v2/src/tests.rs index 7b2255db..5434721e 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/tests.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/tests.rs @@ -22,7 +22,7 @@ const PYTH_PROGRAM_ID: &str = "pythWSnswVUd12oZpeFP8e9CVaEqJg25g1Vtc2biRsT"; const STAKE_TOKEN_MINT_DECIMALS: u8 = 6; const MINT_AMOUNT: u64 = 1000000000000; -const STAKE_AMOUNT: u64 = 100000; +const STAKE_AMOUNT: u64 = 100_000; const TOKEN_FEED_ID: &str = "0xeaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a"; @@ -281,6 +281,51 @@ fn restaking_test_deliver() -> Result<()> { println!(" Signature: {}", tx); + /* + Update the token price + */ + println!("\nUpdating the token price"); + + let token_feed_id = get_feed_id_from_hex(TOKEN_FEED_ID).unwrap(); + let sol_feed_id = get_feed_id_from_hex(SOL_PRICE_FEED_ID).unwrap(); + let shard_buffer = 0_u16.to_le_bytes(); + + let token_price_acc = Pubkey::find_program_address( + &[&shard_buffer, &token_feed_id], + &Pubkey::from_str(PYTH_PROGRAM_ID).unwrap(), + ) + .0; + + let sol_price_acc = Pubkey::find_program_address( + &[&shard_buffer, &sol_feed_id], + &Pubkey::from_str(PYTH_PROGRAM_ID).unwrap(), + ) + .0; + + let tx = program + .request() + .accounts(crate::accounts::UpdateTokenPrice { + signer: authority.pubkey(), + common_state, + token_mint: token_mint_key, + token_price_feed: token_price_acc, + sol_price_feed: sol_price_acc, + system_program: solana_program::system_program::ID, + chain, + trie, + guest_chain_program: solana_ibc::ID, + instruction: solana_program::sysvar::instructions::ID, + }) + .args(crate::instruction::UpdateTokenPrice {}) + .payer(authority.clone()) + .signer(&*authority) + .send_with_spinner_and_config(RpcSendTransactionConfig { + skip_preflight: true, + ..Default::default() + })?; + + println!(" Signature: {}", tx); + /* * Withdrawing the stake */ From 9a6a23368127a67de25cd9b5ec75c65a79fca875 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Tue, 23 Jul 2024 16:35:06 -0400 Subject: [PATCH 28/40] use proper conversions when calculating the amount from the price --- .../programs/restaking-v2/src/lib.rs | 25 +++++++++++-------- .../programs/restaking-v2/src/tests.rs | 6 ++--- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index f1fb0755..2ba1f1bf 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -277,8 +277,7 @@ pub mod restaking_v2 { // Call guest chain program to update the stake equally let validators_len = common_state.validators.len() as u64; - let stake_per_validator = - (amount / common_state.validators.len() as u64) as i128; + let stake_per_validator = (amount / validators_len) as i128; let stake_remainder = (amount % validators_len) as i128; let set_stake_ix = solana_ibc::cpi::accounts::SetStake { @@ -491,15 +490,19 @@ pub mod restaking_v2 { let token_decimals = ctx.accounts.token_mint.decimals; - // since the exponents are predominanlty negative, we switch the exponents and convert - // them to absolute value. - let final_amount_in_sol = (token_price.price as i128 * - 10_i128.pow(sol_price.exponent.abs().try_into().unwrap()) * - 10i128.pow(SOL_DECIMALS as u32)) - as f64 / - (sol_price.price as i128 * - 10_i128.pow(token_price.exponent.abs().try_into().unwrap()) * - 10i128.pow(token_decimals as u32)) as f64; + // There would be a slight loss in precision due to the conversion from f64 to u64 + // but only when the price is very large. And since it has exponents, the price being + // extremely large would be quite rare. + let final_amount_in_sol = + token_price.price as f64 / sol_price.price as f64; + + let final_amount_in_sol = final_amount_in_sol * + 10_f64.powi( + (i32::from(SOL_DECIMALS) + token_price.exponent) - + (i32::from(token_decimals) + sol_price.exponent), + ); + + msg!("Final amount in sol {}", final_amount_in_sol); let multipled_price = final_amount_in_sol * 10f64.powi(SOL_DECIMALS as i32); diff --git a/solana/restaking-v2/programs/restaking-v2/src/tests.rs b/solana/restaking-v2/programs/restaking-v2/src/tests.rs index 5434721e..14138808 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/tests.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/tests.rs @@ -355,7 +355,7 @@ fn restaking_test_deliver() -> Result<()> { guest_chain_program: solana_ibc::ID, instruction: solana_program::sysvar::instructions::ID, }) - .args(crate::instruction::Withdraw { amount: STAKE_AMOUNT }) + .args(crate::instruction::Withdraw { amount: STAKE_AMOUNT / 2 }) .payer(authority.clone()) .signer(&*authority) .send_with_spinner_and_config(RpcSendTransactionConfig { @@ -375,14 +375,14 @@ fn restaking_test_deliver() -> Result<()> { staker_receipt_token_acc_balance_after.ui_amount.unwrap()) * 10_f64.powf(crate::RECEIPT_TOKEN_DECIMALS.into())) .round() as u64, - STAKE_AMOUNT + STAKE_AMOUNT / 2 ); assert_eq!( ((staker_token_acc_balance_after.ui_amount.unwrap() - staker_token_acc_balance_before.ui_amount.unwrap()) * 10_f64.powf(STAKE_TOKEN_MINT_DECIMALS.into())) .round() as u64, - STAKE_AMOUNT + STAKE_AMOUNT / 2 ); println!(" Signature: {}", tx); From 0c9ebeb7e55ef58f1e4a48ab5c2444fdf307c820 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Tue, 23 Jul 2024 16:38:21 -0400 Subject: [PATCH 29/40] remove unused update_frequency attr --- solana/restaking-v2/programs/restaking-v2/src/lib.rs | 4 ---- solana/restaking-v2/programs/restaking-v2/src/tests.rs | 1 - 2 files changed, 5 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index 2ba1f1bf..62142a3f 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -726,7 +726,6 @@ pub struct NewTokenPayload { pub address: Pubkey, pub oracle_address: Option, pub max_update_time_in_sec: u64, - pub update_frequency_in_sec: u64, } /// Struct which stores the token address and price information. The price @@ -750,8 +749,6 @@ pub struct StakeToken { /// If the price is not updated after the `max_update_time` below, /// the above price should be considered invalid. pub max_update_time_in_sec: u64, // 8 - /// The frequency at which the price should be updated. - pub update_frequency_in_sec: u64, // 8 /// mapping of the validator index with their stake in the above token pub delegations: Vec, // n * 16 } @@ -764,7 +761,6 @@ impl From for StakeToken { latest_price: 0, last_updated_in_sec: 0, max_update_time_in_sec: payload.max_update_time_in_sec, - update_frequency_in_sec: payload.update_frequency_in_sec, delegations: vec![], } } diff --git a/solana/restaking-v2/programs/restaking-v2/src/tests.rs b/solana/restaking-v2/programs/restaking-v2/src/tests.rs index 14138808..159fc18c 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/tests.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/tests.rs @@ -124,7 +124,6 @@ fn restaking_test_deliver() -> Result<()> { address: token_mint_key, oracle_address: Some(TOKEN_FEED_ID.to_string()), max_update_time_in_sec: 60, - update_frequency_in_sec: 60, }; let tx = program From 83cf5572b0c8a2c623c1313791b1c50728d0275e Mon Sep 17 00:00:00 2001 From: dhruvja Date: Tue, 23 Jul 2024 17:26:40 -0400 Subject: [PATCH 30/40] ignore the remainder --- .../programs/restaking-v2/src/lib.rs | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index 62142a3f..0d06f4fb 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -144,7 +144,6 @@ pub mod restaking_v2 { }; let stake_per_validator = amount / validators_len; - let stake_remainder = amount % validators_len; let set_stake_ix = solana_ibc::cpi::accounts::SetStake { sender: ctx.accounts.staker.to_account_info(), @@ -162,15 +161,10 @@ pub mod restaking_v2 { let set_stake_arg = common_state .validators .iter() - .enumerate() - .map(|(index, validator)| { + .map(|validator| { ( sigverify::ed25519::PubKey::from(*validator), - if index == 0 { - (stake_per_validator + stake_remainder) as i128 - } else { - stake_per_validator as i128 - }, + stake_per_validator as i128, ) }) .collect::>(); @@ -278,7 +272,6 @@ pub mod restaking_v2 { // Call guest chain program to update the stake equally let validators_len = common_state.validators.len() as u64; let stake_per_validator = (amount / validators_len) as i128; - let stake_remainder = (amount % validators_len) as i128; let set_stake_ix = solana_ibc::cpi::accounts::SetStake { sender: ctx.accounts.staker.to_account_info(), @@ -296,15 +289,10 @@ pub mod restaking_v2 { let set_stake_arg = common_state .validators .iter() - .enumerate() - .map(|(index, validator)| { + .map(|validator| { ( sigverify::ed25519::PubKey::from(*validator), - if index == 0 { - -(stake_per_validator + stake_remainder) - } else { - -stake_per_validator - }, + -stake_per_validator, ) }) .collect::>(); From 6ee16e3b159690df33cb876d4e53493fc7863e3b Mon Sep 17 00:00:00 2001 From: dhruvja Date: Tue, 23 Jul 2024 17:29:19 -0400 Subject: [PATCH 31/40] fix clippy --- solana/restaking-v2/programs/restaking-v2/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index 0d06f4fb..b7c3e032 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -458,7 +458,7 @@ pub mod restaking_v2 { random_value = if random_value == 0 { random_value + 1 } else { random_value }; msg!("Random value {}", random_value); - sol_price.price = sol_price.price * random_value; + sol_price.price *= random_value; (token_price, sol_price) } else { let maximum_age_in_sec: u64 = 30; From 631868664191992370be0124604900d9f3ceb4c4 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Mon, 29 Jul 2024 13:05:45 -0400 Subject: [PATCH 32/40] add fee payer to send transfer method --- Cargo.toml | 1 + .../programs/restaking-v2/src/lib.rs | 2 +- .../solana-ibc/programs/solana-ibc/src/lib.rs | 18 +++++++++++++----- .../programs/solana-ibc/src/tests.rs | 2 ++ 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 331ec2fa..6f126bec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ codegen-units = 1 [workspace.dependencies] anchor-lang = { version = "0.29.0", features = ["init-if-needed"] } anchor-spl = "0.29.0" +anchor-gen = "0.3.1" ascii = "1.1.0" bs58 = { version = "0.5.0", features = ["alloc"] } base64 = { version = "0.21", default-features = false, features = ["alloc"] } diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index b7c3e032..4f03f999 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -146,7 +146,7 @@ pub mod restaking_v2 { let stake_per_validator = amount / validators_len; let set_stake_ix = solana_ibc::cpi::accounts::SetStake { - sender: ctx.accounts.staker.to_account_info(), + sender: ctx.accounts.fee_payer.to_account_info(), chain: ctx.accounts.chain.to_account_info(), trie: ctx.accounts.trie.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), diff --git a/solana/solana-ibc/programs/solana-ibc/src/lib.rs b/solana/solana-ibc/programs/solana-ibc/src/lib.rs index 1aa429dc..c51969bf 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/lib.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/lib.rs @@ -458,16 +458,16 @@ pub mod solana_ibc { let fee_collector = ctx.accounts.fee_collector.as_ref().unwrap().to_account_info(); - let sender = ctx.accounts.sender.to_account_info(); + let fee_payer = ctx.accounts.fee_payer.to_account_info(); let system_program = ctx.accounts.system_program.to_account_info(); solana_program::program::invoke( &solana_program::system_instruction::transfer( - &sender.key(), + &fee_payer.key(), &fee_collector.key(), fee_amount, ), - &[sender.clone(), fee_collector.clone(), system_program.clone()], + &[fee_payer.clone(), fee_collector.clone(), system_program.clone()], )?; ibc::apps::transfer::handler::send_transfer( @@ -756,6 +756,9 @@ pub struct MockDeliver<'info> { #[derive(Accounts)] #[instruction(hashed_full_denom: CryptoHash)] pub struct SendTransfer<'info> { + #[account(mut)] + fee_payer: Signer<'info>, + #[account(mut)] sender: Signer<'info>, @@ -781,14 +784,14 @@ pub struct SendTransfer<'info> { mint_authority: Option>, #[account(mut)] token_mint: Option>>, - #[account(init_if_needed, payer = sender, seeds = [ + #[account(init_if_needed, payer = fee_payer, seeds = [ ESCROW, hashed_full_denom.as_ref() ], bump, token::mint = token_mint, token::authority = mint_authority)] escrow_account: Option>>, #[account(mut, associated_token::mint = token_mint, associated_token::authority = sender)] receiver_token_account: Option>>, - #[account(init_if_needed, payer = sender, seeds = [FEE_SEED], bump, space = 0)] + #[account(init_if_needed, payer = fee_payer, seeds = [FEE_SEED], bump, space = 0)] /// CHECK: fee_collector: Option>, @@ -887,6 +890,9 @@ fn check_staking_program(program_id: &Pubkey) -> Result<()> { Pubkey::new_from_array(hex_literal::hex!( "a1d0177376e0e90b580181247c1a63b73e473b47bc5b06f70a6a4844e0b05015" )), + Pubkey::new_from_array(hex_literal::hex!( + "d52008b8539f6fcf99629e0bc4c7b240123b3dd1e6f195c200f95758204934e4" + )), ]; match expected_program_ids.contains(program_id) { false => Err(error::Error::InvalidCPICall.into()), @@ -898,8 +904,10 @@ fn check_staking_program(program_id: &Pubkey) -> Result<()> { fn test_staking_program() { const GOOD_ONE: &str = "8n3FHwYxFgQCQc2FNFkwDUf9mcqupxXcCvgfHbApMLv3"; const GOOD_TWO: &str = "BtegF7pQSriyP7gSkDpAkPDMvTS8wfajHJSmvcVoC7kg"; + const GOOD_THREE: &str = "FLxAuGfjKZFFBjxYSTYcgJ1W5LJSUU1NrAViRNAskRfq"; const BAD: &str = "75pAU4CJcp8Z9eoXcL6pSU8sRK5vn3NEpgvV9VJtc5hy"; check_staking_program(&GOOD_ONE.parse().unwrap()).unwrap(); check_staking_program(&GOOD_TWO.parse().unwrap()).unwrap(); + check_staking_program(&GOOD_THREE.parse().unwrap()).unwrap(); check_staking_program(&BAD.parse().unwrap()).unwrap_err(); } diff --git a/solana/solana-ibc/programs/solana-ibc/src/tests.rs b/solana/solana-ibc/programs/solana-ibc/src/tests.rs index c5dbea8d..275002db 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/tests.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/tests.rs @@ -538,6 +538,7 @@ fn anchor_test_deliver() -> Result<()> { 1_000_000u32, )) .accounts(accounts::SendTransfer { + fee_payer: authority.pubkey(), sender: authority.pubkey(), receiver: Some(receiver.pubkey()), storage, @@ -693,6 +694,7 @@ fn anchor_test_deliver() -> Result<()> { 1_000_000u32, )) .accounts(accounts::SendTransfer { + fee_payer: authority.pubkey(), sender: receiver.pubkey(), receiver: Some(authority.pubkey()), storage, From a0deea555e9f81e2c07837aa6d9a6256a526f1b1 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Thu, 15 Aug 2024 12:41:45 +0530 Subject: [PATCH 33/40] remove guest chain program id --- solana/restaking-v2/programs/restaking-v2/src/lib.rs | 3 --- solana/restaking-v2/programs/restaking-v2/src/tests.rs | 1 - 2 files changed, 4 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index 4f03f999..a1d69ead 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -32,7 +32,6 @@ pub mod restaking_v2 { ctx: Context, whitelisted_tokens: Vec, initial_validators: Vec, - guest_chain_program_id: Pubkey, ) -> Result<()> { msg!("Initializng Restaking program"); @@ -59,7 +58,6 @@ pub mod restaking_v2 { common_state.whitelisted_tokens = whitelisted_tokens.into_iter().map(StakeToken::from).collect(); common_state.validators = initial_validators; - common_state.guest_chain_program_id = guest_chain_program_id; Ok(()) } @@ -760,7 +758,6 @@ pub struct CommonState { pub admin: Pubkey, pub whitelisted_tokens: Vec, pub validators: Vec, - pub guest_chain_program_id: Pubkey, pub new_admin_proposal: Option, } diff --git a/solana/restaking-v2/programs/restaking-v2/src/tests.rs b/solana/restaking-v2/programs/restaking-v2/src/tests.rs index 159fc18c..8982ce06 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/tests.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/tests.rs @@ -136,7 +136,6 @@ fn restaking_test_deliver() -> Result<()> { .args(crate::instruction::Initialize { whitelisted_tokens: vec![new_token_mint], initial_validators: vec![authority.pubkey()], - guest_chain_program_id: solana_ibc::ID, }) .payer(authority.clone()) .signer(&*authority) From f0e4ca4400ccb9d0d9851b5e4472fac05bfc84e1 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Thu, 15 Aug 2024 22:31:48 +0530 Subject: [PATCH 34/40] remove remainder from amount when depositing --- .../programs/restaking-v2/src/lib.rs | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index a1d69ead..959275fd 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -92,6 +92,27 @@ pub mod restaking_v2 { let seeds = seeds.as_ref(); let seeds = core::slice::from_ref(&seeds); + let validators_len = common_state.validators.len() as u64; + + // Making sure that the amount is equally divisible between all validators + let amount = amount - (amount % validators_len); + + let amount_in_sol = if whitelisted_token.oracle_address.is_some() { + // Check if the price is stale + let current_time = Clock::get()?.unix_timestamp as u64; + + if (current_time - whitelisted_token.last_updated_in_sec) > + whitelisted_token.max_update_time_in_sec + { + return Err(error!(ErrorCodes::PriceTooStale)); + } + + (whitelisted_token.latest_price * amount) / + 10u64.pow(SOL_DECIMALS as u32) + } else { + amount + }; + let transfer_ix = Transfer { from: ctx.accounts.staker_token_account.to_account_info(), to: ctx.accounts.escrow_token_account.to_account_info(), @@ -120,28 +141,8 @@ pub mod restaking_v2 { anchor_spl::token::mint_to(cpi_ctx, amount)?; // Call guest chain program to update the stake equally - - let validators_len = common_state.validators.len() as u64; - - let original_amount = amount; - - let amount = if whitelisted_token.oracle_address.is_some() { - // Check if the price is stale - let current_time = Clock::get()?.unix_timestamp as u64; - - if (current_time - whitelisted_token.last_updated_in_sec) > - whitelisted_token.max_update_time_in_sec - { - return Err(error!(ErrorCodes::PriceTooStale)); - } - - (whitelisted_token.latest_price * amount) / - 10u64.pow(SOL_DECIMALS as u32) - } else { - amount - }; - - let stake_per_validator = amount / validators_len; + + let stake_per_validator = amount_in_sol / validators_len; let set_stake_ix = solana_ibc::cpi::accounts::SetStake { sender: ctx.accounts.fee_payer.to_account_info(), @@ -176,10 +177,10 @@ pub mod restaking_v2 { if delegations_len <= index { common_state.whitelisted_tokens[whitelisted_token_idx] .delegations - .push(original_amount as u128) + .push(amount as u128) } else { common_state.whitelisted_tokens[whitelisted_token_idx] - .delegations[index] += original_amount as u128 + .delegations[index] += amount as u128 } }); @@ -450,8 +451,8 @@ pub mod restaking_v2 { )?; let token_price = token_price_feed.get_price_unchecked(&feed_id)?; - // Using a random value since the price doesnt change when running locally since - // the accounts are cloned during genesis and remain unchanged. + // Using a random value since the price doesnt change when running locally. + // The accounts are cloned during genesis and remain unchanged. let mut random_value = Clock::get()?.unix_timestamp % 10; random_value = if random_value == 0 { random_value + 1 } else { random_value }; From 7a49524dfb6effd8d10b6786c08e3fc47de37d33 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Thu, 15 Aug 2024 22:41:20 +0530 Subject: [PATCH 35/40] add pause flag for tokens --- .../programs/restaking-v2/src/lib.rs | 56 ++++++++++++++----- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index 959275fd..fb3e70ba 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -83,6 +83,10 @@ pub mod restaking_v2 { let whitelisted_token = &common_state.whitelisted_tokens[whitelisted_token_idx]; + if whitelisted_token.paused { + return Err(error!(ErrorCodes::TokenDepositIsPaused)); + } + if ctx.accounts.staker_token_account.amount < amount { return Err(error!(ErrorCodes::NotEnoughTokensToStake)); } @@ -101,14 +105,14 @@ pub mod restaking_v2 { // Check if the price is stale let current_time = Clock::get()?.unix_timestamp as u64; - if (current_time - whitelisted_token.last_updated_in_sec) > - whitelisted_token.max_update_time_in_sec + if (current_time - whitelisted_token.last_updated_in_sec) + > whitelisted_token.max_update_time_in_sec { return Err(error!(ErrorCodes::PriceTooStale)); } - (whitelisted_token.latest_price * amount) / - 10u64.pow(SOL_DECIMALS as u32) + (whitelisted_token.latest_price * amount) + / 10u64.pow(SOL_DECIMALS as u32) } else { amount }; @@ -141,7 +145,7 @@ pub mod restaking_v2 { anchor_spl::token::mint_to(cpi_ctx, amount)?; // Call guest chain program to update the stake equally - + let stake_per_validator = amount_in_sol / validators_len; let set_stake_ix = solana_ibc::cpi::accounts::SetStake { @@ -257,13 +261,13 @@ pub mod restaking_v2 { // Check if the price is stale let current_time = Clock::get()?.unix_timestamp as u64; - if (current_time - whitelisted_token.last_updated_in_sec) > - whitelisted_token.max_update_time_in_sec + if (current_time - whitelisted_token.last_updated_in_sec) + > whitelisted_token.max_update_time_in_sec { return Err(error!(ErrorCodes::PriceTooStale)); } - (whitelisted_token.latest_price * amount) / - 10u64.pow(SOL_DECIMALS as u32) + (whitelisted_token.latest_price * amount) + / 10u64.pow(SOL_DECIMALS as u32) } else { amount }; @@ -391,6 +395,27 @@ pub mod restaking_v2 { Ok(()) } + /// Updates the token pause flag for specified token. + /// + /// Requires the admin to call this method. + pub fn update_token_pause_flag( + ctx: Context, + mint: Pubkey, + paused: bool, + ) -> Result<()> { + msg!("Updating token pause flag for {} to {}", mint, paused); + let staking_params = &mut ctx.accounts.common_state; + let whitelisted_token = staking_params + .whitelisted_tokens + .iter_mut() + .find(|x| x.address == mint) + .ok_or_else(|| error!(ErrorCodes::InvalidTokenMint))?; + + whitelisted_token.paused = paused; + + Ok(()) + } + /// Adds new validator who are part of social consensus /// /// This method checks if any of the new validators to be added are already part of @@ -483,10 +508,10 @@ pub mod restaking_v2 { let final_amount_in_sol = token_price.price as f64 / sol_price.price as f64; - let final_amount_in_sol = final_amount_in_sol * - 10_f64.powi( - (i32::from(SOL_DECIMALS) + token_price.exponent) - - (i32::from(token_decimals) + sol_price.exponent), + let final_amount_in_sol = final_amount_in_sol + * 10_f64.powi( + (i32::from(SOL_DECIMALS) + token_price.exponent) + - (i32::from(token_decimals) + sol_price.exponent), ); msg!("Final amount in sol {}", final_amount_in_sol); @@ -738,6 +763,8 @@ pub struct StakeToken { pub max_update_time_in_sec: u64, // 8 /// mapping of the validator index with their stake in the above token pub delegations: Vec, // n * 16 + /// If the token is paused, it cannot be deposited + pub paused: bool, } impl From for StakeToken { @@ -749,6 +776,7 @@ impl From for StakeToken { last_updated_in_sec: 0, max_update_time_in_sec: payload.max_update_time_in_sec, delegations: vec![], + paused: false, } } } @@ -788,4 +816,6 @@ pub enum ErrorCodes { TokenListContainDuplicates, #[msg("The validator list in the instruction argument contain duplicates")] ValidatorListContainDuplicates, + #[msg("Deposit for the token is paused")] + TokenDepositIsPaused, } From 5b52f9bccdb623b7fde6c46562ddd587a5f8b09c Mon Sep 17 00:00:00 2001 From: dhruvja Date: Wed, 21 Aug 2024 12:55:04 +0530 Subject: [PATCH 36/40] remove fee payer --- solana/solana-ibc/programs/solana-ibc/src/lib.rs | 13 +++++-------- solana/solana-ibc/programs/solana-ibc/src/tests.rs | 2 -- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/solana/solana-ibc/programs/solana-ibc/src/lib.rs b/solana/solana-ibc/programs/solana-ibc/src/lib.rs index 764488f7..ecab7576 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/lib.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/lib.rs @@ -463,16 +463,16 @@ pub mod solana_ibc { let fee_collector = ctx.accounts.fee_collector.as_ref().unwrap().to_account_info(); - let fee_payer = ctx.accounts.fee_payer.to_account_info(); + let sender = ctx.accounts.sender.to_account_info(); let system_program = ctx.accounts.system_program.to_account_info(); solana_program::program::invoke( &solana_program::system_instruction::transfer( - &fee_payer.key(), + &sender.key(), &fee_collector.key(), fee_amount, ), - &[fee_payer.clone(), fee_collector.clone(), system_program.clone()], + &[sender.clone(), fee_collector.clone(), system_program.clone()], )?; ibc::apps::transfer::handler::send_transfer( @@ -809,9 +809,6 @@ pub struct MockDeliver<'info> { #[derive(Accounts)] #[instruction(hashed_full_denom: CryptoHash)] pub struct SendTransfer<'info> { - #[account(mut)] - fee_payer: Signer<'info>, - #[account(mut)] sender: Signer<'info>, @@ -837,14 +834,14 @@ pub struct SendTransfer<'info> { mint_authority: Option>, #[account(mut)] token_mint: Option>>, - #[account(init_if_needed, payer = fee_payer, seeds = [ + #[account(init_if_needed, payer = sender, seeds = [ ESCROW, hashed_full_denom.as_ref() ], bump, token::mint = token_mint, token::authority = mint_authority)] escrow_account: Option>>, #[account(mut, associated_token::mint = token_mint, associated_token::authority = sender)] receiver_token_account: Option>>, - #[account(init_if_needed, payer = fee_payer, seeds = [FEE_SEED], bump, space = 0)] + #[account(init_if_needed, payer = sender, seeds = [FEE_SEED], bump, space = 0)] /// CHECK: fee_collector: Option>, diff --git a/solana/solana-ibc/programs/solana-ibc/src/tests.rs b/solana/solana-ibc/programs/solana-ibc/src/tests.rs index df45aaff..77abd998 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/tests.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/tests.rs @@ -520,7 +520,6 @@ fn anchor_test_deliver() -> Result<()> { 1_000_000u32, )) .accounts(accounts::SendTransfer { - fee_payer: authority.pubkey(), sender: authority.pubkey(), receiver: Some(receiver.pubkey()), storage, @@ -676,7 +675,6 @@ fn anchor_test_deliver() -> Result<()> { 1_000_000u32, )) .accounts(accounts::SendTransfer { - fee_payer: authority.pubkey(), sender: receiver.pubkey(), receiver: Some(authority.pubkey()), storage, From 1044781724c990dbfeec9cc4d610ce7485dd804a Mon Sep 17 00:00:00 2001 From: dhruvja Date: Mon, 2 Sep 2024 14:30:33 +0530 Subject: [PATCH 37/40] increase maximum age for oracle --- solana/restaking-v2/programs/restaking-v2/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index fb3e70ba..d236a26d 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -485,7 +485,7 @@ pub mod restaking_v2 { sol_price.price *= random_value; (token_price, sol_price) } else { - let maximum_age_in_sec: u64 = 30; + let maximum_age_in_sec: u64 = 300; let feed_id: [u8; 32] = get_feed_id_from_hex(token_feed_id)?; let sol_price = sol_price_feed.get_price_no_older_than( &Clock::get()?, From f1905127dcb24e4a914ea9b4ea92112da2bd8ca0 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Wed, 4 Sep 2024 13:29:05 +0530 Subject: [PATCH 38/40] add a method to reallocate common state --- Anchor.toml | 16 +++++----- .../programs/restaking-v2/src/lib.rs | 30 +++++++++++++++++-- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/Anchor.toml b/Anchor.toml index 01ef2480..520fc402 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -1,30 +1,28 @@ +[toolchain] + [features] seeds = false skip-lint = false [programs.devnet] restaking = "8n3FHwYxFgQCQc2FNFkwDUf9mcqupxXcCvgfHbApMLv3" +restaking_v2 = "H69iS4rPnrRAMciLJcpEY3tRtFro7Mo7a2YAU8Q1busv" solana_ibc = "2HLLVco5HvwWriNbUhmVwA2pCetRkpgrqwnjcsZdyTKT" -restaking_v2 = "BtegF7pQSriyP7gSkDpAkPDMvTS8wfajHJSmvcVoC7kg" [programs.localnet] restaking = "8n3FHwYxFgQCQc2FNFkwDUf9mcqupxXcCvgfHbApMLv3" +restaking_v2 = "H69iS4rPnrRAMciLJcpEY3tRtFro7Mo7a2YAU8Q1busv" solana_ibc = "2HLLVco5HvwWriNbUhmVwA2pCetRkpgrqwnjcsZdyTKT" -restaking_v2 = "BtegF7pQSriyP7gSkDpAkPDMvTS8wfajHJSmvcVoC7kg" [registry] url = "https://api.apr.dev" [provider] -cluster = "localnet" -wallet = "~/.config/solana/id.json" +cluster = "Localnet" +wallet = "/Users/dhruvjain/.config/solana/id.json" [workspace] -members = [ - "solana/restaking/programs/restaking", - "solana/solana-ibc/programs/solana-ibc", - "solana/restaking-v2/programs/restaking-v2" -] +members = ["solana/restaking/programs/restaking", "solana/solana-ibc/programs/solana-ibc", "solana/restaking-v2/programs/restaking-v2"] [scripts] test = "./solana-test.sh" diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index d236a26d..9f24009a 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -4,7 +4,7 @@ use anchor_spl::token::{Mint, Token, TokenAccount}; use pyth_solana_receiver_sdk::price_update::PriceUpdateV2; use solana_ibc::program::SolanaIbc; -declare_id!("BtegF7pQSriyP7gSkDpAkPDMvTS8wfajHJSmvcVoC7kg"); +declare_id!("H69iS4rPnrRAMciLJcpEY3tRtFro7Mo7a2YAU8Q1busv"); pub const COMMON_SEED: &[u8] = b"common"; pub const ESCROW_SEED: &[u8] = b"escrow"; @@ -396,7 +396,7 @@ pub mod restaking_v2 { } /// Updates the token pause flag for specified token. - /// + /// /// Requires the admin to call this method. pub fn update_token_pause_flag( ctx: Context, @@ -578,6 +578,32 @@ pub mod restaking_v2 { Ok(()) } + + pub fn realloc_common_state( + ctx: Context, + new_space: usize, + ) -> Result<()> { + let common_state = ctx.accounts.common_state.to_account_info(); + let minimum_rent_required = Rent::get()?.minimum_balance(new_space); + let available_lamports = common_state.lamports(); + if available_lamports < minimum_rent_required { + let lamports_needed = minimum_rent_required - available_lamports; + solana_program::program::invoke( + &solana_program::system_instruction::transfer( + &ctx.accounts.admin.key(), + &common_state.key(), + lamports_needed, + ), + &[ + ctx.accounts.admin.to_account_info(), + common_state.to_account_info(), + ], + )?; + } + + common_state.realloc(new_space, false)?; + Ok(()) + } } #[derive(Accounts)] From 025a490d2f4514a17687a29c7ced7c0c370a8fd1 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Thu, 12 Sep 2024 12:11:12 +0400 Subject: [PATCH 39/40] make decimals as 6 --- solana/restaking-v2/programs/restaking-v2/src/lib.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index 9f24009a..52f28bf6 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -500,7 +500,8 @@ pub mod restaking_v2 { (token_price, sol_price) }; - let token_decimals = ctx.accounts.token_mint.decimals; + // ALL stablecoin tokens have 6 decimals + let token_decimals = 6; // There would be a slight loss in precision due to the conversion from f64 to u64 // but only when the price is very large. And since it has exponents, the price being @@ -715,7 +716,8 @@ pub struct UpdateTokenPrice<'info> { #[account(mut, seeds = [COMMON_SEED], bump)] pub common_state: Account<'info, CommonState>, - pub token_mint: Account<'info, Mint>, + /// CHECK: + pub token_mint: UncheckedAccount<'info>, pub token_price_feed: Account<'info, PriceUpdateV2>, pub sol_price_feed: Account<'info, PriceUpdateV2>, @@ -748,6 +750,8 @@ pub struct UpdateStakingParams<'info> { #[account(mut, seeds = [COMMON_SEED], bump, has_one = admin)] pub common_state: Account<'info, CommonState>, + + pub system_program: Program<'info, System>, } #[derive(Accounts)] @@ -757,6 +761,7 @@ pub struct UpdateAdmin<'info> { #[account(mut, seeds = [COMMON_SEED], bump)] pub common_state: Account<'info, CommonState>, + } #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] From 360e64e1ccfce2616a3f6641f56e29e15f5efe73 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Thu, 10 Oct 2024 22:36:15 +0530 Subject: [PATCH 40/40] fix multiplication overflow --- solana/restaking-v2/programs/restaking-v2/src/lib.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/solana/restaking-v2/programs/restaking-v2/src/lib.rs b/solana/restaking-v2/programs/restaking-v2/src/lib.rs index 52f28bf6..1995e308 100644 --- a/solana/restaking-v2/programs/restaking-v2/src/lib.rs +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -111,8 +111,9 @@ pub mod restaking_v2 { return Err(error!(ErrorCodes::PriceTooStale)); } - (whitelisted_token.latest_price * amount) - / 10u64.pow(SOL_DECIMALS as u32) + let amount_u128 = (whitelisted_token.latest_price as u128 * amount as u128) + / 10u128.pow(SOL_DECIMALS as u32); + amount_u128 as u64 } else { amount }; @@ -266,8 +267,9 @@ pub mod restaking_v2 { { return Err(error!(ErrorCodes::PriceTooStale)); } - (whitelisted_token.latest_price * amount) - / 10u64.pow(SOL_DECIMALS as u32) + let amount_u128 = (whitelisted_token.latest_price as u128 * amount as u128) + / 10u128.pow(SOL_DECIMALS as u32); + amount_u128 as u64 } else { amount };