Skip to content

Commit

Permalink
Merge branch 'main' into cavey/lif
Browse files Browse the repository at this point in the history
  • Loading branch information
jkbpvsc authored Jul 1, 2024
2 parents d33b8d1 + 92877d8 commit 6e9d575
Show file tree
Hide file tree
Showing 15 changed files with 588 additions and 41 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,15 @@ jobs:
- run: ./scripts/test-program.sh marginfi
shell: bash

- run: ./scripts/test-program.sh liquidity_incentive_program
- run: ./scripts/test-program.sh liquidity-incentive-program
shell: bash

fuzz:
name: Fuzz The marginfi Program
runs-on: ubuntu-latest
env:
RUST_TOOLCHAIN: 1.77.1
RUSTUP_TOOLCHAIN: nightly-2024-03-26
RUST_TOOLCHAIN: 1.77.1
RUSTUP_TOOLCHAIN: nightly-2024-03-26

steps:
- uses: actions/checkout@v3
Expand All @@ -110,7 +110,7 @@ jobs:
- name: Install full rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
profile: minimal
toolchain: nightly-2024-03-26
components: rust-src
override: true
Expand Down
1 change: 1 addition & 0 deletions clients/rust/marginfi-cli/src/processor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod emissions;
#[cfg(feature = "admin")]
pub mod admin;

#[cfg(feature = "admin")]
pub mod group;

