diff --git a/Anchor.toml b/Anchor.toml index b5c355d7..520fc402 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -1,27 +1,28 @@ +[toolchain] + [features] seeds = false skip-lint = false [programs.devnet] restaking = "8n3FHwYxFgQCQc2FNFkwDUf9mcqupxXcCvgfHbApMLv3" +restaking_v2 = "H69iS4rPnrRAMciLJcpEY3tRtFro7Mo7a2YAU8Q1busv" solana_ibc = "2HLLVco5HvwWriNbUhmVwA2pCetRkpgrqwnjcsZdyTKT" [programs.localnet] restaking = "8n3FHwYxFgQCQc2FNFkwDUf9mcqupxXcCvgfHbApMLv3" +restaking_v2 = "H69iS4rPnrRAMciLJcpEY3tRtFro7Mo7a2YAU8Q1busv" solana_ibc = "2HLLVco5HvwWriNbUhmVwA2pCetRkpgrqwnjcsZdyTKT" [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" -] +members = ["solana/restaking/programs/restaking", "solana/solana-ibc/programs/solana-ibc", "solana/restaking-v2/programs/restaking-v2"] [scripts] test = "./solana-test.sh" @@ -39,3 +40,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 48703ff9..e33c486f 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" @@ -3912,6 +3930,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" @@ -4258,6 +4307,22 @@ dependencies = [ "solana-program", ] +[[package]] +name = "restaking_v2" +version = "0.1.0" +dependencies = [ + "anchor-client", + "anchor-lang", + "anchor-spl", + "anyhow", + "pyth-solana-receiver-sdk", + "solana-ibc", + "solana-program", + "solana-signature-verifier", + "spl-associated-token-account", + "spl-token", +] + [[package]] name = "ring" version = "0.16.20" @@ -4807,6 +4872,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 a76ac7b1..6939b37d 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", @@ -30,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"] } @@ -67,6 +69,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" # TODO(mina86): Change to "1" once we update the toolchain. Building 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 {} + diff --git a/solana/restaking-v2/README.md b/solana/restaking-v2/README.md new file mode 100644 index 00000000..ef8cbb5f --- /dev/null +++ b/solana/restaking-v2/README.md @@ -0,0 +1,23 @@ +# Restaking V2 + +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. + +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. + +For example: JitoSOL deposited to validator A and B would be different on rollup even though it is the same token on Solana. + +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. +- 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/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..4ef97435 --- /dev/null +++ b/solana/restaking-v2/programs/restaking-v2/Cargo.toml @@ -0,0 +1,32 @@ +[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 +pyth-solana-receiver-sdk.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..1995e308 --- /dev/null +++ b/solana/restaking-v2/programs/restaking-v2/src/lib.rs @@ -0,0 +1,854 @@ +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!("H69iS4rPnrRAMciLJcpEY3tRtFro7Mo7a2YAU8Q1busv"); + +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; +pub const SOL_DECIMALS: u8 = 9; + +pub const SOL_PRICE_FEED_ID: &str = + "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; + +#[cfg(test)] +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; + + use super::*; + + pub fn initialize( + ctx: Context, + whitelisted_tokens: Vec, + initial_validators: Vec, + ) -> Result<()> { + msg!("Initializng Restaking program"); + + 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(); + common_state.validators = initial_validators; + + 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(); + + let whitelisted_token_idx = common_state + .whitelisted_tokens + .iter() + .position(|x| &x.address == stake_token_mint) + .ok_or_else(|| error!(ErrorCodes::InvalidTokenMint))?; + + 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)); + } + + 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 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)); + } + + let amount_u128 = (whitelisted_token.latest_price as u128 * amount as u128) + / 10u128.pow(SOL_DECIMALS as u32); + amount_u128 as u64 + } else { + amount + }; + + 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_in_sol / validators_len; + + let set_stake_ix = solana_ibc::cpi::accounts::SetStake { + 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(), + 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), + stake_per_validator as i128, + ) + }) + .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(amount as u128) + } else { + common_state.whitelisted_tokens[whitelisted_token_idx] + .delegations[index] += amount as u128 + } + }); + + msg!("Depositing {}", amount); + + 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 stake_token_mint = &ctx.accounts.token_mint.key(); + + let whitelisted_token_idx = common_state + .whitelisted_tokens + .iter() + .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(); + 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)?; + + 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)); + } + let amount_u128 = (whitelisted_token.latest_price as u128 * amount as u128) + / 10u128.pow(SOL_DECIMALS as u32); + amount_u128 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 = (amount / validators_len) 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), + -stake_per_validator, + ) + }) + .collect::>(); + + set_stake_arg.iter().enumerate().for_each(|(index, _validator)| { + common_state.whitelisted_tokens[whitelisted_token_idx] + .delegations[index] -= original_amount as u128; + }); + + msg!("Withdrawing {}", amount); + + 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_else(|| error!(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 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| { + whitelisted_token_mint.address == token_mint.address + }, + ) + }); + + if contains_mint { + return Err(error!(ErrorCodes::TokenAlreadyWhitelisted)); + } + + let new_token_mints = new_token_mints + .into_iter() + .map(StakeToken::from) + .collect::>(); + + staking_params + .whitelisted_tokens + .extend_from_slice(new_token_mints.as_slice()); + + 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 + /// 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 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)); + + if contains_validator { + return Err(error!(ErrorCodes::ValidatorAlreadyAdded)); + } + + staking_params.validators.extend_from_slice(new_validators.as_slice()); + + 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); + + 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 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. + // 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 *= random_value; + (token_price, sol_price) + } else { + 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()?, + 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) + }; + + // 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 + // 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); + let final_amount_in_sol = multipled_price.round() as u64; + + msg!( + "The price of solana is ({} ± {}) * 10^{} and final price in dec \ + {} \n + The price of solana is ({} ± {}) * 10^{}", + sol_price.price, + sol_price.conf, + sol_price.exponent, + final_amount_in_sol, + token_price.price, + token_price.conf, + token_price.exponent, + ); + + let previous_price = staked_token.latest_price; + + msg!("This is staked token {:?}", staked_token); + + let set_stake_arg = staked_token + .delegations + .iter() + .enumerate() + .map(|(validator_idx, amount)| { + let amount = *amount as i128; + let validator = validators[validator_idx]; + 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(); + + 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(()) + } + + 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)] +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> { + pub staker: Signer<'info>, + + #[account(mut)] + pub fee_payer: 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 = 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 = 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 = 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>, + 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 UpdateTokenPrice<'info> { + #[account(mut)] + pub signer: Signer<'info>, + + #[account(mut, seeds = [COMMON_SEED], bump)] + pub common_state: Account<'info, CommonState>, + + /// CHECK: + pub token_mint: UncheckedAccount<'info>, + + 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)] + pub admin: Signer<'info>, + + #[account(mut, seeds = [COMMON_SEED], bump, has_one = admin)] + pub common_state: Account<'info, CommonState>, + + pub system_program: Program<'info, System>, +} + +#[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>, + +} + +#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] +pub struct NewTokenPayload { + pub address: Pubkey, + pub oracle_address: Option, + pub max_update_time_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 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 + /// 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 + /// 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 { + 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, + delegations: vec![], + paused: false, + } + } +} + +#[account] +#[derive(Debug)] +pub struct CommonState { + pub admin: Pubkey, + pub whitelisted_tokens: Vec, + pub validators: Vec, + 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 deposited")] + 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, + #[msg( + "Oracle address not found. Maybe its price doesnt need to be updated?" + )] + 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, + #[msg("Deposit for the token is paused")] + TokenDepositIsPaused, +} 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..8982ce06 --- /dev/null +++ b/solana/restaking-v2/programs/restaking-v2/src/tests.rs @@ -0,0 +1,389 @@ +use std::rc::Rc; +use std::str::FromStr; +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 pyth_solana_receiver_sdk::price_update::get_feed_id_from_hex; +use spl_token::instruction::initialize_mint2; + +use crate::{CommonState, 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 = 100_000; + +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); + 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()), + STAKE_TOKEN_MINT_DECIMALS, + ) + .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 new_token_mint = NewTokenPayload { + address: token_mint_key, + oracle_address: Some(TOKEN_FEED_ID.to_string()), + max_update_time_in_sec: 60, + }; + + 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![new_token_mint], + initial_validators: vec![authority.pubkey()], + }) + .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; + + /* + 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 + */ + 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, + fee_payer: authority.pubkey(), + 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 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) + .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(STAKE_TOKEN_MINT_DECIMALS.into())) + .round() as u64, + STAKE_AMOUNT + ); + + 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 + */ + 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 / 2 }) + .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 / 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 / 2 + ); + + 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 00000000..1d29853c Binary files /dev/null and b/solana/restaking-v2/restaking-flow.png differ diff --git a/solana/restaking/tests/restaking.ts b/solana/restaking/tests/restaking.ts index d5784976..e3a69a78 100644 --- a/solana/restaking/tests/restaking.ts +++ b/solana/restaking/tests/restaking.ts @@ -557,4 +557,4 @@ describe("restaking", () => { throw error; } }); -}); +}); \ No newline at end of file diff --git a/solana/solana-ibc/programs/solana-ibc/src/lib.rs b/solana/solana-ibc/programs/solana-ibc/src/lib.rs index aa58c9c9..ecab7576 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/lib.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/lib.rs @@ -958,6 +958,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()), @@ -969,8 +972,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(); }