Skip to content

Commit

Permalink
Staked collateral (#248)
Browse files Browse the repository at this point in the history
* Adds the ability to use SOL assets staked to vanilla Solana staking (https://github.com/anza-xyz/agave/tree/master/sdk/program/src/stake) as collateral to borrow SOL.
* Validators (or anyone: the process is permissionless) will use the Single Pool program (https://github.com/solana-labs/solana-program-library/tree/master/single-pool) to create an "LST" that uses that just that validator (unlike a traditional LST which will have several validators). After a bank for this LST is created, the LST can be deposited as collateral in the main pool. Only SOL can be borrowed against this kind of asset.
* Adds Bankrun simulation of creating a validator, native SOL staking + delegation from users, deposit into a Single Pool, and integration with mrgnlend
  • Loading branch information
jgur-psyops authored Dec 24, 2024
1 parent 464de33 commit b2e769d
Show file tree
Hide file tree
Showing 59 changed files with 5,697 additions and 608 deletions.
94 changes: 60 additions & 34 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
25 changes: 20 additions & 5 deletions Anchor.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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

Expand All @@ -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"
5 changes: 5 additions & 0 deletions clients/rust/marginfi-cli/src/entrypoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ impl From<BankOperationalStateArg> for BankOperationalState {
}
}

#[allow(clippy::large_enum_variant)]
#[derive(Debug, Parser)]
pub enum BankCommand {
Get {
Expand Down Expand Up @@ -291,6 +292,8 @@ pub enum BankCommand {
pf_or: Option<f64>,
#[clap(long, arg_enum, help = "Bank risk tier")]
risk_tier: Option<RiskTierArg>,
#[clap(long, help = "0 = default, 1 = SOL, 2 = Staked SOL LST")]
asset_tag: Option<u8>,
#[clap(long, arg_enum, help = "Bank oracle type")]
oracle_type: Option<OracleTypeArg>,
#[clap(long, help = "Bank oracle account")]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,23 @@
"@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",
"ts-mocha": "^10.0.0",
"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"
}
}
7 changes: 7 additions & 0 deletions programs/brick/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,17 @@ pub mod brick {
) -> Result<()> {
Err(ErrorCode::ProgramDisabled.into())
}

pub fn initialize(_ctx: Context<Initialize>, _val: u64) -> Result<()> {
Ok(())
}
}

#[error_code]
pub enum ErrorCode {
#[msg("This program is temporarily disabled.")]
ProgramDisabled,
}

#[derive(Accounts)]
pub struct Initialize {}
18 changes: 18 additions & 0 deletions programs/marginfi/fuzz/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
23 changes: 23 additions & 0 deletions programs/marginfi/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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");
Expand All @@ -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);
Expand Down Expand Up @@ -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;
20 changes: 13 additions & 7 deletions programs/marginfi/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<MarginfiError> for ProgramError {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit b2e769d

Please sign in to comment.