use {
Expand Down
1 change: 1 addition & 0 deletions clients/rust/marginfi-cli/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ pub fn calc_emissions_rate(ui_rate: f64, emissions_mint_decimals: u8) -> u64 {
(ui_rate * 10u64.pow(emissions_mint_decimals as u32) as f64) as u64
}

#[cfg(feature = "admin")]
pub fn ui_to_native(ui_amount: f64, decimals: u8) -> u64 {
(ui_amount * (10u64.pow(decimals as u32) as f64)) as u64
}
Expand Down
4 changes: 2 additions & 2 deletions programs/marginfi/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ pub enum MarginfiError {
MissingBankAccount,
#[msg("Invalid Bank account")] // 6009
InvalidBankAccount,
#[msg("Bad account health")] // 6010
BadAccountHealth,
#[msg("RiskEngine rejected due to either bad health or stale oracles")] // 6010
RiskEngineInitRejected,
#[msg("Lending account balance slots are full")] // 6011
LendingAccountBalanceSlotsFull,
#[msg("Bank already exists")] // 6012
Expand Down
37 changes: 30 additions & 7 deletions programs/marginfi/src/state/marginfi_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ impl<'a, 'b> BankAccountWithPriceFeed<'a, 'b> {
/// 3. Initial requirement is discounted by the initial discount, if enabled and the usd limit is exceeded.
/// 4. Assets are only calculated for collateral risk tier.
/// 5. Oracle errors are ignored for deposits in isolated risk tier.
pub fn calc_weighted_assets_and_liabilities_values(
fn calc_weighted_assets_and_liabilities_values(
&self,
requirement_type: RequirementType,
) -> MarginfiResult<(I80F48, I80F48)> {
Expand Down Expand Up @@ -246,7 +246,18 @@ impl<'a, 'b> BankAccountWithPriceFeed<'a, 'b> {
) -> MarginfiResult<I80F48> {
match bank.config.risk_tier {
RiskTier::Collateral => {
let price_feed = self.try_get_price_feed()?;
let price_feed = self.try_get_price_feed();

if matches!(
(&price_feed, requirement_type),
(&Err(PriceFeedError::StaleOracle), RequirementType::Initial)
) {
debug!("Skipping stale oracle");
return Ok(I80F48::ZERO);
}

let price_feed = price_feed?;

let mut asset_weight = bank
.config
.get_weight(requirement_type, BalanceSide::Assets);
Expand Down Expand Up @@ -301,10 +312,10 @@ impl<'a, 'b> BankAccountWithPriceFeed<'a, 'b> {
)
}

fn try_get_price_feed(&self) -> MarginfiResult<&OraclePriceFeedAdapter> {
fn try_get_price_feed(&self) -> std::result::Result<&OraclePriceFeedAdapter, PriceFeedError> {
match self.price_feed.as_ref() {
Ok(a) => Ok(a),
Err(_) => Err(MarginfiError::StaleOracle)?,
Err(_) => Err(PriceFeedError::StaleOracle),
}
}

Expand All @@ -314,6 +325,18 @@ impl<'a, 'b> BankAccountWithPriceFeed<'a, 'b> {
}
}

enum PriceFeedError {
StaleOracle,
}

impl From<PriceFeedError> for Error {
fn from(value: PriceFeedError) -> Self {
match value {
PriceFeedError::StaleOracle => error!(MarginfiError::StaleOracle),
}
}
}

/// Calculate the value of an asset, given its quantity with a decimal exponent, and a price with a decimal exponent, and an optional weight.
#[inline]
pub fn calc_value(
Expand Down Expand Up @@ -411,9 +434,9 @@ impl<'a, 'b> RiskEngine<'a, 'b> {
})
}

/// Checks account is healty after performing actions that increase risk (removing liquidity).
/// Checks account is healthy after performing actions that increase risk (removing liquidity).
///
/// `IN_FLASHLOAN_FLAG` behaviour.
/// `IN_FLASHLOAN_FLAG` behavior.
/// - Health check is skipped.
/// - `remaining_ais` can be an empty vec.
pub fn check_account_init_health(
Expand Down Expand Up @@ -474,7 +497,7 @@ impl<'a, 'b> RiskEngine<'a, 'b> {

check!(
total_weighted_assets >= total_weighted_liabilities,
MarginfiError::BadAccountHealth
MarginfiError::RiskEngineInitRejected
);

self.check_account_risk_tiers()?;
Expand Down
6 changes: 6 additions & 0 deletions programs/marginfi/src/state/price.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@ pub struct PythEmaPriceFeed {
impl PythEmaPriceFeed {
pub fn load_checked(ai: &AccountInfo, current_time: i64, max_age: u64) -> MarginfiResult<Self> {
let price_feed = load_pyth_price_feed(ai)?;

debug!(
"Oracle age: {}s",
price_feed.get_price_unchecked().publish_time - current_time
);

let ema_price = price_feed
.get_ema_price_no_older_than(current_time, max_age)
.ok_or(MarginfiError::StaleOracle)?;
Expand Down
80 changes: 73 additions & 7 deletions programs/marginfi/tests/bank_ignore_stale_isolated_banks.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
use fixtures::{
assert_custom_error,
test::{BankMint, TestFixture, TestSettings, PYTH_SOL_FEED, PYTH_USDC_FEED},
test::{
BankMint, TestFixture, TestSettings, PYTH_SOL_EQUIVALENT_FEED, PYTH_SOL_FEED,
PYTH_USDC_FEED,
},
};
use marginfi::prelude::MarginfiError;
use solana_program_test::tokio;

#[tokio::test]
/// Borrowing with deposits to a non isolated stale bank should error
async fn non_isolated_stale_should_error() -> anyhow::Result<()> {
/// Borrowing with deposits in two banks (1 stale, 1 non-stale) should error with bad health
/// Account has enough total collateral to borrow, but one bank is stale and the non-stale collateral left
/// is not sufficient, so the borrow fails with bad health
async fn stale_bank_should_error() -> anyhow::Result<()> {
let test_f = TestFixture::new(Some(TestSettings::all_banks_payer_not_admin())).await;

let usdc_bank = test_f.get_bank(&BankMint::USDC);
let sol_bank = test_f.get_bank(&BankMint::SOL);
let sol_eq_bank = test_f.get_bank(&BankMint::SolEquivalent);

test_f.set_pyth_oracle_timestamp(PYTH_SOL_FEED, 120).await;
// Make SOLE feed stale
test_f.set_pyth_oracle_timestamp(PYTH_USDC_FEED, 120).await;
test_f
.set_pyth_oracle_timestamp(PYTH_SOL_EQUIVALENT_FEED, 0)
.await;
test_f.set_pyth_oracle_timestamp(PYTH_SOL_FEED, 120).await;
test_f.advance_time(120).await;

// Fund SOL lender
Expand All @@ -41,11 +50,11 @@ async fn non_isolated_stale_should_error() -> anyhow::Result<()> {
let borrower_token_account_f_sol = test_f.sol_mint.create_token_account_and_mint_to(0).await;

borrower_mfi_account_f
.try_bank_deposit(borrower_token_account_f_usdc.key, usdc_bank, 1_000)
.try_bank_deposit(borrower_token_account_f_usdc.key, usdc_bank, 500)
.await?;

borrower_mfi_account_f
.try_bank_deposit(borrower_token_account_f_sol_eq.key, sol_eq_bank, 1_000)
.try_bank_deposit(borrower_token_account_f_sol_eq.key, sol_eq_bank, 100)
.await?;

// Borrow SOL
Expand All @@ -54,7 +63,64 @@ async fn non_isolated_stale_should_error() -> anyhow::Result<()> {
.await;

assert!(res.is_err());
assert_custom_error!(res.unwrap_err(), MarginfiError::StaleOracle);
assert_custom_error!(res.unwrap_err(), MarginfiError::RiskEngineInitRejected);

Ok(())
}

#[tokio::test]
/// Borrowing with deposits in two banks (1 stale) should not error if the non-stale collateral is sufficient
async fn non_stale_bank_should_error() -> anyhow::Result<()> {
let test_f = TestFixture::new(Some(TestSettings::all_banks_payer_not_admin())).await;

let usdc_bank = test_f.get_bank(&BankMint::USDC);
let sol_eq_bank = test_f.get_bank(&BankMint::SolEquivalent);
let sol_bank = test_f.get_bank(&BankMint::SOL);

// Make USDC feed stale
test_f.set_pyth_oracle_timestamp(PYTH_USDC_FEED, 0).await;
test_f.set_pyth_oracle_timestamp(PYTH_SOL_FEED, 120).await;
test_f
.set_pyth_oracle_timestamp(PYTH_SOL_EQUIVALENT_FEED, 120)
.await;
test_f.advance_time(120).await;

// Fund SOL lender
let lender_mfi_account_f = test_f.create_marginfi_account().await;
let lender_token_account_sol = test_f
.sol_mint
.create_token_account_and_mint_to(1_000)
.await;
lender_mfi_account_f
.try_bank_deposit(lender_token_account_sol.key, sol_bank, 1_000)
.await?;

// Fund SOL borrower
let borrower_mfi_account_f = test_f.create_marginfi_account().await;
let borrower_token_account_f_usdc = test_f
.usdc_mint
.create_token_account_and_mint_to(1_000)
.await;
let borrower_token_account_f_sol_eq = test_f
.sol_equivalent_mint
.create_token_account_and_mint_to(1_000)
.await;
let borrower_token_account_f_sol = test_f.sol_mint.create_token_account_and_mint_to(0).await;

borrower_mfi_account_f
.try_bank_deposit(borrower_token_account_f_usdc.key, usdc_bank, 15)
.await?;

borrower_mfi_account_f
.try_bank_deposit(borrower_token_account_f_sol_eq.key, sol_eq_bank, 100)
.await?;

// Borrow SOL
let res = borrower_mfi_account_f
.try_bank_borrow(borrower_token_account_f_sol.key, sol_bank, 99)
.await;

assert!(res.is_ok());

Ok(())
}
Expand Down
28 changes: 18 additions & 10 deletions programs/marginfi/tests/bank_variable_oracle_staleness.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
use fixtures::{
assert_custom_error,
test::{BankMint, TestFixture, TestSettings, PYTH_SOL_FEED, PYTH_USDC_FEED},
test::{
BankMint, TestFixture, TestSettings, PYTH_SOL_EQUIVALENT_FEED, PYTH_SOL_FEED,
PYTH_USDC_FEED,
},
};
use marginfi::{prelude::MarginfiError, state::marginfi_group::BankConfigOpt};
use solana_program_test::tokio;

#[tokio::test]
/// Borrowing with deposits to a non isolated stale bank should error
/// Borrowing should fail if the total (non-isolated), non-stale collateral value is insufficient
async fn bank_oracle_staleness_test() -> anyhow::Result<()> {
let test_f = TestFixture::new(Some(TestSettings::all_banks_payer_not_admin())).await;

let usdc_bank = test_f.get_bank(&BankMint::USDC);
let sol_bank = test_f.get_bank(&BankMint::SOL);
let sol_eq_bank = test_f.get_bank(&BankMint::SolEquivalent);

test_f.set_pyth_oracle_timestamp(PYTH_SOL_FEED, 120).await;
// Make SOLE feed stale
test_f.set_pyth_oracle_timestamp(PYTH_USDC_FEED, 120).await;
test_f
.set_pyth_oracle_timestamp(PYTH_SOL_EQUIVALENT_FEED, 0)
.await;
test_f.set_pyth_oracle_timestamp(PYTH_SOL_FEED, 120).await;
test_f.advance_time(120).await;

// Fund SOL lender
Expand All @@ -41,35 +48,36 @@ async fn bank_oracle_staleness_test() -> anyhow::Result<()> {
let borrower_token_account_f_sol = test_f.sol_mint.create_token_account_and_mint_to(0).await;

borrower_mfi_account_f
.try_bank_deposit(borrower_token_account_f_usdc.key, usdc_bank, 1_000)
.try_bank_deposit(borrower_token_account_f_usdc.key, usdc_bank, 500)
.await?;

borrower_mfi_account_f
.try_bank_deposit(borrower_token_account_f_sol_eq.key, sol_eq_bank, 1_000)
.try_bank_deposit(borrower_token_account_f_sol_eq.key, sol_eq_bank, 50)
.await?;

// Borrow SOL
let res = borrower_mfi_account_f
.try_bank_borrow(borrower_token_account_f_sol.key, sol_bank, 99)
.try_bank_borrow_with_nonce(borrower_token_account_f_sol.key, sol_bank, 99, 1)
.await;

assert!(res.is_err());
assert_custom_error!(res.unwrap_err(), MarginfiError::StaleOracle);
assert_custom_error!(res.unwrap_err(), MarginfiError::RiskEngineInitRejected);

println!("Borrowing failed as expected");

// Make SOL feed non-stale
usdc_bank
.update_config(BankConfigOpt {
oracle_max_age: Some(200),
..Default::default()
})
.await?;

sol_bank
.update_config(BankConfigOpt {
oracle_max_age: Some(200),
..Default::default()
})
.await?;

sol_eq_bank
.update_config(BankConfigOpt {
oracle_max_age: Some(200),
Expand All @@ -79,7 +87,7 @@ async fn bank_oracle_staleness_test() -> anyhow::Result<()> {

// Borrow SOL
let res = borrower_mfi_account_f
.try_bank_borrow(borrower_token_account_f_sol.key, sol_bank, 98)
.try_bank_borrow_with_nonce(borrower_token_account_f_sol.key, sol_bank, 99, 2)
.await;

assert!(res.is_ok());
Expand Down
4 changes: 2 additions & 2 deletions programs/marginfi/tests/marginfi_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,7 @@ async fn marginfi_account_borrow_failure_not_enough_collateral() -> anyhow::Resu
.try_bank_borrow(borrower_token_account_f_sol.key, sol_bank, 101)
.await;

assert_custom_error!(res.unwrap_err(), MarginfiError::BadAccountHealth);
assert_custom_error!(res.unwrap_err(), MarginfiError::RiskEngineInitRejected);

let res = borrower_mfi_account_f
.try_bank_borrow(borrower_token_account_f_sol.key, sol_bank, 100)
Expand Down Expand Up @@ -1932,7 +1932,7 @@ async fn flashloan_fail_account_health() -> anyhow::Result<()> {

assert_custom_error!(
flash_loan_result.unwrap_err(),
MarginfiError::BadAccountHealth
MarginfiError::RiskEngineInitRejected
);

Ok(())
Expand Down
2 changes: 1 addition & 1 deletion programs/marginfi/tests/marginfi_group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1253,7 +1253,7 @@ async fn marginfi_group_init_limit_0() -> anyhow::Result<()> {
.await;

assert!(res.is_err());
assert_custom_error!(res.unwrap_err(), MarginfiError::BadAccountHealth);
assert_custom_error!(res.unwrap_err(), MarginfiError::RiskEngineInitRejected);

sol_depositor
.try_bank_withdraw(usdc_token_account.key, usdc_bank, 1900, Some(true))
Expand Down
Loading

0 comments on commit 6e9d575

Please sign in to comment.