diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 519b28673..fa411c538 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -115,45 +115,71 @@ jobs: - name: Pass after fuzzing run: echo "Fuzzing completed" - # localnet-test-marginfi: - # name: Anchor localnet tests marginfi - # runs-on: ubuntu-latest - - # steps: - # - uses: actions/checkout@v3 - - # - uses: ./.github/actions/setup-common/ - # - uses: ./.github/actions/setup-anchor-cli/ - - # - uses: ./.github/actions/build-workspace/ - - # - name: Install Node.js dependencies - # run: yarn install - - # - name: Build marginfi program - # run: anchor build -p marginfi -- --no-default-features + localnet-test-marginfi: + name: Anchor localnet tests marginfi + runs-on: ubuntu-latest - # - name: Build liquidity incentive program - # run: anchor build -p liquidity_incentive_program -- --no-default-features + steps: + - uses: actions/checkout@v3 - # - name: Build mocks program - # run: anchor build -p mocks + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: "20.10.0" - # - name: Start Solana Test Validator - # run: | - # solana-test-validator --reset --limit-ledger-size 1000 \ + - uses: ./.github/actions/setup-common/ + - uses: ./.github/actions/setup-anchor-cli/ - # - name: Wait for Validator to Start - # run: sleep 60 + - uses: ./.github/actions/build-workspace/ - # - name: Deploy Liquidity Incentive Program - # run: solana program deploy --program-id Lip1111111111111111111111111111111111111111 target/deploy/liquidity_incentive_program.so + - name: Install Node.js dependencies + run: yarn install - # - name: Deploy Marginfi Program - # run: solana program deploy --program-id 2jGhuVUuy3umdzByFx8sNWUAaf5vaeuDm78RDPEnhrMr target/deploy/marginfi.so + - name: Build marginfi program + run: anchor build -p marginfi -- --no-default-features - # - name: Deploy Mocks Program - # run: solana program deploy --program-id 5XaaR94jBubdbrRrNW7DtRvZeWvLhSHkEGU3jHTEXV3C target/deploy/mocks.so + - name: Build mocks program + run: anchor build -p mocks - # - name: Run tests - # run: anchor test --skip-build --skip-local-validator + # Handles extraneous (os error 2) that appears during testing in some versions of solana. See: + # https://solana.stackexchange.com/questions/1648/error-no-such-file-or-directory-os-error-2-error-from-anchor-test + - name: Run Anchor tests + run: | + set +e + anchor test --skip-build 2>&1 | tee test_output.log + ANCHOR_EXIT_CODE=$? + set -e + + if grep -q "failing" test_output.log; then + echo "Real test failure detected." + exit 1 + fi + + if grep -q "No such file or directory (os error 2)" test_output.log; then + echo "Extraneous error detected, ignoring it..." + exit 0 + fi + + if [ $ANCHOR_EXIT_CODE -ne 0 ]; then + echo "Anchor test exited with code $ANCHOR_EXIT_CODE due to an unexpected error." + exit 1 + else + echo "Test run completed successfully without extraneous errors." + exit 0 + fi + + # - name: Start Solana Test Validator + # run: | + # solana-test-validator --reset --limit-ledger-size 1000 \ + + # - name: Wait for Validator to Start + # run: sleep 60 + + # - name: Deploy Liquidity Incentive Program + # run: solana program deploy --program-id Lip1111111111111111111111111111111111111111 target/deploy/liquidity_incentive_program.so + + # - name: Deploy Marginfi Program + # run: solana program deploy --program-id 2jGhuVUuy3umdzByFx8sNWUAaf5vaeuDm78RDPEnhrMr target/deploy/marginfi.so + + # - name: Deploy Mocks Program + # run: solana program deploy --program-id 5XaaR94jBubdbrRrNW7DtRvZeWvLhSHkEGU3jHTEXV3C target/deploy/mocks.so diff --git a/Anchor.toml b/Anchor.toml index 9c621eea2..5d7661d37 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -1,15 +1,17 @@ [toolchain] anchor_version = "0.30.1" solana_version = "1.18.17" +# Getting "thread 'main' panicked at cli/src/lib.rs:545:18:"? Check your toolchain matches the above. [features] resolution = true skip-lint = false [programs.localnet] -liquidity_incentive_program = "Lip1111111111111111111111111111111111111111" +# liquidity_incentive_program = "Lip1111111111111111111111111111111111111111" marginfi = "2jGhuVUuy3umdzByFx8sNWUAaf5vaeuDm78RDPEnhrMr" mocks = "5XaaR94jBubdbrRrNW7DtRvZeWvLhSHkEGU3jHTEXV3C" +spl_single_pool = "SVSPxpvHdN29nkVg9rPapPNDddN5DipNLRUFhyjFThE" # cloned from solana-labs repo (see below) [programs.mainnet] liquidity_incentive_program = "LipsxuAkFkwa4RKNzn51wAsW7Dedzt1RNHMkTkDEZUW" @@ -19,15 +21,18 @@ marginfi = "MFv2hWf31Z9kbCa1snEPYctwafyhdvnV7FZnsebVacA" url = "https://api.apr.dev" [provider] -cluster = "localnet" -# cluster = "https://devnet.rpcpool.com/" +cluster = "Localnet" wallet = "~/.config/solana/id.json" +# (remove RUST_LOG= to see bankRun logs) [scripts] -test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/*.spec.ts --exit --require tests/rootHooks.ts" +test = "RUST_LOG= yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/*.spec.ts --exit --require tests/rootHooks.ts" + +# Staked collateral tests only +# test = "RUST_LOG= yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/s*.spec.ts --exit --require tests/rootHooks.ts" [test] -startup_wait = 5000 +startup_wait = 60000 shutdown_wait = 2000 upgradeable = false @@ -44,6 +49,16 @@ filename = "tests/fixtures/bonk_bank.json" address = "4kNXetv8hSv9PzvzPZzEs1CTH6ARRRi2b8h6jk1ad1nP" filename = "tests/fixtures/cloud_bank.json" +[[test.validator.account]] +address = "Fe5QkKPVAh629UPP5aJ8sDZu8HTfe6M26jDQkKyXVhoA" +filename = "tests/fixtures/pyusd_bank.json" + [[test.validator.account]] address = "8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN" filename = "tests/fixtures/localnet_usdc.json" + +# To update: +# clone https://github.com/solana-labs/solana-program-library/tree/master and run cargo build-sbf in spl_single_pool +[[test.genesis]] +address = "SVSPxpvHdN29nkVg9rPapPNDddN5DipNLRUFhyjFThE" # spl single pool program +program = "tests/fixtures/spl_single_pool.so" diff --git a/clients/rust/marginfi-cli/src/entrypoint.rs b/clients/rust/marginfi-cli/src/entrypoint.rs index 55851a493..3b27abd7d 100644 --- a/clients/rust/marginfi-cli/src/entrypoint.rs +++ b/clients/rust/marginfi-cli/src/entrypoint.rs @@ -244,6 +244,7 @@ impl From for BankOperationalState { } } +#[allow(clippy::large_enum_variant)] #[derive(Debug, Parser)] pub enum BankCommand { Get { @@ -291,6 +292,8 @@ pub enum BankCommand { pf_or: Option, #[clap(long, arg_enum, help = "Bank risk tier")] risk_tier: Option, + #[clap(long, help = "0 = default, 1 = SOL, 2 = Staked SOL LST")] + asset_tag: Option, #[clap(long, arg_enum, help = "Bank oracle type")] oracle_type: Option, #[clap(long, help = "Bank oracle account")] @@ -740,6 +743,7 @@ fn bank(subcmd: BankCommand, global_options: &GlobalOptions) -> Result<()> { pf_ir, pf_or, risk_tier, + asset_tag, oracle_type, oracle_key, usd_init_limit, @@ -792,6 +796,7 @@ fn bank(subcmd: BankCommand, global_options: &GlobalOptions) -> Result<()> { protocol_origination_fee: pf_or.map(|x| I80F48::from_num(x).into()), }), risk_tier: risk_tier.map(|x| x.into()), + asset_tag, total_asset_value_init_limit: usd_init_limit, oracle_max_age, permissionless_bad_debt_settlement, diff --git a/package.json b/package.json index 808ea02f3..c7f215e6e 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@coral-xyz/spl-token": "^0.30.1", "@solana/spl-token": "^0.4.8", "@solana/web3.js": "^1.95.2", + "@solana/spl-single-pool-classic": "^1.0.2", "@mrgnlabs/mrgn-common": "^1.8.0", "@mrgnlabs/marginfi-client-v2": "^4.0.0", "mocha": "^10.2.0", @@ -15,12 +16,15 @@ "bignumber.js": "^9.1.2" }, "devDependencies": { + "anchor-bankrun": "^0.4.0", + "solana-bankrun": "^0.3.0", "@types/bn.js": "^5.1.0", "@types/chai": "^4.3.0", "@types/mocha": "^9.0.0", "chai": "^4.3.4", "prettier": "^2.6.2", "ts-node": "^10.9.1", - "typescript": "^4.3.5" + "typescript": "^4.3.5", + "big.js": "^6.2.1" } } diff --git a/programs/brick/src/lib.rs b/programs/brick/src/lib.rs index 47f475e78..107285048 100644 --- a/programs/brick/src/lib.rs +++ b/programs/brick/src/lib.rs @@ -13,6 +13,10 @@ pub mod brick { ) -> Result<()> { Err(ErrorCode::ProgramDisabled.into()) } + + pub fn initialize(_ctx: Context, _val: u64) -> Result<()> { + Ok(()) + } } #[error_code] @@ -20,3 +24,6 @@ pub enum ErrorCode { #[msg("This program is temporarily disabled.")] ProgramDisabled, } + +#[derive(Accounts)] +pub struct Initialize {} diff --git a/programs/marginfi/fuzz/README.md b/programs/marginfi/fuzz/README.md index 4d2539b86..ebd3e99a3 100644 --- a/programs/marginfi/fuzz/README.md +++ b/programs/marginfi/fuzz/README.md @@ -28,3 +28,21 @@ Before the invoke we also copy to a local cache and revert the state if the inst ### Actions The framework uses the arbitrary library to generate a random sequence of actions that are then processed on the same state. + +### How to Run + +Run `python3 ./generate_corpus.py`. You may use python if you don't have python3 installed, or you may need to install python. + +Build with `cargo build`. + +If this fails, you probably need to update your Rust toolchain: + +`rustup install nightly-2024-06-05` + +And possibly: + +`rustup component add rust-src --toolchain nightly-2024-06-05-x86_64-unknown-linux-gnu` + +Run with `cargo +nightly-2024-06-05 fuzz run lend -Zbuild-std --strip-dead-code --no-cfg-fuzzing -- -max_total_time=300` + +To rerun some tests after a failure: `cargo +nightly-2024-06-05 fuzz run -Zbuild-std lend artifacts/lend/crash-ae5084b9433152babdaf7dcd75781eacd7ea55c7`, replacing the hash after crash- with the one you see in the terminal. diff --git a/programs/marginfi/src/constants.rs b/programs/marginfi/src/constants.rs index 0310c3003..dc8680717 100644 --- a/programs/marginfi/src/constants.rs +++ b/programs/marginfi/src/constants.rs @@ -13,6 +13,7 @@ pub const INSURANCE_VAULT_SEED: &str = "insurance_vault"; pub const FEE_VAULT_SEED: &str = "fee_vault"; pub const FEE_STATE_SEED: &str = "feestate"; +pub const STAKED_SETTINGS_SEED: &str = "staked_settings"; pub const EMISSIONS_AUTH_SEED: &str = "emissions_auth_seed"; pub const EMISSIONS_TOKEN_ACCOUNT_SEED: &str = "emissions_token_account_seed"; @@ -28,6 +29,17 @@ cfg_if::cfg_if! { } } +// TODO update to the actual deployment key on mainnet/devnet/staging +cfg_if::cfg_if! { + if #[cfg(feature = "devnet")] { + pub const SPL_SINGLE_POOL_ID: Pubkey = pubkey!("SVSPxpvHdN29nkVg9rPapPNDddN5DipNLRUFhyjFThE"); + } else if #[cfg(any(feature = "mainnet-beta", feature = "staging"))] { + pub const SPL_SINGLE_POOL_ID: Pubkey = pubkey!("SVSPxpvHdN29nkVg9rPapPNDddN5DipNLRUFhyjFThE"); + } else { + pub const SPL_SINGLE_POOL_ID: Pubkey = pubkey!("SVSPxpvHdN29nkVg9rPapPNDddN5DipNLRUFhyjFThE"); + } +} + cfg_if::cfg_if! { if #[cfg(feature = "devnet")] { pub const SWITCHBOARD_PULL_ID: Pubkey = pubkey!("Aio4gaXjXzJNVLtzwtNVmSqGKpANtXhybbkhtAC94ji2"); @@ -36,6 +48,8 @@ cfg_if::cfg_if! { } } +pub const NATIVE_STAKE_ID: Pubkey = pubkey!("Stake11111111111111111111111111111111111111"); + /// TODO: Make these variable per bank pub const LIQUIDATION_LIQUIDATOR_FEE: I80F48 = I80F48!(0.025); pub const LIQUIDATION_INSURANCE_FEE: I80F48 = I80F48!(0.025); @@ -153,3 +167,12 @@ pub const PROTOCOL_FEE_FIXED_DEFAULT: I80F48 = I80F48!(0.01); pub const MIN_PYTH_PUSH_VERIFICATION_LEVEL: VerificationLevel = VerificationLevel::Full; pub const PYTH_PUSH_PYTH_SPONSORED_SHARD_ID: u16 = 0; pub const PYTH_PUSH_MARGINFI_SPONSORED_SHARD_ID: u16 = 3301; + +/// A regular asset that can be comingled with any other regular asset or with `ASSET_TAG_SOL` +pub const ASSET_TAG_DEFAULT: u8 = 0; +/// Accounts with a SOL position can comingle with **either** `ASSET_TAG_DEFAULT` or +/// `ASSET_TAG_STAKED` positions, but not both +pub const ASSET_TAG_SOL: u8 = 1; +/// Staked SOL assets. Accounts with a STAKED position can only deposit other STAKED assets or SOL +/// (`ASSET_TAG_SOL`) and can only borrow SOL (`ASSET_TAG_SOL`) +pub const ASSET_TAG_STAKED: u8 = 2; diff --git a/programs/marginfi/src/errors.rs b/programs/marginfi/src/errors.rs index 98ff22aba..a12acc9c5 100644 --- a/programs/marginfi/src/errors.rs +++ b/programs/marginfi/src/errors.rs @@ -12,7 +12,7 @@ pub enum MarginfiError { BankAssetCapacityExceeded, #[msg("Invalid transfer")] // 6004 InvalidTransfer, - #[msg("Missing Pyth or Bank account")] // 6005 + #[msg("Missing Oracle, Bank, LST mint, or Sol Pool")] // 6005 MissingPythOrBankAccount, #[msg("Missing Pyth account")] // 6006 MissingPythAccount, @@ -86,18 +86,24 @@ pub enum MarginfiError { IllegalFlashloan, #[msg("Illegal flag")] // 6041 IllegalFlag, - #[msg("Illegal balance state")] // 6043 + #[msg("Illegal balance state")] // 6042 IllegalBalanceState, - #[msg("Illegal account authority transfer")] // 6044 + #[msg("Illegal account authority transfer")] // 6043 IllegalAccountAuthorityTransfer, - #[msg("Unauthorized")] // 6045 + #[msg("Unauthorized")] // 6044 Unauthorized, - #[msg("Invalid account authority")] // 6046 + #[msg("Invalid account authority")] // 6045 IllegalAction, - #[msg("Token22 Banks require mint account as first remaining account")] // 6047 + #[msg("Token22 Banks require mint account as first remaining account")] // 6046 T22MintRequired, - #[msg("Invalid ATA for global fee account")] // 6048 + #[msg("Invalid ATA for global fee account")] // 6047 InvalidFeeAta, + #[msg("Use add pool permissionless instead")] // 6048 + AddedStakedPoolManually, + #[msg("Staked SOL accounts can only deposit staked assets and borrow SOL")] // 6049 + AssetTagMismatch, + #[msg("Stake pool validation failed: check the stake pool, mint, or sol pool")] // 6050 + StakePoolValidationFailed, } impl From for ProgramError { diff --git a/programs/marginfi/src/instructions/marginfi_account/borrow.rs b/programs/marginfi/src/instructions/marginfi_account/borrow.rs index 3e20ffc2c..82a44615a 100644 --- a/programs/marginfi/src/instructions/marginfi_account/borrow.rs +++ b/programs/marginfi/src/instructions/marginfi_account/borrow.rs @@ -8,7 +8,7 @@ use crate::{ marginfi_account::{BankAccountWrapper, MarginfiAccount, RiskEngine, DISABLED_FLAG}, marginfi_group::{Bank, BankVaultType}, }, - utils, + utils::{self, validate_asset_tags}, }; use anchor_lang::prelude::*; use anchor_spl::token_interface::{TokenAccount, TokenInterface}; @@ -63,6 +63,8 @@ pub fn lending_account_borrow<'info>( { let mut bank = bank_loader.load_mut()?; + validate_asset_tags(&bank, &marginfi_account)?; + let liquidity_vault_authority_bump = bank.liquidity_vault_authority_bump; let origination_fee_rate: I80F48 = bank .config diff --git a/programs/marginfi/src/instructions/marginfi_account/deposit.rs b/programs/marginfi/src/instructions/marginfi_account/deposit.rs index 1f8cca936..6285eb623 100644 --- a/programs/marginfi/src/instructions/marginfi_account/deposit.rs +++ b/programs/marginfi/src/instructions/marginfi_account/deposit.rs @@ -7,7 +7,7 @@ use crate::{ marginfi_account::{BankAccountWrapper, MarginfiAccount, DISABLED_FLAG}, marginfi_group::Bank, }, - utils, + utils::{self, validate_asset_tags}, }; use anchor_lang::prelude::*; use anchor_spl::token_interface::TokenInterface; @@ -45,6 +45,8 @@ pub fn lending_account_deposit<'info>( let mut bank = bank_loader.load_mut()?; let mut marginfi_account = marginfi_account_loader.load_mut()?; + validate_asset_tags(&bank, &marginfi_account)?; + check!( !marginfi_account.get_flag(DISABLED_FLAG), MarginfiError::AccountDisabled diff --git a/programs/marginfi/src/instructions/marginfi_account/liquidate.rs b/programs/marginfi/src/instructions/marginfi_account/liquidate.rs index aeed8d5d5..8355a529c 100644 --- a/programs/marginfi/src/instructions/marginfi_account/liquidate.rs +++ b/programs/marginfi/src/instructions/marginfi_account/liquidate.rs @@ -5,6 +5,7 @@ use crate::events::{AccountEventHeader, LendingAccountLiquidateEvent, Liquidatio use crate::state::marginfi_account::{calc_amount, calc_value, RiskEngine}; use crate::state::marginfi_group::{Bank, BankVaultType}; use crate::state::price::{OraclePriceFeedAdapter, OraclePriceType, PriceAdapter, PriceBias}; +use crate::utils::{validate_asset_tags, validate_bank_asset_tags}; use crate::{ bank_signer, constants::{LIQUIDITY_VAULT_AUTHORITY_SEED, LIQUIDITY_VAULT_SEED}, @@ -91,6 +92,25 @@ pub fn lending_account_liquidate<'info>( "Asset and liability bank cannot be the same" ); + // Liquidators must repay debts in allowed asset types. A SOL debt can be repaid in any asset. A + // Staked Collateral debt must be repaid in SOL or staked collateral. A Default asset debt can + // be repaid in any Default asset or SOL. + { + let asset_bank = ctx.accounts.asset_bank.load()?; + let liab_bank = ctx.accounts.liab_bank.load()?; + validate_bank_asset_tags(&asset_bank, &liab_bank)?; + + // Sanity check user/liquidator accounts will not contain positions with mismatching tags + // after liquidation. + // * Note: user will be repaid in liab_bank + let user_acc = ctx.accounts.liquidatee_marginfi_account.load()?; + validate_asset_tags(&liab_bank, &user_acc)?; + // * Note: Liquidator repays liab bank, and is paid in asset_bank. + let liquidator_acc = ctx.accounts.liquidator_marginfi_account.load()?; + validate_asset_tags(&liab_bank, &liquidator_acc)?; + validate_asset_tags(&asset_bank, &liquidator_acc)?; + } // release immutable borrow of asset_bank/liab_bank + liquidatee/liquidator user accounts + let LendingAccountLiquidate { liquidator_marginfi_account: liquidator_marginfi_account_loader, liquidatee_marginfi_account: liquidatee_marginfi_account_loader, diff --git a/programs/marginfi/src/instructions/marginfi_group/add_pool.rs b/programs/marginfi/src/instructions/marginfi_group/add_pool.rs index ebd1f49bd..31e50c71a 100644 --- a/programs/marginfi/src/instructions/marginfi_group/add_pool.rs +++ b/programs/marginfi/src/instructions/marginfi_group/add_pool.rs @@ -1,14 +1,16 @@ use crate::{ + check, constants::{ - FEE_STATE_SEED, FEE_VAULT_AUTHORITY_SEED, FEE_VAULT_SEED, INSURANCE_VAULT_AUTHORITY_SEED, - INSURANCE_VAULT_SEED, LIQUIDITY_VAULT_AUTHORITY_SEED, LIQUIDITY_VAULT_SEED, + ASSET_TAG_STAKED, FEE_STATE_SEED, FEE_VAULT_AUTHORITY_SEED, FEE_VAULT_SEED, + INSURANCE_VAULT_AUTHORITY_SEED, INSURANCE_VAULT_SEED, LIQUIDITY_VAULT_AUTHORITY_SEED, + LIQUIDITY_VAULT_SEED, }, events::{GroupEventHeader, LendingPoolBankCreateEvent}, state::{ fee_state::FeeState, marginfi_group::{Bank, BankConfig, BankConfigCompact, MarginfiGroup}, }, - MarginfiResult, + MarginfiError, MarginfiResult, }; use anchor_lang::prelude::*; use anchor_spl::token_interface::*; @@ -42,6 +44,10 @@ pub fn lending_pool_add_bank( } = ctx.accounts; let mut bank = bank_loader.load_init()?; + check!( + bank_config.asset_tag != ASSET_TAG_STAKED, + MarginfiError::AddedStakedPoolManually + ); let liquidity_vault_bump = ctx.bumps.liquidity_vault; let liquidity_vault_authority_bump: u8 = ctx.bumps.liquidity_vault_authority; @@ -68,7 +74,8 @@ pub fn lending_pool_add_bank( ); bank.config.validate()?; - bank.config.validate_oracle_setup(ctx.remaining_accounts)?; + bank.config + .validate_oracle_setup(ctx.remaining_accounts, None, None, None)?; emit!(LendingPoolBankCreateEvent { header: GroupEventHeader { diff --git a/programs/marginfi/src/instructions/marginfi_group/add_pool_permissionless.rs b/programs/marginfi/src/instructions/marginfi_group/add_pool_permissionless.rs new file mode 100644 index 000000000..51c1b9cce --- /dev/null +++ b/programs/marginfi/src/instructions/marginfi_group/add_pool_permissionless.rs @@ -0,0 +1,244 @@ +// Adds a ASSET_TAG_STAKED type bank to a group with sane defaults. Used by validators to add their +// stake pool to a group so users can borrow SOL against it +use crate::{ + check, + constants::{ + ASSET_TAG_STAKED, FEE_VAULT_AUTHORITY_SEED, FEE_VAULT_SEED, INSURANCE_VAULT_AUTHORITY_SEED, + INSURANCE_VAULT_SEED, LIQUIDITY_VAULT_AUTHORITY_SEED, LIQUIDITY_VAULT_SEED, + SPL_SINGLE_POOL_ID, + }, + events::{GroupEventHeader, LendingPoolBankCreateEvent}, + state::{ + marginfi_group::{ + Bank, BankConfigCompact, BankOperationalState, InterestRateConfig, MarginfiGroup, + }, + price::OracleSetup, + staked_settings::StakedSettings, + }, + MarginfiError, MarginfiResult, +}; +use anchor_lang::prelude::*; +use anchor_spl::token_interface::*; +use fixed_macro::types::I80F48; + +pub fn lending_pool_add_bank_permissionless( + ctx: Context, + _bank_seed: u64, +) -> MarginfiResult { + let LendingPoolAddBankPermissionless { + bank_mint, + liquidity_vault, + insurance_vault, + fee_vault, + bank: bank_loader, + stake_pool, + sol_pool, + .. + } = ctx.accounts; + + let mut bank = bank_loader.load_init()?; + let settings = ctx.accounts.staked_settings.load()?; + let group = ctx.accounts.marginfi_group.load()?; + + let liquidity_vault_bump = ctx.bumps.liquidity_vault; + let liquidity_vault_authority_bump = ctx.bumps.liquidity_vault_authority; + let insurance_vault_bump = ctx.bumps.insurance_vault; + let insurance_vault_authority_bump = ctx.bumps.insurance_vault_authority; + let fee_vault_bump = ctx.bumps.fee_vault; + let fee_vault_authority_bump = ctx.bumps.fee_vault_authority; + + // These are placeholder values: staked collateral positions do not support borrowing and likely + // never will, thus they will earn no interest. + + // Note: Some placeholder values are non-zero to handle downstream validation checks. + let default_ir_config = InterestRateConfig { + optimal_utilization_rate: I80F48!(0.4).into(), + plateau_interest_rate: I80F48!(0.4).into(), + protocol_fixed_fee_apr: I80F48!(0.01).into(), + max_interest_rate: I80F48!(3).into(), + insurance_ir_fee: I80F48!(0.1).into(), + ..Default::default() + }; + + let default_config: BankConfigCompact = BankConfigCompact { + asset_weight_init: settings.asset_weight_init, + asset_weight_maint: settings.asset_weight_maint, + liability_weight_init: I80F48!(1.5).into(), // placeholder + liability_weight_maint: I80F48!(1.25).into(), // placeholder + deposit_limit: settings.deposit_limit, + interest_rate_config: default_ir_config.into(), // placeholder + operational_state: BankOperationalState::Operational, + oracle_setup: OracleSetup::StakedWithPythPush, + oracle_key: settings.oracle, // becomes config.oracle_keys[0] + borrow_limit: 0, + risk_tier: settings.risk_tier, + asset_tag: ASSET_TAG_STAKED, + _pad0: [0; 6], + total_asset_value_init_limit: settings.total_asset_value_init_limit, + oracle_max_age: settings.oracle_max_age, + }; + + *bank = Bank::new( + ctx.accounts.marginfi_group.key(), + default_config.into(), + bank_mint.key(), + bank_mint.decimals, + liquidity_vault.key(), + insurance_vault.key(), + fee_vault.key(), + Clock::get().unwrap().unix_timestamp, + liquidity_vault_bump, + liquidity_vault_authority_bump, + insurance_vault_bump, + insurance_vault_authority_bump, + fee_vault_bump, + fee_vault_authority_bump, + ); + + bank.config.validate()?; + + check!( + stake_pool.owner == &SPL_SINGLE_POOL_ID, + MarginfiError::StakePoolValidationFailed + ); + let lst_mint = bank_mint.key(); + let stake_pool = stake_pool.key(); + let sol_pool = sol_pool.key(); + // The mint (for supply) and stake pool (for sol balance) are recorded for price calculation + bank.config.oracle_keys[1] = lst_mint; + bank.config.oracle_keys[2] = sol_pool; + bank.config.validate_oracle_setup( + ctx.remaining_accounts, + Some(lst_mint), + Some(stake_pool), + Some(sol_pool), + )?; + + emit!(LendingPoolBankCreateEvent { + header: GroupEventHeader { + marginfi_group: ctx.accounts.marginfi_group.key(), + signer: Some(group.admin) + }, + bank: bank_loader.key(), + mint: bank_mint.key(), + }); + + Ok(()) +} + +#[derive(Accounts)] +#[instruction(bank_seed: u64)] +pub struct LendingPoolAddBankPermissionless<'info> { + pub marginfi_group: AccountLoader<'info, MarginfiGroup>, + + #[account( + has_one = marginfi_group + )] + pub staked_settings: AccountLoader<'info, StakedSettings>, + + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// Mint of the spl-single-pool LST (a PDA derived from `stake_pool`) + /// + /// CHECK: passing a mint here that is not actually a staked collateral LST is not possible + /// because the sol_pool and stake_pool will not derive to a valid PDA which is also owned by + /// the staking program and spl-single-pool program. + pub bank_mint: Box>, + + /// CHECK: Validated using `stake_pool` + pub sol_pool: AccountInfo<'info>, + + /// CHECK: We validate this is correct backwards, by deriving the PDA of the `bank_mint` using + /// this key. + /// + /// If derives the same `bank_mint`, then this must be the correct stake pool for that mint, and + /// we can subsequently use it to validate the `sol_pool` + pub stake_pool: AccountInfo<'info>, + + #[account( + init, + space = 8 + std::mem::size_of::(), + payer = fee_payer, + seeds = [ + marginfi_group.key().as_ref(), + bank_mint.key().as_ref(), + &bank_seed.to_le_bytes(), + ], + bump, + )] + pub bank: AccountLoader<'info, Bank>, + + /// CHECK: ⋐ ͡⋄ ω ͡⋄ ⋑ + #[account( + seeds = [ + LIQUIDITY_VAULT_AUTHORITY_SEED.as_bytes(), + bank.key().as_ref(), + ], + bump + )] + pub liquidity_vault_authority: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + token::mint = bank_mint, + token::authority = liquidity_vault_authority, + seeds = [ + LIQUIDITY_VAULT_SEED.as_bytes(), + bank.key().as_ref(), + ], + bump, + )] + pub liquidity_vault: Box>, + + /// CHECK: ⋐ ͡⋄ ω ͡⋄ ⋑ + #[account( + seeds = [ + INSURANCE_VAULT_AUTHORITY_SEED.as_bytes(), + bank.key().as_ref(), + ], + bump + )] + pub insurance_vault_authority: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + token::mint = bank_mint, + token::authority = insurance_vault_authority, + seeds = [ + INSURANCE_VAULT_SEED.as_bytes(), + bank.key().as_ref(), + ], + bump, + )] + pub insurance_vault: Box>, + + /// CHECK: ⋐ ͡⋄ ω ͡⋄ ⋑ + #[account( + seeds = [ + FEE_VAULT_AUTHORITY_SEED.as_bytes(), + bank.key().as_ref(), + ], + bump + )] + pub fee_vault_authority: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + token::mint = bank_mint, + token::authority = fee_vault_authority, + seeds = [ + FEE_VAULT_SEED.as_bytes(), + bank.key().as_ref(), + ], + bump, + )] + pub fee_vault: Box>, + + pub rent: Sysvar<'info, Rent>, + pub token_program: Interface<'info, TokenInterface>, + pub system_program: Program<'info, System>, +} diff --git a/programs/marginfi/src/instructions/marginfi_group/add_pool_with_seed.rs b/programs/marginfi/src/instructions/marginfi_group/add_pool_with_seed.rs index a0bcee795..3cb3d8f6a 100644 --- a/programs/marginfi/src/instructions/marginfi_group/add_pool_with_seed.rs +++ b/programs/marginfi/src/instructions/marginfi_group/add_pool_with_seed.rs @@ -1,14 +1,16 @@ use crate::{ + check, constants::{ - FEE_STATE_SEED, FEE_VAULT_AUTHORITY_SEED, FEE_VAULT_SEED, INSURANCE_VAULT_AUTHORITY_SEED, - INSURANCE_VAULT_SEED, LIQUIDITY_VAULT_AUTHORITY_SEED, LIQUIDITY_VAULT_SEED, + ASSET_TAG_STAKED, FEE_STATE_SEED, FEE_VAULT_AUTHORITY_SEED, FEE_VAULT_SEED, + INSURANCE_VAULT_AUTHORITY_SEED, INSURANCE_VAULT_SEED, LIQUIDITY_VAULT_AUTHORITY_SEED, + LIQUIDITY_VAULT_SEED, }, events::{GroupEventHeader, LendingPoolBankCreateEvent}, state::{ fee_state::FeeState, marginfi_group::{Bank, BankConfig, BankConfigCompact, MarginfiGroup}, }, - MarginfiResult, + MarginfiError, MarginfiResult, }; use anchor_lang::prelude::*; use anchor_spl::token_interface::*; @@ -42,6 +44,10 @@ pub fn lending_pool_add_bank_with_seed( } = ctx.accounts; let mut bank = bank_loader.load_init()?; + check!( + bank_config.asset_tag != ASSET_TAG_STAKED, + MarginfiError::AddedStakedPoolManually + ); let liquidity_vault_bump = ctx.bumps.liquidity_vault; let liquidity_vault_authority_bump = ctx.bumps.liquidity_vault_authority; @@ -68,7 +74,8 @@ pub fn lending_pool_add_bank_with_seed( ); bank.config.validate()?; - bank.config.validate_oracle_setup(ctx.remaining_accounts)?; + bank.config + .validate_oracle_setup(ctx.remaining_accounts, None, None, None)?; emit!(LendingPoolBankCreateEvent { header: GroupEventHeader { diff --git a/programs/marginfi/src/instructions/marginfi_group/configure.rs b/programs/marginfi/src/instructions/marginfi_group/configure.rs index e4f6e2b94..97099f1df 100644 --- a/programs/marginfi/src/instructions/marginfi_group/configure.rs +++ b/programs/marginfi/src/instructions/marginfi_group/configure.rs @@ -35,6 +35,7 @@ pub struct MarginfiGroupConfigure<'info> { pub marginfi_group: AccountLoader<'info, MarginfiGroup>, #[account( + // TODO moving to `marginfi_group` as `has_one` adds a mystery signer? address = marginfi_group.load()?.admin, )] pub admin: Signer<'info>, diff --git a/programs/marginfi/src/instructions/marginfi_group/configure_bank.rs b/programs/marginfi/src/instructions/marginfi_group/configure_bank.rs index 600241e5e..dd6ad5492 100644 --- a/programs/marginfi/src/instructions/marginfi_group/configure_bank.rs +++ b/programs/marginfi/src/instructions/marginfi_group/configure_bank.rs @@ -38,7 +38,8 @@ pub fn lending_pool_configure_bank( bank.configure(&bank_config)?; if bank_config.oracle.is_some() { - bank.config.validate_oracle_setup(ctx.remaining_accounts)?; + bank.config + .validate_oracle_setup(ctx.remaining_accounts, None, None, None)?; } emit!(LendingPoolBankConfigureEvent { @@ -157,6 +158,8 @@ pub struct LendingPoolSetupEmissions<'info> { )] pub emissions_token_account: Box>, + /// NOTE: This is a TokenAccount, spl transfer will validate it. + /// /// CHECK: Account provided only for funding rewards #[account(mut)] pub emissions_funding_account: AccountInfo<'info>, diff --git a/programs/marginfi/src/instructions/marginfi_group/edit_stake_settings.rs b/programs/marginfi/src/instructions/marginfi_group/edit_stake_settings.rs new file mode 100644 index 000000000..c4a6f97f1 --- /dev/null +++ b/programs/marginfi/src/instructions/marginfi_group/edit_stake_settings.rs @@ -0,0 +1,69 @@ +// Used by the group admin to edit the default features of staked collateral banks. Remember to +// propagate afterwards. +use crate::state::marginfi_group::{RiskTier, WrappedI80F48}; +use crate::state::staked_settings::StakedSettings; +use crate::{set_if_some, MarginfiGroup}; +use anchor_lang::prelude::*; + +pub fn edit_staked_settings( + ctx: Context, + settings: StakedSettingsEditConfig, +) -> Result<()> { + // let group = ctx.accounts.marginfi_group.load()?; + let mut staked_settings = ctx.accounts.staked_settings.load_mut()?; + // require_keys_eq!(group.admin, ctx.accounts.admin.key()); + + set_if_some!(staked_settings.oracle, settings.oracle); + set_if_some!( + staked_settings.asset_weight_init, + settings.asset_weight_init + ); + set_if_some!( + staked_settings.asset_weight_maint, + settings.asset_weight_maint + ); + set_if_some!(staked_settings.deposit_limit, settings.deposit_limit); + set_if_some!( + staked_settings.total_asset_value_init_limit, + settings.total_asset_value_init_limit + ); + set_if_some!(staked_settings.oracle_max_age, settings.oracle_max_age); + set_if_some!(staked_settings.risk_tier, settings.risk_tier); + + staked_settings.validate()?; + + Ok(()) +} + +#[derive(Accounts)] +pub struct EditStakedSettings<'info> { + #[account( + has_one = admin + )] + pub marginfi_group: AccountLoader<'info, MarginfiGroup>, + + pub admin: Signer<'info>, + + #[account( + mut, + has_one = marginfi_group + )] + pub staked_settings: AccountLoader<'info, StakedSettings>, +} + +#[derive(AnchorDeserialize, AnchorSerialize, Default)] +pub struct StakedSettingsEditConfig { + pub oracle: Option, + + pub asset_weight_init: Option, + pub asset_weight_maint: Option, + + pub deposit_limit: Option, + pub total_asset_value_init_limit: Option, + + pub oracle_max_age: Option, + /// WARN: You almost certainly want "Collateral", using Isolated risk tier makes the asset + /// worthless as collateral, making all outstanding accounts eligible to be liquidated, and is + /// generally useful only when creating a staked collateral pool for rewards purposes only. + pub risk_tier: Option, +} diff --git a/programs/marginfi/src/instructions/marginfi_group/init_staked_settings.rs b/programs/marginfi/src/instructions/marginfi_group/init_staked_settings.rs new file mode 100644 index 000000000..35966ccb5 --- /dev/null +++ b/programs/marginfi/src/instructions/marginfi_group/init_staked_settings.rs @@ -0,0 +1,75 @@ +// Used by the group admin to enable staked collateral banks and configure their default features +use crate::constants::STAKED_SETTINGS_SEED; +use crate::state::marginfi_group::{RiskTier, WrappedI80F48}; +use crate::state::staked_settings::StakedSettings; +use crate::MarginfiGroup; +use anchor_lang::prelude::*; + +pub fn initialize_staked_settings( + ctx: Context, + settings: StakedSettingsConfig, +) -> Result<()> { + let mut staked_settings = ctx.accounts.staked_settings.load_init()?; + + *staked_settings = StakedSettings::new( + ctx.accounts.staked_settings.key(), + ctx.accounts.marginfi_group.key(), + settings.oracle, + settings.asset_weight_init, + settings.asset_weight_maint, + settings.deposit_limit, + settings.total_asset_value_init_limit, + settings.oracle_max_age, + settings.risk_tier, + ); + + staked_settings.validate()?; + + Ok(()) +} + +#[derive(Accounts)] +pub struct InitStakedSettings<'info> { + #[account( + has_one = admin + )] + pub marginfi_group: AccountLoader<'info, MarginfiGroup>, + + pub admin: Signer<'info>, + + /// Pays the init fee + #[account(mut)] + pub fee_payer: Signer<'info>, + + #[account( + init, + seeds = [ + STAKED_SETTINGS_SEED.as_bytes(), + marginfi_group.key().as_ref() + ], + bump, + payer = fee_payer, + space = 8 + StakedSettings::LEN, + )] + pub staked_settings: AccountLoader<'info, StakedSettings>, + + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} + +#[derive(AnchorDeserialize, AnchorSerialize, Default)] +pub struct StakedSettingsConfig { + pub oracle: Pubkey, + + pub asset_weight_init: WrappedI80F48, + pub asset_weight_maint: WrappedI80F48, + + pub deposit_limit: u64, + pub total_asset_value_init_limit: u64, + + pub oracle_max_age: u16, + /// WARN: You almost certainly want "Collateral", using Isolated risk tier makes the asset + /// worthless as collateral, and is generally useful only when creating a staked collateral pool + /// for rewards purposes only. + pub risk_tier: RiskTier, +} diff --git a/programs/marginfi/src/instructions/marginfi_group/mod.rs b/programs/marginfi/src/instructions/marginfi_group/mod.rs index 47ae9d15e..88a9f2d23 100644 --- a/programs/marginfi/src/instructions/marginfi_group/mod.rs +++ b/programs/marginfi/src/instructions/marginfi_group/mod.rs @@ -1,25 +1,33 @@ mod accrue_bank_interest; mod add_pool; +mod add_pool_permissionless; mod add_pool_with_seed; mod collect_bank_fees; mod config_group_fee; mod configure; mod configure_bank; mod edit_global_fee; +mod edit_stake_settings; mod handle_bankruptcy; mod init_global_fee_state; +mod init_staked_settings; mod initialize; mod propagate_fee_state; +mod propagate_staked_settings; pub use accrue_bank_interest::*; pub use add_pool::*; +pub use add_pool_permissionless::*; pub use add_pool_with_seed::*; pub use collect_bank_fees::*; pub use config_group_fee::*; pub use configure::*; pub use configure_bank::*; pub use edit_global_fee::*; +pub use edit_stake_settings::*; pub use handle_bankruptcy::*; pub use init_global_fee_state::*; +pub use init_staked_settings::*; pub use initialize::*; pub use propagate_fee_state::*; +pub use propagate_staked_settings::*; diff --git a/programs/marginfi/src/instructions/marginfi_group/propagate_staked_settings.rs b/programs/marginfi/src/instructions/marginfi_group/propagate_staked_settings.rs new file mode 100644 index 000000000..d9515bef1 --- /dev/null +++ b/programs/marginfi/src/instructions/marginfi_group/propagate_staked_settings.rs @@ -0,0 +1,50 @@ +use crate::constants::ASSET_TAG_STAKED; +// Permissionless ix to propagate a group's staked collateral settings to any bank in that group +use crate::state::marginfi_group::Bank; +use crate::state::staked_settings::StakedSettings; +use crate::MarginfiGroup; +use anchor_lang::prelude::*; + +pub fn propagate_staked_settings(ctx: Context) -> Result<()> { + let settings = ctx.accounts.staked_settings.load()?; + let mut bank = ctx.accounts.bank.load_mut()?; + + // Only validate the oracle if it has changed + if settings.oracle != bank.config.oracle_keys[0] { + bank.config + .validate_oracle_setup(ctx.remaining_accounts, None, None, None)?; + } + + bank.config.oracle_keys[0] = settings.oracle; + bank.config.asset_weight_init = settings.asset_weight_init; + bank.config.asset_weight_maint = settings.asset_weight_maint; + bank.config.deposit_limit = settings.deposit_limit; + bank.config.total_asset_value_init_limit = settings.total_asset_value_init_limit; + bank.config.oracle_max_age = settings.oracle_max_age; + bank.config.risk_tier = settings.risk_tier; + + bank.config.validate()?; + // ...Possibly emit event. + + Ok(()) +} + +#[derive(Accounts)] +pub struct PropagateStakedSettings<'info> { + pub marginfi_group: AccountLoader<'info, MarginfiGroup>, + + #[account( + has_one = marginfi_group + )] + pub staked_settings: AccountLoader<'info, StakedSettings>, + + #[account( + mut, + constraint = { + let bank = bank.load()?; + bank.group == marginfi_group.key() && + bank.config.asset_tag == ASSET_TAG_STAKED + } + )] + pub bank: AccountLoader<'info, Bank>, +} diff --git a/programs/marginfi/src/lib.rs b/programs/marginfi/src/lib.rs index 5f3a76dfd..7ba7c8023 100644 --- a/programs/marginfi/src/lib.rs +++ b/programs/marginfi/src/lib.rs @@ -58,6 +58,13 @@ pub mod marginfi { marginfi_group::lending_pool_add_bank_with_seed(ctx, bank_config.into(), bank_seed) } + pub fn lending_pool_add_bank_permissionless( + ctx: Context, + bank_seed: u64, + ) -> MarginfiResult { + marginfi_group::lending_pool_add_bank_permissionless(ctx, bank_seed) + } + pub fn lending_pool_configure_bank( ctx: Context, bank_config_opt: BankConfigOpt, @@ -264,6 +271,28 @@ pub mod marginfi { pub fn config_group_fee(ctx: Context, flag: u64) -> MarginfiResult { marginfi_group::config_group_fee(ctx, flag) } + + /// (group admin only) Init the Staked Settings account, which is used to create staked + /// collateral banks, and must run before any staked collateral bank can be created with + /// `add_pool_permissionless`. Running this ix effectively opts the group into the staked + /// collateral feature. + pub fn init_staked_settings( + ctx: Context, + settings: StakedSettingsConfig, + ) -> MarginfiResult { + marginfi_group::initialize_staked_settings(ctx, settings) + } + + pub fn edit_staked_settings( + ctx: Context, + settings: StakedSettingsEditConfig, + ) -> MarginfiResult { + marginfi_group::edit_staked_settings(ctx, settings) + } + + pub fn propagate_staked_settings(ctx: Context) -> MarginfiResult { + marginfi_group::propagate_staked_settings(ctx) + } } #[cfg(not(feature = "no-entrypoint"))] diff --git a/programs/marginfi/src/macros.rs b/programs/marginfi/src/macros.rs index 593dfa2af..e942eaa6b 100644 --- a/programs/marginfi/src/macros.rs +++ b/programs/marginfi/src/macros.rs @@ -107,3 +107,14 @@ macro_rules! assert_struct_align { static_assertions::const_assert_eq!(std::mem::align_of::<$struct>(), $align); }; } + +#[macro_export] +macro_rules! live { + () => { + cfg!(any( + feature = "mainnet-beta", + feature = "staging", + feature = "devnet" + )) + }; +} diff --git a/programs/marginfi/src/state/marginfi_account.rs b/programs/marginfi/src/state/marginfi_account.rs index ea3944d71..460a69c74 100644 --- a/programs/marginfi/src/state/marginfi_account.rs +++ b/programs/marginfi/src/state/marginfi_account.rs @@ -5,9 +5,9 @@ use super::{ use crate::{ assert_struct_align, assert_struct_size, check, constants::{ - BANKRUPT_THRESHOLD, EMISSIONS_FLAG_BORROW_ACTIVE, EMISSIONS_FLAG_LENDING_ACTIVE, - EMPTY_BALANCE_THRESHOLD, EXP_10_I80F48, MIN_EMISSIONS_START_TIME, SECONDS_PER_YEAR, - ZERO_AMOUNT_THRESHOLD, + ASSET_TAG_DEFAULT, ASSET_TAG_STAKED, BANKRUPT_THRESHOLD, EMISSIONS_FLAG_BORROW_ACTIVE, + EMISSIONS_FLAG_LENDING_ACTIVE, EMPTY_BALANCE_THRESHOLD, EXP_10_I80F48, + MIN_EMISSIONS_START_TIME, SECONDS_PER_YEAR, ZERO_AMOUNT_THRESHOLD, }, debug, math_error, prelude::{MarginfiError, MarginfiResult}, @@ -41,6 +41,9 @@ pub struct MarginfiAccount { /// Flags: /// - DISABLED_FLAG = 1 << 0 = 1 - This flag indicates that the account is disabled, /// and no further actions can be taken on it. + /// - IN_FLASHLOAN_FLAG (1 << 1) + /// - FLASHLOAN_ENABLED_FLAG (1 << 2) + /// - TRANSFER_AUTHORITY_ALLOWED_FLAG (1 << 3) pub account_flags: u64, // 8 pub _padding: [u64; 63], // 504 } @@ -172,41 +175,58 @@ impl<'info> BankAccountWithPriceFeed<'_, 'info> { .filter(|balance| balance.active) .collect::>(); - debug!("Expecting {} remaining accounts", active_balances.len() * 2); + let expected_accounts = active_balances + .iter() + .map(|balance| { + if balance.bank_asset_tag == ASSET_TAG_STAKED { + 4 + } else { + 2 + } + }) + .sum::(); + + debug!("Expecting {} remaining accounts", expected_accounts); debug!("Got {} remaining accounts", remaining_ais.len()); check!( - active_balances.len() * 2 <= remaining_ais.len(), + expected_accounts <= remaining_ais.len(), MarginfiError::MissingPythOrBankAccount ); let clock = Clock::get()?; + let mut account_index = 0; active_balances .iter() - .enumerate() - .map(|(i, balance)| { - let bank_index = i * 2; - let oracle_ai_idx = bank_index + 1; - - let bank_ai = remaining_ais.get(bank_index).unwrap(); + .map(|balance| { + // Determine number of accounts to process for this balance + let num_accounts = if balance.bank_asset_tag == ASSET_TAG_STAKED { + 4 + } else { + 2 + }; + // Get the bank + let bank_ai = remaining_ais.get(account_index).unwrap(); check!( balance.bank_pk.eq(bank_ai.key), MarginfiError::InvalidBankAccount ); + let bank_al = AccountLoader::::try_from(bank_ai)?; + let bank = bank_al.load()?; - let price_adapter = { - let oracle_ais = &remaining_ais[oracle_ai_idx..oracle_ai_idx + 1]; - let bank_al = AccountLoader::::try_from(bank_ai)?; - let bank = bank_al.load()?; + // Get the oracle, and the LST mint and sol pool if applicable (staked only) + let oracle_ai_idx = account_index + 1; + let oracle_ais = &remaining_ais[oracle_ai_idx..oracle_ai_idx + num_accounts - 1]; - Box::new(OraclePriceFeedAdapter::try_from_bank_config( - &bank.config, - oracle_ais, - &clock, - )) - }; + let price_adapter = Box::new(OraclePriceFeedAdapter::try_from_bank_config( + &bank.config, + oracle_ais, + &clock, + )); + + account_index += num_accounts; Ok(BankAccountWithPriceFeed { bank: bank_ai.clone(), @@ -320,6 +340,8 @@ impl<'info> BankAccountWithPriceFeed<'_, 'info> { Some(PriceBias::High), )?; + // If `ASSET_TAG_STAKED` assets can ever be borrowed, accomodate for that here... + calc_value( bank.get_liability_amount(self.balance.liability_shares.into())?, higher_price, @@ -750,7 +772,10 @@ assert_struct_align!(Balance, 8); pub struct Balance { pub active: bool, pub bank_pk: Pubkey, - pub _pad0: [u8; 7], + /// Inherited from the bank when the position is first created and CANNOT BE CHANGED after that. + /// Note that all balances created before the addition of this feature use `ASSET_TAG_DEFAULT` + pub bank_asset_tag: u8, + pub _pad0: [u8; 6], pub asset_shares: WrappedI80F48, pub liability_shares: WrappedI80F48, pub emissions_outstanding: WrappedI80F48, @@ -822,7 +847,8 @@ impl Balance { Balance { active: false, bank_pk: Pubkey::default(), - _pad0: [0; 7], + bank_asset_tag: ASSET_TAG_DEFAULT, + _pad0: [0; 6], asset_shares: WrappedI80F48::from(I80F48::ZERO), liability_shares: WrappedI80F48::from(I80F48::ZERO), emissions_outstanding: WrappedI80F48::from(I80F48::ZERO), @@ -882,7 +908,8 @@ impl<'a> BankAccountWrapper<'a> { lending_account.balances[empty_index] = Balance { active: true, bank_pk: *bank_pk, - _pad0: [0; 7], + bank_asset_tag: bank.config.asset_tag, + _pad0: [0; 6], asset_shares: I80F48::ZERO.into(), liability_shares: I80F48::ZERO.into(), emissions_outstanding: I80F48::ZERO.into(), @@ -1412,7 +1439,8 @@ mod test { balances: [Balance { active: true, bank_pk: bank_pk.into(), - _pad0: [0; 7], + bank_asset_tag: ASSET_TAG_DEFAULT, + _pad0: [0; 6], asset_shares: WrappedI80F48::default(), liability_shares: WrappedI80F48::default(), emissions_outstanding: WrappedI80F48::default(), diff --git a/programs/marginfi/src/state/marginfi_group.rs b/programs/marginfi/src/state/marginfi_group.rs index 146a7f39e..579766e06 100644 --- a/programs/marginfi/src/state/marginfi_group.rs +++ b/programs/marginfi/src/state/marginfi_group.rs @@ -21,6 +21,7 @@ use crate::{ }; use crate::{ borsh::{BorshDeserialize, BorshSerialize}, + constants::ASSET_TAG_DEFAULT, constants::FREEZE_SETTINGS, }; use anchor_lang::prelude::borsh; @@ -544,6 +545,7 @@ impl Bank { emissions_rate: 0, emissions_remaining: I80F48::ZERO.into(), emissions_mint: Pubkey::default(), + collected_program_fees_outstanding: I80F48::ZERO.into(), ..Default::default() } } @@ -702,6 +704,8 @@ impl Bank { set_if_some!(self.config.risk_tier, config.risk_tier); + set_if_some!(self.config.asset_tag, config.asset_tag); + set_if_some!( self.config.total_asset_value_init_limit, config.total_asset_value_init_limit @@ -1168,18 +1172,22 @@ impl Display for BankOperationalState { } #[repr(u8)] -#[derive(Copy, Clone, Debug, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, AnchorSerialize, AnchorDeserialize, PartialEq, Eq, Default)] pub enum RiskTier { - Collateral, + #[default] + Collateral = 0, /// ## Isolated Risk /// Assets in this trance can be borrowed only in isolation. /// They can't be borrowed together with other assets. /// /// For example, if users has USDC, and wants to borrow XYZ which is isolated, /// they can't borrow XYZ together with SOL, only XYZ alone. - Isolated, + Isolated = 1, } +unsafe impl Zeroable for RiskTier {} +unsafe impl Pod for RiskTier {} + #[repr(C)] #[cfg_attr( any(feature = "test", feature = "client"), @@ -1206,7 +1214,17 @@ pub struct BankConfigCompact { pub risk_tier: RiskTier, - pub _pad0: [u8; 7], + /// Determines what kinds of assets users of this bank can interact with. + /// Options: + /// * ASSET_TAG_DEFAULT (0) - A regular asset that can be comingled with any other regular asset + /// or with `ASSET_TAG_SOL` + /// * ASSET_TAG_SOL (1) - Accounts with a SOL position can comingle with **either** + /// `ASSET_TAG_DEFAULT` or `ASSET_TAG_STAKED` positions, but not both + /// * ASSET_TAG_STAKED (2) - Staked SOL assets. Accounts with a STAKED position can only deposit + /// other STAKED assets or SOL (`ASSET_TAG_SOL`) and can only borrow SOL + pub asset_tag: u8, + + pub _pad0: [u8; 6], /// USD denominated limit for calculating asset value for initialization margin requirements. /// Example, if total SOL deposits are equal to $1M and the limit it set to $500K, @@ -1244,7 +1262,8 @@ impl From for BankConfig { _pad0: [0; 6], borrow_limit: config.borrow_limit, risk_tier: config.risk_tier, - _pad1: [0; 7], + asset_tag: config.asset_tag, + _pad1: [0; 6], total_asset_value_init_limit: config.total_asset_value_init_limit, oracle_max_age: config.oracle_max_age, _padding: [0; 38], @@ -1266,7 +1285,8 @@ impl From for BankConfigCompact { oracle_key: config.oracle_keys[0], borrow_limit: config.borrow_limit, risk_tier: config.risk_tier, - _pad0: [0; 7], + asset_tag: config.asset_tag, + _pad0: [0; 6], total_asset_value_init_limit: config.total_asset_value_init_limit, oracle_max_age: config.oracle_max_age, } @@ -1305,7 +1325,17 @@ pub struct BankConfig { pub risk_tier: RiskTier, - pub _pad1: [u8; 7], + /// Determines what kinds of assets users of this bank can interact with. + /// Options: + /// * ASSET_TAG_DEFAULT (0) - A regular asset that can be comingled with any other regular asset + /// or with `ASSET_TAG_SOL` + /// * ASSET_TAG_SOL (1) - Accounts with a SOL position can comingle with **either** + /// `ASSET_TAG_DEFAULT` or `ASSET_TAG_STAKED` positions, but not both + /// * ASSET_TAG_STAKED (2) - Staked SOL assets. Accounts with a STAKED position can only deposit + /// other STAKED assets or SOL (`ASSET_TAG_SOL`) and can only borrow SOL + pub asset_tag: u8, + + pub _pad1: [u8; 6], /// USD denominated limit for calculating asset value for initialization margin requirements. /// Example, if total SOL deposits are equal to $1M and the limit it set to $500K, @@ -1320,6 +1350,7 @@ pub struct BankConfig { /// Time window in seconds for the oracle price feed to be considered live. pub oracle_max_age: u16, + // Note: 6 bytes of padding to next 8 byte alignment, then end padding pub _padding: [u8; 38], } @@ -1338,7 +1369,8 @@ impl Default for BankConfig { oracle_keys: [Pubkey::default(); MAX_ORACLE_KEYS], _pad0: [0; 6], risk_tier: RiskTier::Isolated, - _pad1: [0; 7], + asset_tag: ASSET_TAG_DEFAULT, + _pad1: [0; 6], total_asset_value_init_limit: TOTAL_ASSET_VALUE_INIT_LIMIT_INACTIVE, oracle_max_age: 0, _padding: [0; 38], @@ -1420,12 +1452,21 @@ impl BankConfig { self.borrow_limit != u64::MAX } - pub fn validate_oracle_setup(&self, ais: &[AccountInfo]) -> MarginfiResult { + /// * lst_mint, stake_pool, sol_pool - required only if configuring + /// `OracleSetup::StakedWithPythPush` on initial setup. If configuring a staked bank after + /// initial setup, can be omitted + pub fn validate_oracle_setup( + &self, + ais: &[AccountInfo], + lst_mint: Option, + stake_pool: Option, + sol_pool: Option, + ) -> MarginfiResult { check!( self.oracle_max_age >= ORACLE_MIN_AGE, MarginfiError::InvalidOracleSetup ); - OraclePriceFeedAdapter::validate_bank_config(self, ais)?; + OraclePriceFeedAdapter::validate_bank_config(self, ais, lst_mint, stake_pool, sol_pool)?; Ok(()) } @@ -1443,7 +1484,10 @@ impl BankConfig { } pub fn get_pyth_push_oracle_feed_id(&self) -> Option<&FeedId> { - if matches!(self.oracle_setup, OracleSetup::PythPushOracle) { + if matches!( + self.oracle_setup, + OracleSetup::PythPushOracle | OracleSetup::StakedWithPythPush + ) { let bytes: &[u8; 32] = self.oracle_keys[0].as_ref().try_into().unwrap(); Some(bytes) } else { @@ -1511,6 +1555,8 @@ pub struct BankConfigOpt { pub risk_tier: Option, + pub asset_tag: Option, + pub total_asset_value_init_limit: Option, pub oracle_max_age: Option, diff --git a/programs/marginfi/src/state/mod.rs b/programs/marginfi/src/state/mod.rs index 7b5dec9e2..1fa883b0f 100644 --- a/programs/marginfi/src/state/mod.rs +++ b/programs/marginfi/src/state/mod.rs @@ -2,3 +2,4 @@ pub mod fee_state; pub mod marginfi_account; pub mod marginfi_group; pub mod price; +pub mod staked_settings; diff --git a/programs/marginfi/src/state/price.rs b/programs/marginfi/src/state/price.rs index 2fe51ceb3..f8c1396fd 100644 --- a/programs/marginfi/src/state/price.rs +++ b/programs/marginfi/src/state/price.rs @@ -1,11 +1,12 @@ use std::{cell::Ref, cmp::min}; use anchor_lang::prelude::*; +use anchor_spl::token::Mint; use enum_dispatch::enum_dispatch; use fixed::types::I80F48; use pyth_sdk_solana::{state::SolanaPriceAccount, Price, PriceFeed}; use pyth_solana_receiver_sdk::price_update::{self, FeedId, PriceUpdateV2}; -use switchboard_on_demand::{CurrentResult, PullFeedAccountData}; +use switchboard_on_demand::{CurrentResult, PullFeedAccountData, SPL_TOKEN_PROGRAM_ID}; use switchboard_solana::{ AggregatorAccountData, AggregatorResolutionMode, SwitchboardDecimal, SWITCHBOARD_PROGRAM_ID, }; @@ -16,9 +17,10 @@ use crate::{ check, constants::{ CONF_INTERVAL_MULTIPLE, EXP_10, EXP_10_I80F48, MAX_CONF_INTERVAL, - MIN_PYTH_PUSH_VERIFICATION_LEVEL, PYTH_ID, STD_DEV_MULTIPLE, SWITCHBOARD_PULL_ID, + MIN_PYTH_PUSH_VERIFICATION_LEVEL, NATIVE_STAKE_ID, PYTH_ID, SPL_SINGLE_POOL_ID, + STD_DEV_MULTIPLE, SWITCHBOARD_PULL_ID, }, - debug, math_error, + debug, live, math_error, prelude::*, }; @@ -35,6 +37,7 @@ pub enum OracleSetup { SwitchboardV2, PythPushOracle, SwitchboardPull, + StakedWithPythPush, } #[derive(Copy, Clone, Debug)] @@ -71,9 +74,9 @@ pub enum OraclePriceFeedAdapter { } impl OraclePriceFeedAdapter { - pub fn try_from_bank_config( + pub fn try_from_bank_config<'info>( bank_config: &BankConfig, - ais: &[AccountInfo], + ais: &'info [AccountInfo<'info>], clock: &Clock, ) -> MarginfiResult { Self::try_from_bank_config_with_max_age( @@ -84,9 +87,9 @@ impl OraclePriceFeedAdapter { ) } - pub fn try_from_bank_config_with_max_age( + pub fn try_from_bank_config_with_max_age<'info>( bank_config: &BankConfig, - ais: &[AccountInfo], + ais: &'info [AccountInfo<'info>], clock: &Clock, max_age: u64, ) -> MarginfiResult { @@ -148,12 +151,102 @@ impl OraclePriceFeedAdapter { SwitchboardPullPriceFeed::load_checked(&ais[0], clock.unix_timestamp, max_age)?, )) } + OracleSetup::StakedWithPythPush => { + check!(ais.len() == 3, MarginfiError::InvalidOracleAccount); + + check!( + ais[1].key == &bank_config.oracle_keys[1] + && ais[2].key == &bank_config.oracle_keys[2], + MarginfiError::InvalidOracleAccount + ); + + let lst_mint = Account::<'info, Mint>::try_from(&ais[1]).unwrap(); + let lst_supply = lst_mint.supply; + let sol_pool_balance = ais[2].lamports(); + // Note: exchange rate is `sol_pool_balance / lst_supply`, but we will do the + // division last to avoid precision loss. Division does not need to be + // decimal-adjusted because both SOL and stake positions use 9 decimals + + // Note: mainnet/staging/devnet use "push" oracles, localnet uses legacy + if cfg!(any( + feature = "mainnet-beta", + feature = "staging", + feature = "devnet" + )) { + let account_info = &ais[0]; + + check!( + account_info.owner == &pyth_solana_receiver_sdk::id(), + MarginfiError::InvalidOracleAccount + ); + + let price_feed_id = bank_config.get_pyth_push_oracle_feed_id().unwrap(); + let mut feed = PythPushOraclePriceFeed::load_checked( + account_info, + price_feed_id, + clock, + max_age, + )?; + let adjusted_price = (feed.price.price as i128) + .checked_mul(sol_pool_balance as i128) + .ok_or_else(math_error!())? + .checked_div(lst_supply as i128) + .ok_or_else(math_error!())?; + feed.price.price = adjusted_price.try_into().unwrap(); + + let adjusted_ema_price = (feed.ema_price.price as i128) + .checked_mul(sol_pool_balance as i128) + .ok_or_else(math_error!())? + .checked_div(lst_supply as i128) + .ok_or_else(math_error!())?; + feed.ema_price.price = adjusted_ema_price.try_into().unwrap(); + + let price = OraclePriceFeedAdapter::PythPushOracle(feed); + Ok(price) + } else { + // Localnet only + check!( + ais[0].key == &bank_config.oracle_keys[0], + MarginfiError::InvalidOracleAccount + ); + + let account_info = &ais[0]; + let mut feed = PythLegacyPriceFeed::load_checked( + account_info, + clock.unix_timestamp, + max_age, + )?; + + let adjusted_price = (feed.price.price as i128) + .checked_mul(sol_pool_balance as i128) + .ok_or_else(math_error!())? + .checked_div(lst_supply as i128) + .ok_or_else(math_error!())?; + feed.price.price = adjusted_price.try_into().unwrap(); + + let adjusted_ema_price = (feed.ema_price.price as i128) + .checked_mul(sol_pool_balance as i128) + .ok_or_else(math_error!())? + .checked_div(lst_supply as i128) + .ok_or_else(math_error!())?; + feed.ema_price.price = adjusted_ema_price.try_into().unwrap(); + + let price = OraclePriceFeedAdapter::PythLegacy(feed); + Ok(price) + } + } } } + /// * lst_mint, stake_pool, sol_pool - required only if configuring + /// `OracleSetup::StakedWithPythPush` initially. (subsequent validations of staked banks can + /// omit these) pub fn validate_bank_config( bank_config: &BankConfig, oracle_ais: &[AccountInfo], + lst_mint: Option, + stake_pool: Option, + sol_pool: Option, ) -> MarginfiResult { match bank_config.oracle_setup { OracleSetup::None => Err(MarginfiError::OracleNotSetup.into()), @@ -200,6 +293,78 @@ impl OraclePriceFeedAdapter { Ok(()) } + OracleSetup::StakedWithPythPush => { + if lst_mint.is_some() && stake_pool.is_some() && sol_pool.is_some() { + check!(oracle_ais.len() == 3, MarginfiError::InvalidOracleAccount); + + // Note: mainnet/staging/devnet use "push" oracles, localnet uses legacy + if live!() { + PythPushOraclePriceFeed::check_ai_and_feed_id( + &oracle_ais[0], + bank_config.get_pyth_push_oracle_feed_id().unwrap(), + )?; + } else { + // Localnet only + check!( + oracle_ais[0].key == &bank_config.oracle_keys[0], + MarginfiError::InvalidOracleAccount + ); + + PythLegacyPriceFeed::check_ais(&oracle_ais[0])?; + } + + let lst_mint = lst_mint.unwrap(); + let stake_pool = stake_pool.unwrap(); + let sol_pool = sol_pool.unwrap(); + + let program_id = &SPL_SINGLE_POOL_ID; + let stake_pool_bytes = &stake_pool.to_bytes(); + // Validate the given stake_pool derives the same lst_mint, proving stake_pool is correct + let (exp_mint, _) = + Pubkey::find_program_address(&[b"mint", stake_pool_bytes], program_id); + check!( + exp_mint == lst_mint, + MarginfiError::StakePoolValidationFailed + ); + // Validate the now-proven stake_pool derives the given sol_pool + let (exp_pool, _) = + Pubkey::find_program_address(&[b"stake", stake_pool_bytes], program_id); + check!( + exp_pool == sol_pool.key(), + MarginfiError::StakePoolValidationFailed + ); + + // Sanity check the mint. Note: spl-single-pool uses a classic Token, never Token22 + check!( + oracle_ais[1].owner == &SPL_TOKEN_PROGRAM_ID + && oracle_ais[1].key() == lst_mint, + MarginfiError::StakePoolValidationFailed + ); + // Sanity check the pool is a native stake pool. Note: the native staking program is + // written in vanilla Solana and has no Anchor discriminator. + check!( + oracle_ais[2].owner == &NATIVE_STAKE_ID && oracle_ais[2].key() == sol_pool, + MarginfiError::StakePoolValidationFailed + ); + + Ok(()) + } else { + // light validation (after initial setup, only the Pyth oracle needs to be validated) + check!(oracle_ais.len() == 1, MarginfiError::InvalidOracleAccount); + // Note: mainnet/staging/devnet use push oracles, localnet uses legacy push + if live!() { + PythPushOraclePriceFeed::check_ai_and_feed_id( + &oracle_ais[0], + bank_config.get_pyth_push_oracle_feed_id().unwrap(), + )?; + } else { + // Localnet only + PythLegacyPriceFeed::check_ais(&oracle_ais[0])?; + } + + Ok(()) + } + } } } } @@ -214,13 +379,22 @@ impl PythLegacyPriceFeed { pub fn load_checked(ai: &AccountInfo, current_time: i64, max_age: u64) -> MarginfiResult { let price_feed = load_pyth_price_feed(ai)?; - let ema_price = price_feed - .get_ema_price_no_older_than(current_time, max_age) - .ok_or(MarginfiError::StaleOracle)?; + // Note: mainnet/staging/devnet use oracle age, localnet ignores oracle age + let ema_price = if live!() { + price_feed + .get_ema_price_no_older_than(current_time, max_age) + .ok_or(MarginfiError::StaleOracle)? + } else { + price_feed.get_ema_price_unchecked() + }; - let price = price_feed - .get_price_no_older_than(current_time, max_age) - .ok_or(MarginfiError::StaleOracle)?; + let price = if live!() { + price_feed + .get_price_no_older_than(current_time, max_age) + .ok_or(MarginfiError::StaleOracle)? + } else { + price_feed.get_price_unchecked() + }; Ok(Self { ema_price: Box::new(ema_price), diff --git a/programs/marginfi/src/state/staked_settings.rs b/programs/marginfi/src/state/staked_settings.rs new file mode 100644 index 000000000..1c50420c4 --- /dev/null +++ b/programs/marginfi/src/state/staked_settings.rs @@ -0,0 +1,116 @@ +use anchor_lang::prelude::*; +use fixed::types::I80F48; +use fixed_macro::types::I80F48; + +use crate::{assert_struct_align, assert_struct_size, check, MarginfiError, MarginfiResult}; + +use super::marginfi_group::{RiskTier, WrappedI80F48}; + +assert_struct_size!(StakedSettings, 256); +assert_struct_align!(StakedSettings, 8); + +/// Unique per-group. Staked Collateral banks created under a group automatically use these +/// settings. Groups that have not created this struct cannot create staked collateral banks. When +/// this struct updates, changes must be permissionlessly propogated to staked collateral banks. +/// Administrators can also edit the bank manually, i.e. with configure_bank, to temporarily make +/// changes such as raising the deposit limit for a single bank. +#[account(zero_copy)] +#[repr(C)] +pub struct StakedSettings { + /// This account's own key. A PDA derived from `marginfi_group` and `STAKED_SETTINGS_SEED` + pub key: Pubkey, + /// Group for which these settings apply + pub marginfi_group: Pubkey, + /// Generally, the Pyth push oracle for SOL + pub oracle: Pubkey, + + pub asset_weight_init: WrappedI80F48, + pub asset_weight_maint: WrappedI80F48, + + pub deposit_limit: u64, + pub total_asset_value_init_limit: u64, + + pub oracle_max_age: u16, + pub risk_tier: RiskTier, + _pad0: [u8; 5], + + /// The following values are irrelevant because staked collateral positions do not support + /// borrowing. + // * interest_config, + // * liability_weight_init + // * liability_weight_maint + // * borrow_limit + _reserved0: [u8; 8], + _reserved1: [u8; 32], + _reserved2: [u8; 64], +} + +impl StakedSettings { + pub const LEN: usize = std::mem::size_of::(); + + pub fn new( + key: Pubkey, + marginfi_group: Pubkey, + oracle: Pubkey, + asset_weight_init: WrappedI80F48, + asset_weight_maint: WrappedI80F48, + deposit_limit: u64, + total_asset_value_init_limit: u64, + oracle_max_age: u16, + risk_tier: RiskTier, + ) -> Self { + StakedSettings { + key, + marginfi_group, + oracle, + asset_weight_init, + asset_weight_maint, + deposit_limit, + total_asset_value_init_limit, + oracle_max_age, + risk_tier, + ..Default::default() + } + } + + /// Same as `bank.validate()`, except that liability rates and interest rates do not exist in + /// this context (since Staked Collateral accounts cannot be borrowed against and such Banks + /// will use placeholders for those values) + pub fn validate(&self) -> MarginfiResult { + let asset_init_w = I80F48::from(self.asset_weight_init); + let asset_maint_w = I80F48::from(self.asset_weight_maint); + + check!( + asset_init_w >= I80F48::ZERO && asset_init_w <= I80F48::ONE, + MarginfiError::InvalidConfig + ); + check!(asset_maint_w >= asset_init_w, MarginfiError::InvalidConfig); + + if self.risk_tier == RiskTier::Isolated { + check!(asset_init_w == I80F48::ZERO, MarginfiError::InvalidConfig); + check!(asset_maint_w == I80F48::ZERO, MarginfiError::InvalidConfig); + } + + Ok(()) + } +} + +impl Default for StakedSettings { + fn default() -> Self { + StakedSettings { + key: Pubkey::default(), + marginfi_group: Pubkey::default(), + oracle: Pubkey::default(), + asset_weight_init: I80F48!(0.8).into(), + asset_weight_maint: I80F48!(0.9).into(), + deposit_limit: 1_000_000, + total_asset_value_init_limit: 1_000_000, + oracle_max_age: 10, + risk_tier: RiskTier::Collateral, + _pad0: [0; 5], + _reserved0: [0; 8], + _reserved1: [0; 32], + _reserved2: [0; 64], + } + } +} diff --git a/programs/marginfi/src/utils.rs b/programs/marginfi/src/utils.rs index fc79d68b7..f9fd9ba0e 100644 --- a/programs/marginfi/src/utils.rs +++ b/programs/marginfi/src/utils.rs @@ -1,6 +1,10 @@ use crate::{ bank_authority_seed, bank_seed, - state::marginfi_group::{Bank, BankVaultType}, + constants::{ASSET_TAG_DEFAULT, ASSET_TAG_SOL, ASSET_TAG_STAKED}, + state::{ + marginfi_account::MarginfiAccount, + marginfi_group::{Bank, BankVaultType}, + }, MarginfiError, MarginfiResult, }; use anchor_lang::prelude::*; @@ -192,3 +196,63 @@ pub fn hex_to_bytes(hex: &str) -> Vec { }) .collect() } + +/// Validate that after a deposit to Bank, the users's account contains either all Default/SOL +/// balances, or all Staked/Sol balances. Default and Staked assets cannot mix. +pub fn validate_asset_tags(bank: &Bank, marginfi_account: &MarginfiAccount) -> MarginfiResult { + let mut has_default_asset = false; + let mut has_staked_asset = false; + + for balance in marginfi_account.lending_account.balances.iter() { + if balance.active { + match balance.bank_asset_tag { + ASSET_TAG_DEFAULT => has_default_asset = true, + ASSET_TAG_SOL => { /* Do nothing, SOL can mix with any asset type */ } + ASSET_TAG_STAKED => has_staked_asset = true, + _ => panic!("unsupported asset tag"), + } + } + } + + // 1. Regular assets (DEFAULT) cannot mix with Staked assets + if bank.config.asset_tag == ASSET_TAG_DEFAULT && has_staked_asset { + return err!(MarginfiError::AssetTagMismatch); + } + + // 2. Staked SOL cannot mix with Regular asset (DEFAULT) + if bank.config.asset_tag == ASSET_TAG_STAKED && has_default_asset { + return err!(MarginfiError::AssetTagMismatch); + } + + Ok(()) +} + +/// Validate that two banks are compatible based on their asset tags. See the following combinations +/// (* is wildcard, e.g. any tag): +/// +/// Allowed: +/// 1) Default/Default +/// 2) Sol/* +/// 3) Staked/Staked +/// +/// Forbidden: +/// 1) Default/Staked +/// +/// Returns an error if the two banks have mismatching asset tags according to the above. +pub fn validate_bank_asset_tags(bank_a: &Bank, bank_b: &Bank) -> MarginfiResult { + let is_bank_a_default = bank_a.config.asset_tag == ASSET_TAG_DEFAULT; + let is_bank_a_staked = bank_a.config.asset_tag == ASSET_TAG_STAKED; + let is_bank_b_default = bank_b.config.asset_tag == ASSET_TAG_DEFAULT; + let is_bank_b_staked = bank_b.config.asset_tag == ASSET_TAG_STAKED; + // Note: Sol is compatible with all other tags and doesn't matter... + + // 1. Default assets cannot mix with Staked assets + if is_bank_a_default && is_bank_b_staked { + return err!(MarginfiError::AssetTagMismatch); + } + if is_bank_a_staked && is_bank_b_default { + return err!(MarginfiError::AssetTagMismatch); + } + + Ok(()) +} diff --git a/programs/marginfi/tests/admin_actions/setup_bank.rs b/programs/marginfi/tests/admin_actions/setup_bank.rs index e2dccd205..8e408c33b 100644 --- a/programs/marginfi/tests/admin_actions/setup_bank.rs +++ b/programs/marginfi/tests/admin_actions/setup_bank.rs @@ -87,6 +87,7 @@ async fn add_bank_success() -> anyhow::Result<()> { emissions_rate, emissions_remaining, emissions_mint, + collected_program_fees_outstanding, _padding_0, _padding_1, .. // ignore internal padding @@ -116,6 +117,7 @@ async fn add_bank_success() -> anyhow::Result<()> { assert_eq!(emissions_rate, 0); assert_eq!(emissions_mint, Pubkey::new_from_array([0; 32])); assert_eq!(emissions_remaining, I80F48!(0.0).into()); + assert_eq!(collected_program_fees_outstanding, I80F48!(0.0).into()); assert_eq!(_padding_0, <[[u64; 2]; 27] as Default>::default()); assert_eq!(_padding_1, <[[u64; 2]; 32] as Default>::default()); @@ -220,6 +222,7 @@ async fn add_bank_with_seed_success() -> anyhow::Result<()> { emissions_rate, emissions_remaining, emissions_mint, + collected_program_fees_outstanding, _padding_0, _padding_1, .. // ignore internal padding @@ -249,6 +252,7 @@ async fn add_bank_with_seed_success() -> anyhow::Result<()> { assert_eq!(emissions_rate, 0); assert_eq!(emissions_mint, Pubkey::new_from_array([0; 32])); assert_eq!(emissions_remaining, I80F48!(0.0).into()); + assert_eq!(collected_program_fees_outstanding, I80F48!(0.0).into()); assert_eq!(_padding_0, <[[u64; 2]; 27] as Default>::default()); assert_eq!(_padding_1, <[[u64; 2]; 32] as Default>::default()); @@ -343,6 +347,7 @@ async fn configure_bank_success(bank_mint: BankMint) -> anyhow::Result<()> { operational_state, oracle, risk_tier, + asset_tag, total_asset_value_init_limit, oracle_max_age, permissionless_bad_debt_settlement, @@ -387,6 +392,7 @@ async fn configure_bank_success(bank_mint: BankMint) -> anyhow::Result<()> { check_bank_field!(borrow_limit); check_bank_field!(operational_state); check_bank_field!(risk_tier); + check_bank_field!(asset_tag); check_bank_field!(total_asset_value_init_limit); check_bank_field!(oracle_max_age); diff --git a/programs/marginfi/tests/misc/regression.rs b/programs/marginfi/tests/misc/regression.rs index 823ddebe4..5742cecc0 100644 --- a/programs/marginfi/tests/misc/regression.rs +++ b/programs/marginfi/tests/misc/regression.rs @@ -4,10 +4,13 @@ use anchor_lang::AccountDeserialize; use anyhow::bail; use base64::{prelude::BASE64_STANDARD, Engine}; use fixed::types::I80F48; -use marginfi::state::{ - marginfi_account::MarginfiAccount, - marginfi_group::{Bank, BankOperationalState, RiskTier}, - price::OracleSetup, +use marginfi::{ + constants::ASSET_TAG_DEFAULT, + state::{ + marginfi_account::MarginfiAccount, + marginfi_group::{Bank, BankOperationalState, RiskTier}, + price::OracleSetup, + }, }; use solana_account_decoder::UiAccountData; use solana_cli_output::CliAccount; @@ -50,7 +53,8 @@ async fn account_field_values_reg() -> anyhow::Result<()> { balance_1.bank_pk, pubkey!("2s37akK2eyBbp8DZgCm7RtsaEz8eJP3Nxd4urLHQv7yB") ); - assert_eq!(balance_1._pad0, [0; 7]); + assert_eq!(balance_1.bank_asset_tag, ASSET_TAG_DEFAULT); + assert_eq!(balance_1._pad0, [0; 6]); assert_eq!( I80F48::from(balance_1.asset_shares), I80F48::from_str("1650216221.466876226897366").unwrap() @@ -75,7 +79,8 @@ async fn account_field_values_reg() -> anyhow::Result<()> { balance_2.bank_pk, pubkey!("CCKtUs6Cgwo4aaQUmBPmyoApH2gUDErxNZCAntD6LYGh") ); - assert_eq!(balance_2._pad0, [0; 7]); + assert_eq!(balance_2.bank_asset_tag, ASSET_TAG_DEFAULT); + assert_eq!(balance_2._pad0, [0; 6]); assert_eq!( I80F48::from(balance_2.asset_shares), I80F48::from_str("0").unwrap() @@ -125,7 +130,8 @@ async fn account_field_values_reg() -> anyhow::Result<()> { balance_1.bank_pk, pubkey!("6hS9i46WyTq1KXcoa2Chas2Txh9TJAVr6n1t3tnrE23K") ); - assert_eq!(balance_1._pad0, [0; 7]); + assert_eq!(balance_1.bank_asset_tag, ASSET_TAG_DEFAULT); + assert_eq!(balance_1._pad0, [0; 6]); assert_eq!( I80F48::from(balance_1.asset_shares), I80F48::from_str("470.952530958931234").unwrap() @@ -150,7 +156,8 @@ async fn account_field_values_reg() -> anyhow::Result<()> { balance_2.bank_pk, pubkey!("11111111111111111111111111111111") ); - assert_eq!(balance_2._pad0, [0; 7]); + assert_eq!(balance_2.bank_asset_tag, ASSET_TAG_DEFAULT); + assert_eq!(balance_2._pad0, [0; 6]); assert_eq!( I80F48::from(balance_2.asset_shares), I80F48::from_str("0").unwrap() @@ -200,7 +207,8 @@ async fn account_field_values_reg() -> anyhow::Result<()> { balance_1.bank_pk, pubkey!("11111111111111111111111111111111") ); - assert_eq!(balance_1._pad0, [0; 7]); + assert_eq!(balance_1.bank_asset_tag, ASSET_TAG_DEFAULT); + assert_eq!(balance_1._pad0, [0; 6]); assert_eq!( I80F48::from(balance_1.asset_shares), I80F48::from_str("0").unwrap() @@ -635,7 +643,8 @@ async fn bank_field_values_reg() -> anyhow::Result<()> { assert_eq!(bank.config._pad0, [0; 6]); assert_eq!(bank.config.borrow_limit, 2000000000000); assert_eq!(bank.config.risk_tier, RiskTier::Collateral); - assert_eq!(bank.config._pad1, [0; 7]); + assert_eq!(bank.config.asset_tag, ASSET_TAG_DEFAULT); + assert_eq!(bank.config._pad1, [0; 6]); assert_eq!(bank.config.total_asset_value_init_limit, 0); assert_eq!(bank.config.oracle_max_age, 300); assert_eq!(bank.config._padding, [0; 38]); @@ -654,6 +663,11 @@ async fn bank_field_values_reg() -> anyhow::Result<()> { bank.emissions_mint, pubkey!("2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo") ); + // Legacy banks have no program fees + assert_eq!( + I80F48::from(bank.collected_program_fees_outstanding), + I80F48::from_str("0").unwrap() + ); assert_eq!(bank._padding_0, [[0, 0]; 27]); assert_eq!(bank._padding_1, [[0, 0]; 32]); diff --git a/tests/01_initGroup.spec.ts b/tests/01_initGroup.spec.ts index dd750e35b..d53624f74 100644 --- a/tests/01_initGroup.spec.ts +++ b/tests/01_initGroup.spec.ts @@ -1,15 +1,34 @@ -import { Program, workspace } from "@coral-xyz/anchor"; -import { Transaction } from "@solana/web3.js"; -import { groupInitialize } from "./utils/instructions"; +import { BN, Program, workspace } from "@coral-xyz/anchor"; +import { PublicKey, Transaction } from "@solana/web3.js"; +import { + editStakedSettings, + groupInitialize, + initStakedSettings, +} from "./utils/group-instructions"; import { Marginfi } from "../target/types/marginfi"; import { + ecosystem, globalFeeWallet, groupAdmin, marginfiGroup, + oracles, PROGRAM_FEE_FIXED, PROGRAM_FEE_RATE, + users, + verbose, } from "./rootHooks"; -import { assertI80F48Approx, assertKeysEqual } from "./utils/genericTests"; +import { + assertBNEqual, + assertI80F48Approx, + assertKeysEqual, +} from "./utils/genericTests"; +import { assert } from "chai"; +import { bigNumberToWrappedI80F48 } from "@mrgnlabs/mrgn-common"; +import { deriveStakedSettings } from "./utils/pdas"; +import { + defaultStakedInterestSettings, + StakedSettingsEdit, +} from "./utils/types"; describe("Init group", () => { const program = workspace.Marginfi as Program; @@ -32,6 +51,10 @@ describe("Init group", () => { marginfiGroup.publicKey ); assertKeysEqual(group.admin, groupAdmin.wallet.publicKey); + if (verbose) { + console.log("*init group: " + marginfiGroup.publicKey); + console.log(" group admin: " + group.admin); + } const feeCache = group.feeStateCache; const tolerance = 0.00001; @@ -39,4 +62,215 @@ describe("Init group", () => { assertI80F48Approx(feeCache.programFeeRate, PROGRAM_FEE_RATE, tolerance); assertKeysEqual(feeCache.globalFeeWallet, globalFeeWallet); }); + + it("(attacker) Tries to init staked settings - should fail", async () => { + const settings = defaultStakedInterestSettings( + oracles.wsolOracle.publicKey + ); + let failed = false; + try { + await users[0].mrgnProgram.provider.sendAndConfirm( + new Transaction().add( + await initStakedSettings(users[0].mrgnProgram, { + group: marginfiGroup.publicKey, + feePayer: groupAdmin.wallet.publicKey, + settings: settings, + }) + ) + ); + } catch (err) { + // generic signature error + failed = true; + } + + assert.ok(failed, "Transaction succeeded when it should have failed"); + }); + + it("(admin) Init staked settings for group - opts in to use staked collateral", async () => { + const settings = defaultStakedInterestSettings( + oracles.wsolOracle.publicKey + ); + await groupAdmin.mrgnProgram.provider.sendAndConfirm( + new Transaction().add( + await initStakedSettings(groupAdmin.mrgnProgram, { + group: marginfiGroup.publicKey, + feePayer: groupAdmin.wallet.publicKey, + settings: settings, + }) + ) + ); + + const [settingsKey] = deriveStakedSettings( + program.programId, + marginfiGroup.publicKey + ); + if (verbose) { + console.log("*init staked settings: " + settingsKey); + } + + let settingsAcc = await program.account.stakedSettings.fetch(settingsKey); + assertKeysEqual(settingsAcc.key, settingsKey); + assertKeysEqual(settingsAcc.oracle, oracles.wsolOracle.publicKey); + assertI80F48Approx(settingsAcc.assetWeightInit, 0.8); + assertI80F48Approx(settingsAcc.assetWeightMaint, 0.9); + assertBNEqual(settingsAcc.depositLimit, 1_000_000_000_000); + assertBNEqual(settingsAcc.totalAssetValueInitLimit, 150_000_000); + assert.equal(settingsAcc.oracleMaxAge, 60); + assert.deepEqual(settingsAcc.riskTier, { collateral: {} }); + }); + + it("(attacker) Tries to edit staked settings - should fail", async () => { + const settings: StakedSettingsEdit = { + oracle: PublicKey.default, + assetWeightInit: bigNumberToWrappedI80F48(0.2), + assetWeightMaint: bigNumberToWrappedI80F48(0.3), + depositLimit: new BN(42), + totalAssetValueInitLimit: new BN(43), + oracleMaxAge: 44, + riskTier: { + isolated: undefined, + }, + }; + let failed = false; + try { + const [settingsKey] = deriveStakedSettings( + program.programId, + marginfiGroup.publicKey + ); + + await users[0].mrgnProgram.provider.sendAndConfirm( + new Transaction().add( + await editStakedSettings(users[0].mrgnProgram, { + settingsKey: settingsKey, + settings: settings, + }) + ) + ); + } catch (err) { + // generic signature error + failed = true; + } + assert.ok(failed, "Transaction succeeded when it should have failed"); + }); + + // Note: there are no Staked Collateral positions in the end to end test suite (those are in the + // BankRun suite e.g. s01) so these settings do nothing. + + it("(admin) Edit staked settings for group", async () => { + const settings: StakedSettingsEdit = { + oracle: PublicKey.default, + assetWeightInit: bigNumberToWrappedI80F48(0.2), + assetWeightMaint: bigNumberToWrappedI80F48(0.3), + depositLimit: new BN(42), + totalAssetValueInitLimit: new BN(43), + oracleMaxAge: 44, + riskTier: { + collateral: undefined, + }, + }; + const [settingsKey] = deriveStakedSettings( + program.programId, + marginfiGroup.publicKey + ); + + await groupAdmin.mrgnProgram.provider.sendAndConfirm( + new Transaction().add( + await editStakedSettings(groupAdmin.mrgnProgram, { + settingsKey: settingsKey, + settings: settings, + }) + ) + ); + + if (verbose) { + console.log("*edit staked settings: " + settingsKey); + } + + let settingsAcc = await program.account.stakedSettings.fetch(settingsKey); + assertKeysEqual(settingsAcc.key, settingsKey); + assertKeysEqual(settingsAcc.oracle, PublicKey.default); + assertI80F48Approx(settingsAcc.assetWeightInit, 0.2); + assertI80F48Approx(settingsAcc.assetWeightMaint, 0.3); + assertBNEqual(settingsAcc.depositLimit, 42); + assertBNEqual(settingsAcc.totalAssetValueInitLimit, 43); + assert.equal(settingsAcc.oracleMaxAge, 44); + assert.deepEqual(settingsAcc.riskTier, { collateral: {} }); // no change + }); + + it("(admin) Partial settings update", async () => { + const settings: StakedSettingsEdit = { + oracle: null, + assetWeightInit: null, + assetWeightMaint: null, + depositLimit: null, + totalAssetValueInitLimit: null, + oracleMaxAge: 60, + riskTier: null, + }; + const [settingsKey] = deriveStakedSettings( + program.programId, + marginfiGroup.publicKey + ); + + await groupAdmin.mrgnProgram.provider.sendAndConfirm( + new Transaction().add( + await editStakedSettings(groupAdmin.mrgnProgram, { + settingsKey: settingsKey, + settings: settings, + }) + ) + ); + + let settingsAcc = await program.account.stakedSettings.fetch(settingsKey); + // No change + assertKeysEqual(settingsAcc.key, settingsKey); + assertKeysEqual(settingsAcc.oracle, PublicKey.default); + assertI80F48Approx(settingsAcc.assetWeightInit, 0.2); + assertI80F48Approx(settingsAcc.assetWeightMaint, 0.3); + assertBNEqual(settingsAcc.depositLimit, 42); + assertBNEqual(settingsAcc.totalAssetValueInitLimit, 43); + assert.deepEqual(settingsAcc.riskTier, { collateral: {} }); + + assert.equal(settingsAcc.oracleMaxAge, 60); + }); + + // Note: Isolated riskTier requires the weights to be zero, so this is invalid... + it("(admin) Bad settings update - should fail", async () => { + const settings: StakedSettingsEdit = { + oracle: null, + assetWeightInit: null, + assetWeightMaint: null, + depositLimit: null, + totalAssetValueInitLimit: null, + oracleMaxAge: 60, + riskTier: { + isolated: undefined, + }, + }; + const [settingsKey] = deriveStakedSettings( + program.programId, + marginfiGroup.publicKey + ); + + let failed = false; + try { + await groupAdmin.mrgnProgram.provider.sendAndConfirm( + new Transaction().add( + await editStakedSettings(groupAdmin.mrgnProgram, { + settingsKey: settingsKey, + settings: settings, + }) + ) + ); + } catch (err) { + // TODO create a util for this that fails with more detail + assert.ok( + err.logs.some((log: string) => + log.includes("Error Code: InvalidConfig") + ) + ); + failed = true; + } + assert.ok(failed, "Transaction succeeded when it should have failed"); + }); }); diff --git a/tests/02_configGroup.spec.ts b/tests/02_configGroup.spec.ts index 431c8d043..f44302f74 100644 --- a/tests/02_configGroup.spec.ts +++ b/tests/02_configGroup.spec.ts @@ -6,7 +6,7 @@ import { workspace, } from "@coral-xyz/anchor"; import { Keypair, Transaction } from "@solana/web3.js"; -import { groupConfigure } from "./utils/instructions"; +import { groupConfigure } from "./utils/group-instructions"; import { Marginfi } from "../target/types/marginfi"; import { groupAdmin, marginfiGroup } from "./rootHooks"; import { assertKeysEqual } from "./utils/genericTests"; diff --git a/tests/03_addBank.spec.ts b/tests/03_addBank.spec.ts index f54b12345..6dff0cd9d 100644 --- a/tests/03_addBank.spec.ts +++ b/tests/03_addBank.spec.ts @@ -1,6 +1,6 @@ import { BN, Program, workspace } from "@coral-xyz/anchor"; import { PublicKey, Transaction } from "@solana/web3.js"; -import { addBank } from "./utils/instructions"; +import { addBank } from "./utils/group-instructions"; import { Marginfi } from "../target/types/marginfi"; import { bankKeypairA, @@ -11,6 +11,7 @@ import { INIT_POOL_ORIGINATION_FEE, marginfiGroup, oracles, + printBuffers, verbose, } from "./rootHooks"; import { @@ -20,7 +21,7 @@ import { assertKeyDefault, assertKeysEqual, } from "./utils/genericTests"; -import { defaultBankConfig } from "./utils/types"; +import { ASSET_TAG_DEFAULT, defaultBankConfig } from "./utils/types"; import { deriveLiquidityVaultAuthority, deriveLiquidityVault, @@ -75,7 +76,9 @@ describe("Lending pool add bank (add bank to group)", () => { let bankData = ( await program.provider.connection.getAccountInfo(bankKey) ).data.subarray(8); - printBufferGroups(bankData, 16, 896); + if (printBuffers) { + printBufferGroups(bankData, 16, 896); + } const bank = await program.account.bank.fetch(bankKey); const config = bank.config; @@ -126,6 +129,7 @@ describe("Lending pool add bank (add bank to group)", () => { assertI80F48Equal(config.assetWeightInit, 1); assertI80F48Equal(config.assetWeightMaint, 1); assertI80F48Equal(config.liabilityWeightInit, 1); + assertI80F48Equal(config.liabilityWeightMaint, 1); assertBNEqual(config.depositLimit, 100_000_000_000); const tolerance = 0.000001; @@ -139,10 +143,13 @@ describe("Lending pool add bank (add bank to group)", () => { assertI80F48Approx(interest.protocolIrFee, 0.04, tolerance); assertI80F48Approx(interest.protocolOriginationFee, 0.01, tolerance); + assertI80F48Approx(interest.protocolOriginationFee, 0.01, tolerance); + assert.deepEqual(config.operationalState, { operational: {} }); assert.deepEqual(config.oracleSetup, { pythLegacy: {} }); assertBNEqual(config.borrowLimit, 100_000_000_000); assert.deepEqual(config.riskTier, { collateral: {} }); + assert.equal(config.assetTag, ASSET_TAG_DEFAULT); assertBNEqual(config.totalAssetValueInitLimit, 1_000_000_000_000); assert.equal(config.oracleMaxAge, 100); @@ -184,7 +191,9 @@ describe("Lending pool add bank (add bank to group)", () => { let bonkBankData = ( await program.provider.connection.getAccountInfo(bonkBankKey) ).data.subarray(8); - printBufferGroups(bonkBankData, 16, 896); + if (printBuffers) { + printBufferGroups(bonkBankData, 16, 896); + } let cloudBankKey = new PublicKey( "4kNXetv8hSv9PzvzPZzEs1CTH6ARRRi2b8h6jk1ad1nP" @@ -192,7 +201,9 @@ describe("Lending pool add bank (add bank to group)", () => { let cloudBankData = ( await program.provider.connection.getAccountInfo(cloudBankKey) ).data.subarray(8); - printBufferGroups(cloudBankData, 16, 896); + if (printBuffers) { + printBufferGroups(cloudBankData, 16, 896); + } const bbk = bonkBankKey; const bb = await program.account.bank.fetch(bonkBankKey); @@ -255,6 +266,9 @@ describe("Lending pool add bank (add bank to group)", () => { // assertI80F48Equal(interest.protocolFixedFeeApr, 0); // assertI80F48Equal(interest.protocolIrFee, 0); + // Bank added before this feature existed, should be zero + assertI80F48Equal(bonkInterest.protocolOriginationFee, 0); + assert.deepEqual(bonkConfig.operationalState, { operational: {} }); assert.deepEqual(bonkConfig.oracleSetup, { pythPushOracle: {} }); // roughly 26.41 billion BONK with 5 decimals. @@ -302,6 +316,9 @@ describe("Lending pool add bank (add bank to group)", () => { // 1 million CLOUD with 9 decimals (1_000_000_000_000_000) assertBNEqual(cloudConfig.depositLimit, 1_000_000_000_000_000); + // Bank added before this feature existed, should be zero + assertI80F48Equal(cloudInterest.protocolOriginationFee, 0); + assert.deepEqual(cloudConfig.operationalState, { operational: {} }); assert.deepEqual(cloudConfig.oracleSetup, { switchboardV2: {} }); // 50,000 CLOUD with 9 decimals (50_000_000_000_000) @@ -309,5 +326,24 @@ describe("Lending pool add bank (add bank to group)", () => { assert.deepEqual(cloudConfig.riskTier, { isolated: {} }); assertBNEqual(cloudConfig.totalAssetValueInitLimit, 0); assert.equal(cloudConfig.oracleMaxAge, 60); + + // Assert emissions mint (one of the last fields) is also aligned correctly. + let pyUsdcBankKey = new PublicKey( + "Fe5QkKPVAh629UPP5aJ8sDZu8HTfe6M26jDQkKyXVhoA" + ); + let pyUsdcBankData = ( + await program.provider.connection.getAccountInfo(pyUsdcBankKey) + ).data.subarray(8); + if (printBuffers) { + printBufferGroups(pyUsdcBankData, 16, 896); + } + + const pb = await program.account.bank.fetch(pyUsdcBankKey); + assertKeysEqual( + pb.emissionsMint, + new PublicKey("2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo") + ); }); }); + +// TODO add bank with seed diff --git a/tests/04_configureBank.spec.ts b/tests/04_configureBank.spec.ts index da194892e..1341717ac 100644 --- a/tests/04_configureBank.spec.ts +++ b/tests/04_configureBank.spec.ts @@ -1,10 +1,11 @@ import { BN, Program, workspace } from "@coral-xyz/anchor"; +import { configureBank } from "./utils/group-instructions"; import { PublicKey, Transaction } from "@solana/web3.js"; -import { configureBank } from "./utils/instructions"; import { Marginfi } from "../target/types/marginfi"; import { bankKeypairUsdc, groupAdmin, marginfiGroup } from "./rootHooks"; import { assertBNEqual, assertI80F48Approx } from "./utils/genericTests"; import { assert } from "chai"; +import { InterestRateConfigRaw } from "@mrgnlabs/marginfi-client-v2"; import { bigNumberToWrappedI80F48 } from "@mrgnlabs/mrgn-common"; import { ASSET_TAG_SOL, @@ -84,7 +85,7 @@ describe("Lending pool configure bank", () => { assert.deepEqual(config.oracleSetup, { pythLegacy: {} }); // no change assertBNEqual(config.borrowLimit, 10000); assert.deepEqual(config.riskTier, { collateral: {} }); // no change - // assert.equal(config.assetTag, ASSET_TAG_SOL); // TODO when staked collateral added + assert.equal(config.assetTag, ASSET_TAG_SOL); assertBNEqual(config.totalAssetValueInitLimit, 15000); assert.equal(config.oracleMaxAge, 50); }); diff --git a/tests/05_setupEmissions.spec.ts b/tests/05_setupEmissions.spec.ts new file mode 100644 index 000000000..b14b26679 --- /dev/null +++ b/tests/05_setupEmissions.spec.ts @@ -0,0 +1,154 @@ +import { + AnchorProvider, + BN, + getProvider, + Program, + Wallet, + workspace, +} from "@coral-xyz/anchor"; +import { Transaction } from "@solana/web3.js"; +import { setupEmissions, updateEmissions } from "./utils/group-instructions"; +import { Marginfi } from "../target/types/marginfi"; +import { + bankKeypairUsdc, + ecosystem, + groupAdmin, + marginfiGroup, + verbose, +} from "./rootHooks"; +import { + assertBNEqual, + assertI80F48Approx, + assertKeysEqual, + getTokenBalance, +} from "./utils/genericTests"; +import { assert } from "chai"; +import { + EMISSIONS_FLAG_BORROW_ACTIVE, + EMISSIONS_FLAG_LENDING_ACTIVE, +} from "./utils/types"; +import { createMintToInstruction } from "@solana/spl-token"; +import { deriveEmissionsAuth, deriveEmissionsTokenAccount } from "./utils/pdas"; + +describe("Lending pool set up emissions", () => { + const program = workspace.Marginfi as Program; + const provider = getProvider() as AnchorProvider; + const wallet = provider.wallet as Wallet; + + const emissionRate = new BN(500_000 * 10 ** ecosystem.tokenBDecimals); + const totalEmissions = new BN(1_000_000 * 10 ** ecosystem.tokenBDecimals); + + it("Mint token B to the group admin for funding emissions", async () => { + let tx: Transaction = new Transaction(); + tx.add( + createMintToInstruction( + ecosystem.tokenBMint.publicKey, + groupAdmin.tokenBAccount, + wallet.publicKey, + BigInt(100_000_000) * BigInt(10 ** ecosystem.tokenBDecimals) + ) + ); + await program.provider.sendAndConfirm(tx); + }); + + it("(admin) Set up to token B emissions on (USDC) bank - happy path", async () => { + const adminBBefore = await getTokenBalance( + provider, + groupAdmin.tokenBAccount + ); + const [emissionsAccKey] = deriveEmissionsTokenAccount( + program.programId, + bankKeypairUsdc.publicKey, + ecosystem.tokenBMint.publicKey + ); + // Note: an uninitialized account that does nothing... + const [emissionsAuthKey] = deriveEmissionsAuth( + program.programId, + bankKeypairUsdc.publicKey, + ecosystem.tokenBMint.publicKey + ); + + await groupAdmin.mrgnProgram!.provider.sendAndConfirm!( + new Transaction().add( + await setupEmissions(program, { + marginfiGroup: marginfiGroup.publicKey, + admin: groupAdmin.wallet.publicKey, + bank: bankKeypairUsdc.publicKey, + emissionsMint: ecosystem.tokenBMint.publicKey, + fundingAccount: groupAdmin.tokenBAccount, + emissionsFlags: new BN( + EMISSIONS_FLAG_BORROW_ACTIVE + EMISSIONS_FLAG_LENDING_ACTIVE + ), + emissionsRate: emissionRate, + totalEmissions: totalEmissions, + }) + ) + ); + + if (verbose) { + console.log("Started token B borrow/lending emissions on USDC bank"); + } + + const [bank, adminBAfter, emissionsAccAfter] = await Promise.all([ + program.account.bank.fetch(bankKeypairUsdc.publicKey), + getTokenBalance(provider, groupAdmin.tokenBAccount), + getTokenBalance(provider, emissionsAccKey), + ]); + + assertKeysEqual(bank.emissionsMint, ecosystem.tokenBMint.publicKey); + assertBNEqual(bank.emissionsRate, emissionRate); + assertI80F48Approx(bank.emissionsRemaining, totalEmissions); + assertBNEqual( + bank.flags, + new BN(EMISSIONS_FLAG_BORROW_ACTIVE + EMISSIONS_FLAG_LENDING_ACTIVE) + ); + assert.equal(adminBBefore - adminBAfter, totalEmissions.toNumber()); + assert.equal(emissionsAccAfter, totalEmissions.toNumber()); + }); + + it("(admin) Add more token B emissions on (USDC) bank - happy path", async () => { + const [emissionsAccKey] = deriveEmissionsTokenAccount( + program.programId, + bankKeypairUsdc.publicKey, + ecosystem.tokenBMint.publicKey + ); + const [adminBBefore, emissionsAccBefore] = await Promise.all([ + getTokenBalance(provider, groupAdmin.tokenBAccount), + getTokenBalance(provider, emissionsAccKey), + ]); + + await groupAdmin.mrgnProgram!.provider.sendAndConfirm!( + new Transaction().add( + await updateEmissions(program, { + marginfiGroup: marginfiGroup.publicKey, + admin: groupAdmin.wallet.publicKey, + bank: bankKeypairUsdc.publicKey, + emissionsMint: ecosystem.tokenBMint.publicKey, + fundingAccount: groupAdmin.tokenBAccount, + emissionsFlags: null, + emissionsRate: null, + additionalEmissions: totalEmissions, + }) + ) + ); + + const [bank, adminBAfter, emissionsAccAfter] = await Promise.all([ + program.account.bank.fetch(bankKeypairUsdc.publicKey), + getTokenBalance(provider, groupAdmin.tokenBAccount), + getTokenBalance(provider, emissionsAccKey), + ]); + + assertKeysEqual(bank.emissionsMint, ecosystem.tokenBMint.publicKey); + assertBNEqual(bank.emissionsRate, emissionRate); + assertI80F48Approx(bank.emissionsRemaining, totalEmissions.muln(2)); + assertBNEqual( + bank.flags, + new BN(EMISSIONS_FLAG_BORROW_ACTIVE + EMISSIONS_FLAG_LENDING_ACTIVE) + ); + assert.equal(adminBBefore - adminBAfter, totalEmissions.toNumber()); + assert.equal( + emissionsAccAfter, + emissionsAccBefore + totalEmissions.toNumber() + ); + }); +}); diff --git a/tests/06_initUser.spec.ts b/tests/06_initUser.spec.ts new file mode 100644 index 000000000..6fbb90b44 --- /dev/null +++ b/tests/06_initUser.spec.ts @@ -0,0 +1,69 @@ +import { Program, workspace } from "@coral-xyz/anchor"; +import { Keypair, Transaction } from "@solana/web3.js"; +import { Marginfi } from "../target/types/marginfi"; +import { marginfiGroup, users } from "./rootHooks"; +import { + assertBNEqual, + assertI80F48Equal, + assertKeyDefault, + assertKeysEqual, +} from "./utils/genericTests"; +import { assert } from "chai"; +import { accountInit } from "./utils/user-instructions"; +import { USER_ACCOUNT } from "./utils/mocks"; + +describe("Initialize user account", () => { + const program = workspace.Marginfi as Program; + + it("(user 0) Initialize user account - happy path", async () => { + const accountKeypair = Keypair.generate(); + const accountKey = accountKeypair.publicKey; + users[0].accounts.set(USER_ACCOUNT, accountKey); + + let tx: Transaction = new Transaction(); + tx.add( + await accountInit(program, { + marginfiGroup: marginfiGroup.publicKey, + marginfiAccount: accountKey, + authority: users[0].wallet.publicKey, + feePayer: users[0].wallet.publicKey, + }) + ); + await users[0].mrgnProgram.provider.sendAndConfirm(tx, [ + accountKeypair, + ]); + + const userAcc = await program.account.marginfiAccount.fetch(accountKey); + assertKeysEqual(userAcc.group, marginfiGroup.publicKey); + assertKeysEqual(userAcc.authority, users[0].wallet.publicKey); + const balances = userAcc.lendingAccount.balances; + for (let i = 0; i < balances.length; i++) { + assert.equal(balances[i].active, false); + assertKeyDefault(balances[i].bankPk); + assertI80F48Equal(balances[i].assetShares, 0); + assertI80F48Equal(balances[i].liabilityShares, 0); + assertI80F48Equal(balances[i].emissionsOutstanding, 0); + assertBNEqual(balances[i].lastUpdate, 0); + } + assertBNEqual(userAcc.accountFlags, 0); + }); + + it("(user 1) Initialize user account - happy path", async () => { + const accountKeypair = Keypair.generate(); + const accountKey = accountKeypair.publicKey; + users[1].accounts.set(USER_ACCOUNT, accountKey); + + let tx: Transaction = new Transaction(); + tx.add( + await accountInit(program, { + marginfiGroup: marginfiGroup.publicKey, + marginfiAccount: accountKey, + authority: users[1].wallet.publicKey, + feePayer: users[1].wallet.publicKey, + }) + ); + await users[1].mrgnProgram.provider.sendAndConfirm(tx, [ + accountKeypair, + ]); + }); +}); diff --git a/tests/07_deposit.spec.ts b/tests/07_deposit.spec.ts new file mode 100644 index 000000000..f4fdbbf20 --- /dev/null +++ b/tests/07_deposit.spec.ts @@ -0,0 +1,163 @@ +import { + AnchorProvider, + BN, + getProvider, + Program, + Wallet, + workspace, +} from "@coral-xyz/anchor"; +import { Transaction } from "@solana/web3.js"; +import { Marginfi } from "../target/types/marginfi"; +import { + bankKeypairA, + bankKeypairUsdc, + ecosystem, + marginfiGroup, + users, + verbose, +} from "./rootHooks"; +import { + assertBNApproximately, + assertI80F48Approx, + assertI80F48Equal, + getTokenBalance, +} from "./utils/genericTests"; +import { assert } from "chai"; +import { depositIx } from "./utils/user-instructions"; +import { USER_ACCOUNT } from "./utils/mocks"; +import { createMintToInstruction } from "@solana/spl-token"; +import { deriveLiquidityVault } from "./utils/pdas"; + +describe("Deposit funds", () => { + const program = workspace.Marginfi as Program; + const provider = getProvider() as AnchorProvider; + const wallet = provider.wallet as Wallet; + const depositAmountA = 2; + const depositAmountA_native = new BN( + depositAmountA * 10 ** ecosystem.tokenADecimals + ); + + const depositAmountUsdc = 100; + const depositAmountUsdc_native = new BN( + depositAmountUsdc * 10 ** ecosystem.usdcDecimals + ); + + it("(Fund user 0 and user 1 USDC/Token A token accounts", async () => { + let tx = new Transaction(); + for (let i = 0; i < users.length; i++) { + tx.add( + createMintToInstruction( + ecosystem.tokenAMint.publicKey, + users[i].tokenAAccount, + wallet.publicKey, + 100 * 10 ** ecosystem.tokenADecimals + ) + ); + tx.add( + createMintToInstruction( + ecosystem.usdcMint.publicKey, + users[i].usdcAccount, + wallet.publicKey, + 10000 * 10 ** ecosystem.usdcDecimals + ) + ); + } + await program.provider.sendAndConfirm(tx); + }); + + it("(user 0) deposit token A to bank - happy path", async () => { + const user = users[0]; + const [bankLiquidityVault] = deriveLiquidityVault( + program.programId, + bankKeypairA.publicKey + ); + const [userABefore, vaultABefore] = await Promise.all([ + getTokenBalance(provider, user.tokenAAccount), + getTokenBalance(provider, bankLiquidityVault), + ]); + if (verbose) { + console.log("user 0 A before: " + userABefore.toLocaleString()); + console.log("vault A before: " + vaultABefore.toLocaleString()); + } + + const user0Account = user.accounts.get(USER_ACCOUNT); + + await users[0].mrgnProgram.provider.sendAndConfirm( + new Transaction().add( + await depositIx(program, { + marginfiGroup: marginfiGroup.publicKey, + marginfiAccount: user0Account, + authority: user.wallet.publicKey, + bank: bankKeypairA.publicKey, + tokenAccount: user.tokenAAccount, + amount: depositAmountA_native, + }) + ) + ); + + const userAcc = await program.account.marginfiAccount.fetch(user0Account); + const balances = userAcc.lendingAccount.balances; + assert.equal(balances[0].active, true); + // Note: The first deposit issues shares 1:1 and the shares use the same decimals + assertI80F48Approx(balances[0].assetShares, depositAmountA_native); + assertI80F48Equal(balances[0].liabilityShares, 0); + assertI80F48Equal(balances[0].emissionsOutstanding, 0); + + let now = Math.floor(Date.now() / 1000); + assertBNApproximately(balances[0].lastUpdate, now, 2); + + const [userAAfter, vaultAAfter] = await Promise.all([ + getTokenBalance(provider, user.tokenAAccount), + getTokenBalance(provider, bankLiquidityVault), + ]); + if (verbose) { + console.log("user 0 A after: " + userAAfter.toLocaleString()); + console.log("vault A after: " + vaultAAfter.toLocaleString()); + } + assert.equal(userABefore - depositAmountA_native.toNumber(), userAAfter); + assert.equal(vaultABefore + depositAmountA_native.toNumber(), vaultAAfter); + }); + + it("(user 1) deposit USDC to bank - happy path", async () => { + const user = users[1]; + const userUsdcBefore = await getTokenBalance(provider, user.usdcAccount); + if (verbose) { + console.log("user 1 USDC before: " + userUsdcBefore.toLocaleString()); + } + + const user1Account = user.accounts.get(USER_ACCOUNT); + + await users[1].mrgnProgram.provider.sendAndConfirm( + new Transaction().add( + await depositIx(program, { + marginfiGroup: marginfiGroup.publicKey, + marginfiAccount: user1Account, + authority: user.wallet.publicKey, + bank: bankKeypairUsdc.publicKey, + tokenAccount: user.usdcAccount, + amount: depositAmountUsdc_native, + }) + ) + ); + + const userAcc = await program.account.marginfiAccount.fetch(user1Account); + const balances = userAcc.lendingAccount.balances; + assert.equal(balances[0].active, true); + // Note: The first deposit issues shares 1:1 and the shares use the same decimals + assertI80F48Approx(balances[0].assetShares, depositAmountUsdc_native); + assertI80F48Equal(balances[0].liabilityShares, 0); + assertI80F48Equal(balances[0].emissionsOutstanding, 0); + + let now = Math.floor(Date.now() / 1000); + assertBNApproximately(balances[0].lastUpdate, now, 2); + + const userUsdcAfter = await getTokenBalance(provider, user.usdcAccount); + if (verbose) { + console.log("user 1 USDC after: " + userUsdcAfter.toLocaleString()); + } + assert.equal( + userUsdcBefore - depositAmountUsdc_native.toNumber(), + userUsdcAfter + ); + }); +}); diff --git a/tests/08_borrow.spec.ts b/tests/08_borrow.spec.ts new file mode 100644 index 000000000..785c04702 --- /dev/null +++ b/tests/08_borrow.spec.ts @@ -0,0 +1,177 @@ +import { + AnchorProvider, + BN, + getProvider, + Program, + Wallet, + workspace, +} from "@coral-xyz/anchor"; +import { Transaction } from "@solana/web3.js"; +import { Marginfi } from "../target/types/marginfi"; +import { + bankKeypairA, + bankKeypairUsdc, + ecosystem, + marginfiGroup, + oracles, + users, + verbose, +} from "./rootHooks"; +import { + assertBNApproximately, + assertI80F48Approx, + assertI80F48Equal, + getTokenBalance, +} from "./utils/genericTests"; +import { assert } from "chai"; +import { borrowIx, depositIx } from "./utils/user-instructions"; +import { USER_ACCOUNT } from "./utils/mocks"; +import { createMintToInstruction } from "@solana/spl-token"; +import { updatePriceAccount } from "./utils/pyth_mocks"; +import { wrappedI80F48toBigNumber } from "@mrgnlabs/mrgn-common"; + +describe("Borrow funds", () => { + const program = workspace.Marginfi as Program; + const provider = getProvider() as AnchorProvider; + const wallet = provider.wallet as Wallet; + + // Bank has 100 USDC available to borrow + // User has 2 Token A (worth $20) deposited + const borrowAmountUsdc = 5; + const borrowAmountUsdc_native = new BN( + borrowAmountUsdc * 10 ** ecosystem.usdcDecimals + ); + + it("Oracle data refreshes", async () => { + const usdcPrice = BigInt(oracles.usdcPrice * 10 ** oracles.usdcDecimals); + await updatePriceAccount( + oracles.usdcOracle, + { + exponent: -oracles.usdcDecimals, + aggregatePriceInfo: { + price: usdcPrice, + conf: usdcPrice / BigInt(100), // 1% of the price + }, + twap: { + // aka ema + valueComponent: usdcPrice, + }, + }, + wallet + ); + + const tokenAPrice = BigInt( + oracles.tokenAPrice * 10 ** oracles.tokenADecimals + ); + await updatePriceAccount( + oracles.tokenAOracle, + { + exponent: -oracles.tokenADecimals, + aggregatePriceInfo: { + price: tokenAPrice, + conf: tokenAPrice / BigInt(100), // 1% of the price + }, + twap: { + // aka ema + valueComponent: tokenAPrice, + }, + }, + wallet + ); + }); + + it("(user 0) borrows USDC against their token A position - happy path", async () => { + const user = users[0]; + const bank = bankKeypairUsdc.publicKey; + const userUsdcBefore = await getTokenBalance(provider, user.usdcAccount); + const bankBefore = await program.account.bank.fetch(bank); + if (verbose) { + console.log("user 0 USDC before: " + userUsdcBefore.toLocaleString()); + console.log( + "usdc fees owed to bank: " + + wrappedI80F48toBigNumber( + bankBefore.collectedGroupFeesOutstanding + ).toString() + ); + console.log( + "usdc fees owed to program: " + + wrappedI80F48toBigNumber( + bankBefore.collectedProgramFeesOutstanding + ).toString() + ); + } + + const user0Account = user.accounts.get(USER_ACCOUNT); + + await users[0].mrgnProgram.provider.sendAndConfirm( + new Transaction().add( + await borrowIx(program, { + marginfiGroup: marginfiGroup.publicKey, + marginfiAccount: user0Account, + authority: user.wallet.publicKey, + bank: bank, + tokenAccount: user.usdcAccount, + remaining: [ + bankKeypairA.publicKey, + oracles.tokenAOracle.publicKey, + bank, + oracles.usdcOracle.publicKey, + ], + amount: borrowAmountUsdc_native, + }) + ) + ); + + const userAcc = await program.account.marginfiAccount.fetch(user0Account); + const bankAfter = await program.account.bank.fetch(bank); + const balances = userAcc.lendingAccount.balances; + const userUsdcAfter = await getTokenBalance(provider, user.usdcAccount); + if (verbose) { + console.log("user 0 USDC after: " + userUsdcAfter.toLocaleString()); + console.log( + "usdc fees owed to bank: " + + wrappedI80F48toBigNumber( + bankAfter.collectedGroupFeesOutstanding + ).toString() + ); + console.log( + "usdc fees owed to program: " + + wrappedI80F48toBigNumber( + bankAfter.collectedProgramFeesOutstanding + ).toString() + ); + } + + assert.equal(balances[1].active, true); + assertI80F48Equal(balances[1].assetShares, 0); + // Note: The first borrow issues shares 1:1 and the shares use the same decimals + // Note: An origination fee of 0.01 is also incurred here (configured during addBank) + const originationFee_native = borrowAmountUsdc_native.toNumber() * 0.01; + const amtUsdcWithFee_native = new BN( + borrowAmountUsdc_native.toNumber() + originationFee_native + ); + assertI80F48Approx(balances[1].liabilityShares, amtUsdcWithFee_native); + assertI80F48Equal(balances[1].emissionsOutstanding, 0); + + let now = Math.floor(Date.now() / 1000); + assertBNApproximately(balances[1].lastUpdate, now, 2); + + assert.equal( + userUsdcAfter - borrowAmountUsdc_native.toNumber(), + userUsdcBefore + ); + + // The origination fee is recorded on the bank. The group gets 98%, the program gets the + // remaining 2% (see PROGRAM_FEE_RATE) + const origination_fee_group = originationFee_native * 0.98; + const origination_fee_program = originationFee_native * 0.02; + assertI80F48Approx( + bankAfter.collectedGroupFeesOutstanding, + origination_fee_group + ); + assertI80F48Approx( + bankAfter.collectedProgramFeesOutstanding, + origination_fee_program + ); + }); +}); diff --git a/tests/fixtures/pyusd_bank.json b/tests/fixtures/pyusd_bank.json new file mode 100644 index 000000000..e59459d4d --- /dev/null +++ b/tests/fixtures/pyusd_bank.json @@ -0,0 +1,14 @@ +{ + "pubkey": "Fe5QkKPVAh629UPP5aJ8sDZu8HTfe6M26jDQkKyXVhoA", + "account": { + "lamports": 13864320, + "data": [ + "jjGm8jJCYbwXkkg7bIoqh7dHHYFPlZH5OVyECpzj2fTVun06S4p0ngbS7qNW7Dx5ehg9nAnFDP63jomJRxUTYMZvKp+sEMXJ9AAAAAAAAABqo0cIVQABAAAAAAAAAAAAerjNbJYAAQAAAAAAAAAAAD+qZNIhko6I815Qcr1imt+4Mav6RP0FS+3wO1rdzHb2//9rJOLCuVHJSdCU2JfYrAEawvoNUZ3rL2c5ocvqnISYjvz/AAAAAJVXzdUKawYAAAAAAAAAAAA78ZdGK1AwaMp/IAX8Hd174GpPOkna9ir36HsJ3+vLJf7+AAAAAAAAiYrUtxDQkwAAAAAAAAAAAKHCFhvpYRpXBQAAAAAAAADyQbEwcR8DqSUAAAAAAAAAujPXZgAAAAAAAIBmZmYAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAQAEAAAAAAAAAAACamZmZmRkBAAAAAAAAAAAAAID0IOa1AACamZmZmdkAAAAAAAAAAAAAmpmZmZkZAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMP1KFyPAgAAAAAAAAAAAADNzMzMzAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA8HaG3PX8B593VSzdmz3/NZEOVrRT3CqcG7FOExZ52aSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgPQg5rUAAAAAAAAAAAAAAAAAAAAAAAAsAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAADoAwAAAAAAALimWlHsykcAAAAAAAAAAAAXkkg7bIoqh7dHHYFPlZH5OVyECpzj2fTVun06S4p0ngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "base64" + ], + "owner": "stag8sTKds2h4KzjUw3zKTsxbqvT4XKHdaR9X9E6Rct", + "executable": false, + "rentEpoch": 18446744073709551615, + "space": 1864 + } +} \ No newline at end of file diff --git a/tests/fixtures/spl_single_pool.so b/tests/fixtures/spl_single_pool.so new file mode 100755 index 000000000..79c650a19 Binary files /dev/null and b/tests/fixtures/spl_single_pool.so differ diff --git a/tests/rootHooks.ts b/tests/rootHooks.ts index a4885eb4f..a17a6a347 100644 --- a/tests/rootHooks.ts +++ b/tests/rootHooks.ts @@ -4,10 +4,11 @@ import { echoEcosystemInfo, Ecosystem, getGenericEcosystem, - mockUser, + MockUser as MockUser, Oracles, setupTestUser, SetupTestUserOptions, + Validator, } from "./utils/mocks"; import { Marginfi } from "../target/types/marginfi"; import { @@ -15,21 +16,48 @@ import { LAMPORTS_PER_SOL, PublicKey, SystemProgram, + SYSVAR_STAKE_HISTORY_PUBKEY, Transaction, + VoteInit, + VoteProgram, } from "@solana/web3.js"; import { setupPythOracles } from "./utils/pyth_mocks"; -import { initGlobalFeeState } from "./utils/instructions"; +import { BankrunProvider } from "anchor-bankrun"; +import { BanksClient, ProgramTestContext, startAnchor } from "solana-bankrun"; +import path from "path"; +import { + findPoolAddress, + findPoolMintAddress, + findPoolStakeAddress, + findPoolStakeAuthorityAddress, + SinglePoolProgram, +} from "@solana/spl-single-pool-classic"; +import { SINGLE_POOL_PROGRAM_ID } from "./utils/types"; +import { assertKeysEqual } from "./utils/genericTests"; +import { assert } from "chai"; +import { decodeSinglePool } from "./utils/spl-staking-utils"; import { bigNumberToWrappedI80F48 } from "@mrgnlabs/mrgn-common"; +import { initGlobalFeeState } from "./utils/group-instructions"; +import { deriveGlobalFeeState } from "./utils/pdas"; export const ecosystem: Ecosystem = getGenericEcosystem(); export let oracles: Oracles = undefined; +/** Show various information about accounts and tests */ export const verbose = true; +/** Show the raw buffer printout of various structs */ +export const printBuffers = false; /** The program owner is also the provider wallet */ -export let globalProgramAdmin: mockUser = undefined; -export let groupAdmin: mockUser = undefined; +export let globalProgramAdmin: MockUser = undefined; +/** Administers the mrgnlend group and/or stake holder accounts */ +export let groupAdmin: MockUser = undefined; +/** Administers valiator votes and withdraws */ +export let validatorAdmin: MockUser = undefined; +export const users: MockUser[] = []; +export const numUsers = 3; + +export const validators: Validator[] = []; +export const numValidators = 2; export let globalFeeWallet: PublicKey = undefined; -export const users: mockUser[] = []; -export const numUsers = 2; /** Lamports charged when creating any pool */ export const INIT_POOL_ORIGINATION_FEE = 1000; @@ -43,31 +71,50 @@ export const marginfiGroup = Keypair.generate(); export const bankKeypairUsdc = Keypair.generate(); /** Bank for token A */ export const bankKeypairA = Keypair.generate(); +/** Bank for "WSOL", which is treated the same as SOL */ +export const bankKeypairSol = Keypair.generate(); + +export let bankrunContext: ProgramTestContext; +export let bankRunProvider: BankrunProvider; +export let bankrunProgram: Program; +export let banksClient: BanksClient; +/** keys copied into the bankrun instance */ +let copyKeys: PublicKey[] = []; export const mochaHooks = { beforeAll: async () => { - const program = workspace.Marginfi as Program; + // If this is false, you are in the wrong environment to run this test suite, try polyfill. + console.log("Environment supports crypto: ", !!global.crypto?.subtle); + + const mrgnProgram = workspace.Marginfi as Program; const provider = AnchorProvider.local(); const wallet = provider.wallet as Wallet; + copyKeys.push(wallet.publicKey); + if (verbose) { console.log("Global Ecosystem Information "); echoEcosystemInfo(ecosystem, { skipA: false, skipB: false, skipUsdc: false, - skipWsol: true, + skipWsol: false, }); console.log(""); } + const { ixes: wsolIxes, mint: wsolMint } = await createSimpleMint( + provider.publicKey, + provider.connection, + ecosystem.wsolDecimals, + ecosystem.wsolMint + ); const { ixes: usdcIxes, mint: usdcMint } = await createSimpleMint( provider.publicKey, provider.connection, ecosystem.usdcDecimals, ecosystem.usdcMint ); - const { ixes: aIxes, mint: aMint } = await createSimpleMint( provider.publicKey, provider.connection, @@ -80,15 +127,31 @@ export const mochaHooks = { ecosystem.tokenBDecimals, ecosystem.tokenBMint ); - const tx = new Transaction(); - tx.add(...usdcIxes); - tx.add(...aIxes); - tx.add(...bIxes); + const initMintsTx = new Transaction(); + initMintsTx.add(...wsolIxes); + initMintsTx.add(...usdcIxes); + initMintsTx.add(...aIxes); + initMintsTx.add(...bIxes); + + await provider.sendAndConfirm(initMintsTx, [ + wsolMint, + usdcMint, + aMint, + bMint, + ]); + copyKeys.push( + wsolMint.publicKey, + usdcMint.publicKey, + aMint.publicKey, + bMint.publicKey + ); + + let miscSetupTx = new Transaction(); let globalFeeKeypair = Keypair.generate(); globalFeeWallet = globalFeeKeypair.publicKey; // Send some sol to the global fee wallet for rent - tx.add( + miscSetupTx.add( SystemProgram.transfer({ fromPubkey: wallet.publicKey, toPubkey: globalFeeWallet, @@ -97,8 +160,8 @@ export const mochaHooks = { ); // Init the global fee state - tx.add( - await initGlobalFeeState(program, { + miscSetupTx.add( + await initGlobalFeeState(mrgnProgram, { payer: provider.publicKey, admin: wallet.payer.publicKey, wallet: globalFeeWallet, @@ -108,19 +171,30 @@ export const mochaHooks = { }) ); - await provider.sendAndConfirm(tx, [usdcMint, aMint, bMint]); + await provider.sendAndConfirm(miscSetupTx); + copyKeys.push( + globalFeeWallet, + deriveGlobalFeeState(mrgnProgram.programId)[0] + ); const setupUserOptions: SetupTestUserOptions = { - marginProgram: program, + marginProgram: mrgnProgram, forceWallet: undefined, // If mints are created, typically create the ATA too, otherwise pass undefined... - wsolMint: undefined, + wsolMint: ecosystem.wsolMint.publicKey, tokenAMint: ecosystem.tokenAMint.publicKey, tokenBMint: ecosystem.tokenBMint.publicKey, usdcMint: ecosystem.usdcMint.publicKey, }; groupAdmin = await setupTestUser(provider, wallet.payer, setupUserOptions); + validatorAdmin = await setupTestUser( + provider, + wallet.payer, + setupUserOptions + ); + copyKeys.push(groupAdmin.usdcAccount); + copyKeys.push(groupAdmin.wallet.publicKey); for (let i = 0; i < numUsers; i++) { const user = await setupTestUser( @@ -128,7 +202,7 @@ export const mochaHooks = { wallet.payer, setupUserOptions ); - users.push(user); + addUser(user); } // Global admin uses the payer wallet... @@ -151,5 +225,186 @@ export const mochaHooks = { ecosystem.tokenBDecimals, verbose ); + copyKeys.push(oracles.wsolOracle.publicKey); + copyKeys.push(oracles.usdcOracle.publicKey); + copyKeys.push(oracles.tokenAOracle.publicKey); + + for (let i = 0; i < numValidators; i++) { + const validator = await createValidator( + provider, + validatorAdmin.wallet, + validatorAdmin.wallet.publicKey + ); + if (verbose) { + console.log("Validator vote acc [" + i + "]: " + validator.voteAccount); + } + addValidator(validator); + + let { poolKey, poolMintKey, poolAuthority, poolStake } = + await createSplStakePool(provider, validator); + if (verbose) { + console.log(" pool..... " + poolKey); + console.log(" mint..... " + poolMintKey); + console.log(" auth..... " + poolAuthority); + console.log(" stake.... " + poolStake); + } + } + + // copyKeys.push(StakeProgram.programId); + copyKeys.push(SYSVAR_STAKE_HISTORY_PUBKEY); + + const accountKeys = copyKeys; + + const accounts = await provider.connection.getMultipleAccountsInfo( + accountKeys + ); + const addedAccounts = accountKeys.map((address, index) => ({ + address, + info: accounts[index], + })); + + bankrunContext = await startAnchor(path.resolve(), [], addedAccounts); + bankRunProvider = new BankrunProvider(bankrunContext); + bankrunProgram = new Program(mrgnProgram.idl, bankRunProvider); + banksClient = bankrunContext.banksClient; + + if (verbose) { + console.log("---End ecosystem setup---"); + console.log(""); + } }, }; + +const addValidator = (validator: Validator) => { + validators.push(validator); + // copyKeys.push(validator.authorizedVoter); + // copyKeys.push(validator.authorizedWithdrawer); + // copyKeys.push(validator.node); + copyKeys.push(validator.voteAccount); +}; + +const addUser = (user: MockUser) => { + users.push(user); + copyKeys.push(user.tokenAAccount); + // copyKeys.push(user.tokenBAccount); + copyKeys.push(user.usdcAccount); + copyKeys.push(user.wallet.publicKey); + copyKeys.push(user.wsolAccount); +}; + +/** + * Create a mock validator with given vote/withdraw authority + * * Note: Spl Pool fields (splPool, mint, authority, stake) are initialized to pubkey default. + * @param provider + * @param authorizedVoter - also pays init fees + * @param authorizedWithdrawer - also pays init fees + * @param comission - defaults to 0 + */ +export const createValidator = async ( + provider: AnchorProvider, + authorizedVoter: Keypair, + authorizedWithdrawer: PublicKey, + commission: number = 0 // Commission rate from 0 to 100 +) => { + const voteAccount = Keypair.generate(); + const node = Keypair.generate(); + + const tx = new Transaction().add( + // Create the vote account + SystemProgram.createAccount({ + fromPubkey: authorizedVoter.publicKey, + newAccountPubkey: voteAccount.publicKey, + lamports: await provider.connection.getMinimumBalanceForRentExemption( + VoteProgram.space + ), + space: VoteProgram.space, + programId: VoteProgram.programId, + }), + // Initialize the vote account + VoteProgram.initializeAccount({ + votePubkey: voteAccount.publicKey, + nodePubkey: node.publicKey, + voteInit: new VoteInit( + node.publicKey, + authorizedVoter.publicKey, + authorizedWithdrawer, + commission + ), + }) + ); + + await provider.sendAndConfirm(tx, [voteAccount, authorizedVoter, node]); + + const validator: Validator = { + node: node.publicKey, + authorizedVoter: authorizedVoter.publicKey, + authorizedWithdrawer: authorizedWithdrawer, + voteAccount: voteAccount.publicKey, + splPool: PublicKey.default, + splMint: PublicKey.default, + splAuthority: PublicKey.default, + splSolPool: PublicKey.default, + bank: PublicKey.default, + }; + + return validator; +}; + +/** + * Create a single-validator spl stake pool. Copys the pool, mint, authority, and stake accounts to + * the copyKeys slice to be deployed to bankrun + * @param provider + * @param validator - mutated, adds the spl keys (pool, mint, authority, stake) + */ +export const createSplStakePool = async ( + provider: AnchorProvider, + validator: Validator +) => { + let tx = await SinglePoolProgram.initialize( + // @ts-ignore // Doesn't matter + provider.connection, + validator.voteAccount, + users[0].wallet.publicKey, + true + ); + + // @ts-ignore // Doesn't matter + await provider.sendAndConfirm(tx, [users[0].wallet]); + + // Note: import the id from @solana/spl-single-pool (the classic version doesn't have it) + const poolKey = await findPoolAddress( + SINGLE_POOL_PROGRAM_ID, + validator.voteAccount + ); + validator.splPool = poolKey; + copyKeys.push(poolKey); + + const poolAcc = await provider.connection.getAccountInfo(poolKey); + // Rudimentary validation that this account now exists and is owned by the single pool program + assertKeysEqual(poolAcc.owner, SINGLE_POOL_PROGRAM_ID); + assert.equal(poolAcc.executable, false); + + const pool = decodeSinglePool(poolAcc.data); + assertKeysEqual(pool.voteAccountAddress, validator.voteAccount); + + const poolMintKey = await findPoolMintAddress( + SINGLE_POOL_PROGRAM_ID, + poolKey + ); + validator.splMint = poolMintKey; + copyKeys.push(poolMintKey); + + const poolStake = await findPoolStakeAddress(SINGLE_POOL_PROGRAM_ID, poolKey); + validator.splSolPool = poolStake; + copyKeys.push(poolStake); + + const poolAuthority = await findPoolStakeAuthorityAddress( + SINGLE_POOL_PROGRAM_ID, + poolKey + ); + validator.splAuthority = poolAuthority; + // Note: accounts that do not exist (blank PDAs) cannot be pushed to bankrun + // copyKeys.push(poolAuthority); + + return { poolKey, poolMintKey, poolAuthority, poolStake }; +}; diff --git a/tests/s01_usersStake.spec.ts b/tests/s01_usersStake.spec.ts new file mode 100644 index 000000000..931c9ce9e --- /dev/null +++ b/tests/s01_usersStake.spec.ts @@ -0,0 +1,316 @@ +import { BN } from "@coral-xyz/anchor"; +import { + LAMPORTS_PER_SOL, + PublicKey, + SystemProgram, + Transaction, +} from "@solana/web3.js"; +import { + bankrunContext, + bankRunProvider, + users, + validators, + verbose, + banksClient, + bankrunProgram, +} from "./rootHooks"; +import { + createStakeAccount, + delegateStake, + getEpochAndSlot, + getStakeAccount, + getStakeActivation, +} from "./utils/stake-utils"; +import { + assertBNEqual, + assertKeysEqual, + getTokenBalance, +} from "./utils/genericTests"; +import { u64MAX_BN } from "./utils/types"; +import { getAssociatedTokenAddressSync } from "@mrgnlabs/mrgn-common"; +import { + depositToSinglePoolIxes, + getBankrunBlockhash, +} from "./utils/spl-staking-utils"; +import { assert } from "chai"; +import { LST_ATA, STAKE_ACC } from "./utils/mocks"; + +describe("User stakes some native and creates an account", () => { + /** Users's validator 0 stake account */ + let user0StakeAccount: PublicKey; + const stake = 10; + + it("(user 0) Create user stake account and stake to validator", async () => { + let { createTx, stakeAccountKeypair } = createStakeAccount( + users[0], + stake * LAMPORTS_PER_SOL + ); + // Note: bankrunContext.lastBlockhash only works if non-bankrun tests didn't run previously + createTx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + createTx.sign(users[0].wallet, stakeAccountKeypair); + await banksClient.processTransaction(createTx); + user0StakeAccount = stakeAccountKeypair.publicKey; + + if (verbose) { + console.log("Create stake account: " + user0StakeAccount); + console.log( + " Stake: " + + stake + + " SOL (" + + (stake * LAMPORTS_PER_SOL).toLocaleString() + + " in native)" + ); + } + users[0].accounts.set("v0_stakeAcc", user0StakeAccount); + + let delegateTx = delegateStake( + users[0], + user0StakeAccount, + validators[0].voteAccount + ); + delegateTx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + delegateTx.sign(users[0].wallet); + await banksClient.processTransaction(delegateTx); + + if (verbose) { + console.log("user 0 delegated to " + validators[0].voteAccount); + } + + let { epoch, slot } = await getEpochAndSlot(banksClient); + const stakeAccountInfo = await bankRunProvider.connection.getAccountInfo( + user0StakeAccount + ); + const stakeAccBefore = getStakeAccount(stakeAccountInfo.data); + const meta = stakeAccBefore.meta; + const delegation = stakeAccBefore.stake.delegation; + const rent = new BN(meta.rentExemptReserve.toString()); + + assertKeysEqual(delegation.voterPubkey, validators[0].voteAccount); + assertBNEqual( + new BN(delegation.stake.toString()), + new BN(10 * LAMPORTS_PER_SOL).sub(rent) + ); + assertBNEqual(new BN(delegation.activationEpoch.toString()), epoch); + assertBNEqual(new BN(delegation.deactivationEpoch.toString()), u64MAX_BN); + + const stakeStatusBefore = await getStakeActivation( + bankRunProvider.connection, + user0StakeAccount, + epoch + ); + if (verbose) { + console.log("It is now epoch: " + epoch + " slot " + slot); + console.log( + "Stake active: " + + stakeStatusBefore.active.toLocaleString() + + " inactive " + + stakeStatusBefore.inactive.toLocaleString() + + " status: " + + stakeStatusBefore.status + ); + } + }); + + it("(user 1/2) Stakes and delegates too", async () => { + await stakeAndDelegateForUser(1, stake); + await stakeAndDelegateForUser(2, stake); + }); + + const stakeAndDelegateForUser = async ( + userIndex: number, + stakeAmount: number + ) => { + const user = users[userIndex]; + let { createTx, stakeAccountKeypair } = createStakeAccount( + user, + stakeAmount * LAMPORTS_PER_SOL + ); + + createTx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + createTx.sign(user.wallet, stakeAccountKeypair); + await banksClient.processTransaction(createTx); + user.accounts.set(STAKE_ACC, stakeAccountKeypair.publicKey); + + let delegateTx = delegateStake( + user, + stakeAccountKeypair.publicKey, + validators[0].voteAccount + ); + delegateTx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + delegateTx.sign(user.wallet); + await banksClient.processTransaction(delegateTx); + }; + + it("Advance the epoch", async () => { + bankrunContext.warpToEpoch(1n); + + let { epoch: epochAfterWarp, slot: slotAfterWarp } = await getEpochAndSlot( + banksClient + ); + if (verbose) { + console.log( + "Warped to epoch: " + epochAfterWarp + " slot " + slotAfterWarp + ); + } + + const stakeStatusAfter = await getStakeActivation( + bankRunProvider.connection, + user0StakeAccount, + epochAfterWarp + ); + if (verbose) { + console.log( + "Stake active: " + + stakeStatusAfter.active.toLocaleString() + + " inactive " + + stakeStatusAfter.inactive.toLocaleString() + + " status: " + + stakeStatusAfter.status + ); + console.log(""); + } + + // Advance a few slots and send some dummy txes to end the rewards period + + // NOTE: ALL STAKE PROGRAM IXES ARE DISABLED DURING THE REWARDS PERIOD. THIS MUST OCCUR OR THE + // STAKE PROGRAM CANNOT RUN + + if (verbose) { + console.log("Now stalling for a few slots to end the rewards period..."); + } + for (let i = 0; i < 3; i++) { + bankrunContext.warpToSlot(BigInt(i + slotAfterWarp + 1)); + const dummyTx = new Transaction(); + dummyTx.add( + SystemProgram.transfer({ + fromPubkey: users[0].wallet.publicKey, + toPubkey: bankrunProgram.provider.publicKey, + lamports: i, + }) + ); + dummyTx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + dummyTx.sign(users[0].wallet); + await banksClient.processTransaction(dummyTx); + } + + let { epoch, slot } = await getEpochAndSlot(banksClient); + if (verbose) { + console.log("It is now epoch: " + epoch + " slot " + slot); + } + }); + + it("(user 0) Deposits " + stake + "stake to the v0 LST pool", async () => { + const userStakeAccount = users[0].accounts.get(STAKE_ACC); + // Note: use `findPoolMintAddress(SINGLE_POOL_PROGRAM_ID, splPool);` if mint is not known. + const lstAta = getAssociatedTokenAddressSync( + validators[0].splMint, + users[0].wallet.publicKey + ); + users[0].accounts.set(LST_ATA, lstAta); + + // Note: user stake account exists before, but is closed after + // Here we note the balance of the stake account prior + const stakeAccountInfo = await bankRunProvider.connection.getAccountInfo( + userStakeAccount + ); + const stakeAccBefore = getStakeAccount(stakeAccountInfo.data); + const rent = new BN(stakeAccBefore.meta.rentExemptReserve.toString()); + const delegationBefore = Number( + stakeAccBefore.stake.delegation.stake.toString() + ); + assertBNEqual( + new BN(delegationBefore), + new BN(10 * LAMPORTS_PER_SOL).sub(rent) + ); + + // The spl stake pool account is already infused with 1 SOL at init + const splStakeInfoBefore = await bankRunProvider.connection.getAccountInfo( + validators[0].splSolPool + ); + const splStakePoolBefore = getStakeAccount(splStakeInfoBefore.data); + const delegationSplPoolBefore = new BN( + splStakePoolBefore.stake.delegation.stake.toString() + ); + if (verbose) { + console.log("pool stake before: " + delegationSplPoolBefore.toString()); + } + + // Create lst ata, transfer authority, execute the deposit + let tx = new Transaction(); + const ixes = await depositToSinglePoolIxes( + bankRunProvider.connection, + users[0].wallet.publicKey, + validators[0].splPool, + userStakeAccount, + verbose + ); + tx.add(...ixes); + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(users[0].wallet); + await banksClient.processTransaction(tx); + + // The stake account no longer exists + try { + const accountInfo = await bankRunProvider.connection.getAccountInfo( + userStakeAccount + ); + assert.ok( + accountInfo === null, + "The account should not exist, but it does." + ); + } catch (err) { + assert.ok(true, "The account does not exist."); + } + + const [lstAfter, splStakePoolInfo] = await Promise.all([ + getTokenBalance(bankRunProvider, lstAta), + bankRunProvider.connection.getAccountInfo(validators[0].splSolPool), + ]); + if (verbose) { + console.log("lst after: " + lstAfter.toLocaleString()); + } + // LST tokens are issued 1:1 with stake because there has been zero appreciation + // Also note that LST tokens use the same decimals. + assert.equal(lstAfter, delegationBefore); + + const splStakePool = getStakeAccount(splStakePoolInfo.data); + const delegationSplPoolAfter = new BN( + splStakePool.stake.delegation.stake.toString() + ); + if (verbose) { + console.log("pool stake after: " + delegationSplPoolAfter.toString()); + } + // The stake pool gained all of the stake that was held in the user stake acc + assertBNEqual( + delegationSplPoolAfter.sub(delegationSplPoolBefore), + delegationBefore + ); + }); + + it("(user 1/2) deposits " + stake + " to the v0 stake pool too", async () => { + await depositForUser(1); + await depositForUser(2); + }); + + const depositForUser = async (userIndex: number) => { + const user = users[userIndex]; + let tx = new Transaction(); + const ixes = await depositToSinglePoolIxes( + bankRunProvider.connection, + user.wallet.publicKey, + validators[0].splPool, + user.accounts.get(STAKE_ACC), + verbose + ); + tx.add(...ixes); + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(user.wallet); + await banksClient.processTransaction(tx); + + const lstAta = getAssociatedTokenAddressSync( + validators[0].splMint, + user.wallet.publicKey + ); + user.accounts.set(LST_ATA, lstAta); + }; +}); diff --git a/tests/s02_addBank.spec.ts b/tests/s02_addBank.spec.ts new file mode 100644 index 000000000..5e87cc19f --- /dev/null +++ b/tests/s02_addBank.spec.ts @@ -0,0 +1,504 @@ +import { BN, Program, workspace } from "@coral-xyz/anchor"; +import { AccountMeta, Keypair, PublicKey, Transaction } from "@solana/web3.js"; +import { + addBank, + addBankPermissionless, + groupInitialize, + initStakedSettings, +} from "./utils/group-instructions"; +import { Marginfi } from "../target/types/marginfi"; +import { + bankKeypairSol, + bankKeypairUsdc, + bankrunContext, + bankrunProgram, + banksClient, + ecosystem, + groupAdmin, + marginfiGroup, + oracles, + users, + validators, + verbose, +} from "./rootHooks"; +import { + assertBankrunTxFailed, + assertBNEqual, + assertI80F48Approx, + assertI80F48Equal, + assertKeyDefault, + assertKeysEqual, +} from "./utils/genericTests"; +import { + ASSET_TAG_DEFAULT, + ASSET_TAG_SOL, + ASSET_TAG_STAKED, + defaultBankConfig, + defaultStakedInterestSettings, + I80F48_ONE, + SINGLE_POOL_PROGRAM_ID, +} from "./utils/types"; +import { assert } from "chai"; +import { getBankrunBlockhash } from "./utils/spl-staking-utils"; +import { + deriveBankWithSeed, + deriveFeeVault, + deriveFeeVaultAuthority, + deriveInsuranceVault, + deriveInsuranceVaultAuthority, + deriveLiquidityVault, + deriveLiquidityVaultAuthority, + deriveStakedSettings, +} from "./utils/pdas"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; + +describe("Init group and add banks with asset category flags", () => { + const program = workspace.Marginfi as Program; + + it("(admin) Init group - happy path", async () => { + let tx = new Transaction(); + + tx.add( + await groupInitialize(program, { + marginfiGroup: marginfiGroup.publicKey, + admin: groupAdmin.wallet.publicKey, + }) + ); + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(groupAdmin.wallet, marginfiGroup); + await banksClient.processTransaction(tx); + + let group = await bankrunProgram.account.marginfiGroup.fetch( + marginfiGroup.publicKey + ); + assertKeysEqual(group.admin, groupAdmin.wallet.publicKey); + if (verbose) { + console.log("*init group: " + marginfiGroup.publicKey); + console.log(" group admin: " + group.admin); + } + }); + + // TODO add bank permissionless fails prior to opting in + + it("(admin) Init staked settings for group - opts in to use staked collateral", async () => { + const settings = defaultStakedInterestSettings( + oracles.wsolOracle.publicKey + ); + let tx = new Transaction(); + + tx.add( + await initStakedSettings(groupAdmin.mrgnProgram, { + group: marginfiGroup.publicKey, + feePayer: groupAdmin.wallet.publicKey, + settings: settings, + }) + ); + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(groupAdmin.wallet); + await banksClient.processTransaction(tx); + + const [settingsKey] = deriveStakedSettings( + program.programId, + marginfiGroup.publicKey + ); + if (verbose) { + console.log("*init staked settings: " + settingsKey); + } + + let settingsAcc = await bankrunProgram.account.stakedSettings.fetch( + settingsKey + ); + assertKeysEqual(settingsAcc.key, settingsKey); + assertKeysEqual(settingsAcc.oracle, oracles.wsolOracle.publicKey); + assertI80F48Approx(settingsAcc.assetWeightInit, 0.8); + assertI80F48Approx(settingsAcc.assetWeightMaint, 0.9); + assertBNEqual(settingsAcc.depositLimit, 1_000_000_000_000); + assertBNEqual(settingsAcc.totalAssetValueInitLimit, 150_000_000); + assert.equal(settingsAcc.oracleMaxAge, 60); + assert.deepEqual(settingsAcc.riskTier, { collateral: {} }); + }); + + it("(admin) Add bank (USDC) - is neither SOL nor staked LST", async () => { + let setConfig = defaultBankConfig(oracles.usdcOracle.publicKey); + let bankKey = bankKeypairUsdc.publicKey; + + let tx = new Transaction(); + tx.add( + await addBank(program, { + marginfiGroup: marginfiGroup.publicKey, + admin: groupAdmin.wallet.publicKey, + feePayer: groupAdmin.wallet.publicKey, + bankMint: ecosystem.usdcMint.publicKey, + bank: bankKey, + config: setConfig, + }) + ); + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(groupAdmin.wallet, bankKeypairUsdc); + await banksClient.processTransaction(tx); + + if (verbose) { + console.log("*init USDC bank " + bankKey); + } + + const bank = await bankrunProgram.account.bank.fetch(bankKey); + assert.equal(bank.config.assetTag, ASSET_TAG_DEFAULT); + }); + + it("(admin) Add bank (SOL) - is tagged as SOL", async () => { + let setConfig = defaultBankConfig(oracles.wsolOracle.publicKey); + setConfig.assetTag = ASSET_TAG_SOL; + let bankKey = bankKeypairSol.publicKey; + + let tx = new Transaction(); + tx.add( + await addBank(program, { + marginfiGroup: marginfiGroup.publicKey, + admin: groupAdmin.wallet.publicKey, + feePayer: groupAdmin.wallet.publicKey, + bankMint: ecosystem.wsolMint.publicKey, + bank: bankKey, + config: setConfig, + }) + ); + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(groupAdmin.wallet, bankKeypairSol); + await banksClient.processTransaction(tx); + + if (verbose) { + console.log("*init SOL bank " + bankKey); + } + + const bank = await bankrunProgram.account.bank.fetch(bankKey); + assert.equal(bank.config.assetTag, ASSET_TAG_SOL); + }); + + it("(admin) Tries to add staked bank WITH permission - should fail", async () => { + let setConfig = defaultBankConfig(oracles.wsolOracle.publicKey); + setConfig.assetTag = ASSET_TAG_STAKED; + setConfig.borrowLimit = new BN(0); + let bankKeypair = Keypair.generate(); + + let tx = new Transaction(); + tx.add( + await addBank(groupAdmin.mrgnProgram, { + marginfiGroup: marginfiGroup.publicKey, + admin: groupAdmin.wallet.publicKey, + feePayer: groupAdmin.wallet.publicKey, + bankMint: validators[0].splMint, + bank: bankKeypair.publicKey, + config: setConfig, + }) + ); + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(groupAdmin.wallet, bankKeypair); + let result = await banksClient.tryProcessTransaction(tx); + // AddedStakedPoolManually + assertBankrunTxFailed(result, "0x17a0"); + }); + + it("(attacker) Add bank (validator 0) with bad accounts + bad metadata - should fail", async () => { + const [settingsKey] = deriveStakedSettings( + program.programId, + marginfiGroup.publicKey + ); + const goodStakePool = validators[0].splPool; + const goodLstMint = validators[0].splMint; + const goodSolPool = validators[0].splSolPool; + + // Attacker tries to sneak in the wrong validator's information + const badStakePool = validators[1].splPool; + const badLstMint = validators[1].splMint; + const badSolPool = validators[1].splSolPool; + + const stakePools = [goodStakePool, badStakePool]; + const lstMints = [goodLstMint, badLstMint]; + const solPools = [goodSolPool, badSolPool]; + + for (const stakePool of stakePools) { + for (const lstMint of lstMints) { + for (const solPool of solPools) { + // Skip the "all good" combination + if ( + stakePool.equals(goodStakePool) && + lstMint.equals(goodLstMint) && + solPool.equals(goodSolPool) + ) { + continue; + } + + // Skip the "all bad" combination (equivalent to a valid init of validator 1) + if ( + stakePool.equals(badStakePool) && + lstMint.equals(badLstMint) && + solPool.equals(badSolPool) + ) { + continue; + } + + const oracleMeta: AccountMeta = { + pubkey: oracles.wsolOracle.publicKey, + isSigner: false, + isWritable: false, + }; + const lstMeta: AccountMeta = { + pubkey: lstMint, + isSigner: false, + isWritable: false, + }; + const solPoolMeta: AccountMeta = { + pubkey: solPool, + isSigner: false, + isWritable: false, + }; + + const ix = await program.methods + .lendingPoolAddBankPermissionless(new BN(0)) + .accounts({ + stakedSettings: settingsKey, + feePayer: users[0].wallet.publicKey, + bankMint: lstMint, + solPool: solPool, + stakePool: stakePool, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .remainingAccounts([oracleMeta, lstMeta, solPoolMeta]) + .instruction(); + + let tx = new Transaction(); + tx.add(ix); + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(users[0].wallet); + + let result = await banksClient.tryProcessTransaction(tx); + // StakePoolValidationFailed + assertBankrunTxFailed(result, "0x17a2"); + } + } + } + }); + + it("(attacker) Add bank (validator 0) with good accounts but bad metadata - should fail", async () => { + const [settingsKey] = deriveStakedSettings( + program.programId, + marginfiGroup.publicKey + ); + + const goodStakePool = validators[0].splPool; + const goodLstMint = validators[0].splMint; + const goodSolPool = validators[0].splSolPool; + + // Note: StakePool is N/A because we do not pass StakePool in meta. + // const badStakePool = validators[1].splPool; + const badLstMint = validators[1].splMint; + const badSolPool = validators[1].splSolPool; + + const lstMints = [goodLstMint, badLstMint]; + const solPools = [goodSolPool, badSolPool]; + + for (const lstMint of lstMints) { + for (const solPool of solPools) { + // Skip the all-good metadata case + if (lstMint.equals(goodLstMint) && solPool.equals(goodSolPool)) { + continue; + } + + const oracleMeta: AccountMeta = { + pubkey: oracles.wsolOracle.publicKey, + isSigner: false, + isWritable: false, + }; + const lstMeta: AccountMeta = { + pubkey: lstMint, + isSigner: false, + isWritable: false, + }; + const solPoolMeta: AccountMeta = { + pubkey: solPool, + isSigner: false, + isWritable: false, + }; + + const ix = await program.methods + .lendingPoolAddBankPermissionless(new BN(0)) + .accounts({ + stakedSettings: settingsKey, + feePayer: users[0].wallet.publicKey, + bankMint: goodLstMint, // Good key + solPool: goodSolPool, // Good key + stakePool: goodStakePool, // Good key + tokenProgram: TOKEN_PROGRAM_ID, + }) + .remainingAccounts([oracleMeta, lstMeta, solPoolMeta]) // Bad metadata keys + .instruction(); + + let tx = new Transaction(); + tx.add(ix); + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(users[0].wallet); + + let result = await banksClient.tryProcessTransaction(tx); + // StakePoolValidationFailed + assertBankrunTxFailed(result, "0x17a2"); + } + } + + // Bad oracle meta + const oracleMeta: AccountMeta = { + pubkey: oracles.usdcOracle.publicKey, // Bad meta + isSigner: false, + isWritable: false, + }; + const lstMeta: AccountMeta = { + pubkey: goodLstMint, + isSigner: false, + isWritable: false, + }; + const solPoolMeta: AccountMeta = { + pubkey: goodSolPool, + isSigner: false, + isWritable: false, + }; + + const ix = await program.methods + .lendingPoolAddBankPermissionless(new BN(0)) + .accounts({ + stakedSettings: settingsKey, + feePayer: users[0].wallet.publicKey, + bankMint: goodLstMint, // Good key + solPool: goodSolPool, // Good key + stakePool: goodStakePool, // Good key + tokenProgram: TOKEN_PROGRAM_ID, + }) + .remainingAccounts([oracleMeta, lstMeta, solPoolMeta]) // Bad oracle meta + .instruction(); + + let tx = new Transaction(); + tx.add(ix); + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(users[0].wallet); + + let result = await banksClient.tryProcessTransaction(tx); + // Note: different error + assertBankrunTxFailed(result, "0x1777"); + }); + + it("(permissionless) Add staked collateral bank (validator 0) - happy path", async () => { + const [bankKey] = deriveBankWithSeed( + program.programId, + marginfiGroup.publicKey, + validators[0].splMint, + new BN(0) + ); + validators[0].bank = bankKey; + + let tx = new Transaction(); + tx.add( + await addBankPermissionless(program, { + marginfiGroup: marginfiGroup.publicKey, + feePayer: groupAdmin.wallet.publicKey, + pythOracle: oracles.wsolOracle.publicKey, + stakePool: validators[0].splPool, + seed: new BN(0), + }) + ); + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(groupAdmin.wallet); + await banksClient.processTransaction(tx); + + if (verbose) { + console.log("*init LST bank " + validators[0].bank + " (validator 0)"); + } + const bank = await bankrunProgram.account.bank.fetch(validators[0].bank); + const [settingsKey] = deriveStakedSettings( + program.programId, + marginfiGroup.publicKey + ); + const settingsAcc = await bankrunProgram.account.stakedSettings.fetch( + settingsKey + ); + // Noteworthy fields + assert.equal(bank.config.assetTag, ASSET_TAG_STAKED); + + // Standard fields + const config = bank.config; + const interest = config.interestRateConfig; + const id = program.programId; + + assertKeysEqual(bank.mint, validators[0].splMint); + // Note: stake accounts use SOL decimals + assert.equal(bank.mintDecimals, ecosystem.wsolDecimals); + assertKeysEqual(bank.group, marginfiGroup.publicKey); + + // Keys and bumps... + const [_liqAuth, liqAuthBump] = deriveLiquidityVaultAuthority(id, bankKey); + const [liquidityVault, liqVaultBump] = deriveLiquidityVault(id, bankKey); + assertKeysEqual(bank.liquidityVault, liquidityVault); + assert.equal(bank.liquidityVaultBump, liqVaultBump); + assert.equal(bank.liquidityVaultAuthorityBump, liqAuthBump); + + const [_insAuth, insAuthBump] = deriveInsuranceVaultAuthority(id, bankKey); + const [insuranceVault, insurVaultBump] = deriveInsuranceVault(id, bankKey); + assertKeysEqual(bank.insuranceVault, insuranceVault); + assert.equal(bank.insuranceVaultBump, insurVaultBump); + assert.equal(bank.insuranceVaultAuthorityBump, insAuthBump); + + const [_feeVaultAuth, feeAuthBump] = deriveFeeVaultAuthority(id, bankKey); + const [feeVault, feeVaultBump] = deriveFeeVault(id, bankKey); + assertKeysEqual(bank.feeVault, feeVault); + assert.equal(bank.feeVaultBump, feeVaultBump); + assert.equal(bank.feeVaultAuthorityBump, feeAuthBump); + + assertKeyDefault(bank.emissionsMint); + + // Constants/Defaults... + assertI80F48Equal(bank.assetShareValue, 1); + assertI80F48Equal(bank.liabilityShareValue, 1); + assertI80F48Equal(bank.collectedInsuranceFeesOutstanding, 0); + assertI80F48Equal(bank.collectedGroupFeesOutstanding, 0); + assertI80F48Equal(bank.totalLiabilityShares, 0); + assertI80F48Equal(bank.totalAssetShares, 0); + assertBNEqual(bank.flags, 0); + assertBNEqual(bank.emissionsRate, 0); + assertI80F48Equal(bank.emissionsRemaining, 0); + + // Settings and non-default values... + assertI80F48Approx(config.assetWeightInit, settingsAcc.assetWeightInit); + assertI80F48Approx(config.assetWeightMaint, settingsAcc.assetWeightMaint); + assertI80F48Approx(config.liabilityWeightInit, 1.5); + assertI80F48Approx(config.liabilityWeightMaint, 1.25); + assertBNEqual(config.depositLimit, settingsAcc.depositLimit); + + assertI80F48Approx(interest.optimalUtilizationRate, 0.4); + assertI80F48Approx(interest.plateauInterestRate, 0.4); + assertI80F48Approx(interest.maxInterestRate, 3); + + assertI80F48Equal(interest.insuranceFeeFixedApr, 0); + assertI80F48Approx(interest.insuranceIrFee, 0.1); + assertI80F48Approx(interest.protocolFixedFeeApr, 0.01); + assertI80F48Equal(interest.protocolIrFee, 0); + + assertI80F48Equal(interest.protocolOriginationFee, 0); + + assert.deepEqual(config.operationalState, { operational: {} }); + assert.deepEqual(config.oracleSetup, { stakedWithPythPush: {} }); + assertBNEqual(config.borrowLimit, 0); + assert.deepEqual(config.riskTier, settingsAcc.riskTier); + assert.equal(config.assetTag, ASSET_TAG_STAKED); + assertBNEqual( + config.totalAssetValueInitLimit, + settingsAcc.totalAssetValueInitLimit + ); + + // Oracle information.... + assert.equal(config.oracleMaxAge, settingsAcc.oracleMaxAge); + assertKeysEqual(config.oracleKeys[0], settingsAcc.oracle); + assertKeysEqual(config.oracleKeys[1], validators[0].splMint); + assertKeysEqual(config.oracleKeys[2], validators[0].splSolPool); + + assertI80F48Equal(bank.collectedProgramFeesOutstanding, 0); + + // Timing is annoying to test in bankrun context due to clock warping + // assert.approximately(now, bank.lastUpdate.toNumber(), 2); + }); +}); diff --git a/tests/s03_deposit.spec.ts b/tests/s03_deposit.spec.ts new file mode 100644 index 000000000..35daf6a04 --- /dev/null +++ b/tests/s03_deposit.spec.ts @@ -0,0 +1,254 @@ +import { + AnchorProvider, + BN, + getProvider, + Program, + Wallet, + workspace, +} from "@coral-xyz/anchor"; +import { Keypair, Transaction } from "@solana/web3.js"; +import { Marginfi } from "../target/types/marginfi"; +import { + bankKeypairSol, + bankKeypairUsdc, + bankrunContext, + bankrunProgram, + banksClient, + ecosystem, + marginfiGroup, + users, + validators, +} from "./rootHooks"; +import { assertBankrunTxFailed, assertKeysEqual } from "./utils/genericTests"; +import { assert } from "chai"; +import { accountInit, depositIx } from "./utils/user-instructions"; +import { LST_ATA, USER_ACCOUNT } from "./utils/mocks"; +import { createMintToInstruction } from "@solana/spl-token"; +import { getBankrunBlockhash } from "./utils/spl-staking-utils"; + +describe("Deposit funds (included staked assets)", () => { + const program = workspace.Marginfi as Program; + const provider = getProvider() as AnchorProvider; + const wallet = provider.wallet as Wallet; + + it("(Fund user 0 and user 1 USDC/WSOL token accounts", async () => { + let tx = new Transaction(); + for (let i = 0; i < users.length; i++) { + // Note: WSOL is really just an spl token in this implementation, we don't simulate the + // exchange of SOL for WSOL, but that doesn't really matter. + tx.add( + createMintToInstruction( + ecosystem.wsolMint.publicKey, + users[i].wsolAccount, + wallet.publicKey, + 100 * 10 ** ecosystem.wsolDecimals + ) + ); + tx.add( + createMintToInstruction( + ecosystem.usdcMint.publicKey, + users[i].usdcAccount, + wallet.publicKey, + 10000 * 10 ** ecosystem.usdcDecimals + ) + ); + } + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(wallet.payer); + await banksClient.processTransaction(tx); + }); + + it("Initialize user accounts", async () => { + for (let i = 0; i < users.length; i++) { + const userAccKeypair = Keypair.generate(); + const userAccount = userAccKeypair.publicKey; + users[i].accounts.set(USER_ACCOUNT, userAccount); + + let user1Tx: Transaction = new Transaction(); + user1Tx.add( + await accountInit(program, { + marginfiGroup: marginfiGroup.publicKey, + marginfiAccount: userAccount, + authority: users[i].wallet.publicKey, + feePayer: users[i].wallet.publicKey, + }) + ); + user1Tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + user1Tx.sign(users[i].wallet, userAccKeypair); + await banksClient.processTransaction(user1Tx); + } + }); + + it("(user 0) deposit USDC to bank - happy path", async () => { + const user = users[0]; + const userAccount = user.accounts.get(USER_ACCOUNT); + + let tx = new Transaction().add( + await depositIx(program, { + marginfiGroup: marginfiGroup.publicKey, + marginfiAccount: userAccount, + authority: user.wallet.publicKey, + bank: bankKeypairUsdc.publicKey, + tokenAccount: user.usdcAccount, + amount: new BN(10 * 10 ** ecosystem.usdcDecimals), + }) + ); + + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(user.wallet); + await banksClient.tryProcessTransaction(tx); + + // Verify the deposit worked and the account exists + const userAcc = await bankrunProgram.account.marginfiAccount.fetch( + userAccount + ); + const balances = userAcc.lendingAccount.balances; + assert.equal(balances[0].active, true); + assertKeysEqual(balances[0].bankPk, bankKeypairUsdc.publicKey); + }); + + it("(user 0) cannot deposit to staked bank if regular deposits exists - should fail", async () => { + const user = users[0]; + const userAccount = user.accounts.get(USER_ACCOUNT); + const userLstAta = user.accounts.get(LST_ATA); + + let tx = new Transaction().add( + await depositIx(program, { + marginfiGroup: marginfiGroup.publicKey, + marginfiAccount: userAccount, + authority: user.wallet.publicKey, + bank: validators[0].bank, + tokenAccount: userLstAta, + amount: new BN(1 * 10 ** ecosystem.wsolDecimals), + }) + ); + + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(user.wallet); + let result = await banksClient.tryProcessTransaction(tx); + // AssetTagMismatch + assertBankrunTxFailed(result, "0x17a1"); + + // Verify the deposit failed and the entry does not exist + const userAcc = await bankrunProgram.account.marginfiAccount.fetch( + userAccount + ); + const balances = userAcc.lendingAccount.balances; + assert.equal(balances[1].active, false); + }); + + it("(user 1) deposits SOL to SOL bank - happy path", async () => { + const user = users[1]; + const userAccount = user.accounts.get(USER_ACCOUNT); + + let tx = new Transaction().add( + await depositIx(program, { + marginfiGroup: marginfiGroup.publicKey, + marginfiAccount: userAccount, + authority: user.wallet.publicKey, + bank: bankKeypairSol.publicKey, + tokenAccount: user.wsolAccount, + amount: new BN(2 * 10 ** ecosystem.wsolDecimals), + }) + ); + + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(user.wallet); + await banksClient.tryProcessTransaction(tx); + + // Verify the deposit worked and the account exists + const userAcc = await bankrunProgram.account.marginfiAccount.fetch( + userAccount + ); + const balances = userAcc.lendingAccount.balances; + assert.equal(balances[0].active, true); + assertKeysEqual(balances[0].bankPk, bankKeypairSol.publicKey); + }); + + it("(user 1) deposits to staked bank - should succeed (SOL co-mingle is allowed)", async () => { + const user = users[1]; + const userAccount = user.accounts.get(USER_ACCOUNT); + const userLstAta = user.accounts.get(LST_ATA); + + let tx = new Transaction().add( + await depositIx(program, { + marginfiGroup: marginfiGroup.publicKey, + marginfiAccount: userAccount, + authority: user.wallet.publicKey, + bank: validators[0].bank, + tokenAccount: userLstAta, + amount: new BN(1 * 10 ** ecosystem.wsolDecimals), + }) + ); + + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(user.wallet); + await banksClient.tryProcessTransaction(tx); + + // Verify the deposit worked and the entry exists + const userAcc = await bankrunProgram.account.marginfiAccount.fetch( + userAccount + ); + const balances = userAcc.lendingAccount.balances; + assert.equal(balances[1].active, true); + assertKeysEqual(balances[1].bankPk, validators[0].bank); + }); + + it("(user 1) cannot deposit to regular banks (USDC) with staked assets - should fail", async () => { + const user = users[1]; + const userAccount = user.accounts.get(USER_ACCOUNT); + + let tx = new Transaction().add( + await depositIx(program, { + marginfiGroup: marginfiGroup.publicKey, + marginfiAccount: userAccount, + authority: user.wallet.publicKey, + bank: bankKeypairUsdc.publicKey, + tokenAccount: user.usdcAccount, + amount: new BN(1 * 10 ** ecosystem.usdcDecimals), + }) + ); + + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(user.wallet); + let result = await banksClient.tryProcessTransaction(tx); + // AssetTagMismatch + assertBankrunTxFailed(result, "0x17a1"); + + // Verify the deposit failed and the entry does not exist + const userAcc = await bankrunProgram.account.marginfiAccount.fetch( + userAccount + ); + const balances = userAcc.lendingAccount.balances; + assert.equal(balances[2].active, false); + }); + + it("(user 2) deposits to staked bank - should succeed", async () => { + const user = users[2]; + const userAccount = user.accounts.get(USER_ACCOUNT); + const userLstAta = user.accounts.get(LST_ATA); + + let tx = new Transaction().add( + await depositIx(program, { + marginfiGroup: marginfiGroup.publicKey, + marginfiAccount: userAccount, + authority: user.wallet.publicKey, + bank: validators[0].bank, + tokenAccount: userLstAta, + amount: new BN(1 * 10 ** ecosystem.wsolDecimals), + }) + ); + + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(user.wallet); + await banksClient.tryProcessTransaction(tx); + + // Verify the deposit worked and the entry exists + const userAcc = await bankrunProgram.account.marginfiAccount.fetch( + userAccount + ); + const balances = userAcc.lendingAccount.balances; + assert.equal(balances[0].active, true); + assertKeysEqual(balances[0].bankPk, validators[0].bank); + }); +}); diff --git a/tests/s04_borrow.spec.ts b/tests/s04_borrow.spec.ts new file mode 100644 index 000000000..20852085f --- /dev/null +++ b/tests/s04_borrow.spec.ts @@ -0,0 +1,124 @@ +import { + AnchorProvider, + BN, + getProvider, + Program, + Wallet, + workspace, +} from "@coral-xyz/anchor"; +import { Keypair, Transaction } from "@solana/web3.js"; +import { Marginfi } from "../target/types/marginfi"; +import { + bankKeypairA, + bankKeypairSol, + bankKeypairUsdc, + bankrunContext, + bankrunProgram, + bankRunProvider, + banksClient, + ecosystem, + groupAdmin, + marginfiGroup, + numUsers, + oracles, + users, + validators, + verbose, +} from "./rootHooks"; +import { + assertBankrunTxFailed, + assertBNApproximately, + assertI80F48Approx, + assertI80F48Equal, + assertKeysEqual, + getTokenBalance, +} from "./utils/genericTests"; +import { assert } from "chai"; +import { accountInit, borrowIx, depositIx } from "./utils/user-instructions"; +import { USER_ACCOUNT } from "./utils/mocks"; +import { createMintToInstruction } from "@solana/spl-token"; +import { deriveLiquidityVault } from "./utils/pdas"; +import { getBankrunBlockhash } from "./utils/spl-staking-utils"; +import { BanksTransactionResultWithMeta } from "solana-bankrun"; + +describe("Deposit funds (included staked assets)", () => { + const program = workspace.Marginfi as Program; + + // User 0 has a USDC deposit position + // User 1 has a SOL [0] and validator 0 Staked [1] deposit position + + it("(user 0) borrows SOL against their USDC position - succeeds (SOL/regular comingle is allowed)", async () => { + const user = users[0]; + const userAccount = user.accounts.get(USER_ACCOUNT); + + let tx = new Transaction().add( + await borrowIx(program, { + marginfiGroup: marginfiGroup.publicKey, + marginfiAccount: userAccount, + authority: user.wallet.publicKey, + bank: bankKeypairSol.publicKey, + tokenAccount: user.wsolAccount, + remaining: [ + bankKeypairUsdc.publicKey, + oracles.usdcOracle.publicKey, + bankKeypairSol.publicKey, + oracles.wsolOracle.publicKey, + ], + amount: new BN(0.01 * 10 ** ecosystem.wsolDecimals), + }) + ); + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(user.wallet); + await banksClient.processTransaction(tx); + + const userAcc = await bankrunProgram.account.marginfiAccount.fetch( + userAccount + ); + const balances = userAcc.lendingAccount.balances; + assert.equal(balances[1].active, true); + assertKeysEqual(balances[1].bankPk, bankKeypairSol.publicKey); + }); + + // Note: Borrowing STAKED assets is generally forbidden (their borrow cap is set to 0) + // If we ever change this, add a test here to validate user 0 cannot borrow staked assets + + it("(user 1) tries to borrow USDC - should fail (Regular assets cannot comingle with Staked)", async () => { + const user = users[1]; + const userAccount = user.accounts.get(USER_ACCOUNT); + + let tx = new Transaction().add( + await borrowIx(program, { + marginfiGroup: marginfiGroup.publicKey, + marginfiAccount: userAccount, + authority: user.wallet.publicKey, + bank: bankKeypairUsdc.publicKey, + tokenAccount: user.usdcAccount, + remaining: [ + bankKeypairSol.publicKey, + oracles.wsolOracle.publicKey, + validators[0].bank, + oracles.wsolOracle.publicKey, // Note the Staked bank uses wsol oracle too + validators[0].splMint, + validators[0].splSolPool, + bankKeypairUsdc.publicKey, + oracles.usdcOracle.publicKey, + ], + amount: new BN(0.1 * 10 ** ecosystem.usdcDecimals), + }) + ); + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(user.wallet); + let result = await banksClient.tryProcessTransaction(tx); + // AssetTagMismatch + assertBankrunTxFailed(result, "0x17a1"); + + // Verify the deposit worked and the entry does not exist + const userAcc = await bankrunProgram.account.marginfiAccount.fetch( + userAccount + ); + const balances = userAcc.lendingAccount.balances; + assert.equal(balances[2].active, false); + }); + + // TODO withdraw user 1's SOL collateral and verify they can borrow SOL +}); diff --git a/tests/s05_solAppreciates.spec.ts b/tests/s05_solAppreciates.spec.ts new file mode 100644 index 000000000..36b2885c2 --- /dev/null +++ b/tests/s05_solAppreciates.spec.ts @@ -0,0 +1,195 @@ +import { + AnchorProvider, + BN, + getProvider, + Program, + Wallet, + workspace, +} from "@coral-xyz/anchor"; +import { LAMPORTS_PER_SOL, SystemProgram, Transaction } from "@solana/web3.js"; +import { Marginfi } from "../target/types/marginfi"; +import { + bankKeypairSol, + bankrunContext, + bankrunProgram, + banksClient, + ecosystem, + marginfiGroup, + oracles, + users, + validators, + verbose, +} from "./rootHooks"; +import { + assertBankrunTxFailed, + assertI80F48Approx, + assertKeysEqual, +} from "./utils/genericTests"; +import { assert } from "chai"; +import { borrowIx } from "./utils/user-instructions"; +import { USER_ACCOUNT } from "./utils/mocks"; +import { getBankrunBlockhash } from "./utils/spl-staking-utils"; +import { wrappedI80F48toBigNumber } from "@mrgnlabs/mrgn-common"; +import { dumpBankrunLogs } from "./utils/tools"; + +describe("Borrow power grows as v0 Staked SOL gains value from appreciation", () => { + const program = workspace.Marginfi as Program; + const provider = getProvider() as AnchorProvider; + const wallet = provider.wallet as Wallet; + + // User 2 has a validator 0 staked depost [0] position - net value = 1 LST token + // Users 0/1/2 deposited 10 SOL each, so a total of 30 is staked with validator 0 + /** SOL to add to the validator as pretend-earned epoch rewards */ + const appreciation = 30; + + it("(user 2) tries to borrow 1.1 SOL against 1 v0 STAKED - fails, not enough funds", async () => { + const user = users[2]; + const userAccount = user.accounts.get(USER_ACCOUNT); + + let tx = new Transaction().add( + await borrowIx(program, { + marginfiGroup: marginfiGroup.publicKey, + marginfiAccount: userAccount, + authority: user.wallet.publicKey, + bank: bankKeypairSol.publicKey, + tokenAccount: user.wsolAccount, + remaining: [ + validators[0].bank, + oracles.wsolOracle.publicKey, + validators[0].splMint, + validators[0].splSolPool, + bankKeypairSol.publicKey, + oracles.wsolOracle.publicKey, + ], + amount: new BN(1.1 * 10 ** ecosystem.wsolDecimals), + }) + ); + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(user.wallet); + let result = await banksClient.tryProcessTransaction(tx); + + // 6010 (Generic risk engine rejection) + assertBankrunTxFailed(result, "0x177a"); + + const userAcc = await bankrunProgram.account.marginfiAccount.fetch( + userAccount + ); + const balances = userAcc.lendingAccount.balances; + assert.equal(balances[1].active, false); + }); + + // Note: there is also some natural appreciation here because a few epochs have elapsed... + + // Here we mock epoch rewards by simply minting SOL into the validator's pool without staking + it("v0 stake grows by " + appreciation + " SOL", async () => { + let tx = new Transaction(); + tx.add( + SystemProgram.transfer({ + fromPubkey: wallet.publicKey, + toPubkey: validators[0].splSolPool, + lamports: appreciation * LAMPORTS_PER_SOL, + }) + ); + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(wallet.payer); + await banksClient.processTransaction(tx); + }); + + it("(user 2 - attacker) ties to sneak in bad lst mint - should fail", async () => { + const user = users[2]; + const userAccount = user.accounts.get(USER_ACCOUNT); + let tx = new Transaction().add( + await borrowIx(program, { + marginfiGroup: marginfiGroup.publicKey, + marginfiAccount: userAccount, + authority: user.wallet.publicKey, + bank: bankKeypairSol.publicKey, + tokenAccount: user.wsolAccount, + remaining: [ + validators[0].bank, + oracles.wsolOracle.publicKey, + validators[1].splMint, // Bad mint + validators[0].splSolPool, + bankKeypairSol.publicKey, + oracles.wsolOracle.publicKey, + ], + amount: new BN(0.1 * 10 ** ecosystem.wsolDecimals), + }) + ); + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(user.wallet); + let result = await banksClient.tryProcessTransaction(tx); + + // Throws 6007 (InvalidOracleAccount) first at `try_from_bank_config_with_max_age` which is + // converted to 6010 (Generic risk engine rejection) downstream + assertBankrunTxFailed(result, "0x177a"); + }); + + it("(user 2 - attacker) ties to sneak in bad sol pool - should fail", async () => { + const user = users[2]; + const userAccount = user.accounts.get(USER_ACCOUNT); + let tx = new Transaction().add( + await borrowIx(program, { + marginfiGroup: marginfiGroup.publicKey, + marginfiAccount: userAccount, + authority: user.wallet.publicKey, + bank: bankKeypairSol.publicKey, + tokenAccount: user.wsolAccount, + remaining: [ + validators[0].bank, + oracles.wsolOracle.publicKey, + validators[0].splMint, + validators[1].splSolPool, // Bad pool + bankKeypairSol.publicKey, + oracles.wsolOracle.publicKey, + ], + amount: new BN(0.2 * 10 ** ecosystem.wsolDecimals), + }) + ); + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(user.wallet); + let result = await banksClient.tryProcessTransaction(tx); + + // Throws 6007 (InvalidOracleAccount) first at `try_from_bank_config_with_max_age` which is + // converted to 6010 (Generic risk engine rejection) downstream + assertBankrunTxFailed(result, "0x177a"); + }); + + // The account is now worth enough for this borrow to succeed! + it("(user 2) borrows 1.1 SOL against their STAKED position - succeeds", async () => { + const user = users[2]; + const userAccount = user.accounts.get(USER_ACCOUNT); + let tx = new Transaction().add( + await borrowIx(program, { + marginfiGroup: marginfiGroup.publicKey, + marginfiAccount: userAccount, + authority: user.wallet.publicKey, + bank: bankKeypairSol.publicKey, + tokenAccount: user.wsolAccount, + remaining: [ + validators[0].bank, + oracles.wsolOracle.publicKey, + validators[0].splMint, + validators[0].splSolPool, + bankKeypairSol.publicKey, + oracles.wsolOracle.publicKey, + ], + // Note: We use a different (slightly higher) amount, so Bankrun treats this as a different + // tx. Using the exact same values as above can cause the test to fail on faster machines + // because the same tx was already sent for this blockhash (i.e. "this transaction has + // already been processed") + amount: new BN(1.111 * 10 ** ecosystem.wsolDecimals), + }) + ); + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(user.wallet); + await banksClient.processTransaction(tx); + + const userAcc = await bankrunProgram.account.marginfiAccount.fetch( + userAccount + ); + const balances = userAcc.lendingAccount.balances; + assert.equal(balances[1].active, true); + assertKeysEqual(balances[1].bankPk, bankKeypairSol.publicKey); + }); +}); diff --git a/tests/s06_propagateSets.spec.ts b/tests/s06_propagateSets.spec.ts new file mode 100644 index 000000000..1d69552eb --- /dev/null +++ b/tests/s06_propagateSets.spec.ts @@ -0,0 +1,182 @@ +import { workspace, Program } from "@coral-xyz/anchor"; +import { PublicKey, Transaction } from "@solana/web3.js"; +import BN from "bn.js"; +import { Marginfi } from "../target/types/marginfi"; +import { + marginfiGroup, + validators, + groupAdmin, + oracles, + bankrunContext, + banksClient, + bankrunProgram, +} from "./rootHooks"; +import { + editStakedSettings, + propagateStakedSettings, +} from "./utils/group-instructions"; +import { deriveBankWithSeed, deriveStakedSettings } from "./utils/pdas"; +import { getBankrunBlockhash } from "./utils/spl-staking-utils"; +import { bigNumberToWrappedI80F48 } from "@mrgnlabs/mrgn-common"; +import { assert } from "chai"; +import { + assertKeysEqual, + assertI80F48Approx, + assertBNEqual, + assertBankrunTxFailed, +} from "./utils/genericTests"; +import { + defaultStakedInterestSettings, + StakedSettingsEdit, +} from "./utils/types"; + +describe("Edit and propagate staked settings", () => { + const program = workspace.Marginfi as Program; + + let settingsKey: PublicKey; + let bankKey: PublicKey; + + before(async () => { + [settingsKey] = deriveStakedSettings( + program.programId, + marginfiGroup.publicKey + ); + [bankKey] = deriveBankWithSeed( + program.programId, + marginfiGroup.publicKey, + validators[0].splMint, + new BN(0) + ); + }); + + it("(admin) edits some settings - happy path", async () => { + const settings: StakedSettingsEdit = { + oracle: oracles.usdcOracle.publicKey, + assetWeightInit: bigNumberToWrappedI80F48(0.2), + assetWeightMaint: bigNumberToWrappedI80F48(0.3), + depositLimit: new BN(42), + totalAssetValueInitLimit: new BN(43), + oracleMaxAge: 44, + riskTier: { + collateral: undefined, + }, + }; + let tx = new Transaction().add( + await editStakedSettings(groupAdmin.mrgnProgram, { + settingsKey: settingsKey, + settings: settings, + }) + ); + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(groupAdmin.wallet); + await banksClient.processTransaction(tx); + + let settingsAcc = await bankrunProgram.account.stakedSettings.fetch( + settingsKey + ); + assertKeysEqual(settingsAcc.key, settingsKey); + assertKeysEqual(settingsAcc.oracle, oracles.usdcOracle.publicKey); + assertI80F48Approx(settingsAcc.assetWeightInit, 0.2); + assertI80F48Approx(settingsAcc.assetWeightMaint, 0.3); + assertBNEqual(settingsAcc.depositLimit, 42); + assertBNEqual(settingsAcc.totalAssetValueInitLimit, 43); + assert.equal(settingsAcc.oracleMaxAge, 44); + assert.deepEqual(settingsAcc.riskTier, { collateral: {} }); + }); + + it("(permissionless) Propagate staked settings to a bank - happy path", async () => { + let tx = new Transaction(); + tx.add( + await propagateStakedSettings(program, { + settings: settingsKey, + bank: bankKey, + oracle: oracles.usdcOracle.publicKey, + }) + ); + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(groupAdmin.wallet); // just to the pay the fee + let result = await banksClient.tryProcessTransaction(tx); + + const bank = await bankrunProgram.account.bank.fetch(bankKey); + const config = bank.config; + assertKeysEqual(config.oracleKeys[0], oracles.usdcOracle.publicKey); + assertI80F48Approx(config.assetWeightInit, 0.2); + assertI80F48Approx(config.assetWeightMaint, 0.3); + assertBNEqual(config.depositLimit, 42); + assertBNEqual(config.totalAssetValueInitLimit, 43); + assert.equal(config.oracleMaxAge, 44); + assert.deepEqual(config.riskTier, { collateral: {} }); + }); + + it("(admin) sets a bad oracle - fails at propagation", async () => { + const settings: StakedSettingsEdit = { + oracle: PublicKey.default, + assetWeightInit: null, + assetWeightMaint: null, + depositLimit: null, + totalAssetValueInitLimit: null, + oracleMaxAge: null, + riskTier: null, + }; + let tx = new Transaction().add( + await editStakedSettings(groupAdmin.mrgnProgram, { + settingsKey: settingsKey, + settings: settings, + }) + ); + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(groupAdmin.wallet); + await banksClient.processTransaction(tx); + + let settingsAcc = await bankrunProgram.account.stakedSettings.fetch( + settingsKey + ); + assertKeysEqual(settingsAcc.oracle, PublicKey.default); + + tx = new Transaction(); + tx.add( + await propagateStakedSettings(program, { + settings: settingsKey, + bank: bankKey, + oracle: PublicKey.default, + }) + ); + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(groupAdmin.wallet); // just to the pay the fee + let result = await banksClient.tryProcessTransaction(tx); + + // 6007 (InvalidOracleAccount) + assertBankrunTxFailed(result, "0x1777"); + }); + + it("(admin) restores default settings - happy path", async () => { + const defaultSettings = defaultStakedInterestSettings( + oracles.wsolOracle.publicKey + ); + const settings: StakedSettingsEdit = { + oracle: defaultSettings.oracle, + assetWeightInit: defaultSettings.assetWeightInit, + assetWeightMaint: defaultSettings.assetWeightMaint, + depositLimit: defaultSettings.depositLimit, + totalAssetValueInitLimit: defaultSettings.totalAssetValueInitLimit, + oracleMaxAge: defaultSettings.oracleMaxAge, + riskTier: defaultSettings.riskTier, + }; + // Note you can pack propagates into the edit tx, so with a LUT you can easily propagate + // hundreds of banks in the same ts as edit + let tx = new Transaction().add( + await editStakedSettings(groupAdmin.mrgnProgram, { + settingsKey: settingsKey, + settings: settings, + }), + await propagateStakedSettings(program, { + settings: settingsKey, + bank: bankKey, + oracle: defaultSettings.oracle, + }) + ); + tx.recentBlockhash = await getBankrunBlockhash(bankrunContext); + tx.sign(groupAdmin.wallet); + await banksClient.processTransaction(tx); + }); +}); diff --git a/tests/utils/genericTests.ts b/tests/utils/genericTests.ts index 56ef649f2..f9b0a2f79 100644 --- a/tests/utils/genericTests.ts +++ b/tests/utils/genericTests.ts @@ -3,9 +3,11 @@ import { WrappedI80F48, wrappedI80F48toBigNumber } from "@mrgnlabs/mrgn-common"; import type { RawAccount } from "@solana/spl-token"; import { AccountLayout } from "@solana/spl-token"; import { PublicKey } from "@solana/web3.js"; +import { BankrunProvider } from "anchor-bankrun"; import BigNumber from "bignumber.js"; import BN from "bn.js"; import { assert } from "chai"; +import { BanksTransactionResultWithMeta } from "solana-bankrun"; /** * Shorthand for `assert.equal(a.toString(), b.toString())` @@ -64,10 +66,10 @@ export const assertI80F48Equal = ( }; /** - * Shorthand to convert I80F48 to a string and compare against a BN, number, or other WrappedI80F48 within a given tolerance + * Shorthand to convert I80F48 to a BigNumber and compare against a BN, number, or other WrappedI80F48 within a given tolerance * @param a * @param b - * @param tolerance - the allowed difference between the two values + * @param tolerance - the allowed difference between the two values (default .000001) */ export const assertI80F48Approx = ( a: WrappedI80F48, @@ -92,7 +94,8 @@ export const assertI80F48Approx = ( if (diff.isGreaterThan(allowedDifference)) { throw new Error( - `Values are not approximately equal. Difference: ${diff.toString()}, Allowed Tolerance: ${tolerance}` + `Values are not approximately equal. A: ${bigA.toString()} B: ${bigB.toString()} + Difference: ${diff.toString()}, Allowed Tolerance: ${tolerance}` ); } }; @@ -131,7 +134,7 @@ export const assertBNApproximately = ( * @returns */ export const getTokenBalance = async ( - provider: AnchorProvider, + provider: AnchorProvider | BankrunProvider, account: PublicKey ) => { const accountInfo = await provider.connection.getAccountInfo(account); @@ -173,3 +176,23 @@ export const waitUntil = async ( const toWait = Math.ceil(time - now) * 1000; await new Promise((r) => setTimeout(r, toWait)); }; + +/** + * Assert a bankrun Tx executed with `tryProcessTransaction` failed with the expected error code. + * Throws an error if the tx succeeded or a different error was found. + * @param result + * @param expectedErrorCode - In hex, as you see in Anchor logs, e.g. for error 6047 pass `0x179f` + */ +export const assertBankrunTxFailed = ( + result: BanksTransactionResultWithMeta, + expectedErrorCode: string +) => { + expectedErrorCode = expectedErrorCode.toLocaleLowerCase(); + assert(result.meta.logMessages.length > 0); + assert(result.result, "TX succeeded when it should have failed"); + const lastLog = result.meta.logMessages.pop(); + assert( + lastLog.includes(expectedErrorCode), + "\nExpected code " + expectedErrorCode + " but got: " + lastLog + ); +}; diff --git a/tests/utils/group-instructions.ts b/tests/utils/group-instructions.ts new file mode 100644 index 000000000..3bf5bcb6f --- /dev/null +++ b/tests/utils/group-instructions.ts @@ -0,0 +1,451 @@ +import { BN, Program } from "@coral-xyz/anchor"; +import { AccountMeta, PublicKey, SYSVAR_RENT_PUBKEY } from "@solana/web3.js"; +import { Marginfi } from "../../target/types/marginfi"; +import { + deriveBankWithSeed, + deriveFeeVault, + deriveFeeVaultAuthority, + deriveInsuranceVault, + deriveInsuranceVaultAuthority, + deriveLiquidityVault, + deriveLiquidityVaultAuthority, + deriveStakedSettings, +} from "./pdas"; +import { + BankConfig, + BankConfigOptWithAssetTag, + SINGLE_POOL_PROGRAM_ID, + StakedSettingsConfig, + StakedSettingsEdit, +} from "./types"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { BankConfigOptRaw } from "@mrgnlabs/marginfi-client-v2"; +import { WrappedI80F48 } from "@mrgnlabs/mrgn-common"; + +export const MAX_ORACLE_KEYS = 5; + +/** + * * admin/feePayer - must sign + * * bank - use a fresh keypair, must sign + */ +export type AddBankArgs = { + marginfiGroup: PublicKey; + admin: PublicKey; + feePayer: PublicKey; + bankMint: PublicKey; + bank: PublicKey; + config: BankConfig; +}; + +export const addBank = (program: Program, args: AddBankArgs) => { + // const id = program.programId; + // const bank = args.bank; + + // Note: oracle is passed as a key in config AND as an acc in remaining accs + const oracleMeta: AccountMeta = { + pubkey: args.config.oracleKey, + isSigner: false, + isWritable: false, + }; + + const ix = program.methods + .lendingPoolAddBank({ + assetWeightInit: args.config.assetWeightInit, + assetWeightMaint: args.config.assetWeightMaint, + liabilityWeightInit: args.config.liabilityWeightInit, + liabilityWeightMaint: args.config.liabilityWeightMain, + depositLimit: args.config.depositLimit, + interestRateConfig: args.config.interestRateConfig, + operationalState: args.config.operationalState, + oracleSetup: args.config.oracleSetup, + oracleKey: args.config.oracleKey, + borrowLimit: args.config.borrowLimit, + riskTier: args.config.riskTier, + assetTag: args.config.assetTag, + pad0: [0, 0, 0, 0, 0, 0], + totalAssetValueInitLimit: args.config.totalAssetValueInitLimit, + oracleMaxAge: args.config.oracleMaxAge, + }) + .accounts({ + marginfiGroup: args.marginfiGroup, + admin: args.admin, + feePayer: args.feePayer, + bankMint: args.bankMint, + bank: args.bank, + // globalFeeState: deriveGlobalFeeState(id), + // globalFeeWallet: args.globalFeeWallet, + // liquidityVaultAuthority = deriveLiquidityVaultAuthority(id, bank); + // liquidityVault = deriveLiquidityVault(id, bank); + // insuranceVaultAuthority = deriveInsuranceVaultAuthority(id, bank); + // insuranceVault = deriveInsuranceVault(id, bank); + // feeVaultAuthority = deriveFeeVaultAuthority(id, bank); + // feeVault = deriveFeeVault(id, bank); + // rent = SYSVAR_RENT_PUBKEY + tokenProgram: TOKEN_PROGRAM_ID, + // systemProgram: SystemProgram.programId, + }) + .remainingAccounts([oracleMeta]) + .instruction(); + + return ix; +}; + +/** + * newAdmin - (Optional) pass null to keep current admin + * admin - must sign, must be current admin of marginfiGroup + */ +export type GroupConfigureArgs = { + newAdmin: PublicKey | null; + marginfiGroup: PublicKey; + admin: PublicKey; +}; + +export const groupConfigure = ( + program: Program, + args: GroupConfigureArgs +) => { + const ix = program.methods + .marginfiGroupConfigure({ admin: args.newAdmin }) + .accounts({ + marginfiGroup: args.marginfiGroup, + admin: args.admin, + }) + .instruction(); + + return ix; +}; + +export type GroupInitializeArgs = { + marginfiGroup: PublicKey; + admin: PublicKey; +}; + +export const groupInitialize = ( + program: Program, + args: GroupInitializeArgs +) => { + const ix = program.methods + .marginfiGroupInitialize() + .accounts({ + marginfiGroup: args.marginfiGroup, + // feeState: deriveGlobalFeeState(id), + admin: args.admin, + // systemProgram: SystemProgram.programId, + }) + .instruction(); + + return ix; +}; + +export type ConfigureBankArgs = { + marginfiGroup: PublicKey; + admin: PublicKey; + bank: PublicKey; + bankConfigOpt: BankConfigOptWithAssetTag; // BankConfigOptRaw + assetTag +}; + +export const configureBank = ( + program: Program, + args: ConfigureBankArgs +) => { + const ix = program.methods + .lendingPoolConfigureBank(args.bankConfigOpt) + .accounts({ + marginfiGroup: args.marginfiGroup, + admin: args.admin, + bank: args.bank, + }) + .instruction(); + return ix; +}; + +export type SetupEmissionsArgs = { + marginfiGroup: PublicKey; + admin: PublicKey; + bank: PublicKey; + emissionsMint: PublicKey; + fundingAccount: PublicKey; + emissionsFlags: BN; + emissionsRate: BN; + totalEmissions: BN; +}; + +export const setupEmissions = ( + program: Program, + args: SetupEmissionsArgs +) => { + const ix = program.methods + .lendingPoolSetupEmissions( + args.emissionsFlags, + args.emissionsRate, + args.totalEmissions + ) + .accounts({ + marginfiGroup: args.marginfiGroup, + admin: args.admin, + bank: args.bank, + emissionsMint: args.emissionsMint, + // emissionsAuth: deriveEmissionsAuth() + // emissionsTokenAccount: deriveEmissionsTokenAccount() + emissionsFundingAccount: args.fundingAccount, + tokenProgram: TOKEN_PROGRAM_ID, + // systemProgram: SystemProgram.programId, + }) + .instruction(); + return ix; +}; + +export type UpdateEmissionsArgs = { + marginfiGroup: PublicKey; + admin: PublicKey; + bank: PublicKey; + emissionsMint: PublicKey; + fundingAccount: PublicKey; + emissionsFlags: BN | null; + emissionsRate: BN | null; + additionalEmissions: BN | null; +}; + +export const updateEmissions = ( + program: Program, + args: UpdateEmissionsArgs +) => { + const ix = program.methods + .lendingPoolUpdateEmissionsParameters( + args.emissionsFlags, + args.emissionsRate, + args.additionalEmissions + ) + .accounts({ + marginfiGroup: args.marginfiGroup, + admin: args.admin, + bank: args.bank, + emissionsMint: args.emissionsMint, + // emissionsAuth: deriveEmissionsAuth() + // emissionsTokenAccount: deriveEmissionsTokenAccount() + emissionsFundingAccount: args.fundingAccount, + tokenProgram: TOKEN_PROGRAM_ID, + // systemProgram: SystemProgram.programId, + }) + .instruction(); + return ix; +}; + +// ************* Below this line, not yet included in package **************** + +export type InitGlobalFeeStateArgs = { + payer: PublicKey; + admin: PublicKey; + wallet: PublicKey; + bankInitFlatSolFee: number; + programFeeFixed: WrappedI80F48; + programFeeRate: WrappedI80F48; +}; + +export const initGlobalFeeState = ( + program: Program, + args: InitGlobalFeeStateArgs +) => { + const ix = program.methods + .initGlobalFeeState( + args.admin, + args.wallet, + args.bankInitFlatSolFee, + args.programFeeFixed, + args.programFeeRate + ) + .accounts({ + payer: args.payer, + // feeState = deriveGlobalFeeState(id), + // rent = SYSVAR_RENT_PUBKEY, + // systemProgram: SystemProgram.programId, + }) + .instruction(); + + return ix; +}; + +export type EditGlobalFeeStateArgs = { + admin: PublicKey; + wallet: PublicKey; + bankInitFlatSolFee: number; + programFeeFixed: WrappedI80F48; + programFeeRate: WrappedI80F48; +}; + +// TODO add test for this +export const editGlobalFeeState = ( + program: Program, + args: EditGlobalFeeStateArgs +) => { + const ix = program.methods + .editGlobalFeeState( + args.wallet, + args.bankInitFlatSolFee, + args.programFeeFixed, + args.programFeeRate + ) + .accounts({ + globalFeeAdmin: args.admin, + // feeState = deriveGlobalFeeState(id), + }) + .instruction(); + + return ix; +}; + +// TODO propagate fee state and test + +export type InitStakedSettingsArgs = { + group: PublicKey; + feePayer: PublicKey; + settings: StakedSettingsConfig; +}; + +export const initStakedSettings = ( + program: Program, + args: InitStakedSettingsArgs +) => { + const ix = program.methods + .initStakedSettings(args.settings) + .accounts({ + marginfiGroup: args.group, + // admin: args.admin, // implied from group + feePayer: args.feePayer, + // staked_settings: deriveStakedSettings() + // rent = SYSVAR_RENT_PUBKEY, + // systemProgram: SystemProgram.programId, + }) + .instruction(); + + return ix; +}; + +export type EditStakedSettingsArgs = { + settingsKey: PublicKey; + settings: StakedSettingsEdit; +}; + +export const editStakedSettings = ( + program: Program, + args: EditStakedSettingsArgs +) => { + const ix = program.methods + .editStakedSettings(args.settings) + .accounts({ + // marginfiGroup: args.group, // implied from stakedSettings + // admin: args.admin, // implied from group + stakedSettings: args.settingsKey, + // rent = SYSVAR_RENT_PUBKEY, + // systemProgram: SystemProgram.programId, + }) + .instruction(); + + return ix; +}; + +/** + * oracle - required only if settings updates the oracle key + */ +export type PropagateStakedSettingsArgs = { + settings: PublicKey; + bank: PublicKey; + oracle?: PublicKey; +}; + +export const propagateStakedSettings = ( + program: Program, + args: PropagateStakedSettingsArgs +) => { + const remainingAccounts = args.oracle + ? [ + { + pubkey: args.oracle, + isSigner: false, + isWritable: false, + } as AccountMeta, + ] + : []; + + const ix = program.methods + .propagateStakedSettings() + .accounts({ + // marginfiGroup: args.group, // implied from stakedSettings + stakedSettings: args.settings, + bank: args.bank, + }) + .remainingAccounts(remainingAccounts) + .instruction(); + + return ix; +}; + +export type AddBankPermissionlessArgs = { + marginfiGroup: PublicKey; + feePayer: PublicKey; + pythOracle: PublicKey; + stakePool: PublicKey; + seed: BN; +}; + +export const addBankPermissionless = ( + program: Program, + args: AddBankPermissionlessArgs +) => { + const [settingsKey] = deriveStakedSettings( + program.programId, + args.marginfiGroup + ); + const [lstMint] = PublicKey.findProgramAddressSync( + [Buffer.from("mint"), args.stakePool.toBuffer()], + SINGLE_POOL_PROGRAM_ID + ); + const [solPool] = PublicKey.findProgramAddressSync( + [Buffer.from("stake"), args.stakePool.toBuffer()], + SINGLE_POOL_PROGRAM_ID + ); + + // Note: oracle and lst mint/pool are also passed in meta for validation + const oracleMeta: AccountMeta = { + pubkey: args.pythOracle, + isSigner: false, + isWritable: false, + }; + const lstMeta: AccountMeta = { + pubkey: lstMint, + isSigner: false, + isWritable: false, + }; + const solPoolMeta: AccountMeta = { + pubkey: solPool, + isSigner: false, + isWritable: false, + }; + + const ix = program.methods + .lendingPoolAddBankPermissionless(args.seed) + .accounts({ + // marginfiGroup: args.marginfiGroup, // implied from stakedSettings + stakedSettings: settingsKey, + feePayer: args.feePayer, + bankMint: lstMint, + solPool: solPool, + stakePool: args.stakePool, + // bank: bankKey, // deriveBankWithSeed + // globalFeeState: deriveGlobalFeeState(id), + // globalFeeWallet: // implied from globalFeeState, + // liquidityVaultAuthority = deriveLiquidityVaultAuthority(id, bank); + // liquidityVault = deriveLiquidityVault(id, bank); + // insuranceVaultAuthority = deriveInsuranceVaultAuthority(id, bank); + // insuranceVault = deriveInsuranceVault(id, bank); + // feeVaultAuthority = deriveFeeVaultAuthority(id, bank); + // feeVault = deriveFeeVault(id, bank); + // rent = SYSVAR_RENT_PUBKEY + tokenProgram: TOKEN_PROGRAM_ID, + // systemProgram: SystemProgram.programId, + }) + .remainingAccounts([oracleMeta, lstMeta, solPoolMeta]) + .instruction(); + + return ix; +}; diff --git a/tests/utils/mocks.ts b/tests/utils/mocks.ts index 99ab0db25..ac0398e5e 100644 --- a/tests/utils/mocks.ts +++ b/tests/utils/mocks.ts @@ -85,7 +85,7 @@ export const echoEcosystemInfo = ( /** * A typical user, with a wallet, ATAs for mock tokens, and a program to sign/send txes with. */ -export type mockUser = { +export type MockUser = { wallet: Keypair; /** Users's ATA for wsol*/ wsolAccount: PublicKey; @@ -97,8 +97,17 @@ export type mockUser = { usdcAccount: PublicKey; /** A program that uses the user's wallet */ mrgnProgram: Program | undefined; + /** A map to store arbitrary accounts related to the user using a string key */ + accounts: Map; }; +/** in mockUser.accounts, key used to get/set the users's account for group 0 */ +export const USER_ACCOUNT: string = "g0_acc"; +/** in mockUser.accounts, key used to get/set the users's LST ATA for validator 0 */ +export const LST_ATA = "v0_lstAta"; +/** in mockUser.accounts, key used to get/set the users's LST stake account for validator 0 */ +export const STAKE_ACC = "v0_stakeAcc"; + /** * Options to skip various parts of mock user setup */ @@ -200,7 +209,7 @@ export const setupTestUser = async ( await provider.sendAndConfirm(tx, [wallet]); - const user: mockUser = { + const user: MockUser = { wallet: userWalletKeypair, wsolAccount: wsolAccount, tokenAAccount: tokenAAccount, @@ -210,6 +219,7 @@ export const setupTestUser = async ( mrgnProgram: options.marginProgram ? getUserMarginfiProgram(options.marginProgram, userWalletKeypair) : undefined, + accounts: new Map(), }; return user; }; @@ -276,19 +286,19 @@ export const createSimpleMint = async ( }; export type Oracles = { - wsolOracle: Keypair, - wsolPrice: number, - wsolDecimals: number, - usdcOracle: Keypair, - usdcPrice: number, - usdcDecimals: number, - tokenAOracle: Keypair, - tokenAPrice: number, - tokenADecimals: number, - tokenBOracle: Keypair, - tokenBPrice: number, - tokenBDecimals:number, -} + wsolOracle: Keypair; + wsolPrice: number; + wsolDecimals: number; + usdcOracle: Keypair; + usdcPrice: number; + usdcDecimals: number; + tokenAOracle: Keypair; + tokenAPrice: number; + tokenADecimals: number; + tokenBOracle: Keypair; + tokenBPrice: number; + tokenBDecimals: number; +}; /** * Creates an account to store data arbitrary data. @@ -344,4 +354,21 @@ export const storeMockAccount = async ( .instruction() ); await program.provider.sendAndConfirm(tx, [wallet.payer, account]); -}; \ No newline at end of file +}; + +export type Validator = { + node: PublicKey; + authorizedVoter: PublicKey; + authorizedWithdrawer: PublicKey; + voteAccount: PublicKey; + /** The spl stake pool itself, all PDAs derive from this key */ + splPool: PublicKey; + /** spl pool's mint for the LST (a PDA automatically created on init) */ + splMint: PublicKey; + /** spl pool's authority for LST management, a PDA with no data/lamports */ + splAuthority: PublicKey; + /** spl pool's stake account (a PDA automatically created on init, contains the SOL held by the pool) */ + splSolPool: PublicKey; + /** bank created for this validator's LST on the "main" group */ + bank: PublicKey; +}; diff --git a/tests/utils/pdas.ts b/tests/utils/pdas.ts index 028b44310..3780dd11c 100644 --- a/tests/utils/pdas.ts +++ b/tests/utils/pdas.ts @@ -1,3 +1,4 @@ +import { BN } from "@coral-xyz/anchor"; import { PublicKey } from "@solana/web3.js"; export const deriveLiquidityVaultAuthority = ( @@ -51,9 +52,63 @@ export const deriveFeeVault = (programId: PublicKey, bank: PublicKey) => { ); }; +export const deriveEmissionsAuth = ( + programId: PublicKey, + bank: PublicKey, + mint: PublicKey +) => { + return PublicKey.findProgramAddressSync( + [ + Buffer.from("emissions_auth_seed", "utf-8"), + bank.toBuffer(), + mint.toBuffer(), + ], + programId + ); +}; + +export const deriveEmissionsTokenAccount = ( + programId: PublicKey, + bank: PublicKey, + mint: PublicKey +) => { + return PublicKey.findProgramAddressSync( + [ + Buffer.from("emissions_token_account_seed", "utf-8"), + bank.toBuffer(), + mint.toBuffer(), + ], + programId + ); +}; + +export const deriveBankWithSeed = ( + programId: PublicKey, + group: PublicKey, + bankMint: PublicKey, + seed: BN +) => { + return PublicKey.findProgramAddressSync( + [group.toBuffer(), bankMint.toBuffer(), seed.toArrayLike(Buffer, "le", 8)], + programId + ); +}; + +// ************* Below this line, not yet included in package **************** + export const deriveGlobalFeeState = (programId: PublicKey) => { return PublicKey.findProgramAddressSync( [Buffer.from("feestate", "utf-8")], programId ); }; + +export const deriveStakedSettings = ( + programId: PublicKey, + group: PublicKey +) => { + return PublicKey.findProgramAddressSync( + [Buffer.from("staked_settings", "utf-8"), group.toBuffer()], + programId + ); +}; diff --git a/tests/utils/pyth_mocks.ts b/tests/utils/pyth_mocks.ts index 929a9c755..23ef1b94c 100644 --- a/tests/utils/pyth_mocks.ts +++ b/tests/utils/pyth_mocks.ts @@ -1,4 +1,9 @@ -// Adapted from PsyLend +// TODO the Price struct has changed a bit since this copy-pasta was generated some time ago, +// however price and ema price/expo/conf are in the same spot, so if those are all you need, there's +// no need to update (all modern changes are backwards compatible, new versions of Pyth on-chain +// will still deserialize the price data) + +// Adapated from PsyLend, Jet labs, etc import { Program, Wallet, workspace } from "@coral-xyz/anchor"; import { Keypair, PublicKey } from "@solana/web3.js"; import { Oracles, createMockAccount, storeMockAccount } from "./mocks"; @@ -289,6 +294,10 @@ export const writeProductBuffer = ( * @param wsolDecimals * @param usdcPrice * @param usdcDecimals + * @param tokenAPrice: + * @param tokenADecimals: + * @param tokenBPrice: + * @param tokenBDecimals: * @param verbose * @param skips - set to true to skip sending txes, which makes tests run faster if you don't need * those oracles. diff --git a/tests/utils/spl-staking-utils.ts b/tests/utils/spl-staking-utils.ts new file mode 100644 index 000000000..3893af339 --- /dev/null +++ b/tests/utils/spl-staking-utils.ts @@ -0,0 +1,151 @@ +import { + findPoolMintAddress, + findPoolStakeAuthorityAddress, + SinglePoolInstruction, +} from "@solana/spl-single-pool-classic"; +import { + createAssociatedTokenAccountInstruction, + getAssociatedTokenAddressSync, +} from "@solana/spl-token"; +import { + Connection, + PublicKey, + StakeAuthorizationLayout, + StakeProgram, + TransactionInstruction, +} from "@solana/web3.js"; +import { SINGLE_POOL_PROGRAM_ID } from "./types"; +import { ProgramTestContext } from "solana-bankrun"; + +export enum SinglePoolAccountType { + Uninitialized = 0, + Pool = 1, +} + +export type SinglePool = { + accountType: SinglePoolAccountType; + voteAccountAddress: PublicKey; +}; + +const decodeSinglePoolAccountType = (buffer: Buffer, offset: number) => { + const accountType = buffer.readUInt8(offset); + if (accountType === 0) { + return SinglePoolAccountType.Uninitialized; + } else if (accountType === 1) { + return SinglePoolAccountType.Pool; + } else { + throw new Error("Unknown SinglePoolAccountType"); + } +}; + +/** + * Decode an spl single pool from buffer. + * + * Get the data buffer with `const data = (await provider.connection.getAccountInfo(poolKey)).data;` + * and note that there is no discriminator (i.e. pass data directly without additional slicing) + */ +export const decodeSinglePool = (buffer: Buffer) => { + let offset = 0; + + const accountType = decodeSinglePoolAccountType(buffer, offset); + offset += 1; + + const voteAccountAddress = new PublicKey( + buffer.subarray(offset, offset + 32) + ); + offset += 32; + + return { + accountType, + voteAccountAddress, + }; +}; + +// See `https://www.npmjs.com/package/@solana/spl-single-pool` transactions.ts for the original + +/** + * Builds ixes to create the LST ata as-needed, pass stake authority to the spl pool, and deposit to + * the stake pool + * @param connection + * @param userWallet + * @param splPool + * @param userStakeAccount + * @param verbose + * @returns + */ +export const depositToSinglePoolIxes = async ( + connection: Connection, + userWallet: PublicKey, + splPool: PublicKey, + userStakeAccount: PublicKey, + verbose: boolean = false +) => { + const splMint = await findPoolMintAddress(SINGLE_POOL_PROGRAM_ID, splPool); + + const splAuthority = await findPoolStakeAuthorityAddress( + SINGLE_POOL_PROGRAM_ID, + splPool + ); + + const ixes: TransactionInstruction[] = []; + const lstAta = getAssociatedTokenAddressSync(splMint, userWallet); + try { + await connection.getAccountInfo(lstAta); + if (verbose) { + console.log("Existing LST ata at: " + lstAta); + } + } catch (err) { + if (verbose) { + console.log("Failed to find ata, creating: " + lstAta); + } + ixes.push( + createAssociatedTokenAccountInstruction( + userWallet, + lstAta, + userWallet, + splMint + ) + ); + } + + const authorizeStakerIxes = StakeProgram.authorize({ + stakePubkey: userStakeAccount, + authorizedPubkey: userWallet, + newAuthorizedPubkey: splAuthority, + stakeAuthorizationType: StakeAuthorizationLayout.Staker, + }).instructions; + + ixes.push(...authorizeStakerIxes); + + const authorizeWithdrawIxes = StakeProgram.authorize({ + stakePubkey: userStakeAccount, + authorizedPubkey: userWallet, + newAuthorizedPubkey: splAuthority, + stakeAuthorizationType: StakeAuthorizationLayout.Withdrawer, + }).instructions; + + ixes.push(...authorizeWithdrawIxes); + + const depositIx = await SinglePoolInstruction.depositStake( + splPool, + userStakeAccount, + lstAta, + userWallet + ); + + ixes.push(depositIx); + + return ixes; +}; + +/** + * Generally, use this instead of `bankrunContext.lastBlockhash` (which does not work if the test + * has already run for some time and the blockhash has advanced) + * @param bankrunContext + * @returns + */ +export const getBankrunBlockhash = async ( + bankrunContext: ProgramTestContext +) => { + return (await bankrunContext.banksClient.getLatestBlockhash())[0]; +}; diff --git a/tests/utils/stake-utils.ts b/tests/utils/stake-utils.ts new file mode 100644 index 000000000..a970c46ac --- /dev/null +++ b/tests/utils/stake-utils.ts @@ -0,0 +1,517 @@ +import { + Keypair, + Transaction, + SystemProgram, + StakeProgram, + PublicKey, + Connection, + SYSVAR_CLOCK_PUBKEY, +} from "@solana/web3.js"; +import { MockUser } from "./mocks"; +import { BanksClient } from "solana-bankrun"; +import { BN } from "@coral-xyz/anchor"; + +/** + * Create a stake account for some user + * @param user + * @param amount - in SOL (lamports), in native decimals + * @returns + */ +export const createStakeAccount = (user: MockUser, amount: number) => { + const stakeAccount = Keypair.generate(); + const userPublicKey = user.wallet.publicKey; + + // Create a stake account and fund it with the specified amount of SOL + const tx = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: userPublicKey, + newAccountPubkey: stakeAccount.publicKey, + lamports: amount, + space: StakeProgram.space, // Space required for a stake account + programId: StakeProgram.programId, + }), + StakeProgram.initialize({ + stakePubkey: stakeAccount.publicKey, + authorized: { + staker: userPublicKey, + withdrawer: userPublicKey, + }, + }) + ); + + return { createTx: tx, stakeAccountKeypair: stakeAccount }; +}; + +/** + * Delegate a stake account to a validator. + * @param user - wallet signs + * @param stakeAccount + * @param validatorVoteAccount + */ +export const delegateStake = ( + user: MockUser, + stakeAccount: PublicKey, + validatorVoteAccount: PublicKey +) => { + return StakeProgram.delegate({ + stakePubkey: stakeAccount, + authorizedPubkey: user.wallet.publicKey, + votePubkey: validatorVoteAccount, + }); +}; + +/** + * Delegation information for a StakeAccount + * + * Copied from https://github.com/solana-developers/solana-rpc-get-stake-activation/blob/main/web3js-1.0/src/stake.ts + * */ +export type Delegation = { + voterPubkey: PublicKey; + stake: bigint; + activationEpoch: bigint; + deactivationEpoch: bigint; +}; + +/** + * Parsed content of an on-chain StakeAccount + * + * Copied from https://github.com/solana-developers/solana-rpc-get-stake-activation/blob/main/web3js-1.0/src/stake.ts + * */ +export type StakeAccount = { + discriminant: bigint; + meta: { + rentExemptReserve: bigint; + authorized: { + staker: PublicKey; + withdrawer: PublicKey; + }; + lockup: { + unixTimestamp: bigint; + epoch: bigint; + custodian: PublicKey; + }; + }; + stake: { + delegation: { + voterPubkey: PublicKey; + stake: bigint; + activationEpoch: bigint; + deactivationEpoch: bigint; + }; + creditsObserved: bigint; + }; +}; + +/** + * Decode a StakeAccount from parsed account data. + * + * Copied from https://github.com/solana-developers/solana-rpc-get-stake-activation/blob/main/web3js-1.0/src/stake.ts + * */ +export const getStakeAccount = function (data: Buffer): StakeAccount { + let offset = 0; + + // Discriminant (4 bytes) + const discriminant = data.readBigUInt64LE(offset); + offset += 4; + + // Meta + const rentExemptReserve = data.readBigUInt64LE(offset); + offset += 8; + + // Authorized staker and withdrawer (2 public keys) + const staker = new PublicKey(data.subarray(offset, offset + 32)); + offset += 32; + const withdrawer = new PublicKey(data.subarray(offset, offset + 32)); + offset += 32; + + // Lockup: unixTimestamp, epoch, custodian + const unixTimestamp = data.readBigUInt64LE(offset); + offset += 8; + const epoch = data.readBigUInt64LE(offset); + offset += 8; + const custodian = new PublicKey(data.subarray(offset, offset + 32)); + offset += 32; + + // Stake: Delegation + const voterPubkey = new PublicKey(data.subarray(offset, offset + 32)); + offset += 32; + const stake = data.readBigUInt64LE(offset); + offset += 8; + const activationEpoch = data.readBigUInt64LE(offset); + offset += 8; + const deactivationEpoch = data.readBigUInt64LE(offset); + offset += 8; + + // Credits observed + const creditsObserved = data.readBigUInt64LE(offset); + + // Return the parsed StakeAccount object + return { + discriminant, + meta: { + rentExemptReserve, + authorized: { + staker, + withdrawer, + }, + lockup: { + unixTimestamp, + epoch, + custodian, + }, + }, + stake: { + delegation: { + voterPubkey, + stake, + activationEpoch, + deactivationEpoch, + }, + creditsObserved, + }, + }; +}; + +/** + * Parsed content of an on-chain Stake History Entry + * + * Copied from https://github.com/solana-developers/solana-rpc-get-stake-activation/blob/main/web3js-1.0/src/stake.ts + * */ +export type StakeHistoryEntry = { + epoch: bigint; + effective: bigint; + activating: bigint; + deactivating: bigint; +}; + +/** + * Decode a StakeHistoryEntry from parsed account data. + * + * Copied from https://github.com/solana-developers/solana-rpc-get-stake-activation/blob/main/web3js-1.0/src/stake.ts + * and modified to directly read from buffer + * */ +export const getStakeHistory = function (data: Buffer): StakeHistoryEntry[] { + // Note: Is just `Vec<(Epoch, StakeHistoryEntry)>` internally + const stakeHistory: StakeHistoryEntry[] = []; + const entrySize = 32; // Each entry is 32 bytes (4 x 8-byte u64 fields) + + for ( + // skip the first 8 bytes for the Vec overhead + let offset = 8; + offset + entrySize < data.length; + offset += entrySize + ) { + const epoch = data.readBigUInt64LE(offset); // Note `epoch` is just a u64 renamed + const effective = data.readBigUInt64LE(offset + 8); // u64 effective + const activating = data.readBigUInt64LE(offset + 16); // u64 activating + const deactivating = data.readBigUInt64LE(offset + 24); // u64 deactivating + + // if (epoch < 10 && offset < 300) { + // console.log("epoch " + epoch); + // console.log("e " + effective); + // console.log("a " + activating); + // console.log("d " + deactivating); + // } + + stakeHistory.push({ + epoch, + effective, + activating, + deactivating, + }); + } + + return stakeHistory; +}; + +/** + * Representation of on-chain stake + * + * Copied from https://github.com/solana-developers/solana-rpc-get-stake-activation/blob/main/web3js-1.0/src/delegation.ts + */ +export interface StakeActivatingAndDeactivating { + effective: bigint; + activating: bigint; + deactivating: bigint; +} + +/** + * Representation of on-chain stake excluding deactivating stake + * + * Copied from https://github.com/solana-developers/solana-rpc-get-stake-activation/blob/main/web3js-1.0/src/delegation.ts + */ +export interface EffectiveAndActivating { + effective: bigint; + activating: bigint; +} + +/** + * Get stake histories for a given epoch + * + * Copied from https://github.com/solana-developers/solana-rpc-get-stake-activation/blob/main/web3js-1.0/src/delegation.ts + */ +function getStakeHistoryEntry( + epoch: bigint, + stakeHistory: StakeHistoryEntry[] +): StakeHistoryEntry | null { + for (const entry of stakeHistory) { + if (entry.epoch === epoch) { + return entry; + } + } + return null; +} + +const WARMUP_COOLDOWN_RATE = 0.09; + +/** + * Get on-chain status of activating stake + * + * Copied from https://github.com/solana-developers/solana-rpc-get-stake-activation/blob/main/web3js-1.0/src/delegation.ts + */ +export function getStakeAndActivating( + delegation: Delegation, + targetEpoch: bigint, + stakeHistory: StakeHistoryEntry[] +): EffectiveAndActivating { + if (delegation.activationEpoch === delegation.deactivationEpoch) { + // activated but instantly deactivated; no stake at all regardless of target_epoch + return { + effective: BigInt(0), + activating: BigInt(0), + }; + } else if (targetEpoch === delegation.activationEpoch) { + // all is activating + return { + effective: BigInt(0), + activating: delegation.stake, + }; + } else if (targetEpoch < delegation.activationEpoch) { + // not yet enabled + return { + effective: BigInt(0), + activating: BigInt(0), + }; + } + + let currentEpoch = delegation.activationEpoch; + let entry = getStakeHistoryEntry(currentEpoch, stakeHistory); + if (entry !== null) { + // target_epoch > self.activation_epoch + + // loop from my activation epoch until the target epoch summing up my entitlement + // current effective stake is updated using its previous epoch's cluster stake + let currentEffectiveStake = BigInt(0); + while (entry !== null) { + currentEpoch++; + const remaining = delegation.stake - currentEffectiveStake; + const weight = Number(remaining) / Number(entry.activating); + const newlyEffectiveClusterStake = + Number(entry.effective) * WARMUP_COOLDOWN_RATE; + const newlyEffectiveStake = BigInt( + Math.max(1, Math.round(weight * newlyEffectiveClusterStake)) + ); + + currentEffectiveStake += newlyEffectiveStake; + if (currentEffectiveStake >= delegation.stake) { + currentEffectiveStake = delegation.stake; + break; + } + + if ( + currentEpoch >= targetEpoch || + currentEpoch >= delegation.deactivationEpoch + ) { + break; + } + entry = getStakeHistoryEntry(currentEpoch, stakeHistory); + } + return { + effective: currentEffectiveStake, + activating: delegation.stake - currentEffectiveStake, + }; + } else { + // no history or I've dropped out of history, so assume fully effective + return { + effective: delegation.stake, + activating: BigInt(0), + }; + } +} + +/** + * Get on-chain status of activating and deactivating stake + * + * Copied from https://github.com/solana-developers/solana-rpc-get-stake-activation/blob/main/web3js-1.0/src/delegation.ts + */ +export function getStakeActivatingAndDeactivating( + delegation: Delegation, + targetEpoch: bigint, + stakeHistory: StakeHistoryEntry[] +): StakeActivatingAndDeactivating { + const { effective, activating } = getStakeAndActivating( + delegation, + targetEpoch, + stakeHistory + ); + + // then de-activate some portion if necessary + if (targetEpoch < delegation.deactivationEpoch) { + return { + effective, + activating, + deactivating: BigInt(0), + }; + } else if (targetEpoch == delegation.deactivationEpoch) { + // can only deactivate what's activated + return { + effective, + activating: BigInt(0), + deactivating: effective, + }; + } + let currentEpoch = delegation.deactivationEpoch; + let entry = getStakeHistoryEntry(currentEpoch, stakeHistory); + if (entry !== null) { + // target_epoch > self.activation_epoch + // loop from my deactivation epoch until the target epoch + // current effective stake is updated using its previous epoch's cluster stake + let currentEffectiveStake = effective; + while (entry !== null) { + currentEpoch++; + // if there is no deactivating stake at prev epoch, we should have been + // fully undelegated at this moment + if (entry.deactivating === BigInt(0)) { + break; + } + + // I'm trying to get to zero, how much of the deactivation in stake + // this account is entitled to take + const weight = Number(currentEffectiveStake) / Number(entry.deactivating); + + // portion of newly not-effective cluster stake I'm entitled to at current epoch + const newlyNotEffectiveClusterStake = + Number(entry.effective) * WARMUP_COOLDOWN_RATE; + const newlyNotEffectiveStake = BigInt( + Math.max(1, Math.round(weight * newlyNotEffectiveClusterStake)) + ); + + currentEffectiveStake -= newlyNotEffectiveStake; + if (currentEffectiveStake <= 0) { + currentEffectiveStake = BigInt(0); + break; + } + + if (currentEpoch >= targetEpoch) { + break; + } + entry = getStakeHistoryEntry(currentEpoch, stakeHistory); + } + + // deactivating stake should equal to all of currently remaining effective stake + return { + effective: currentEffectiveStake, + deactivating: currentEffectiveStake, + activating: BigInt(0), + }; + } else { + return { + effective: BigInt(0), + activating: BigInt(0), + deactivating: BigInt(0), + }; + } +} + +/** + * Representation of on-chain stake + * + * Copied from https://github.com/solana-developers/solana-rpc-get-stake-activation/blob/main/web3js-1.0/src/rpc.ts + */ +export interface StakeActivation { + status: string; + active: bigint; + inactive: bigint; +} + +/** + * Get on-chain stake status of a stake account (activating, inactive, etc) + * + * Copied from https://github.com/solana-developers/solana-rpc-get-stake-activation/blob/main/web3js-1.0/src/rpc.ts + */ +export async function getStakeActivation( + connection: Connection, + stakeAddress: PublicKey, + epoch: number | undefined = undefined // Added to bypass connection.getEpochInfo() when using a bankrun provider. +): Promise { + const SYSVAR_STAKE_HISTORY_ADDRESS = new PublicKey( + "SysvarStakeHistory1111111111111111111111111" + ); + const epochInfoPromise = + epoch !== undefined + ? Promise.resolve({ epoch }) + : connection.getEpochInfo(); + const [epochInfo, { stakeAccount, stakeAccountLamports }, stakeHistory] = + await Promise.all([ + epochInfoPromise, + (async () => { + const stakeAccountInfo = await connection.getAccountInfo(stakeAddress); + if (stakeAccountInfo === null) { + throw new Error("Account not found"); + } + const stakeAccount = getStakeAccount(stakeAccountInfo.data); + const stakeAccountLamports = stakeAccountInfo.lamports; + return { stakeAccount, stakeAccountLamports }; + })(), + (async () => { + const stakeHistoryInfo = await connection.getAccountInfo( + SYSVAR_STAKE_HISTORY_ADDRESS + ); + if (stakeHistoryInfo === null) { + throw new Error("StakeHistory not found"); + } + return getStakeHistory(stakeHistoryInfo.data); + })(), + ]); + + const targetEpoch = epoch ? epoch : epochInfo.epoch; + const { effective, activating, deactivating } = + getStakeActivatingAndDeactivating( + stakeAccount.stake.delegation, + BigInt(targetEpoch), + stakeHistory + ); + + let status; + if (deactivating > 0) { + status = "deactivating"; + } else if (activating > 0) { + status = "activating"; + } else if (effective > 0) { + status = "active"; + } else { + status = "inactive"; + } + const inactive = + BigInt(stakeAccountLamports) - + effective - + stakeAccount.meta.rentExemptReserve; + + return { + status, + active: effective, + inactive, + }; +} + +export const getEpochAndSlot = async (banksClient: BanksClient) => { + let clock = await banksClient.getAccount(SYSVAR_CLOCK_PUBKEY); + + // Slot is bytes 0-8 + let slot = new BN(clock.data.slice(0, 8), 10, "le").toNumber(); + + // Epoch is bytes 16-24 + let epoch = new BN(clock.data.slice(16, 24), 10, "le").toNumber(); + + return { epoch, slot }; +}; diff --git a/tests/utils/stakeCollatizer/pdas.ts b/tests/utils/stakeCollatizer/pdas.ts new file mode 100644 index 000000000..dd9fa3019 --- /dev/null +++ b/tests/utils/stakeCollatizer/pdas.ts @@ -0,0 +1,33 @@ +import { PublicKey } from "@solana/web3.js"; + +export const deriveStakeHolder = ( + programId: PublicKey, + voteAccount: PublicKey, + admin: PublicKey +) => { + return PublicKey.findProgramAddressSync( + [ + Buffer.from("stakeholder", "utf-8"), + voteAccount.toBuffer(), + admin.toBuffer(), + ], + programId + ); +}; + +export const deriveStakeHolderStakeAccount = ( + programId: PublicKey, + stakeholder: PublicKey +) => { + return PublicKey.findProgramAddressSync( + [Buffer.from("stakeacc", "utf-8"), stakeholder.toBuffer()], + programId + ); +}; + +export const deriveStakeUser = (programId: PublicKey, payer: PublicKey) => { + return PublicKey.findProgramAddressSync( + [Buffer.from("stakeuser", "utf-8"), payer.toBuffer()], + programId + ); +}; diff --git a/tests/utils/tools.ts b/tests/utils/tools.ts index 0ddcbad6b..c73355f64 100644 --- a/tests/utils/tools.ts +++ b/tests/utils/tools.ts @@ -1,3 +1,5 @@ +import { BanksTransactionResultWithMeta } from "solana-bankrun"; + /** * Function to print bytes from a Buffer in groups with column labels and color highlighting for non-zero values * @param buffer - The Buffer to process @@ -26,9 +28,11 @@ export const printBufferGroups = ( // Function to calculate RGB color based on row index const calculateGradientColor = (startIndex) => { const maxIndex = 255 * 3; - const normalizedIndex = (startIndex % maxIndex); + const normalizedIndex = startIndex % maxIndex; - let r = 0, g = 0, b = 0; + let r = 0, + g = 0, + b = 0; if (normalizedIndex < 255) { b = 255; @@ -70,9 +74,13 @@ export const printBufferGroups = ( const label = `${i.toString().padStart(3, " ")}-${(i + groupLength - 1) .toString() .padStart(3, " ")}`; - console.log( - `${color}${label}\x1b[0m | ${group.join(" | ")}` - ); + console.log(`${color}${label}\x1b[0m | ${group.join(" | ")}`); } } }; + +export const dumpBankrunLogs = (result: BanksTransactionResultWithMeta) => { + for (let i = 0; i < result.meta.logMessages.length; i++) { + console.log(i + " " + result.meta.logMessages[i]); + } +}; diff --git a/tests/utils/types.ts b/tests/utils/types.ts index 20bb0cb12..0b2b514fa 100644 --- a/tests/utils/types.ts +++ b/tests/utils/types.ts @@ -106,6 +106,7 @@ export const defaultBankConfigOptRaw = () => { riskTier: { collateral: undefined, }, + assetTag: ASSET_TAG_DEFAULT, totalAssetValueInitLimit: new BN(100_000_000_000), interestRateConfig: defaultInterestRateConfigRaw(), operationalState: { @@ -171,7 +172,7 @@ export const defaultStakedInterestSettings = (oracle: PublicKey) => { assetWeightMaint: bigNumberToWrappedI80F48(0.9), depositLimit: new BN(1_000_000_000_000), // 1000 SOL totalAssetValueInitLimit: new BN(150_000_000), - oracleMaxAge: 10, + oracleMaxAge: 60, riskTier: { collateral: undefined, }, @@ -238,6 +239,7 @@ export type BankConfigOptRaw = { depositLimit: BN | null; borrowLimit: BN | null; riskTier: { collateral: {} } | { isolated: {} } | null; + assetTag: number, totalAssetValueInitLimit: BN | null; interestRateConfig: InterestRateConfigRawWithOrigination | null; diff --git a/tests/utils/user-instructions.ts b/tests/utils/user-instructions.ts new file mode 100644 index 000000000..59f624952 --- /dev/null +++ b/tests/utils/user-instructions.ts @@ -0,0 +1,113 @@ +import { BN, Program } from "@coral-xyz/anchor"; +import { AccountMeta, PublicKey } from "@solana/web3.js"; +import { Marginfi } from "../../target/types/marginfi"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; + +export type AccountInitArgs = { + marginfiGroup: PublicKey; + marginfiAccount: PublicKey; + authority: PublicKey; + feePayer: PublicKey; +}; + +/** + * Init a user account for some group. + * * fee payer and authority must both sign. + * * account must be a fresh keypair and must also sign + * @param program + * @param args + * @returns + */ +export const accountInit = ( + program: Program, + args: AccountInitArgs +) => { + const ix = program.methods + .marginfiAccountInitialize() + .accounts({ + marginfiGroup: args.marginfiGroup, + marginfiAccount: args.marginfiAccount, + authority: args.authority, + feePayer: args.feePayer, + // systemProgram + }) + .instruction(); + + return ix; +}; + +export type DepositArgs = { + marginfiGroup: PublicKey; + marginfiAccount: PublicKey; + authority: PublicKey; + bank: PublicKey; + tokenAccount: PublicKey; + amount: BN; +}; + +/** + * Deposit to a bank + * * `authority` must sign and own the `tokenAccount` + * @param program + * @param args + * @returns + */ +export const depositIx = (program: Program, args: DepositArgs) => { + const ix = program.methods + .lendingAccountDeposit(args.amount) + .accounts({ + marginfiGroup: args.marginfiGroup, + marginfiAccount: args.marginfiAccount, + signer: args.authority, + bank: args.bank, + signerTokenAccount: args.tokenAccount, + // bankLiquidityVault = deriveLiquidityVault(id, bank) + tokenProgram: TOKEN_PROGRAM_ID, + }) + .instruction(); + + return ix; +}; + +export type BorrowIxArgs = { + marginfiGroup: PublicKey; + marginfiAccount: PublicKey; + authority: PublicKey; + bank: PublicKey; + tokenAccount: PublicKey; + remaining: PublicKey[]; + amount: BN; +}; + +/** + * Borrow from a bank + * * `authority` - must sign, but does not have to own the `tokenAccount` + * * `remaining` - pass bank/oracles for each bank the user is involved with, in the SAME ORDER they + * appear in userAcc.balances (e.g. `[bank0, oracle0, bank1, oracle1]`) + * @param program + * @param args + * @returns + */ +export const borrowIx = (program: Program, args: BorrowIxArgs) => { + const oracleMeta: AccountMeta[] = args.remaining.map((pubkey) => ({ + pubkey, + isSigner: false, + isWritable: false, + })); + const ix = program.methods + .lendingAccountBorrow(args.amount) + .accounts({ + marginfiGroup: args.marginfiGroup, + marginfiAccount: args.marginfiAccount, + signer: args.authority, + bank: args.bank, + destinationTokenAccount: args.tokenAccount, + // bankLiquidityVaultAuthority = deriveLiquidityVaultAuthority(id, bank); + // bankLiquidityVault = deriveLiquidityVault(id, bank) + tokenProgram: TOKEN_PROGRAM_ID, + }) + .remainingAccounts(oracleMeta) + .instruction(); + + return ix; +}; diff --git a/yarn.lock b/yarn.lock index 8436f363c..971c66a5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,14 +2,7 @@ # yarn lockfile v1 -"@babel/runtime@^7.12.5", "@babel/runtime@^7.24.8": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.0.tgz#3af9a91c1b739c569d5d80cc917280919c544ecb" - integrity sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw== - dependencies: - regenerator-runtime "^0.14.0" - -"@babel/runtime@^7.25.0": +"@babel/runtime@^7.12.5", "@babel/runtime@^7.25.0": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== @@ -122,9 +115,9 @@ "@jridgewell/trace-mapping" "0.3.9" "@grpc/grpc-js@^1.8.13": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.11.1.tgz#a92f33e98f1959feffcd1b25a33b113d2c977b70" - integrity sha512-gyt/WayZrVPH2w/UTLansS7F9Nwld472JxxaETamrM8HNlsa+jSLNyKAZmhxI2Me4c3mQHFiS1wWHDY1g1Kthw== + version "1.12.4" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.12.4.tgz#3208808435ebf1e495f9a5c5c5a0bc3dc8c9e891" + integrity sha512-NBhrxEWnFh0FxeA0d//YP95lRFsSx2TNLEUQg4/W+5f/BMxcCjgOOIT24iD+ZB/tZw057j44DaIxja7w4XMrhg== dependencies: "@grpc/proto-loader" "^0.7.13" "@js-sdsl/ordered-map" "^4.4.2" @@ -162,6 +155,48 @@ resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz#9299f82874bab9e4c7f9c48d865becbfe8d6907c" integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw== +"@metaplex-foundation/umi-options@^0.8.9": + version "0.8.9" + resolved "https://registry.yarnpkg.com/@metaplex-foundation/umi-options/-/umi-options-0.8.9.tgz#9c9e269d9eee7d055ad6831dcb30a30127dcb0c5" + integrity sha512-jSQ61sZMPSAk/TXn8v8fPqtz3x8d0/blVZXLLbpVbo2/T5XobiI6/MfmlUosAjAUaQl6bHRF8aIIqZEFkJiy4A== + +"@metaplex-foundation/umi-public-keys@^0.8.9": + version "0.8.9" + resolved "https://registry.yarnpkg.com/@metaplex-foundation/umi-public-keys/-/umi-public-keys-0.8.9.tgz#ca7a927c924ed8e28d0f8bb3dc0f2adc1f9011ec" + integrity sha512-CxMzN7dgVGOq9OcNCJe2casKUpJ3RmTVoOvDFyeoTQuK+vkZ1YSSahbqC1iGuHEtKTLSjtWjKvUU6O7zWFTw3Q== + dependencies: + "@metaplex-foundation/umi-serializers-encodings" "^0.8.9" + +"@metaplex-foundation/umi-serializers-core@^0.8.9": + version "0.8.9" + resolved "https://registry.yarnpkg.com/@metaplex-foundation/umi-serializers-core/-/umi-serializers-core-0.8.9.tgz#cd5ae763a59e54dd01f1284f4a6bf4e78e4aab9c" + integrity sha512-WT82tkiYJ0Qmscp7uTj1Hz6aWQPETwaKLAENAUN5DeWghkuBKtuxyBKVvEOuoXerJSdhiAk0e8DWA4cxcTTQ/w== + +"@metaplex-foundation/umi-serializers-encodings@^0.8.9": + version "0.8.9" + resolved "https://registry.yarnpkg.com/@metaplex-foundation/umi-serializers-encodings/-/umi-serializers-encodings-0.8.9.tgz#0f02605ee3e6fbeac1abc4fb267a7cc96ecb4410" + integrity sha512-N3VWLDTJ0bzzMKcJDL08U3FaqRmwlN79FyE4BHj6bbAaJ9LEHjDQ9RJijZyWqTm0jE7I750fU7Ow5EZL38Xi6Q== + dependencies: + "@metaplex-foundation/umi-serializers-core" "^0.8.9" + +"@metaplex-foundation/umi-serializers-numbers@^0.8.9": + version "0.8.9" + resolved "https://registry.yarnpkg.com/@metaplex-foundation/umi-serializers-numbers/-/umi-serializers-numbers-0.8.9.tgz#28c10367f6aebac0276ec1bce81d0d8db54b05de" + integrity sha512-NtBf1fnVNQJHFQjLFzRu2i9GGnigb9hOm/Gfrk628d0q0tRJB7BOM3bs5C61VAs7kJs4yd+pDNVAERJkknQ7Lg== + dependencies: + "@metaplex-foundation/umi-serializers-core" "^0.8.9" + +"@metaplex-foundation/umi-serializers@^0.8.9": + version "0.8.9" + resolved "https://registry.yarnpkg.com/@metaplex-foundation/umi-serializers/-/umi-serializers-0.8.9.tgz#af6c5bb1a3276cbe252fd08e359b305ed80a3343" + integrity sha512-Sve8Etm3zqvLSUfza+MYRkjTnCpiaAFT7VWdqeHzA3n58P0AfT3p74RrZwVt/UFkxI+ln8BslwBDJmwzcPkuHw== + dependencies: + "@metaplex-foundation/umi-options" "^0.8.9" + "@metaplex-foundation/umi-public-keys" "^0.8.9" + "@metaplex-foundation/umi-serializers-core" "^0.8.9" + "@metaplex-foundation/umi-serializers-encodings" "^0.8.9" + "@metaplex-foundation/umi-serializers-numbers" "^0.8.9" + "@mrgnlabs/marginfi-client-v2@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@mrgnlabs/marginfi-client-v2/-/marginfi-client-v2-4.0.0.tgz#50676767dc9a06b5ffaccb25f3dc8f7a24b6f52d" @@ -179,22 +214,7 @@ decimal.js "^10.4.3" superstruct "^1.0.4" -"@mrgnlabs/mrgn-common@*": - version "1.7.0" - resolved "https://registry.yarnpkg.com/@mrgnlabs/mrgn-common/-/mrgn-common-1.7.0.tgz#0a40b7696057ee4119f4fe3950ead11623085545" - integrity sha512-vDeVmcSRB4tAXDZPNTrzlOIhWQFk+CD5akuBl6vaULwRrxMwkSmOxvkgnXHdxXvKgTH9CfU/0exkevOc4VXslQ== - dependencies: - "@coral-xyz/anchor" "^0.30.1" - "@solana/buffer-layout-utils" "^0.2.0" - "@solana/wallet-adapter-base" "^0.9.23" - "@solana/web3.js" "^1.93.2" - bignumber.js "^9.1.2" - bs58 "^6.0.0" - decimal.js "^10.4.3" - numeral "^2.0.6" - superstruct "^1.0.4" - -"@mrgnlabs/mrgn-common@^1.8.0": +"@mrgnlabs/mrgn-common@*", "@mrgnlabs/mrgn-common@^1.8.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@mrgnlabs/mrgn-common/-/mrgn-common-1.8.0.tgz#76df1104b3a6b04054c56c297b4dc6a27236d8fe" integrity sha512-6VQ/2Ob8alyI1jsY3RETsWJ5W/myEp+h2yu1mFI/yzKHLffQMlt50FWPSDmCe2TBmkBioW48HOl+PqQf3+Wfbg== @@ -217,29 +237,27 @@ "@solana/buffer-layout" "=4.0.0" "@solana/buffer-layout-utils" "=0.2.0" -"@noble/curves@^1.0.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.5.0.tgz#7a9b9b507065d516e6dce275a1e31db8d2a100dd" - integrity sha512-J5EKamIHnKPyClwVrzmaf5wSdQXgdHcPZIZLu3bwnbeCx8/7NPK5q2ZBWF+5FvYGByjiQQsJYX6jfgB2wDPn3A== - dependencies: - "@noble/hashes" "1.4.0" - -"@noble/curves@^1.4.2": - version "1.4.2" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.2.tgz#40309198c76ed71bc6dbf7ba24e81ceb4d0d1fe9" - integrity sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw== +"@noble/curves@^1.0.0", "@noble/curves@^1.4.2": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.7.0.tgz#0512360622439256df892f21d25b388f52505e45" + integrity sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw== dependencies: - "@noble/hashes" "1.4.0" + "@noble/hashes" "1.6.0" "@noble/ed25519@^1.7.1": version "1.7.3" resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.3.tgz#57e1677bf6885354b466c38e2b620c62f45a7123" integrity sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ== -"@noble/hashes@1.4.0", "@noble/hashes@^1.3.0", "@noble/hashes@^1.3.1", "@noble/hashes@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" - integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== +"@noble/hashes@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.6.0.tgz#d4bfb516ad6e7b5111c216a5cc7075f4cf19e6c5" + integrity sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ== + +"@noble/hashes@^1.3.0", "@noble/hashes@^1.3.1", "@noble/hashes@^1.4.0": + version "1.6.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.6.1.tgz#df6e5943edcea504bac61395926d6fd67869a0d5" + integrity sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w== "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" @@ -302,17 +320,17 @@ bn.js "^5.2.1" "@pythnetwork/pyth-solana-receiver@^0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@pythnetwork/pyth-solana-receiver/-/pyth-solana-receiver-0.8.0.tgz#d7bf3c5c97a0f0eab8ac19f53b11664117e1152d" - integrity sha512-5lhLtggAqsiHtffTPM8vcKJmhBdxzidBmiNNUlqPyg9XmhZ4Z+roY0dfzluEoX5xer9rEA1ThsBpX0bG1DRIGA== + version "0.8.2" + resolved "https://registry.yarnpkg.com/@pythnetwork/pyth-solana-receiver/-/pyth-solana-receiver-0.8.2.tgz#e1c54de017bad6c321c22245fe240e020e9c853d" + integrity sha512-WrrdwwhSYvvB5vJEL+SfPnfuxgkRKMeKdZvGFFwe6ENrMhrQCM05oDkvNNYfXATLcpQGRAyBu9l1xIxUxixpqw== dependencies: "@coral-xyz/anchor" "^0.29.0" "@noble/hashes" "^1.4.0" "@pythnetwork/price-service-sdk" ">=1.6.0" - "@pythnetwork/solana-utils" "*" + "@pythnetwork/solana-utils" "0.4.2" "@solana/web3.js" "^1.90.0" -"@pythnetwork/solana-utils@*": +"@pythnetwork/solana-utils@0.4.2": version "0.4.2" resolved "https://registry.yarnpkg.com/@pythnetwork/solana-utils/-/solana-utils-0.4.2.tgz#3e220eed518c02ad702ebb023488afd7c5649a87" integrity sha512-hKo7Bcs/kDWA5Fnqhg9zJSB94NMoUDIDjHjSi/uvZOzwizISUQI6oY3LWd2CXzNh4f8djjY2BS5iNHaM4cm8Bw== @@ -322,6 +340,19 @@ bs58 "^5.0.0" jito-ts "^3.0.1" +"@solana/addresses@2.0.0-experimental.21e994f": + version "2.0.0-experimental.21e994f" + resolved "https://registry.yarnpkg.com/@solana/addresses/-/addresses-2.0.0-experimental.21e994f.tgz#51509c4e48c3feae573f30a0ad7736d2054b1bdf" + integrity sha512-zmg+ALhjxZApKJKSjeGK7EgMT9NywdvGKlAjyNL2fieiFWp0lRTBmWyjPBCQQGdJjBkayCscq3GQkDF2MhC6fg== + dependencies: + "@metaplex-foundation/umi-serializers" "^0.8.9" + "@solana/assertions" "2.0.0-experimental.21e994f" + +"@solana/assertions@2.0.0-experimental.21e994f": + version "2.0.0-experimental.21e994f" + resolved "https://registry.yarnpkg.com/@solana/assertions/-/assertions-2.0.0-experimental.21e994f.tgz#a67143b41aaf1d810176b943a203f1508f4095df" + integrity sha512-iGOUpOqkqxzQ/xi4Q3YLiBQPASiQ43NYTalmQm99hmOhySRA4+yyQTmMW1PJ8FAm7Zf86cCiYTf19Exa7+DxoQ== + "@solana/buffer-layout-utils@=0.2.0", "@solana/buffer-layout-utils@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@solana/buffer-layout-utils/-/buffer-layout-utils-0.2.0.tgz#b45a6cab3293a2eb7597cceb474f229889d875ca" @@ -346,20 +377,6 @@ dependencies: buffer "~6.0.3" -"@solana/codecs-core@2.0.0-preview.2": - version "2.0.0-preview.2" - resolved "https://registry.yarnpkg.com/@solana/codecs-core/-/codecs-core-2.0.0-preview.2.tgz#689784d032fbc1fedbde40bb25d76cdcecf6553b" - integrity sha512-gLhCJXieSCrAU7acUJjbXl+IbGnqovvxQLlimztPoGgfLQ1wFYu+XJswrEVQqknZYK1pgxpxH3rZ+OKFs0ndQg== - dependencies: - "@solana/errors" "2.0.0-preview.2" - -"@solana/codecs-core@2.0.0-preview.4": - version "2.0.0-preview.4" - resolved "https://registry.yarnpkg.com/@solana/codecs-core/-/codecs-core-2.0.0-preview.4.tgz#770826105f2f884110a21662573e7a2014654324" - integrity sha512-A0VVuDDA5kNKZUinOqHxJQK32aKTucaVbvn31YenGzHX1gPqq+SOnFwgaEY6pq4XEopSmaK16w938ZQS8IvCnw== - dependencies: - "@solana/errors" "2.0.0-preview.4" - "@solana/codecs-core@2.0.0-rc.1": version "2.0.0-rc.1" resolved "https://registry.yarnpkg.com/@solana/codecs-core/-/codecs-core-2.0.0-rc.1.tgz#1a2d76b9c7b9e7b7aeb3bd78be81c2ba21e3ce22" @@ -367,24 +384,6 @@ dependencies: "@solana/errors" "2.0.0-rc.1" -"@solana/codecs-data-structures@2.0.0-preview.2": - version "2.0.0-preview.2" - resolved "https://registry.yarnpkg.com/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-preview.2.tgz#e82cb1b6d154fa636cd5c8953ff3f32959cc0370" - integrity sha512-Xf5vIfromOZo94Q8HbR04TbgTwzigqrKII0GjYr21K7rb3nba4hUW2ir8kguY7HWFBcjHGlU5x3MevKBOLp3Zg== - dependencies: - "@solana/codecs-core" "2.0.0-preview.2" - "@solana/codecs-numbers" "2.0.0-preview.2" - "@solana/errors" "2.0.0-preview.2" - -"@solana/codecs-data-structures@2.0.0-preview.4": - version "2.0.0-preview.4" - resolved "https://registry.yarnpkg.com/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-preview.4.tgz#f8a2470982a9792334737ea64000ccbdff287247" - integrity sha512-nt2k2eTeyzlI/ccutPcG36M/J8NAYfxBPI9h/nQjgJ+M+IgOKi31JV8StDDlG/1XvY0zyqugV3I0r3KAbZRJpA== - dependencies: - "@solana/codecs-core" "2.0.0-preview.4" - "@solana/codecs-numbers" "2.0.0-preview.4" - "@solana/errors" "2.0.0-preview.4" - "@solana/codecs-data-structures@2.0.0-rc.1": version "2.0.0-rc.1" resolved "https://registry.yarnpkg.com/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-rc.1.tgz#d47b2363d99fb3d643f5677c97d64a812982b888" @@ -394,22 +393,6 @@ "@solana/codecs-numbers" "2.0.0-rc.1" "@solana/errors" "2.0.0-rc.1" -"@solana/codecs-numbers@2.0.0-preview.2": - version "2.0.0-preview.2" - resolved "https://registry.yarnpkg.com/@solana/codecs-numbers/-/codecs-numbers-2.0.0-preview.2.tgz#56995c27396cd8ee3bae8bd055363891b630bbd0" - integrity sha512-aLZnDTf43z4qOnpTcDsUVy1Ci9im1Md8thWipSWbE+WM9ojZAx528oAql+Cv8M8N+6ALKwgVRhPZkto6E59ARw== - dependencies: - "@solana/codecs-core" "2.0.0-preview.2" - "@solana/errors" "2.0.0-preview.2" - -"@solana/codecs-numbers@2.0.0-preview.4": - version "2.0.0-preview.4" - resolved "https://registry.yarnpkg.com/@solana/codecs-numbers/-/codecs-numbers-2.0.0-preview.4.tgz#6a53b456bb7866f252d8c032c81a92651e150f66" - integrity sha512-Q061rLtMadsO7uxpguT+Z7G4UHnjQ6moVIxAQxR58nLxDPCC7MB1Pk106/Z7NDhDLHTcd18uO6DZ7ajHZEn2XQ== - dependencies: - "@solana/codecs-core" "2.0.0-preview.4" - "@solana/errors" "2.0.0-preview.4" - "@solana/codecs-numbers@2.0.0-rc.1": version "2.0.0-rc.1" resolved "https://registry.yarnpkg.com/@solana/codecs-numbers/-/codecs-numbers-2.0.0-rc.1.tgz#f34978ddf7ea4016af3aaed5f7577c1d9869a614" @@ -418,24 +401,6 @@ "@solana/codecs-core" "2.0.0-rc.1" "@solana/errors" "2.0.0-rc.1" -"@solana/codecs-strings@2.0.0-preview.2": - version "2.0.0-preview.2" - resolved "https://registry.yarnpkg.com/@solana/codecs-strings/-/codecs-strings-2.0.0-preview.2.tgz#8bd01a4e48614d5289d72d743c3e81305d445c46" - integrity sha512-EgBwY+lIaHHgMJIqVOGHfIfpdmmUDNoNO/GAUGeFPf+q0dF+DtwhJPEMShhzh64X2MeCZcmSO6Kinx0Bvmmz2g== - dependencies: - "@solana/codecs-core" "2.0.0-preview.2" - "@solana/codecs-numbers" "2.0.0-preview.2" - "@solana/errors" "2.0.0-preview.2" - -"@solana/codecs-strings@2.0.0-preview.4": - version "2.0.0-preview.4" - resolved "https://registry.yarnpkg.com/@solana/codecs-strings/-/codecs-strings-2.0.0-preview.4.tgz#4d06bb722a55a5d04598d362021bfab4bd446760" - integrity sha512-YDbsQePRWm+xnrfS64losSGRg8Wb76cjK1K6qfR8LPmdwIC3787x9uW5/E4icl/k+9nwgbIRXZ65lpF+ucZUnw== - dependencies: - "@solana/codecs-core" "2.0.0-preview.4" - "@solana/codecs-numbers" "2.0.0-preview.4" - "@solana/errors" "2.0.0-preview.4" - "@solana/codecs-strings@2.0.0-rc.1": version "2.0.0-rc.1" resolved "https://registry.yarnpkg.com/@solana/codecs-strings/-/codecs-strings-2.0.0-rc.1.tgz#e1d9167075b8c5b0b60849f8add69c0f24307018" @@ -445,28 +410,6 @@ "@solana/codecs-numbers" "2.0.0-rc.1" "@solana/errors" "2.0.0-rc.1" -"@solana/codecs@2.0.0-preview.2": - version "2.0.0-preview.2" - resolved "https://registry.yarnpkg.com/@solana/codecs/-/codecs-2.0.0-preview.2.tgz#d6615fec98f423166fb89409f9a4ad5b74c10935" - integrity sha512-4HHzCD5+pOSmSB71X6w9ptweV48Zj1Vqhe732+pcAQ2cMNnN0gMPMdDq7j3YwaZDZ7yrILVV/3+HTnfT77t2yA== - dependencies: - "@solana/codecs-core" "2.0.0-preview.2" - "@solana/codecs-data-structures" "2.0.0-preview.2" - "@solana/codecs-numbers" "2.0.0-preview.2" - "@solana/codecs-strings" "2.0.0-preview.2" - "@solana/options" "2.0.0-preview.2" - -"@solana/codecs@2.0.0-preview.4": - version "2.0.0-preview.4" - resolved "https://registry.yarnpkg.com/@solana/codecs/-/codecs-2.0.0-preview.4.tgz#a1923cc78a6f64ebe656c7ec6335eb6b70405b22" - integrity sha512-gLMupqI4i+G4uPi2SGF/Tc1aXcviZF2ybC81x7Q/fARamNSgNOCUUoSCg9nWu1Gid6+UhA7LH80sWI8XjKaRog== - dependencies: - "@solana/codecs-core" "2.0.0-preview.4" - "@solana/codecs-data-structures" "2.0.0-preview.4" - "@solana/codecs-numbers" "2.0.0-preview.4" - "@solana/codecs-strings" "2.0.0-preview.4" - "@solana/options" "2.0.0-preview.4" - "@solana/codecs@2.0.0-rc.1": version "2.0.0-rc.1" resolved "https://registry.yarnpkg.com/@solana/codecs/-/codecs-2.0.0-rc.1.tgz#146dc5db58bd3c28e04b4c805e6096c2d2a0a875" @@ -478,22 +421,6 @@ "@solana/codecs-strings" "2.0.0-rc.1" "@solana/options" "2.0.0-rc.1" -"@solana/errors@2.0.0-preview.2": - version "2.0.0-preview.2" - resolved "https://registry.yarnpkg.com/@solana/errors/-/errors-2.0.0-preview.2.tgz#e0ea8b008c5c02528d5855bc1903e5e9bbec322e" - integrity sha512-H2DZ1l3iYF5Rp5pPbJpmmtCauWeQXRJapkDg8epQ8BJ7cA2Ut/QEtC3CMmw/iMTcuS6uemFNLcWvlOfoQhvQuA== - dependencies: - chalk "^5.3.0" - commander "^12.0.0" - -"@solana/errors@2.0.0-preview.4": - version "2.0.0-preview.4" - resolved "https://registry.yarnpkg.com/@solana/errors/-/errors-2.0.0-preview.4.tgz#056ba76b6dd900dafa70117311bec3aef0f5250b" - integrity sha512-kadtlbRv2LCWr8A9V22On15Us7Nn8BvqNaOB4hXsTB3O0fU40D1ru2l+cReqLcRPij4znqlRzW9Xi0m6J5DIhA== - dependencies: - chalk "^5.3.0" - commander "^12.1.0" - "@solana/errors@2.0.0-rc.1": version "2.0.0-rc.1" resolved "https://registry.yarnpkg.com/@solana/errors/-/errors-2.0.0-rc.1.tgz#3882120886eab98a37a595b85f81558861b29d62" @@ -502,24 +429,22 @@ chalk "^5.3.0" commander "^12.1.0" -"@solana/options@2.0.0-preview.2": - version "2.0.0-preview.2" - resolved "https://registry.yarnpkg.com/@solana/options/-/options-2.0.0-preview.2.tgz#13ff008bf43a5056ef9a091dc7bb3f39321e867e" - integrity sha512-FAHqEeH0cVsUOTzjl5OfUBw2cyT8d5Oekx4xcn5hn+NyPAfQJgM3CEThzgRD6Q/4mM5pVUnND3oK/Mt1RzSE/w== - dependencies: - "@solana/codecs-core" "2.0.0-preview.2" - "@solana/codecs-numbers" "2.0.0-preview.2" +"@solana/functional@2.0.0-experimental.21e994f": + version "2.0.0-experimental.21e994f" + resolved "https://registry.yarnpkg.com/@solana/functional/-/functional-2.0.0-experimental.21e994f.tgz#e7ebdc8fcb14a0a2bc7d0f7df8667d171f54a10b" + integrity sha512-FMXFiTA+hsc9FCv0r47oF7njq/K9x7zh0H+To7tpeqwN65LtJPu5BMG7xZY3rn5TrudgKw6XPuIr3ARbI8+IWA== + +"@solana/instructions@2.0.0-experimental.21e994f": + version "2.0.0-experimental.21e994f" + resolved "https://registry.yarnpkg.com/@solana/instructions/-/instructions-2.0.0-experimental.21e994f.tgz#f308fdb671252ff52fcf08366fe7a1800b0d54b1" + integrity sha512-PuJJzvT7wtwE5UcGavUppnfVWnoxL8CPhZBb96HpOaQhQ2JuyhN445bfav5KkaUMCE6ubrVzOEqzrbtygD3aBg== -"@solana/options@2.0.0-preview.4": - version "2.0.0-preview.4" - resolved "https://registry.yarnpkg.com/@solana/options/-/options-2.0.0-preview.4.tgz#212d35d1da87c7efb13de4d3569ad9eb070f013d" - integrity sha512-tv2O/Frxql/wSe3jbzi5nVicIWIus/BftH+5ZR+r9r3FO0/htEllZS5Q9XdbmSboHu+St87584JXeDx3xm4jaA== +"@solana/keys@2.0.0-experimental.21e994f": + version "2.0.0-experimental.21e994f" + resolved "https://registry.yarnpkg.com/@solana/keys/-/keys-2.0.0-experimental.21e994f.tgz#52e9307a0a0055f2bbff23e76b38cb4ba7f75da3" + integrity sha512-Qsm7ARy69PdIuis7TZy8ELyhq0pcRFPXtaZ8vLFUvsukrcWRowiJ8JJs6Q3tA+gQK5vUn9ABp7a7Qs0FHzgbyw== dependencies: - "@solana/codecs-core" "2.0.0-preview.4" - "@solana/codecs-data-structures" "2.0.0-preview.4" - "@solana/codecs-numbers" "2.0.0-preview.4" - "@solana/codecs-strings" "2.0.0-preview.4" - "@solana/errors" "2.0.0-preview.4" + "@solana/assertions" "2.0.0-experimental.21e994f" "@solana/options@2.0.0-rc.1": version "2.0.0-rc.1" @@ -532,29 +457,47 @@ "@solana/codecs-strings" "2.0.0-rc.1" "@solana/errors" "2.0.0-rc.1" -"@solana/spl-token-group@^0.0.5": - version "0.0.5" - resolved "https://registry.yarnpkg.com/@solana/spl-token-group/-/spl-token-group-0.0.5.tgz#f955dcca782031c85e862b2b46878d1bb02db6c2" - integrity sha512-CLJnWEcdoUBpQJfx9WEbX3h6nTdNiUzswfFdkABUik7HVwSNA98u5AYvBVK2H93d9PGMOHAak2lHW9xr+zAJGQ== +"@solana/rpc-core@2.0.0-experimental.21e994f": + version "2.0.0-experimental.21e994f" + resolved "https://registry.yarnpkg.com/@solana/rpc-core/-/rpc-core-2.0.0-experimental.21e994f.tgz#294c0ea4d99c1bd6b11bb0c0cc67847adb6f3c3a" + integrity sha512-T7VcTLRi4dsqmpFYdnvcHZFS8Vcgdi6funMUrXcM7ofQqb8vWGJnlX6AX0eIZiVsmoYk5Ki8wW4D6Ul6bXZyZg== dependencies: - "@solana/codecs" "2.0.0-preview.4" - "@solana/spl-type-length-value" "0.1.0" + "@metaplex-foundation/umi-serializers" "^0.8.9" -"@solana/spl-token-metadata@^0.1.2": +"@solana/rpc-transport@2.0.0-experimental.21e994f": + version "2.0.0-experimental.21e994f" + resolved "https://registry.yarnpkg.com/@solana/rpc-transport/-/rpc-transport-2.0.0-experimental.21e994f.tgz#2b8c3f97f4853711daaeed03ff0700a1d44aca4a" + integrity sha512-PfGPzRuEodhfLyOD8ZneYQ389SWYgmj1Q/HWQZo8yZMsiAaW/lqCygoW88lecxXKlZF5gJYrBX154kgvGqEM7g== + +"@solana/spl-single-pool-classic@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@solana/spl-single-pool-classic/-/spl-single-pool-classic-1.0.2.tgz#f675cdf39037cd42a3a4690a030cc77c0837e40c" + integrity sha512-kh2D3KElYsJWZIoksCd5dlC9jsKict7WTS+lZvhaGXTarZbjMqhIaiiMTe5oqKgHSNwavoP05VJ8YlTmbTxTLg== + dependencies: + "@solana/spl-single-pool" "1.0.0" + "@solana/web3.js" "^1.91.6" + +"@solana/spl-single-pool@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@solana/spl-single-pool/-/spl-single-pool-1.0.0.tgz#eec9ca109ad63936b60cab6f2f6f9566e0cd0eeb" + integrity sha512-m2zNzRXcYXibd2n514TQhWM7WkWuCqddNHmxgPDcpSFpwbP9hNUigjGoJeR1khvKjRj5jV+PdiiwBEWi3pExfw== + dependencies: + "@solana/web3.js" "=2.0.0-experimental.21e994f" + +"@solana/spl-token-group@^0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@solana/spl-token-group/-/spl-token-group-0.0.7.tgz#83c00f0cd0bda33115468cd28b89d94f8ec1fee4" + integrity sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug== + dependencies: + "@solana/codecs" "2.0.0-rc.1" + +"@solana/spl-token-metadata@^0.1.2", "@solana/spl-token-metadata@^0.1.6": version "0.1.6" resolved "https://registry.yarnpkg.com/@solana/spl-token-metadata/-/spl-token-metadata-0.1.6.tgz#d240947aed6e7318d637238022a7b0981b32ae80" integrity sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA== dependencies: "@solana/codecs" "2.0.0-rc.1" -"@solana/spl-token-metadata@^0.1.3": - version "0.1.4" - resolved "https://registry.yarnpkg.com/@solana/spl-token-metadata/-/spl-token-metadata-0.1.4.tgz#5cdc3b857a8c4a6877df24e24a8648c4132d22ba" - integrity sha512-N3gZ8DlW6NWDV28+vCCDJoTqaCZiF/jDUnk3o8GRkAFzHObiR60Bs1gXHBa8zCPdvOwiG6Z3dg5pg7+RW6XNsQ== - dependencies: - "@solana/codecs" "2.0.0-preview.2" - "@solana/spl-type-length-value" "0.1.0" - "@solana/spl-token@^0.3.4": version "0.3.11" resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.3.11.tgz#cdc10f9472b29b39c8983c92592cadd06627fb9a" @@ -566,22 +509,24 @@ buffer "^6.0.3" "@solana/spl-token@^0.4.8": - version "0.4.8" - resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.4.8.tgz#a84e4131af957fa9fbd2727e5fc45dfbf9083586" - integrity sha512-RO0JD9vPRi4LsAbMUdNbDJ5/cv2z11MGhtAvFeRzT4+hAGE/FUzRi0tkkWtuCfSIU3twC6CtmAihRp/+XXjWsA== + version "0.4.9" + resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.4.9.tgz#24d032d2935f237925c3b058ba6bb1e1ece5428c" + integrity sha512-g3wbj4F4gq82YQlwqhPB0gHFXfgsC6UmyGMxtSLf/BozT/oKd59465DbnlUK8L8EcimKMavxsVAMoLcEdeCicg== dependencies: "@solana/buffer-layout" "^4.0.0" "@solana/buffer-layout-utils" "^0.2.0" - "@solana/spl-token-group" "^0.0.5" - "@solana/spl-token-metadata" "^0.1.3" + "@solana/spl-token-group" "^0.0.7" + "@solana/spl-token-metadata" "^0.1.6" buffer "^6.0.3" -"@solana/spl-type-length-value@0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@solana/spl-type-length-value/-/spl-type-length-value-0.1.0.tgz#b5930cf6c6d8f50c7ff2a70463728a4637a2f26b" - integrity sha512-JBMGB0oR4lPttOZ5XiUGyvylwLQjt1CPJa6qQ5oM+MBCndfjz2TKKkw0eATlLLcYmq1jBVsNlJ2cD6ns2GR7lA== +"@solana/transactions@2.0.0-experimental.21e994f": + version "2.0.0-experimental.21e994f" + resolved "https://registry.yarnpkg.com/@solana/transactions/-/transactions-2.0.0-experimental.21e994f.tgz#48dc6483a1d57e85cd23c88e854239b2ac0bd097" + integrity sha512-DunbTMBzlC7jmTzkFsRm5DhGe+MjaZ8m+SJ7V520mQq+kxrbPrRmI3ikfUVdejg0WaEV4Dy+RwQ5xllsrJ47kA== dependencies: - buffer "^6.0.3" + "@metaplex-foundation/umi-serializers" "^0.8.9" + "@solana/addresses" "2.0.0-experimental.21e994f" + "@solana/keys" "2.0.0-experimental.21e994f" "@solana/wallet-adapter-base@^0.9.23": version "0.9.23" @@ -601,31 +546,25 @@ "@wallet-standard/base" "^1.0.1" "@wallet-standard/features" "^1.0.3" -"@solana/web3.js@^1.32.0", "@solana/web3.js@^1.68.0", "@solana/web3.js@^1.90.0", "@solana/web3.js@^1.93.2", "@solana/web3.js@^1.95.2": - version "1.95.2" - resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.95.2.tgz#6f8a0362fa75886a21550dbec49aad54481463a6" - integrity sha512-SjlHp0G4qhuhkQQc+YXdGkI8EerCqwxvgytMgBpzMUQTafrkNant3e7pgilBGgjy/iM40ICvWBLgASTPMrQU7w== - dependencies: - "@babel/runtime" "^7.24.8" - "@noble/curves" "^1.4.2" - "@noble/hashes" "^1.4.0" - "@solana/buffer-layout" "^4.0.1" - agentkeepalive "^4.5.0" - bigint-buffer "^1.1.5" - bn.js "^5.2.1" - borsh "^0.7.0" - bs58 "^4.0.1" - buffer "6.0.3" +"@solana/web3.js@=2.0.0-experimental.21e994f": + version "2.0.0-experimental.21e994f" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-2.0.0-experimental.21e994f.tgz#c5568d88903f63c85de700c03b2acef2217d059f" + integrity sha512-Yy0D57nlNTDm0BhBRIM85Sn52T6vjxpBRRdwE/FOJJmN92n0Qpc4mTAwOPfEqoVpiTcluUBZ4l8FAWxjGCFMgQ== + dependencies: + "@metaplex-foundation/umi-serializers" "^0.8.9" + "@solana/addresses" "2.0.0-experimental.21e994f" + "@solana/functional" "2.0.0-experimental.21e994f" + "@solana/instructions" "2.0.0-experimental.21e994f" + "@solana/keys" "2.0.0-experimental.21e994f" + "@solana/rpc-core" "2.0.0-experimental.21e994f" + "@solana/rpc-transport" "2.0.0-experimental.21e994f" + "@solana/transactions" "2.0.0-experimental.21e994f" fast-stable-stringify "^1.0.0" - jayson "^4.1.1" - node-fetch "^2.7.0" - rpc-websockets "^9.0.2" - superstruct "^2.0.2" -"@solana/web3.js@^1.54.0", "@solana/web3.js@^1.93.0", "@solana/web3.js@^1.95.0": - version "1.95.7" - resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.95.7.tgz#0c0f4e883795bb3a93d1f336223e9907a722a475" - integrity sha512-9Sut9HhajumawFIz0wcPxlfBsHxDvq/nbJD/ZtZOXrxOj4WvgQx0AiGGzxG128RYZYjgZbjnwF6OlHsfQ//WRA== +"@solana/web3.js@^1.32.0", "@solana/web3.js@^1.68.0", "@solana/web3.js@^1.90.0", "@solana/web3.js@^1.91.6", "@solana/web3.js@^1.93.2", "@solana/web3.js@^1.95.2", "@solana/web3.js@^1.95.3", "@solana/web3.js@^1.95.8": + version "1.95.8" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.95.8.tgz#2d49abda23f7a79a3cc499ab6680f7be11786ee1" + integrity sha512-sBHzNh7dHMrmNS5xPD1d0Xa2QffW/RXaxu/OysRXBfwTp+LYqGGmMtCYYwrHPrN5rjAmJCsQRNAwv4FM0t3B6g== dependencies: "@babel/runtime" "^7.25.0" "@noble/curves" "^1.4.2" @@ -664,18 +603,19 @@ rpc-websockets "^7.5.1" superstruct "^0.14.2" -"@solworks/soltoolkit-sdk@^0.0.23": - version "0.0.23" - resolved "https://registry.yarnpkg.com/@solworks/soltoolkit-sdk/-/soltoolkit-sdk-0.0.23.tgz#ef32d0aa79f888bcf0f639d280005b2e97cdc624" - integrity sha512-O6lXT3EBR4gmcjt0/33i97VMHVEImwXGi+4TNrDDdifn3tyOUB7V6PR1VGxlavQb9hqmVai3xhedg/rmbQzX7w== +"@solworks/soltoolkit-sdk@^0.0.37": + version "0.0.37" + resolved "https://registry.yarnpkg.com/@solworks/soltoolkit-sdk/-/soltoolkit-sdk-0.0.37.tgz#53800f0e43c56962194b130e02c713d8d7fb6a7c" + integrity sha512-3+mNv9ymup0LTOmZRhIWvqGmf9Col1TKuZ2I9dqrnbSveOBnjCgNDUkfge4qJ2FcWNZdVEvHzUd3UkV902INAg== dependencies: "@solana/buffer-layout" "^4.0.0" "@solana/spl-token" "^0.3.4" - "@solana/web3.js" "^1.54.0" + "@solana/web3.js" "^1.95.3" "@types/bn.js" "^5.1.0" "@types/node" "^18.7.13" "@types/node-fetch" "^2.6.2" bn.js "^5.2.1" + bs58 "^5.0.0" decimal.js "^10.4.0" typescript "^4.8.2" @@ -685,44 +625,44 @@ integrity sha512-zA2oZluZmVvgZEDjF243KWD1S2J+1SH1MVynI0O1KRgDt1lU8nqk7AK3oQfW/WpwT51L5waGSU0xKF/9BTP5Cw== "@swc/helpers@^0.5.11": - version "0.5.12" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.12.tgz#37aaca95284019eb5d2207101249435659709f4b" - integrity sha512-KMZNXiGibsW9kvZAO1Pam2JPTDBm+KSHMMHWdsyI/1DbIZjT2A6Gy3hblVXUMEDvUAKq+e0vL0X0o54owWji7g== + version "0.5.15" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.15.tgz#79efab344c5819ecf83a43f3f9f811fc84b516d7" + integrity sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g== dependencies: - tslib "^2.4.0" + tslib "^2.8.0" -"@switchboard-xyz/common@^2.5.3": - version "2.5.5" - resolved "https://registry.yarnpkg.com/@switchboard-xyz/common/-/common-2.5.5.tgz#773c20584877af86abe724e9787de8f3e6385bce" - integrity sha512-/qUmZlrfQyckvHGzS5Cj2+Ocd3eE64rPjQb1eEocc5dv4HXZMqbBbpM6BwURrQhZ65i3jO1evhTcAk3TVqCA8w== +"@switchboard-xyz/common@^2.5.7": + version "2.5.7" + resolved "https://registry.yarnpkg.com/@switchboard-xyz/common/-/common-2.5.7.tgz#8b781a882318d7e2e661bab3bc3695575c3da20e" + integrity sha512-xUThQ2Zuf+2/nO1J459DC3BrQn1OxytbrU84mxuGZKw4APhtm7hKKCaIdQ1uzdASj9v3NRZ+/KIPeMaQfk+ZZA== dependencies: - "@solana/web3.js" "^1.93.0" - axios "^1.7.2" - big.js "^6.2.1" + "@solana/web3.js" "^1.95.8" + axios "^1.7.8" + big.js "^6.2.2" bn.js "^5.2.1" - bs58 "^5.0.0" + bs58 "^6.0.0" cron-validator "^1.3.1" decimal.js "^10.4.3" js-sha256 "^0.11.0" lodash "^4.17.21" - protobufjs "^7.2.6" - yaml "^2.5.0" + protobufjs "^7.4.0" + yaml "^2.6.1" "@switchboard-xyz/on-demand@^1.2.36": - version "1.2.51" - resolved "https://registry.yarnpkg.com/@switchboard-xyz/on-demand/-/on-demand-1.2.51.tgz#ad42a0855dcff59d3cd7e34ba4dc9ea4531bfddf" - integrity sha512-IqtAEtYdCRQqG8a3tL5WOcLgBco8Iionu60Q+hQzCslQw76zDlkToHkI+71ASulFdZ2z+2XjaKV5ZVqPcYgP7g== + version "1.2.54" + resolved "https://registry.yarnpkg.com/@switchboard-xyz/on-demand/-/on-demand-1.2.54.tgz#ddf88d8bbc9525c2a447505f6ef22323da34db28" + integrity sha512-R7f0LmtV/XEbWhPVTKCWFIzGnbBgu8caP9eOlUapgcR+07oChU2SIyFtoG/bjNbAwKgx8TNVOxEk5RaZe5EyiA== dependencies: "@brokerloop/ttlcache" "^3.2.3" "@coral-xyz/anchor-30" "npm:@coral-xyz/anchor@0.30.1" - "@solana/web3.js" "^1.95.0" - "@solworks/soltoolkit-sdk" "^0.0.23" - "@switchboard-xyz/common" "^2.5.3" - axios "^1.7.4" - big.js "^6.2.1" - bs58 "^5.0.0" + "@solana/web3.js" "^1.95.8" + "@solworks/soltoolkit-sdk" "^0.0.37" + "@switchboard-xyz/common" "^2.5.7" + axios "^1.7.8" + big.js "^6.2.2" + bs58 "^6.0.0" js-yaml "^4.1.0" - protobufjs "^7.2.6" + protobufjs "^7.4.0" "@tsconfig/node10@^1.0.7": version "1.0.11" @@ -745,21 +685,21 @@ integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== "@types/bn.js@^5.1.0": - version "5.1.1" - resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.1.tgz#b51e1b55920a4ca26e9285ff79936bbdec910682" - integrity sha512-qNrYbZqMx0uJAfKnKclPh+dTwK33KfLHYqtyODwd5HnXOjnkhc4qgn3BrK6RWyGZm5+sIFE7Q7Vz6QQtJB7w7g== + version "5.1.6" + resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.6.tgz#9ba818eec0c85e4d3c679518428afdf611d03203" + integrity sha512-Xh8vSwUeMKeYYrj3cX4lGQgFSF/N03r+tv4AiLl1SucqV+uTQpxRcnM8AkXKHwYP9ZPXOYXRr2KPXpVlIvqh9w== dependencies: "@types/node" "*" "@types/chai@^4.3.0": - version "4.3.4" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.4.tgz#e913e8175db8307d78b4e8fa690408ba6b65dee4" - integrity sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw== + version "4.3.20" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.20.tgz#cb291577ed342ca92600430841a00329ba05cecc" + integrity sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ== "@types/connect@^3.4.33": - version "3.4.35" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" - integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== dependencies: "@types/node" "*" @@ -781,17 +721,12 @@ "@types/node" "*" form-data "^4.0.0" -"@types/node@*": - version "18.11.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.13.tgz#dff34f226ec1ac0432ae3b136ec5552bd3b9c0fe" - integrity sha512-IASpMGVcWpUsx5xBOrxMj7Bl8lqfuTY7FKAnPmu5cHkfQVWF8GulWS1jbRqA934qZL35xh5xN/+Xe/i26Bod4w== - -"@types/node@>=13.7.0": - version "22.3.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.3.0.tgz#7f8da0e2b72c27c4f9bd3cb5ef805209d04d4f9e" - integrity sha512-nrWpWVaDZuaVc5X84xJ0vNrLvomM205oQyLsRt7OHNZbSHslcWsvgFR7O7hire2ZonjLrWBbedmotmIlJDVd6g== +"@types/node@*", "@types/node@>=13.7.0": + version "22.10.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.2.tgz#a485426e6d1fdafc7b0d4c7b24e2c78182ddabb9" + integrity sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ== dependencies: - undici-types "~6.18.2" + undici-types "~6.20.0" "@types/node@^12.12.54": version "12.20.55" @@ -799,9 +734,9 @@ integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== "@types/node@^18.7.13": - version "18.19.67" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.67.tgz#77c4b01641a1e3e1509aff7e10d39e4afd5ae06d" - integrity sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ== + version "18.19.68" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.68.tgz#f4f10d9927a7eaf3568c46a6d739cc0967ccb701" + integrity sha512-QGtpFH1vB99ZmTa63K4/FU8twThj4fuVSBkGddTp7uIL/cuoLWIUSL2RcOaigBhfR+hg5pgGkBnkoOxrTVBMKw== dependencies: undici-types "~5.26.4" @@ -818,23 +753,23 @@ "@types/node" "*" "@types/ws@^8.2.2": - version "8.5.12" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e" - integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ== + version "8.5.13" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.13.tgz#6414c280875e2691d0d1e080b05addbf5cb91e20" + integrity sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA== dependencies: "@types/node" "*" -"@wallet-standard/base@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@wallet-standard/base/-/base-1.0.1.tgz#860dd94d47c9e3c5c43b79d91c6afdbd7a36264e" - integrity sha512-1To3ekMfzhYxe0Yhkpri+Fedq0SYcfrOfJi3vbLjMwF2qiKPjTGLwZkf2C9ftdQmxES+hmxhBzTwF4KgcOwf8w== +"@wallet-standard/base@^1.0.1", "@wallet-standard/base@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@wallet-standard/base/-/base-1.1.0.tgz#214093c0597a1e724ee6dbacd84191dfec62bb33" + integrity sha512-DJDQhjKmSNVLKWItoKThJS+CsJQjR9AOBOirBVT1F9YpRyC9oYHE+ZnSf8y8bxUphtKqdQMPVQ2mHohYdRvDVQ== "@wallet-standard/features@^1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@wallet-standard/features/-/features-1.0.3.tgz#c992876c5e4f7a0672f8869c4146c87e0dfe48c8" - integrity sha512-m8475I6W5LTatTZuUz5JJNK42wFRgkJTB0I9tkruMwfqBF2UN2eomkYNVf9RbrsROelCRzSFmugqjKZBFaubsA== + version "1.1.0" + resolved "https://registry.yarnpkg.com/@wallet-standard/features/-/features-1.1.0.tgz#f256d7b18940c8d134f66164330db358a8f5200e" + integrity sha512-hiEivWNztx73s+7iLxsuD1sOJ28xtRix58W7Xnz4XzzA/pF0+aicnWgjOdA10doVDEDZdUuZCIIqG96SFNlDUg== dependencies: - "@wallet-standard/base" "^1.0.1" + "@wallet-standard/base" "^1.1.0" JSONStream@^1.3.5: version "1.3.5" @@ -845,16 +780,16 @@ JSONStream@^1.3.5: through ">=2.2.7 <3" acorn-walk@^8.1.1: - version "8.3.3" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.3.tgz#9caeac29eefaa0c41e3d4c65137de4d6f34df43e" - integrity sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw== + version "8.3.4" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" + integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== dependencies: acorn "^8.11.0" acorn@^8.11.0, acorn@^8.4.1: - version "8.12.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" - integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== + version "8.14.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" + integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== agentkeepalive@^4.2.1, agentkeepalive@^4.3.0, agentkeepalive@^4.5.0: version "4.5.0" @@ -863,6 +798,11 @@ agentkeepalive@^4.2.1, agentkeepalive@^4.3.0, agentkeepalive@^4.5.0: dependencies: humanize-ms "^1.2.1" +anchor-bankrun@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/anchor-bankrun/-/anchor-bankrun-0.4.1.tgz#6fbbf824673f5fcdf353b1f1003d561c14a67c79" + integrity sha512-ryCT84tw+lP4AqRpBsZJbt/KTRoVVKufkxFGd77gnx9iHkbwA5G/9cALk/eqLQm4xeUWTrJSJdEVyg2e74iP9A== + ansi-colors@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" @@ -913,10 +853,10 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== -axios@^1.7.2, axios@^1.7.4: - version "1.7.8" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.8.tgz#1997b1496b394c21953e68c14aaa51b7b5de3d6e" - integrity sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw== +axios@^1.7.8: + version "1.7.9" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a" + integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw== dependencies: follow-redirects "^1.15.6" form-data "^4.0.0" @@ -928,9 +868,9 @@ balanced-match@^1.0.0: integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== base-x@^3.0.2: - version "3.0.9" - resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320" - integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ== + version "3.0.10" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.10.tgz#62de58653f8762b5d6f8d9fe30fa75f7b2585a75" + integrity sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ== dependencies: safe-buffer "^5.0.1" @@ -949,7 +889,7 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -big.js@^6.2.1: +big.js@^6.2.1, big.js@^6.2.2: version "6.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-6.2.2.tgz#be3bb9ac834558b53b099deef2a1d06ac6368e1a" integrity sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ== @@ -967,9 +907,9 @@ bignumber.js@^9.0.1, bignumber.js@^9.1.2: integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== bindings@^1.3.0: version "1.5.0" @@ -1005,11 +945,11 @@ brace-expansion@^2.0.1: balanced-match "^1.0.0" braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" browser-stdout@^1.3.1: version "1.3.1" @@ -1056,9 +996,9 @@ buffer@6.0.3, buffer@^6.0.3, buffer@~6.0.3: ieee754 "^1.2.1" bufferutil@^4.0.1: - version "4.0.7" - resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.7.tgz#60c0d19ba2c992dd8273d3f73772ffc894c153ad" - integrity sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw== + version "4.0.8" + resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.8.tgz#1de6a71092d65d7766c4d8a522b261a6e787e8ea" + integrity sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw== dependencies: node-gyp-build "^4.3.0" @@ -1068,17 +1008,17 @@ camelcase@^6.0.0, camelcase@^6.3.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== chai@^4.3.4: - version "4.3.7" - resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.7.tgz#ec63f6df01829088e8bf55fca839bcd464a8ec51" - integrity sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A== + version "4.5.0" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.5.0.tgz#707e49923afdd9b13a8b0b47d33d732d13812fd8" + integrity sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw== dependencies: assertion-error "^1.1.0" - check-error "^1.0.2" - deep-eql "^4.1.2" - get-func-name "^2.0.0" - loupe "^2.3.1" + check-error "^1.0.3" + deep-eql "^4.1.3" + get-func-name "^2.0.2" + loupe "^2.3.6" pathval "^1.1.1" - type-detect "^4.0.5" + type-detect "^4.1.0" chalk@^4.1.0: version "4.1.2" @@ -1093,10 +1033,12 @@ chalk@^5.3.0: resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== -check-error@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" - integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA== +check-error@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" + integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== + dependencies: + get-func-name "^2.0.2" chokidar@^3.5.3: version "3.6.0" @@ -1150,7 +1092,7 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -commander@^12.0.0, commander@^12.1.0: +commander@^12.1.0: version "12.1.0" resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== @@ -1171,11 +1113,11 @@ cron-validator@^1.3.1: integrity sha512-C1HsxuPCY/5opR55G5/WNzyEGDWFVG+6GLrA+fW/sCTcP6A6NTjUP2AK7B8n2PyFs90kDG2qzwm8LMheADku6A== cross-fetch@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" - integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== + version "3.1.8" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" + integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== dependencies: - node-fetch "2.6.7" + node-fetch "^2.6.12" crypto-hash@^1.3.0: version "1.3.0" @@ -1183,11 +1125,11 @@ crypto-hash@^1.3.0: integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg== debug@^4.3.5: - version "4.3.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" - integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== dependencies: - ms "2.1.2" + ms "^2.1.3" decamelize@^4.0.0: version "4.0.0" @@ -1199,10 +1141,10 @@ decimal.js@^10.4.0, decimal.js@^10.4.3: resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== -deep-eql@^4.1.2: - version "4.1.3" - resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.3.tgz#7c7775513092f7df98d8df9996dd085eb668cc6d" - integrity sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw== +deep-eql@^4.1.3: + version "4.1.4" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.4.tgz#d0d3912865911bb8fac5afb4e3acfa6a28dc72b7" + integrity sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg== dependencies: type-detect "^4.0.0" @@ -1240,9 +1182,9 @@ dot-case@^3.0.4: tslib "^2.0.3" dotenv@^16.0.3: - version "16.4.5" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" - integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== + version "16.4.7" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26" + integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ== emoji-regex@^8.0.0: version "8.0.0" @@ -1262,9 +1204,9 @@ es6-promisify@^5.0.0: es6-promise "^4.0.3" escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== escape-string-regexp@^4.0.0: version "4.0.0" @@ -1296,10 +1238,10 @@ file-uri-to-path@1.0.0: resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -1336,19 +1278,19 @@ fs.realpath@^1.0.0: integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-func-name@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" - integrity sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig== +get-func-name@^2.0.1, get-func-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" + integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== glob-parent@~5.1.2: version "5.1.2" @@ -1448,9 +1390,9 @@ isomorphic-ws@^4.0.1: integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== jayson@^4.0.0, jayson@^4.1.0, jayson@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/jayson/-/jayson-4.1.1.tgz#282ff13d3cea09776db684b7eeca98c47b2fa99a" - integrity sha512-5ZWm4Q/0DHPyeMfAsrwViwUS2DMVsQgWh8bEEIVTkfb3DzHZ2L3G5WUnF+AKmGjjM9r1uAv73SaqC1/U4RL45w== + version "4.1.3" + resolved "https://registry.yarnpkg.com/jayson/-/jayson-4.1.3.tgz#db9be2e4287d9fef4fc05b5fe367abe792c2eee8" + integrity sha512-LtXh5aYZodBZ9Fc3j6f2w+MTNcnxteMOrb+QgIouguGOulWi0lieEkOUg+HkjjFs0DGoWDds6bi4E9hpNFLulQ== dependencies: "@types/connect" "^3.4.33" "@types/node" "^12.12.54" @@ -1496,7 +1438,7 @@ json-stringify-safe@^5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== -json5@^1.0.1: +json5@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== @@ -1538,12 +1480,12 @@ long@^5.0.0: resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== -loupe@^2.3.1: - version "2.3.6" - resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.6.tgz#76e4af498103c532d1ecc9be102036a21f787b53" - integrity sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA== +loupe@^2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" + integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== dependencies: - get-func-name "^2.0.0" + get-func-name "^2.0.1" lower-case@^2.0.2: version "2.0.2" @@ -1577,9 +1519,9 @@ minimatch@^5.0.1, minimatch@^5.1.6: brace-expansion "^2.0.1" minimist@^1.2.0, minimist@^1.2.6: - version "1.2.7" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" - integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== mkdirp@^0.5.1: version "0.5.6" @@ -1589,9 +1531,9 @@ mkdirp@^0.5.1: minimist "^1.2.6" mocha@^10.2.0: - version "10.7.0" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.7.0.tgz#9e5cbed8fa9b37537a25bd1f7fb4f6fc45458b9a" - integrity sha512-v8/rBWr2VO5YkspYINnvu81inSz2y3ODJrhO175/Exzor1RcEZZkizgE2A+w/CAXXoESS8Kys5E62dOHGHzULA== + version "10.8.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.8.2.tgz#8d8342d016ed411b12a429eb731b825f961afb96" + integrity sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg== dependencies: ansi-colors "^4.1.3" browser-stdout "^1.3.1" @@ -1614,11 +1556,6 @@ mocha@^10.2.0: yargs-parser "^20.2.9" yargs-unparser "^2.0.0" -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - ms@^2.0.0, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" @@ -1632,14 +1569,7 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -node-fetch@2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== - dependencies: - whatwg-url "^5.0.0" - -node-fetch@^2.6.7, node-fetch@^2.7.0: +node-fetch@^2.6.12, node-fetch@^2.6.7, node-fetch@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -1647,9 +1577,9 @@ node-fetch@^2.6.7, node-fetch@^2.7.0: whatwg-url "^5.0.0" node-gyp-build@^4.3.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40" - integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg== + version "4.8.4" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8" + integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" @@ -1703,29 +1633,11 @@ picomatch@^2.0.4, picomatch@^2.2.1: integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== prettier@^2.6.2: - version "2.8.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.1.tgz#4e1fd11c34e2421bc1da9aea9bd8127cd0a35efc" - integrity sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg== - -protobufjs@^7.2.5: - version "7.3.2" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.3.2.tgz#60f3b7624968868f6f739430cfbc8c9370e26df4" - integrity sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg== - dependencies: - "@protobufjs/aspromise" "^1.1.2" - "@protobufjs/base64" "^1.1.2" - "@protobufjs/codegen" "^2.0.4" - "@protobufjs/eventemitter" "^1.1.0" - "@protobufjs/fetch" "^1.1.0" - "@protobufjs/float" "^1.0.2" - "@protobufjs/inquire" "^1.1.0" - "@protobufjs/path" "^1.1.2" - "@protobufjs/pool" "^1.1.0" - "@protobufjs/utf8" "^1.1.0" - "@types/node" ">=13.7.0" - long "^5.0.0" + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== -protobufjs@^7.2.6: +protobufjs@^7.2.5, protobufjs@^7.4.0: version "7.4.0" resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.4.0.tgz#7efe324ce9b3b61c82aae5de810d287bc08a248a" integrity sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw== @@ -1785,9 +1697,9 @@ rpc-websockets@^7.5.1: utf-8-validate "^5.0.2" rpc-websockets@^9.0.2: - version "9.0.2" - resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-9.0.2.tgz#4c1568d00b8100f997379a363478f41f8f4b242c" - integrity sha512-YzggvfItxMY3Lwuax5rC18inhbjJv9Py7JXRHxTIi94JOLrqBsSsUUc5bbl5W6c11tXhdfpDPK0KzBhoGe8jjw== + version "9.0.4" + resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-9.0.4.tgz#9d8ee82533b5d1e13d9ded729e3e38d0d8fa083f" + integrity sha512-yWZWN0M+bivtoNLnaDbtny4XchdAIF5Q4g/ZsC5UC61Ckbp0QczwO8fg44rV3uYmY4WHd+EZQbn90W1d8ojzqQ== dependencies: "@swc/helpers" "^0.5.11" "@types/uuid" "^8.3.4" @@ -1820,6 +1732,45 @@ snake-case@^3.0.4: dot-case "^3.0.4" tslib "^2.0.3" +solana-bankrun-darwin-arm64@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/solana-bankrun-darwin-arm64/-/solana-bankrun-darwin-arm64-0.3.1.tgz#65ab6cd2e74eef260c38251f4c53721cf5b9030f" + integrity sha512-9LWtH/3/WR9fs8Ve/srdo41mpSqVHmRqDoo69Dv1Cupi+o1zMU6HiEPUHEvH2Tn/6TDbPEDf18MYNfReLUqE6A== + +solana-bankrun-darwin-universal@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/solana-bankrun-darwin-universal/-/solana-bankrun-darwin-universal-0.3.1.tgz#bf691457cf046e8739c021ca11e48de5b4fefd45" + integrity sha512-muGHpVYWT7xCd8ZxEjs/bmsbMp8XBqroYGbE4lQPMDUuLvsJEIrjGqs3MbxEFr71sa58VpyvgywWd5ifI7sGIg== + +solana-bankrun-darwin-x64@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/solana-bankrun-darwin-x64/-/solana-bankrun-darwin-x64-0.3.1.tgz#c6f30c0a6bc3e1621ed90ce7562f26e93bf5303f" + integrity sha512-oCaxfHyt7RC3ZMldrh5AbKfy4EH3YRMl8h6fSlMZpxvjQx7nK7PxlRwMeflMnVdkKKp7U8WIDak1lilIPd3/lg== + +solana-bankrun-linux-x64-gnu@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/solana-bankrun-linux-x64-gnu/-/solana-bankrun-linux-x64-gnu-0.3.1.tgz#78b522f1a581955a48f43a8fb560709c11301cfd" + integrity sha512-PfRFhr7igGFNt2Ecfdzh3li9eFPB3Xhmk0Eib17EFIB62YgNUg3ItRnQQFaf0spazFjjJLnglY1TRKTuYlgSVA== + +solana-bankrun-linux-x64-musl@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/solana-bankrun-linux-x64-musl/-/solana-bankrun-linux-x64-musl-0.3.1.tgz#1a044a132138a0084e82406ec7bf4939f06bed68" + integrity sha512-6r8i0NuXg3CGURql8ISMIUqhE7Hx/O7MlIworK4oN08jYrP0CXdLeB/hywNn7Z8d1NXrox/NpYUgvRm2yIzAsQ== + +solana-bankrun@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/solana-bankrun/-/solana-bankrun-0.3.1.tgz#13665ab7c1c15ec2b3354aae56980d0ded514998" + integrity sha512-inRwON7fBU5lPC36HdEqPeDg15FXJYcf77+o0iz9amvkUMJepcwnRwEfTNyMVpVYdgjTOBW5vg+596/3fi1kGA== + dependencies: + "@solana/web3.js" "^1.68.0" + bs58 "^4.0.1" + optionalDependencies: + solana-bankrun-darwin-arm64 "0.3.1" + solana-bankrun-darwin-universal "0.3.1" + solana-bankrun-darwin-x64 "0.3.1" + solana-bankrun-linux-x64-gnu "0.3.1" + solana-bankrun-linux-x64-musl "0.3.1" + source-map-support@^0.5.6: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" @@ -1963,36 +1914,26 @@ ts-node@^10.9.1: yn "3.1.1" tsconfig-paths@^3.5.0: - version "3.14.1" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" - integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== + version "3.15.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== dependencies: "@types/json5" "^0.0.29" - json5 "^1.0.1" + json5 "^1.0.2" minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.0.3: - version "2.4.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" - integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== - -tslib@^2.4.0: - version "2.6.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" - integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== - -type-detect@^4.0.0, type-detect@^4.0.5: - version "4.0.8" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" - integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +tslib@^2.0.3, tslib@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== -typescript@^4.3.5: - version "4.9.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" - integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== +type-detect@^4.0.0, type-detect@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.1.0.tgz#deb2453e8f08dcae7ae98c626b13dddb0155906c" + integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw== -typescript@^4.8.2: +typescript@^4.3.5, typescript@^4.8.2: version "4.9.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== @@ -2002,10 +1943,10 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -undici-types@~6.18.2: - version "6.18.2" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.18.2.tgz#8b678cf939d4fc9ec56be3c68ed69c619dee28b0" - integrity sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ== +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== utf-8-validate@^5.0.2: version "5.0.10" @@ -2062,16 +2003,16 @@ ws@^7.5.10: integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== ws@^8.5.0: - version "8.11.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" - integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yaml@^2.5.0: +yaml@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.1.tgz#42f2b1ba89203f374609572d5349fb8686500773" integrity sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==