diff --git a/programs/marginfi/src/instructions/marginfi_group/propagate_staked_settings.rs b/programs/marginfi/src/instructions/marginfi_group/propagate_staked_settings.rs index 9f1d8595..94fc263b 100644 --- a/programs/marginfi/src/instructions/marginfi_group/propagate_staked_settings.rs +++ b/programs/marginfi/src/instructions/marginfi_group/propagate_staked_settings.rs @@ -9,6 +9,12 @@ pub fn propagate_staked_settings(ctx: Context) -> Resul 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_staked_oracle_setup(ctx.remaining_accounts)?; + } + 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; @@ -18,7 +24,6 @@ pub fn propagate_staked_settings(ctx: Context) -> Resul bank.config.risk_tier = settings.risk_tier; bank.config.validate()?; - bank.config.validate_staked_oracle_setup(ctx.remaining_accounts)?; // ...Possibly emit event. Ok(()) @@ -37,7 +42,7 @@ pub struct PropagateStakedSettings<'info> { mut, constraint = { let bank = bank.load()?; - bank.group == marginfi_group.key() && + bank.group == marginfi_group.key() && bank.config.asset_tag == ASSET_TAG_STAKED } )] diff --git a/programs/marginfi/src/state/price.rs b/programs/marginfi/src/state/price.rs index fad4eb5d..ebe97655 100644 --- a/programs/marginfi/src/state/price.rs +++ b/programs/marginfi/src/state/price.rs @@ -365,11 +365,6 @@ impl OraclePriceFeedAdapter { _ => err!(MarginfiError::StakePoolValidationFailed), }?; - check!( - oracle_ais[0].key == &bank_config.oracle_keys[0], - MarginfiError::InvalidOracleAccount - ); - check!(oracle_ais.len() == 1, MarginfiError::InvalidOracleAccount); // Note: mainnet/staging/devnet use push oracles, localnet uses legacy push if cfg!(any( diff --git a/tests/01_initGroup.spec.ts b/tests/01_initGroup.spec.ts index e2db8a60..ad481c58 100644 --- a/tests/01_initGroup.spec.ts +++ b/tests/01_initGroup.spec.ts @@ -153,6 +153,10 @@ describe("Init group", () => { 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. Many of the these settings as also wrong + // or don't make sense (e.g. weights > 0 with isolated risk teir) and would fail at propagation + it("(admin) Edit staked settings for group", async () => { const settings: StakedSettingsEdit = { oracle: PublicKey.default, @@ -232,5 +236,4 @@ describe("Init group", () => { assert.equal(settingsAcc.oracleMaxAge, 60); }); - }); diff --git a/tests/s06_propagateSets.spec.ts b/tests/s06_propagateSets.spec.ts new file mode 100644 index 00000000..5af150a3 --- /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.userMarginProgram, { + 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.userMarginProgram, { + 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.userMarginProgram, { + 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/group-instructions.ts b/tests/utils/group-instructions.ts index 2022b723..3bf5bcb6 100644 --- a/tests/utils/group-instructions.ts +++ b/tests/utils/group-instructions.ts @@ -294,6 +294,8 @@ export const editGlobalFeeState = ( return ix; }; +// TODO propagate fee state and test + export type InitStakedSettingsArgs = { group: PublicKey; feePayer: PublicKey; @@ -342,6 +344,42 @@ export const editStakedSettings = ( 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;