diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index ec5c72ba09a..cea10a574cf 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -132,14 +132,14 @@ jobs: - name: Set env vars run: | - source ci/rust-version.sh nightly - echo "RUST_NIGHTLY=$rust_nightly" >> $GITHUB_ENV + source ci/rust-version.sh + echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV source ci/solana-version.sh echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV - uses: actions-rs/toolchain@v1 with: - toolchain: ${{ env.RUST_NIGHTLY }} + toolchain: ${{ env.RUST_STABLE }} override: true profile: minimal @@ -149,7 +149,7 @@ jobs: ~/.cargo/registry ~/.cargo/git # target # Removed due to build dependency caching conflicts - key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_NIGHTLY }} + key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE }} - name: Install dependencies run: | @@ -158,9 +158,9 @@ jobs: echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - name: run test coverage - run: ./coverage.sh token-lending + run: ./coverage.sh - name: Codecov uses: codecov/codecov-action@v3.1.0 with: - file: target/cov/cobertura.xml + directory: target/coverage/ diff --git a/Cargo.lock b/Cargo.lock index 4ed4b9008e8..51f934bd9a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -323,7 +323,7 @@ dependencies = [ "num-traits", "rusticata-macros", "thiserror", - "time 0.3.17", + "time 0.3.15", ] [[package]] @@ -596,27 +596,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bytecheck" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11cac2c12b5adc6570dad2ee1b87eff4955dac476fe12d81e5fdd352e52406f" -dependencies = [ - "bytecheck_derive", - "ptr_meta", -] - -[[package]] -name = "bytecheck_derive" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e576ebe98e605500b3c8041bb888e966653577172df6dd97398714eb30b9bf" -dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", -] - [[package]] name = "bytemuck" version = "1.12.3" @@ -651,9 +630,9 @@ checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" [[package]] name = "bzip2" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" dependencies = [ "bzip2-sys", "libc", @@ -804,9 +783,9 @@ dependencies = [ [[package]] name = "console" -version = "0.15.3" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5556015fe3aad8b968e5d4124980fbe2f6aaee7aeec6b749de1faaa2ca5d0a4c" +checksum = "c9b6515d269224923b26b5febea2ed42b2d5f2ce37284a4dd670fedd6cb8347a" dependencies = [ "encode_unicode", "lazy_static", @@ -2265,6 +2244,15 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "number_prefix" version = "0.4.0" @@ -2604,26 +2592,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "ptr_meta" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", -] - [[package]] name = "pyth-sdk" version = "0.7.0" @@ -2874,7 +2842,7 @@ checksum = "6413f3de1edee53342e6138e75b56d32e7bc6e332b3bd62d497b1929d4cfbcdd" dependencies = [ "pem", "ring", - "time 0.3.17", + "time 0.3.15", "yasna", ] @@ -2924,15 +2892,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "rend" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79af64b4b6362ffba04eef3a4e10829718a4896dac19daa741851c86781edf95" -dependencies = [ - "bytecheck", -] - [[package]] name = "reqwest" version = "0.11.13" @@ -2989,31 +2948,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "rkyv" -version = "0.7.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cec2b3485b07d96ddfd3134767b8a447b45ea4eb91448d0a35180ec0ffd5ed15" -dependencies = [ - "bytecheck", - "hashbrown 0.12.3", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", -] - -[[package]] -name = "rkyv_derive" -version = "0.7.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eaedadc88b53e36dd32d940ed21ae4d850d5916f2581526921f553a72ac34c4" -dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", -] - [[package]] name = "rpassword" version = "6.0.1" @@ -3028,27 +2962,20 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.27.0" +version = "1.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c321ee4e17d2b7abe12b5d20c1231db708dd36185c8a21e9de5fed6da4dbe9" +checksum = "ee9164faf726e4f3ece4978b25ca877ddc6802fa77f38cdccb32c7f805ecd70c" dependencies = [ "arrayvec", - "borsh", - "bytecheck", - "byteorder", - "bytes", "num-traits", - "rand 0.8.5", - "rkyv", "serde", - "serde_json", ] [[package]] name = "rust_decimal_macros" -version = "1.27.0" +version = "1.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a7e2dba1342e9f1166786a4329ba0d6d6b8d9db7e81d702ec9ba3b39591ddff" +checksum = "4903d8db81d2321699ca8318035d6ff805c548868df435813968795a802171b2" dependencies = [ "quote 1.0.23", "rust_decimal", @@ -3235,12 +3162,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - [[package]] name = "security-framework" version = "2.7.0" @@ -4312,6 +4233,8 @@ version = "0.1.0" dependencies = [ "assert_matches", "base64 0.13.1", + "bincode", + "borsh", "bytemuck", "log", "proptest", @@ -4542,9 +4465,9 @@ dependencies = [ [[package]] name = "switchboard-v2" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "851e6a042596afa363d13733422561fdd0677fb2a9eb6d29358677adfce1d233" +checksum = "18444ed98876407dcddc9be7343a8f1cd4468b8813497f3c053bcfe24cffc484" dependencies = [ "anchor-lang", "anchor-spl", @@ -4721,30 +4644,21 @@ dependencies = [ [[package]] name = "time" -version = "0.3.17" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" +checksum = "d634a985c4d4238ec39cacaed2e7ae552fbd3c476b552c1deac3021b7d7eaf0c" dependencies = [ "itoa", - "serde", - "time-core", + "libc", + "num_threads", "time-macros", ] -[[package]] -name = "time-core" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" - [[package]] name = "time-macros" -version = "0.2.6" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" -dependencies = [ - "time-core", -] +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" [[package]] name = "tiny-bip39" @@ -5432,7 +5346,7 @@ dependencies = [ "oid-registry", "rusticata-macros", "thiserror", - "time 0.3.17", + "time 0.3.15", ] [[package]] @@ -5465,7 +5379,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aed2e7a52e3744ab4d0c05c20aa065258e84c49fd4226f5191b2ed29712710b4" dependencies = [ - "time 0.3.17", + "time 0.3.15", ] [[package]] diff --git a/ci/cargo-test-bpf.sh b/ci/cargo-test-bpf.sh index 21a4212d7d2..c29d40e17ec 100755 --- a/ci/cargo-test-bpf.sh +++ b/ci/cargo-test-bpf.sh @@ -32,11 +32,11 @@ run_dir=$(pwd) if [[ -d $run_dir/program ]]; then # Build/test just one BPF program cd $run_dir/program - cargo +"$rust_stable" test-bpf -- --nocapture + RUST_LOG="error" cargo +"$rust_stable" test-bpf -j 1 -- --nocapture else # Build/test all BPF programs for directory in $(ls -d $run_dir/*/); do cd $directory - cargo +"$rust_stable" test-bpf -- --nocapture + RUST_LOG="error" cargo +"$rust_stable" test-bpf -j 1 -- --nocapture done fi diff --git a/ci/install-program-deps.sh b/ci/install-program-deps.sh index c4ec9b4b096..ccfa61a9098 100755 --- a/ci/install-program-deps.sh +++ b/ci/install-program-deps.sh @@ -13,3 +13,4 @@ cargo install honggfuzz --version=0.5.52 --force || true cargo +"$rust_stable" install grcov --force cargo +"$rust_stable" build-bpf --version +rustup component add llvm-tools-preview diff --git a/ci/rust-version.sh b/ci/rust-version.sh index 6082bc7a698..0ed3638da86 100644 --- a/ci/rust-version.sh +++ b/ci/rust-version.sh @@ -18,13 +18,13 @@ if [[ -n $RUST_STABLE_VERSION ]]; then stable_version="$RUST_STABLE_VERSION" else - stable_version=1.63.0 + stable_version=1.65.0 fi if [[ -n $RUST_NIGHTLY_VERSION ]]; then nightly_version="$RUST_NIGHTLY_VERSION" else - nightly_version=2022-01-30 + nightly_version=2022-04-01 fi diff --git a/coverage.sh b/coverage.sh index ccd64d6219b..3bef941b59a 100755 --- a/coverage.sh +++ b/coverage.sh @@ -2,10 +2,8 @@ # # Runs all program tests and builds a code coverage report # +set -ex -source ci/rust-version.sh nightly # get $rust_nightly env variable - -set -e cd "$(dirname "$0")" if ! which grcov; then @@ -13,80 +11,20 @@ if ! which grcov; then exit 1 fi -: "${CI_COMMIT:=local}" -reportName="lcov-${CI_COMMIT:0:9}" - -if [[ -z $1 ]]; then - programs=( - memo/program - token/program - token-lending/program - token-swap/program - ) -else - programs=("$@") -fi - -coverageFlags=(-Zprofile) # Enable coverage -coverageFlags+=("-Clink-dead-code") # Dead code should appear red in the report -coverageFlags+=("-Ccodegen-units=1") # Disable code generation parallelism which is unsupported under -Zprofile (see [rustc issue #51705]). -coverageFlags+=("-Cinline-threshold=0") # Disable inlining, which complicates control flow. -coverageFlags+=("-Copt-level=0") # -coverageFlags+=("-Coverflow-checks=off") # Disable overflow checks, which create unnecessary branches. - -export RUSTFLAGS="${coverageFlags[*]} $RUSTFLAGS" -export CARGO_INCREMENTAL=0 -export RUST_BACKTRACE=1 -export RUST_MIN_STACK=8388608 - -echo "--- remove old coverage results" -if [[ -d target/cov ]]; then - find target/cov -type f -name '*.gcda' -delete -fi -rm -rf target/cov/$reportName -mkdir -p target/cov - -# Mark the base time for a clean room dir -touch target/cov/before-test - -for program in ${programs[@]}; do - here=$PWD - ( - set -ex - cd $program - cargo +"$rust_nightly" test --features test-bpf --target-dir $here/target/cov -- --skip fail_repay_from_diff_reserve - ) -done - -touch target/cov/after-test - -echo "--- grcov" - -# Create a clean room dir only with updated gcda/gcno files for this run, -# because our cached target dir is full of other builds' coverage files -rm -rf target/cov/tmp -mkdir -p target/cov/tmp +rm *.profraw || true +rm **/**/*.profraw || true +rm -r target/coverage || true -# Can't use a simpler construct under the condition of SC2044 and bash 3 -# (macOS's default). See: https://github.com/koalaman/shellcheck/wiki/SC2044 -find target/cov -type f -name '*.gcda' -newer target/cov/before-test ! -newer target/cov/after-test -print0 | - (while IFS= read -r -d '' gcda_file; do - gcno_file="${gcda_file%.gcda}.gcno" - ln -sf "../../../$gcda_file" "target/cov/tmp/$(basename "$gcda_file")" - ln -sf "../../../$gcno_file" "target/cov/tmp/$(basename "$gcno_file")" - done) +# run tests with instrumented binary +RUST_LOG="error" CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='cargo-test-%p-%m.profraw' cargo test --features test-bpf -( - set -x - grcov target/cov/tmp -t html -o target/cov/$reportName - grcov target/cov/tmp -t lcov -o target/cov/lcov.info - grcov target/cov/tmp -t cobertura -o target/cov/cobertura.xml +# generate report +mkdir -p target/coverage/html - cd target/cov - tar zcf report.tar.gz $reportName -) +grcov . --binary-path ./target/debug/deps/ -s . -t html --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/html -ls -l target/cov/ -ln -sf $reportName target/cov/LATEST +grcov . --binary-path ./target/debug/deps/ -s . -t lcov --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/tests.lcov -exit $test_status +# cleanup +rm *.profraw || true +rm **/**/*.profraw || true diff --git a/token-lending/program/Cargo.toml b/token-lending/program/Cargo.toml index 127fd3a7d61..09b775274b6 100644 --- a/token-lending/program/Cargo.toml +++ b/token-lending/program/Cargo.toml @@ -31,6 +31,8 @@ solana-sdk = "=1.14.10" serde = "=1.0.140" serde_yaml = "0.8" thiserror = "1.0" +bincode = "1.3.3" +borsh = "0.9.3" [lib] crate-type = ["cdylib", "lib"] diff --git a/token-lending/program/tests/borrow_obligation_liquidity.rs b/token-lending/program/tests/borrow_obligation_liquidity.rs index fd5562b9157..645a2a0face 100644 --- a/token-lending/program/tests/borrow_obligation_liquidity.rs +++ b/token-lending/program/tests/borrow_obligation_liquidity.rs @@ -2,619 +2,316 @@ mod helpers; -use helpers::*; +use solend_program::state::ReserveFees; +use std::collections::HashSet; + +use helpers::solend_program_test::{ + setup_world, BalanceChecker, Info, SolendProgramTest, TokenBalanceChange, User, +}; +use helpers::{test_reserve_config, wsol_mint}; +use solana_program::native_token::LAMPORTS_PER_SOL; use solana_program_test::*; use solana_sdk::{ - instruction::InstructionError, - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, + instruction::InstructionError, signature::Keypair, transaction::TransactionError, }; +use solend_program::state::{LastUpdate, ObligationLiquidity, ReserveConfig, ReserveLiquidity}; use solend_program::{ error::LendingError, - instruction::{borrow_obligation_liquidity, refresh_obligation, refresh_reserve}, math::Decimal, - processor::process_instruction, - state::{FeeCalculation, INITIAL_COLLATERAL_RATIO}, + state::{LendingMarket, Obligation, Reserve}, }; -use std::u64; - -#[tokio::test] -async fn test_borrow_usdc_fixed_amount() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(55_000); - - const USDC_TOTAL_BORROW_FRACTIONAL: u64 = 1_000 * FRACTIONAL_TO_USDC; - const FEE_AMOUNT: u64 = 100; - const HOST_FEE_AMOUNT: u64 = 20; - - const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; - const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = USDC_TOTAL_BORROW_FRACTIONAL - FEE_AMOUNT; - const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS; - const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 2 * USDC_TOTAL_BORROW_FRACTIONAL; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - let mut reserve_config = test_reserve_config(); - reserve_config.loan_to_value_ratio = 50; - - let sol_oracle = add_sol_oracle(&mut test); - let sol_test_reserve = add_reserve( - &mut test, - &lending_market, - &sol_oracle, - &user_accounts_owner, - AddReserveArgs { - collateral_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, - liquidity_mint_pubkey: spl_token::native_mint::id(), - liquidity_mint_decimals: 9, - config: reserve_config, - mark_fresh: true, - ..AddReserveArgs::default() - }, - ); - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( +async fn setup( + wsol_reserve_config: &ReserveConfig, +) -> ( + SolendProgramTest, + Info, + Info, + Info, + User, + Info, + User, +) { + let (mut test, lending_market, usdc_reserve, wsol_reserve, _, user) = + setup_world(&test_reserve_config(), wsol_reserve_config).await; + + let obligation = lending_market + .init_obligation(&mut test, Keypair::new(), &user) + .await + .expect("This should succeed"); + + lending_market + .deposit(&mut test, &usdc_reserve, &user, 100_000_000) + .await + .expect("This should succeed"); + + let usdc_reserve = test.load_account(usdc_reserve.pubkey).await; + + lending_market + .deposit_obligation_collateral(&mut test, &usdc_reserve, &obligation, &user, 100_000_000) + .await + .expect("This should succeed"); + + let wsol_depositor = User::new_with_balances( &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - mark_fresh: true, - ..AddReserveArgs::default() - }, - ); - - let test_obligation = add_obligation( - &mut test, - &lending_market, - &user_accounts_owner, - AddObligationArgs { - deposits: &[(&sol_test_reserve, SOL_DEPOSIT_AMOUNT_LAMPORTS)], - ..AddObligationArgs::default() - }, - ); - - let (mut banks_client, payer, recent_blockhash) = test.start().await; - - let initial_liquidity_supply = - get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; - - let mut transaction = Transaction::new_with_payer( &[ - refresh_obligation( - solend_program::id(), - test_obligation.pubkey, - vec![sol_test_reserve.pubkey], - ), - borrow_obligation_liquidity( - solend_program::id(), - USDC_BORROW_AMOUNT_FRACTIONAL, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - usdc_test_reserve.config.fee_receiver, - test_obligation.pubkey, - lending_market.pubkey, - test_obligation.owner, - Some(usdc_test_reserve.liquidity_host_pubkey), - ), + (&wsol_mint::id(), 5 * LAMPORTS_PER_SOL), + (&wsol_reserve.account.collateral.mint_pubkey, 0), ], - Some(&payer.pubkey()), - ); - - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); - assert!(banks_client.process_transaction(transaction).await.is_ok()); - - let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; - let obligation = test_obligation.get_state(&mut banks_client).await; - - let (total_fee, host_fee) = usdc_reserve - .config - .fees - .calculate_borrow_fees( - USDC_BORROW_AMOUNT_FRACTIONAL.into(), - FeeCalculation::Exclusive, + ) + .await; + + lending_market + .deposit( + &mut test, + &wsol_reserve, + &wsol_depositor, + 5 * LAMPORTS_PER_SOL, ) + .await .unwrap(); - assert_eq!(total_fee, FEE_AMOUNT); - assert_eq!(host_fee, HOST_FEE_AMOUNT); - - let borrow_amount = - get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; - assert_eq!(borrow_amount, USDC_BORROW_AMOUNT_FRACTIONAL); - - let liquidity = &obligation.borrows[0]; - assert_eq!( - liquidity.borrowed_amount_wads, - Decimal::from(USDC_TOTAL_BORROW_FRACTIONAL) - ); - assert_eq!( - usdc_reserve.liquidity.borrowed_amount_wads, - liquidity.borrowed_amount_wads - ); - let liquidity_supply = - get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; - assert_eq!( - liquidity_supply, - initial_liquidity_supply - USDC_TOTAL_BORROW_FRACTIONAL - ); + // populate market price correctly + lending_market + .refresh_reserve(&mut test, &wsol_reserve) + .await + .unwrap(); - let fee_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.config.fee_receiver).await; - assert_eq!(fee_balance, FEE_AMOUNT - HOST_FEE_AMOUNT); + // populate deposit value correctly. + let obligation = test.load_account::(obligation.pubkey).await; + lending_market + .refresh_obligation(&mut test, &obligation) + .await + .unwrap(); - let host_fee_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_host_pubkey).await; - assert_eq!(host_fee_balance, HOST_FEE_AMOUNT); + let lending_market = test.load_account(lending_market.pubkey).await; + let usdc_reserve = test.load_account(usdc_reserve.pubkey).await; + let wsol_reserve = test.load_account(wsol_reserve.pubkey).await; + let obligation = test.load_account::(obligation.pubkey).await; + + let host_fee_receiver = User::new_with_balances(&mut test, &[(&wsol_mint::id(), 0)]).await; + ( + test, + lending_market, + usdc_reserve, + wsol_reserve, + user, + obligation, + host_fee_receiver, + ) } #[tokio::test] -async fn test_borrow_sol_max_amount() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(60_000); - - const FEE_AMOUNT: u64 = 5000; - const HOST_FEE_AMOUNT: u64 = 1000; - - const USDC_DEPOSIT_AMOUNT_FRACTIONAL: u64 = - 2_000 * FRACTIONAL_TO_USDC * INITIAL_COLLATERAL_RATIO; - const SOL_BORROW_AMOUNT_LAMPORTS: u64 = 50 * LAMPORTS_TO_SOL; - const USDC_RESERVE_COLLATERAL_FRACTIONAL: u64 = 2 * USDC_DEPOSIT_AMOUNT_FRACTIONAL; - const SOL_RESERVE_LIQUIDITY_LAMPORTS: u64 = 2 * SOL_BORROW_AMOUNT_LAMPORTS; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); +async fn test_success() { + let (mut test, lending_market, usdc_reserve, wsol_reserve, user, obligation, host_fee_receiver) = + setup(&ReserveConfig { + fees: ReserveFees { + borrow_fee_wad: 100_000_000_000, + flash_loan_fee_wad: 0, + host_fee_percentage: 20, + }, + ..test_reserve_config() + }) + .await; + + let balance_checker = BalanceChecker::start( + &mut test, + &[&usdc_reserve, &user, &wsol_reserve, &host_fee_receiver], + ) + .await; + + lending_market + .borrow_obligation_liquidity( + &mut test, + &wsol_reserve, + &obligation, + &user, + &host_fee_receiver.get_account(&wsol_mint::id()).unwrap(), + 4 * LAMPORTS_PER_SOL, + ) + .await + .unwrap(); - let mut reserve_config = test_reserve_config(); - reserve_config.loan_to_value_ratio = 50; + // check token balances + let (balance_changes, mint_supply_changes) = + balance_checker.find_balance_changes(&mut test).await; - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - liquidity_amount: USDC_RESERVE_COLLATERAL_FRACTIONAL, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - mark_fresh: true, - ..AddReserveArgs::default() + let expected_balance_changes = HashSet::from([ + TokenBalanceChange { + token_account: wsol_reserve.account.liquidity.supply_pubkey, + mint: wsol_mint::id(), + diff: -((4 * LAMPORTS_PER_SOL + 400) as i128), }, - ); - - let sol_oracle = add_sol_oracle(&mut test); - let sol_test_reserve = add_reserve( - &mut test, - &lending_market, - &sol_oracle, - &user_accounts_owner, - AddReserveArgs { - liquidity_amount: SOL_RESERVE_LIQUIDITY_LAMPORTS, - liquidity_mint_pubkey: spl_token::native_mint::id(), - liquidity_mint_decimals: 9, - config: reserve_config, - mark_fresh: true, - ..AddReserveArgs::default() + TokenBalanceChange { + token_account: user.get_account(&wsol_mint::id()).unwrap(), + mint: wsol_mint::id(), + diff: (4 * LAMPORTS_PER_SOL) as i128, }, - ); - - let test_obligation = add_obligation( - &mut test, - &lending_market, - &user_accounts_owner, - AddObligationArgs { - deposits: &[(&usdc_test_reserve, USDC_DEPOSIT_AMOUNT_FRACTIONAL)], - ..AddObligationArgs::default() + TokenBalanceChange { + token_account: wsol_reserve.account.config.fee_receiver, + mint: wsol_mint::id(), + diff: 320, }, + TokenBalanceChange { + token_account: host_fee_receiver.get_account(&wsol_mint::id()).unwrap(), + mint: wsol_mint::id(), + diff: 80, + }, + ]); + assert_eq!( + balance_changes, expected_balance_changes, + "{:#?} \n {:#?}", + balance_changes, expected_balance_changes ); + assert_eq!(mint_supply_changes, HashSet::new()); - let (mut banks_client, payer, recent_blockhash) = test.start().await; - - let initial_liquidity_supply = - get_token_balance(&mut banks_client, sol_test_reserve.liquidity_supply_pubkey).await; - - let mut transaction = Transaction::new_with_payer( - &[ - refresh_obligation( - solend_program::id(), - test_obligation.pubkey, - vec![usdc_test_reserve.pubkey], - ), - borrow_obligation_liquidity( - solend_program::id(), - u64::MAX, - sol_test_reserve.liquidity_supply_pubkey, - sol_test_reserve.user_liquidity_pubkey, - sol_test_reserve.pubkey, - sol_test_reserve.config.fee_receiver, - test_obligation.pubkey, - lending_market.pubkey, - test_obligation.owner, - Some(sol_test_reserve.liquidity_host_pubkey), - ), - ], - Some(&payer.pubkey()), - ); - - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); - assert!(banks_client.process_transaction(transaction).await.is_ok()); - - let sol_reserve = sol_test_reserve.get_state(&mut banks_client).await; - let obligation = test_obligation.get_state(&mut banks_client).await; - - let (total_fee, host_fee) = sol_reserve - .config - .fees - .calculate_borrow_fees(SOL_BORROW_AMOUNT_LAMPORTS.into(), FeeCalculation::Inclusive) - .unwrap(); - - assert_eq!(total_fee, FEE_AMOUNT); - assert_eq!(host_fee, HOST_FEE_AMOUNT); + // check program state + let lending_market_post = test.load_account(lending_market.pubkey).await; + assert_eq!(lending_market, lending_market_post); - let borrow_amount = - get_token_balance(&mut banks_client, sol_test_reserve.user_liquidity_pubkey).await; - assert_eq!(borrow_amount, SOL_BORROW_AMOUNT_LAMPORTS - FEE_AMOUNT); - - let liquidity = &obligation.borrows[0]; + let wsol_reserve_post = test.load_account::(wsol_reserve.pubkey).await; assert_eq!( - liquidity.borrowed_amount_wads, - Decimal::from(SOL_BORROW_AMOUNT_LAMPORTS) + wsol_reserve_post.account, + Reserve { + last_update: LastUpdate { + slot: 1000, + stale: true + }, + liquidity: ReserveLiquidity { + available_amount: 6 * LAMPORTS_PER_SOL - (4 * LAMPORTS_PER_SOL + 400), + borrowed_amount_wads: Decimal::from(4 * LAMPORTS_PER_SOL + 400), + ..wsol_reserve.account.liquidity + }, + ..wsol_reserve.account + }, + "{:#?}", + wsol_reserve_post ); - let liquidity_supply = - get_token_balance(&mut banks_client, sol_test_reserve.liquidity_supply_pubkey).await; + let obligation_post = test.load_account::(obligation.pubkey).await; assert_eq!( - liquidity_supply, - initial_liquidity_supply - SOL_BORROW_AMOUNT_LAMPORTS + obligation_post.account, + Obligation { + last_update: LastUpdate { + slot: 1000, + stale: true + }, + borrows: vec![ObligationLiquidity { + borrow_reserve: wsol_reserve.pubkey, + borrowed_amount_wads: Decimal::from(4 * LAMPORTS_PER_SOL + 400), + cumulative_borrow_rate_wads: wsol_reserve + .account + .liquidity + .cumulative_borrow_rate_wads, + market_value: Decimal::zero(), // we only update this retroactively on a + // refresh_obligation + }], + deposited_value: Decimal::from(100u64), + borrowed_value: Decimal::zero(), + allowed_borrow_value: Decimal::from(50u64), + unhealthy_borrow_value: Decimal::from(55u64), + ..obligation.account + }, + "{:#?}", + obligation_post.account ); - - let fee_balance = - get_token_balance(&mut banks_client, sol_test_reserve.config.fee_receiver).await; - assert_eq!(fee_balance, FEE_AMOUNT - HOST_FEE_AMOUNT); - - let host_fee_balance = - get_token_balance(&mut banks_client, sol_test_reserve.liquidity_host_pubkey).await; - assert_eq!(host_fee_balance, HOST_FEE_AMOUNT); } +// FIXME this should really be a unit test #[tokio::test] -async fn test_borrow_too_large() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; - const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = 1_000 * FRACTIONAL_TO_USDC + 1; - const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS; - const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 2 * USDC_BORROW_AMOUNT_FRACTIONAL; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); +async fn test_borrow_max() { + let (mut test, lending_market, usdc_reserve, wsol_reserve, user, obligation, host_fee_receiver) = + setup(&ReserveConfig { + fees: ReserveFees { + borrow_fee_wad: 100_000_000_000, + flash_loan_fee_wad: 0, + host_fee_percentage: 20, + }, + ..test_reserve_config() + }) + .await; + + let balance_checker = BalanceChecker::start( + &mut test, + &[&usdc_reserve, &user, &wsol_reserve, &host_fee_receiver], + ) + .await; + + lending_market + .borrow_obligation_liquidity( + &mut test, + &wsol_reserve, + &obligation, + &user, + &host_fee_receiver.get_account(&wsol_mint::id()).unwrap(), + u64::MAX, + ) + .await + .unwrap(); - let mut reserve_config = test_reserve_config(); - reserve_config.loan_to_value_ratio = 50; + // check token balances + let (balance_changes, mint_supply_changes) = + balance_checker.find_balance_changes(&mut test).await; - let sol_oracle = add_sol_oracle(&mut test); - let sol_test_reserve = add_reserve( - &mut test, - &lending_market, - &sol_oracle, - &user_accounts_owner, - AddReserveArgs { - collateral_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, - liquidity_mint_pubkey: spl_token::native_mint::id(), - liquidity_mint_decimals: 9, - config: reserve_config, - mark_fresh: true, - ..AddReserveArgs::default() + let expected_balance_changes = HashSet::from([ + TokenBalanceChange { + token_account: wsol_reserve.account.liquidity.supply_pubkey, + mint: wsol_mint::id(), + diff: -((5 * LAMPORTS_PER_SOL) as i128), }, - ); - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - mark_fresh: true, - ..AddReserveArgs::default() + TokenBalanceChange { + token_account: user.get_account(&wsol_mint::id()).unwrap(), + mint: wsol_mint::id(), + diff: (5 * LAMPORTS_PER_SOL as i128) - 500, }, - ); - - let test_obligation = add_obligation( - &mut test, - &lending_market, - &user_accounts_owner, - AddObligationArgs { - deposits: &[(&sol_test_reserve, SOL_DEPOSIT_AMOUNT_LAMPORTS)], - ..AddObligationArgs::default() + TokenBalanceChange { + token_account: wsol_reserve.account.config.fee_receiver, + mint: wsol_mint::id(), + diff: 400, }, - ); - - let (mut banks_client, payer, recent_blockhash) = test.start().await; - - let mut transaction = Transaction::new_with_payer( - &[ - refresh_obligation( - solend_program::id(), - test_obligation.pubkey, - vec![sol_test_reserve.pubkey], - ), - borrow_obligation_liquidity( - solend_program::id(), - USDC_BORROW_AMOUNT_FRACTIONAL, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - usdc_test_reserve.config.fee_receiver, - test_obligation.pubkey, - lending_market.pubkey, - test_obligation.owner, - Some(usdc_test_reserve.liquidity_host_pubkey), - ), - ], - Some(&payer.pubkey()), - ); - - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); + TokenBalanceChange { + token_account: host_fee_receiver.get_account(&wsol_mint::id()).unwrap(), + mint: wsol_mint::id(), + diff: 100, + }, + ]); - // check that transaction fails assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError( - 1, - InstructionError::Custom(LendingError::BorrowTooLarge as u32) - ) + balance_changes, expected_balance_changes, + "{:#?} \n {:#?}", + balance_changes, expected_balance_changes ); + assert_eq!(mint_supply_changes, HashSet::new()); } #[tokio::test] -async fn test_borrow_limit() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 100000 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; - const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let mut reserve_config = test_reserve_config(); - reserve_config.loan_to_value_ratio = 50; - reserve_config.borrow_limit = 15; - - let sol_oracle = add_sol_oracle(&mut test); - let sol_test_reserve = add_reserve( - &mut test, - &lending_market, - &sol_oracle, - &user_accounts_owner, - AddReserveArgs { - collateral_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, - liquidity_mint_pubkey: spl_token::native_mint::id(), - liquidity_mint_decimals: 9, - config: reserve_config, - mark_fresh: true, - ..AddReserveArgs::default() - }, - ); - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - liquidity_amount: 1_000_000_000, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - mark_fresh: true, - ..AddReserveArgs::default() - }, - ); - - let test_obligation = add_obligation( - &mut test, - &lending_market, - &user_accounts_owner, - AddObligationArgs { - deposits: &[(&sol_test_reserve, SOL_DEPOSIT_AMOUNT_LAMPORTS)], - ..AddObligationArgs::default() - }, - ); - - let mut test_context = test.start_with_context().await; - test_context.warp_to_slot(240).unwrap(); // clock.slot = 240 - - let ProgramTestContext { - mut banks_client, - payer, - last_blockhash: recent_blockhash, - .. - } = test_context; +async fn test_fail_borrow_over_reserve_borrow_limit() { + let (mut test, lending_market, _, wsol_reserve, user, obligation, host_fee_receiver) = + setup(&ReserveConfig { + borrow_limit: LAMPORTS_PER_SOL, + ..test_reserve_config() + }) + .await; + + let res = lending_market + .borrow_obligation_liquidity( + &mut test, + &wsol_reserve, + &obligation, + &user, + &host_fee_receiver.get_account(&wsol_mint::id()).unwrap(), + LAMPORTS_PER_SOL + 1, + ) + .await + .err() + .unwrap() + .unwrap(); - // Try to borrow more than the borrow limit. This transaction should fail - let mut transaction = Transaction::new_with_payer( - &[ - refresh_reserve( - solend_program::id(), - sol_test_reserve.pubkey, - sol_oracle.pyth_price_pubkey, - sol_oracle.switchboard_feed_pubkey, - ), - refresh_reserve( - solend_program::id(), - usdc_test_reserve.pubkey, - usdc_oracle.pyth_price_pubkey, - usdc_oracle.switchboard_feed_pubkey, - ), - refresh_obligation( - solend_program::id(), - test_obligation.pubkey, - vec![sol_test_reserve.pubkey], - ), - borrow_obligation_liquidity( - solend_program::id(), - reserve_config.borrow_limit + 1, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - usdc_test_reserve.config.fee_receiver, - test_obligation.pubkey, - lending_market.pubkey, - test_obligation.owner, - Some(usdc_test_reserve.liquidity_host_pubkey), - ), - ], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( 3, InstructionError::Custom(LendingError::InvalidAmount as u32) ) ); - - let obligation = test_obligation.get_state(&mut banks_client).await; - assert_eq!(obligation.borrowed_value, Decimal::zero()); - - // Also try borrowing INT MAX, which should max out the reserve's borrows. - let mut transaction = Transaction::new_with_payer( - &[ - refresh_reserve( - solend_program::id(), - sol_test_reserve.pubkey, - sol_oracle.pyth_price_pubkey, - sol_oracle.switchboard_feed_pubkey, - ), - refresh_reserve( - solend_program::id(), - usdc_test_reserve.pubkey, - usdc_oracle.pyth_price_pubkey, - usdc_oracle.switchboard_feed_pubkey, - ), - refresh_obligation( - solend_program::id(), - test_obligation.pubkey, - vec![sol_test_reserve.pubkey], - ), - borrow_obligation_liquidity( - solend_program::id(), - u64::MAX, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - usdc_test_reserve.config.fee_receiver, - test_obligation.pubkey, - lending_market.pubkey, - test_obligation.owner, - Some(usdc_test_reserve.liquidity_host_pubkey), - ), - refresh_reserve( - solend_program::id(), - usdc_test_reserve.pubkey, - usdc_oracle.pyth_price_pubkey, - usdc_oracle.switchboard_feed_pubkey, - ), - ], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); - assert!(banks_client.process_transaction(transaction).await.is_ok()); - - let reserve = usdc_test_reserve.get_state(&mut banks_client).await; - assert_eq!( - reserve.liquidity.borrowed_amount_wads, - Decimal::from(reserve_config.borrow_limit) - ); - - // Now try to borrow INT_MAX again, which should fail - let mut transaction = Transaction::new_with_payer( - &[ - refresh_reserve( - solend_program::id(), - usdc_test_reserve.pubkey, - usdc_oracle.pyth_price_pubkey, - usdc_oracle.switchboard_feed_pubkey, - ), - refresh_obligation( - solend_program::id(), - test_obligation.pubkey, - vec![sol_test_reserve.pubkey, usdc_test_reserve.pubkey], - ), - borrow_obligation_liquidity( - solend_program::id(), - u64::MAX, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - usdc_test_reserve.config.fee_receiver, - test_obligation.pubkey, - lending_market.pubkey, - test_obligation.owner, - Some(usdc_test_reserve.liquidity_host_pubkey), - ), - ], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); - - assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError( - 2, - InstructionError::Custom(LendingError::BorrowTooSmall as u32) - ) - ); } diff --git a/token-lending/program/tests/deposit_obligation_collateral.rs b/token-lending/program/tests/deposit_obligation_collateral.rs index a639eb03f15..3d9307c5197 100644 --- a/token-lending/program/tests/deposit_obligation_collateral.rs +++ b/token-lending/program/tests/deposit_obligation_collateral.rs @@ -2,130 +2,118 @@ mod helpers; -use helpers::*; -use solana_program_test::*; -use solana_sdk::{ - signature::{Keypair, Signer}, - transaction::Transaction, -}; -use solend_program::{ - instruction::deposit_obligation_collateral, processor::process_instruction, - state::INITIAL_COLLATERAL_RATIO, +use std::collections::HashSet; + +use helpers::solend_program_test::{ + setup_world, BalanceChecker, Info, SolendProgramTest, TokenBalanceChange, User, }; -use spl_token::instruction::approve; +use helpers::test_reserve_config; + +use solana_program::instruction::InstructionError; +use solana_program_test::*; +use solana_sdk::signature::Keypair; +use solana_sdk::transaction::TransactionError; +use solend_program::math::Decimal; +use solend_program::state::{LastUpdate, LendingMarket, Obligation, ObligationCollateral, Reserve}; + +async fn setup() -> ( + SolendProgramTest, + Info, + Info, + User, + Info, +) { + let (mut test, lending_market, usdc_reserve, _, _, user) = + setup_world(&test_reserve_config(), &test_reserve_config()).await; + + let obligation = lending_market + .init_obligation(&mut test, Keypair::new(), &user) + .await + .expect("This should succeed"); + + lending_market + .deposit(&mut test, &usdc_reserve, &user, 1_000_000) + .await + .expect("This should succeed"); + + let usdc_reserve = test.load_account(usdc_reserve.pubkey).await; + + (test, lending_market, usdc_reserve, user, obligation) +} #[tokio::test] async fn test_success() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(38_000); - - const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 10 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; - const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS; - const SOL_BORROWED_AMOUNT_LAMPORTS: u64 = SOL_DEPOSIT_AMOUNT_LAMPORTS; - - let user_accounts_owner = Keypair::new(); - let user_transfer_authority = Keypair::new(); - - let lending_market = add_lending_market(&mut test); - - let sol_oracle = add_sol_oracle(&mut test); - let sol_test_reserve = add_reserve( - &mut test, - &lending_market, - &sol_oracle, - &user_accounts_owner, - AddReserveArgs { - user_liquidity_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, - liquidity_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, - liquidity_mint_decimals: 9, - liquidity_mint_pubkey: spl_token::native_mint::id(), - borrow_amount: SOL_BORROWED_AMOUNT_LAMPORTS, - config: test_reserve_config(), - mark_fresh: true, - ..AddReserveArgs::default() + let (mut test, lending_market, usdc_reserve, user, obligation) = setup().await; + + let balance_checker = BalanceChecker::start(&mut test, &[&usdc_reserve, &user]).await; + + lending_market + .deposit_obligation_collateral(&mut test, &usdc_reserve, &obligation, &user, 1_000_000) + .await + .expect("This should succeed"); + + // check balance changes + let (balance_changes, mint_supply_changes) = + balance_checker.find_balance_changes(&mut test).await; + let expected_balance_changes = HashSet::from([ + TokenBalanceChange { + token_account: user + .get_account(&usdc_reserve.account.collateral.mint_pubkey) + .unwrap(), + mint: usdc_reserve.account.collateral.mint_pubkey, + diff: -1_000_000, }, - ); + TokenBalanceChange { + token_account: usdc_reserve.account.collateral.supply_pubkey, + mint: usdc_reserve.account.collateral.mint_pubkey, + diff: 1_000_000, + }, + ]); - let test_obligation = add_obligation( - &mut test, - &lending_market, - &user_accounts_owner, - AddObligationArgs::default(), - ); + assert_eq!(balance_changes, expected_balance_changes); + assert_eq!(mint_supply_changes, HashSet::new()); - let mut test_context = test.start_with_context().await; - test_context.warp_to_slot(300).unwrap(); // clock.slot = 300 - - let ProgramTestContext { - mut banks_client, - payer, - last_blockhash: recent_blockhash, - .. - } = test_context; - - test_obligation.validate_state(&mut banks_client).await; - - let initial_collateral_supply_balance = - get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; - let initial_user_collateral_balance = - get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; - let pre_sol_reserve = sol_test_reserve.get_state(&mut banks_client).await; - let old_borrow_rate = pre_sol_reserve.liquidity.cumulative_borrow_rate_wads; - - let mut transaction = Transaction::new_with_payer( - &[ - approve( - &spl_token::id(), - &sol_test_reserve.user_collateral_pubkey, - &user_transfer_authority.pubkey(), - &user_accounts_owner.pubkey(), - &[], - SOL_DEPOSIT_AMOUNT_LAMPORTS, - ) - .unwrap(), - deposit_obligation_collateral( - solend_program::id(), - SOL_DEPOSIT_AMOUNT_LAMPORTS, - sol_test_reserve.user_collateral_pubkey, - sol_test_reserve.collateral_supply_pubkey, - sol_test_reserve.pubkey, - test_obligation.pubkey, - lending_market.pubkey, - test_obligation.owner, - user_transfer_authority.pubkey(), - ), - ], - Some(&payer.pubkey()), - ); - - transaction.sign( - &vec![&payer, &user_accounts_owner, &user_transfer_authority], - recent_blockhash, - ); - assert!(banks_client.process_transaction(transaction).await.is_ok()); + // check program state changes + let lending_market_post = test.load_account(lending_market.pubkey).await; + assert_eq!(lending_market, lending_market_post); - let sol_reserve = sol_test_reserve.get_state(&mut banks_client).await; - assert!(sol_reserve.last_update.stale); + let usdc_reserve_post = test.load_account(usdc_reserve.pubkey).await; + assert_eq!(usdc_reserve, usdc_reserve_post); - // check that collateral tokens were transferred - let collateral_supply_balance = - get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; + let obligation_post = test.load_account::(obligation.pubkey).await; assert_eq!( - collateral_supply_balance, - initial_collateral_supply_balance + SOL_DEPOSIT_AMOUNT_LAMPORTS - ); - let user_collateral_balance = - get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; - assert_eq!( - user_collateral_balance, - initial_user_collateral_balance - SOL_DEPOSIT_AMOUNT_LAMPORTS + obligation_post.account, + Obligation { + last_update: LastUpdate { + slot: 1000, + stale: true, + }, + deposits: vec![ObligationCollateral { + deposit_reserve: usdc_reserve.pubkey, + deposited_amount: 1_000_000, + market_value: Decimal::zero() // this field only gets updated on a refresh + }], + ..obligation.account + } ); +} - assert!(sol_reserve.liquidity.cumulative_borrow_rate_wads > old_borrow_rate); +#[tokio::test] +async fn test_fail_deposit_too_much() { + let (mut test, lending_market, usdc_reserve, user, obligation) = setup().await; + + let res = lending_market + .deposit_obligation_collateral(&mut test, &usdc_reserve, &obligation, &user, 1_000_001) + .await + .err() + .unwrap() + .unwrap(); + + match res { + // InsufficientFunds + TransactionError::InstructionError(0, InstructionError::Custom(1)) => (), + // LendingError::TokenTransferFailed + TransactionError::InstructionError(0, InstructionError::Custom(17)) => (), + e => panic!("unexpected error: {:#?}", e), + }; } diff --git a/token-lending/program/tests/deposit_reserve_liquidity.rs b/token-lending/program/tests/deposit_reserve_liquidity.rs index 6b777e23bc2..60802ffcda5 100644 --- a/token-lending/program/tests/deposit_reserve_liquidity.rs +++ b/token-lending/program/tests/deposit_reserve_liquidity.rs @@ -2,78 +2,162 @@ mod helpers; +use crate::solend_program_test::MintSupplyChange; +use solend_program::state::ReserveConfig; +use std::collections::HashSet; + +use helpers::solend_program_test::{ + setup_world, BalanceChecker, Info, SolendProgramTest, TokenBalanceChange, User, +}; use helpers::*; +use solana_program::instruction::InstructionError; use solana_program_test::*; -use solana_sdk::signature::Keypair; -use solend_program::processor::process_instruction; +use solana_sdk::transaction::TransactionError; +use solend_program::error::LendingError; +use solend_program::state::{ + LastUpdate, LendingMarket, Reserve, ReserveCollateral, ReserveLiquidity, +}; + +async fn setup() -> (SolendProgramTest, Info, Info, User) { + let (test, lending_market, usdc_reserve, _, _, user) = setup_world( + &ReserveConfig { + deposit_limit: 100_000 * FRACTIONAL_TO_USDC, + ..test_reserve_config() + }, + &test_reserve_config(), + ) + .await; + + (test, lending_market, usdc_reserve, user) +} #[tokio::test] async fn test_success() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), + let (mut test, lending_market, usdc_reserve, user) = setup().await; + + let balance_checker = BalanceChecker::start(&mut test, &[&usdc_reserve, &user]).await; + + // deposit + lending_market + .deposit(&mut test, &usdc_reserve, &user, 1_000_000) + .await + .expect("this should succeed"); + + // check token balances + let (token_balance_changes, mint_supply_changes) = + balance_checker.find_balance_changes(&mut test).await; + + assert_eq!( + token_balance_changes, + HashSet::from([ + TokenBalanceChange { + token_account: user.get_account(&usdc_mint::id()).unwrap(), + mint: usdc_mint::id(), + diff: -1_000_000, + }, + TokenBalanceChange { + token_account: user + .get_account(&usdc_reserve.account.collateral.mint_pubkey) + .unwrap(), + mint: usdc_reserve.account.collateral.mint_pubkey, + diff: 1_000_000, + }, + TokenBalanceChange { + token_account: usdc_reserve.account.liquidity.supply_pubkey, + mint: usdc_reserve.account.liquidity.mint_pubkey, + diff: 1_000_000, + }, + ]), + "{:#?}", + token_balance_changes ); - // limit to track compute unit increase - test.set_compute_max_units(50_000); - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - user_liquidity_amount: 100 * FRACTIONAL_TO_USDC, - liquidity_amount: 10_000 * FRACTIONAL_TO_USDC, - liquidity_mint_decimals: usdc_mint.decimals, - liquidity_mint_pubkey: usdc_mint.pubkey, - borrow_amount: 5_000 * FRACTIONAL_TO_USDC, - config: test_reserve_config(), - mark_fresh: true, - ..AddReserveArgs::default() - }, + assert_eq!( + mint_supply_changes, + HashSet::from([MintSupplyChange { + mint: usdc_reserve.account.collateral.mint_pubkey, + diff: 1_000_000, + },]), + "{:#?}", + mint_supply_changes ); - let mut test_context = test.start_with_context().await; - test_context.warp_to_slot(300).unwrap(); // clock.slot = 300 + // check program state + let lending_market_post = test + .load_account::(lending_market.pubkey) + .await; + assert_eq!(lending_market.account, lending_market_post.account); - let ProgramTestContext { - mut banks_client, - payer, - .. - } = test_context; + let usdc_reserve_post = test.load_account::(usdc_reserve.pubkey).await; + assert_eq!( + usdc_reserve_post.account, + Reserve { + last_update: LastUpdate { + slot: 1000, + stale: true + }, + liquidity: ReserveLiquidity { + available_amount: usdc_reserve.account.liquidity.available_amount + 1_000_000, + ..usdc_reserve.account.liquidity + }, + collateral: ReserveCollateral { + mint_total_supply: usdc_reserve.account.collateral.mint_total_supply + 1_000_000, + ..usdc_reserve.account.collateral + }, + ..usdc_reserve.account + } + ); +} + +#[tokio::test] +async fn test_fail_exceed_deposit_limit() { + let (mut test, lending_market, usdc_reserve, user) = setup().await; - let initial_ctoken_amount = - get_token_balance(&mut banks_client, usdc_test_reserve.user_collateral_pubkey).await; - let pre_usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; - let old_borrow_rate = pre_usdc_reserve.liquidity.cumulative_borrow_rate_wads; + let res = lending_market + .deposit(&mut test, &usdc_reserve, &user, 200_000_000_000) + .await + .err() + .unwrap() + .unwrap(); - lending_market - .deposit( - &mut banks_client, - &user_accounts_owner, - &payer, - &usdc_test_reserve, - 100 * FRACTIONAL_TO_USDC, + assert_eq!( + res, + TransactionError::InstructionError( + 0, + InstructionError::Custom(LendingError::InvalidAmount as u32) ) - .await; + ); +} - let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; - assert!(usdc_reserve.last_update.stale); +#[tokio::test] +async fn test_fail_deposit_too_much() { + let (mut test, lending_market, usdc_reserve, user) = setup().await; - let user_remaining_liquidity_amount = - get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; - assert_eq!(user_remaining_liquidity_amount, 0); + // drain original user's funds first + { + let new_user = User::new_with_balances(&mut test, &[(&usdc_mint::id(), 0)]).await; + user.transfer( + &usdc_mint::id(), + new_user.get_account(&usdc_mint::id()).unwrap(), + 1_000_000_000_000, + &mut test, + ) + .await; + } - let final_ctoken_amount = - get_token_balance(&mut banks_client, usdc_test_reserve.user_collateral_pubkey).await; - assert!(final_ctoken_amount - initial_ctoken_amount < 100 * FRACTIONAL_TO_USDC); + // deposit more than user owns + let res = lending_market + .deposit(&mut test, &usdc_reserve, &user, 1) + .await + .err() + .unwrap() + .unwrap(); - assert!(usdc_reserve.liquidity.cumulative_borrow_rate_wads > old_borrow_rate); + match res { + // InsufficientFunds + TransactionError::InstructionError(0, InstructionError::Custom(1)) => (), + // LendingError::TokenTransferFailed + TransactionError::InstructionError(0, InstructionError::Custom(17)) => (), + e => panic!("unexpected error: {:#?}", e), + }; } diff --git a/token-lending/program/tests/deposit_reserve_liquidity_and_obligation_collateral.rs b/token-lending/program/tests/deposit_reserve_liquidity_and_obligation_collateral.rs index 985801a3948..87bd779b5c5 100644 --- a/token-lending/program/tests/deposit_reserve_liquidity_and_obligation_collateral.rs +++ b/token-lending/program/tests/deposit_reserve_liquidity_and_obligation_collateral.rs @@ -2,65 +2,138 @@ mod helpers; +use crate::solend_program_test::MintSupplyChange; +use std::collections::HashSet; + +use helpers::solend_program_test::{ + setup_world, BalanceChecker, Info, SolendProgramTest, TokenBalanceChange, User, +}; use helpers::*; use solana_program_test::*; use solana_sdk::signature::Keypair; -use solend_program::processor::process_instruction; -#[tokio::test] -async fn test_success() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); +use solend_program::math::Decimal; +use solend_program::state::{ + LastUpdate, LendingMarket, Obligation, ObligationCollateral, Reserve, ReserveCollateral, + ReserveLiquidity, +}; - // limit to track compute unit increase - test.set_compute_max_units(70_000); - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - user_liquidity_amount: 100 * FRACTIONAL_TO_USDC, - liquidity_amount: 10_000 * FRACTIONAL_TO_USDC, - liquidity_mint_decimals: usdc_mint.decimals, - liquidity_mint_pubkey: usdc_mint.pubkey, - config: test_reserve_config(), - mark_fresh: true, - ..AddReserveArgs::default() - }, - ); +async fn setup() -> ( + SolendProgramTest, + Info, + Info, + User, + Info, +) { + let (mut test, lending_market, usdc_reserve, _, _, user) = + setup_world(&test_reserve_config(), &test_reserve_config()).await; - let test_obligation = add_obligation( - &mut test, - &lending_market, - &user_accounts_owner, - AddObligationArgs::default(), - ); + let obligation = lending_market + .init_obligation(&mut test, Keypair::new(), &user) + .await + .expect("This should succeed"); + + (test, lending_market, usdc_reserve, user, obligation) +} + +#[tokio::test] +async fn test_success() { + let (mut test, lending_market, usdc_reserve, user, obligation) = setup().await; - let (mut banks_client, payer, _recent_blockhash) = test.start().await; + test.advance_clock_by_slots(1).await; - test_obligation.validate_state(&mut banks_client).await; + let balance_checker = BalanceChecker::start(&mut test, &[&usdc_reserve, &user]).await; + // deposit lending_market - .deposit_obligation_and_collateral( - &mut banks_client, - &user_accounts_owner, - &payer, - &usdc_test_reserve, - &test_obligation, - 100 * FRACTIONAL_TO_USDC, + .deposit_reserve_liquidity_and_obligation_collateral( + &mut test, + &usdc_reserve, + &obligation, + &user, + 1_000_000, ) + .await + .expect("this should succeed"); + + // check token balances + let (token_balance_changes, mint_supply_changes) = + balance_checker.find_balance_changes(&mut test).await; + + assert_eq!( + token_balance_changes, + HashSet::from([ + TokenBalanceChange { + token_account: user.get_account(&usdc_mint::id()).unwrap(), + mint: usdc_mint::id(), + diff: -1_000_000, + }, + TokenBalanceChange { + token_account: usdc_reserve.account.collateral.supply_pubkey, + mint: usdc_reserve.account.collateral.mint_pubkey, + diff: 1_000_000, + }, + TokenBalanceChange { + token_account: usdc_reserve.account.liquidity.supply_pubkey, + mint: usdc_reserve.account.liquidity.mint_pubkey, + diff: 1_000_000, + }, + ]), + "{:#?}", + token_balance_changes + ); + + assert_eq!( + mint_supply_changes, + HashSet::from([MintSupplyChange { + mint: usdc_reserve.account.collateral.mint_pubkey, + diff: 1_000_000, + },]), + "{:#?}", + mint_supply_changes + ); + + // check program state + let lending_market_post = test + .load_account::(lending_market.pubkey) .await; + assert_eq!(lending_market.account, lending_market_post.account); - let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; - assert!(usdc_reserve.last_update.stale); + let usdc_reserve_post = test.load_account::(usdc_reserve.pubkey).await; + assert_eq!( + usdc_reserve_post.account, + Reserve { + last_update: LastUpdate { + slot: 1001, + stale: false, + }, + liquidity: ReserveLiquidity { + available_amount: usdc_reserve.account.liquidity.available_amount + 1_000_000, + ..usdc_reserve.account.liquidity + }, + collateral: ReserveCollateral { + mint_total_supply: usdc_reserve.account.collateral.mint_total_supply + 1_000_000, + ..usdc_reserve.account.collateral + }, + ..usdc_reserve.account + } + ); + + let obligation_post = test.load_account::(obligation.pubkey).await; + assert_eq!( + obligation_post.account, + Obligation { + last_update: LastUpdate { + slot: 1000, + stale: true + }, + deposits: [ObligationCollateral { + deposit_reserve: usdc_reserve.pubkey, + deposited_amount: 1_000_000, + market_value: Decimal::zero() + }] + .to_vec(), + ..obligation.account + } + ); } diff --git a/token-lending/program/tests/flash_borrow_repay.rs b/token-lending/program/tests/flash_borrow_repay.rs index b82118de36d..e2f302e9d30 100644 --- a/token-lending/program/tests/flash_borrow_repay.rs +++ b/token-lending/program/tests/flash_borrow_repay.rs @@ -2,7 +2,14 @@ mod helpers; +use std::collections::HashSet; + use helpers::*; + +use flash_loan_proxy::proxy_program; +use helpers::solend_program_test::{ + setup_world, BalanceChecker, Info, SolendProgramTest, TokenBalanceChange, User, +}; use solana_program::instruction::{AccountMeta, Instruction}; use solana_program::sysvar; use solana_program_test::*; @@ -10,181 +17,188 @@ use solana_sdk::{ instruction::InstructionError, pubkey::Pubkey, signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, + transaction::TransactionError, }; +use solend_program::instruction::LendingInstruction; +use solend_program::state::LastUpdate; use solend_program::{ error::LendingError, - instruction::{ - flash_borrow_reserve_liquidity, flash_repay_reserve_liquidity, LendingInstruction, - }, - processor::process_instruction, + instruction::{flash_borrow_reserve_liquidity, flash_repay_reserve_liquidity}, + state::{LendingMarket, Reserve, ReserveConfig, ReserveFees}, }; use spl_token::error::TokenError; use spl_token::instruction::approve; +async fn setup( + usdc_reserve_config: &ReserveConfig, +) -> ( + SolendProgramTest, + Info, + Info, + User, + User, + User, +) { + let (mut test, lending_market, usdc_reserve, _, lending_market_owner, user) = + setup_world(usdc_reserve_config, &test_reserve_config()).await; + + // deposit 100k USDC + lending_market + .deposit(&mut test, &usdc_reserve, &user, 100_000_000_000) + .await + .expect("This should succeed"); + + let usdc_reserve = test.load_account(usdc_reserve.pubkey).await; + + let host_fee_receiver = User::new_with_balances(&mut test, &[(&usdc_mint::id(), 0)]).await; + + ( + test, + lending_market, + usdc_reserve, + user, + host_fee_receiver, + lending_market_owner, + ) +} + #[tokio::test] async fn test_success() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(60_000); + let (mut test, lending_market, usdc_reserve, user, host_fee_receiver, _) = + setup(&ReserveConfig { + deposit_limit: u64::MAX, + fees: ReserveFees { + borrow_fee_wad: 100_000_000_000, + host_fee_percentage: 20, + flash_loan_fee_wad: 3_000_000_000_000_000, + }, + ..test_reserve_config() + }) + .await; + + let balance_checker = + BalanceChecker::start(&mut test, &[&usdc_reserve, &user, &host_fee_receiver]).await; const FLASH_LOAN_AMOUNT: u64 = 1_000 * FRACTIONAL_TO_USDC; const FEE_AMOUNT: u64 = 3_000_000; const HOST_FEE_AMOUNT: u64 = 600_000; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let mut reserve_config = test_reserve_config(); - reserve_config.fees.host_fee_percentage = 20; - reserve_config.fees.flash_loan_fee_wad = 3_000_000_000_000_000; - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - user_liquidity_amount: FEE_AMOUNT, - liquidity_amount: FLASH_LOAN_AMOUNT, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - ..AddReserveArgs::default() - }, - ); - - let (mut banks_client, payer, recent_blockhash) = test.start().await; - let mut transaction = Transaction::new_with_payer( + test.process_transaction( &[ flash_borrow_reserve_liquidity( solend_program::id(), FLASH_LOAN_AMOUNT, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, + usdc_reserve.account.liquidity.supply_pubkey, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, lending_market.pubkey, ), flash_repay_reserve_liquidity( solend_program::id(), FLASH_LOAN_AMOUNT, 0, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.config.fee_receiver, - usdc_test_reserve.liquidity_host_pubkey, - usdc_test_reserve.pubkey, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.account.liquidity.supply_pubkey, + usdc_reserve.account.config.fee_receiver, + host_fee_receiver.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, lending_market.pubkey, - user_accounts_owner.pubkey(), + user.keypair.pubkey(), ), ], - Some(&payer.pubkey()), - ); - - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); - assert!(banks_client.process_transaction(transaction).await.is_ok()); - - let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; - assert_eq!(usdc_reserve.liquidity.available_amount, FLASH_LOAN_AMOUNT); - assert!(usdc_reserve.last_update.stale); - - let liquidity_supply = - get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; - assert_eq!(liquidity_supply, FLASH_LOAN_AMOUNT); - - let token_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; - assert_eq!(token_balance, 0); + Some(&[&user.keypair]), + ) + .await + .unwrap(); + + // check balance changes + let (balance_changes, mint_supply_changes) = + balance_checker.find_balance_changes(&mut test).await; + let expected_balance_changes = HashSet::from([ + TokenBalanceChange { + token_account: user.get_account(&usdc_mint::id()).unwrap(), + mint: usdc_mint::id(), + diff: -(FEE_AMOUNT as i128), + }, + TokenBalanceChange { + token_account: usdc_reserve.account.config.fee_receiver, + mint: usdc_mint::id(), + diff: (FEE_AMOUNT - HOST_FEE_AMOUNT) as i128, + }, + TokenBalanceChange { + token_account: host_fee_receiver.get_account(&usdc_mint::id()).unwrap(), + mint: usdc_mint::id(), + diff: HOST_FEE_AMOUNT as i128, + }, + ]); + assert_eq!(balance_changes, expected_balance_changes); + assert_eq!(mint_supply_changes, HashSet::new()); - let fee_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.config.fee_receiver).await; - assert_eq!(fee_balance, FEE_AMOUNT - HOST_FEE_AMOUNT); + // check program state changes + let lending_market_post = test + .load_account::(lending_market.pubkey) + .await; + assert_eq!(lending_market, lending_market_post); - let host_fee_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_host_pubkey).await; - assert_eq!(host_fee_balance, HOST_FEE_AMOUNT); + let usdc_reserve_post = test.load_account::(usdc_reserve.pubkey).await; + assert_eq!( + usdc_reserve_post.account, + Reserve { + last_update: LastUpdate { + slot: 1000, + stale: true + }, + ..usdc_reserve.account + } + ); } #[tokio::test] async fn test_fail_disable_flash_loans() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(60_000); + let (mut test, lending_market, usdc_reserve, user, host_fee_receiver, _) = + setup(&ReserveConfig { + deposit_limit: u64::MAX, + fees: ReserveFees { + borrow_fee_wad: 1, + host_fee_percentage: 20, + flash_loan_fee_wad: u64::MAX, + }, + ..test_reserve_config() + }) + .await; const FLASH_LOAN_AMOUNT: u64 = 3_000_000; - const LIQUIDITY_AMOUNT: u64 = 1_000 * FRACTIONAL_TO_USDC; - const FEE_AMOUNT: u64 = 3_000_000; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let mut reserve_config = test_reserve_config(); - reserve_config.fees.flash_loan_fee_wad = u64::MAX; // disabled - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - user_liquidity_amount: FEE_AMOUNT, - liquidity_amount: LIQUIDITY_AMOUNT, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - ..AddReserveArgs::default() - }, - ); - - let (mut banks_client, payer, recent_blockhash) = test.start().await; - let mut transaction = Transaction::new_with_payer( - &[ - flash_borrow_reserve_liquidity( - solend_program::id(), - FLASH_LOAN_AMOUNT, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - ), - flash_repay_reserve_liquidity( - solend_program::id(), - FLASH_LOAN_AMOUNT, - 0, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.config.fee_receiver, - usdc_test_reserve.liquidity_host_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - user_accounts_owner.pubkey(), - ), - ], - Some(&payer.pubkey()), - ); - - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); + let res = test + .process_transaction( + &[ + flash_borrow_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + usdc_reserve.account.liquidity.supply_pubkey, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + ), + flash_repay_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + 0, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.account.liquidity.supply_pubkey, + usdc_reserve.account.config.fee_receiver, + host_fee_receiver.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + user.keypair.pubkey(), + ), + ], + Some(&[&user.keypair]), + ) + .await + .unwrap_err() + .unwrap(); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( 0, InstructionError::Custom(LendingError::FlashLoansDisabled as u32) @@ -194,77 +208,52 @@ async fn test_fail_disable_flash_loans() { #[tokio::test] async fn test_fail_borrow_over_borrow_limit() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(60_000); + let (mut test, lending_market, usdc_reserve, user, host_fee_receiver, _) = + setup(&ReserveConfig { + deposit_limit: u64::MAX, + borrow_limit: 2_000_000, + fees: ReserveFees { + borrow_fee_wad: 1, + host_fee_percentage: 20, + flash_loan_fee_wad: 1, + }, + ..test_reserve_config() + }) + .await; const FLASH_LOAN_AMOUNT: u64 = 3_000_000; - const LIQUIDITY_AMOUNT: u64 = 1_000 * FRACTIONAL_TO_USDC; - const FEE_AMOUNT: u64 = 3_000_000; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let mut reserve_config = test_reserve_config(); - reserve_config.borrow_limit = 2_000_000; - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - user_liquidity_amount: FEE_AMOUNT, - liquidity_amount: LIQUIDITY_AMOUNT, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - ..AddReserveArgs::default() - }, - ); - - let (mut banks_client, payer, recent_blockhash) = test.start().await; - let mut transaction = Transaction::new_with_payer( - &[ - flash_borrow_reserve_liquidity( - solend_program::id(), - FLASH_LOAN_AMOUNT, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - ), - flash_repay_reserve_liquidity( - solend_program::id(), - FLASH_LOAN_AMOUNT, - 0, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.config.fee_receiver, - usdc_test_reserve.liquidity_host_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - user_accounts_owner.pubkey(), - ), - ], - Some(&payer.pubkey()), - ); - - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); + let res = test + .process_transaction( + &[ + flash_borrow_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + usdc_reserve.account.liquidity.supply_pubkey, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + ), + flash_repay_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + 0, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.account.liquidity.supply_pubkey, + usdc_reserve.account.config.fee_receiver, + host_fee_receiver.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + user.keypair.pubkey(), + ), + ], + Some(&[&user.keypair]), + ) + .await + .unwrap_err() + .unwrap(); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( 0, InstructionError::Custom(LendingError::InvalidAmount as u32) @@ -274,85 +263,60 @@ async fn test_fail_borrow_over_borrow_limit() { #[tokio::test] async fn test_fail_double_borrow() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(60_000); + let (mut test, lending_market, usdc_reserve, user, host_fee_receiver, _) = + setup(&ReserveConfig { + deposit_limit: u64::MAX, + borrow_limit: u64::MAX, + fees: ReserveFees { + borrow_fee_wad: 1, + host_fee_percentage: 20, + flash_loan_fee_wad: 1, + }, + ..test_reserve_config() + }) + .await; const FLASH_LOAN_AMOUNT: u64 = 3_000_000; - const LIQUIDITY_AMOUNT: u64 = 1_000 * FRACTIONAL_TO_USDC; - const FEE_AMOUNT: u64 = 3_000_000; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let mut reserve_config = test_reserve_config(); - reserve_config.fees.host_fee_percentage = 20; - reserve_config.fees.flash_loan_fee_wad = 3_000_000_000_000_000; - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - user_liquidity_amount: FEE_AMOUNT, - liquidity_amount: LIQUIDITY_AMOUNT, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - ..AddReserveArgs::default() - }, - ); - - let (mut banks_client, payer, recent_blockhash) = test.start().await; - let mut transaction = Transaction::new_with_payer( - &[ - flash_borrow_reserve_liquidity( - solend_program::id(), - FLASH_LOAN_AMOUNT, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - ), - flash_borrow_reserve_liquidity( - solend_program::id(), - FLASH_LOAN_AMOUNT, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - ), - flash_repay_reserve_liquidity( - solend_program::id(), - FLASH_LOAN_AMOUNT, - 0, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.config.fee_receiver, - usdc_test_reserve.liquidity_host_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - user_accounts_owner.pubkey(), - ), - ], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); + let res = test + .process_transaction( + &[ + flash_borrow_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + usdc_reserve.account.liquidity.supply_pubkey, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + ), + flash_borrow_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + usdc_reserve.account.liquidity.supply_pubkey, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + ), + flash_repay_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + 0, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.account.liquidity.supply_pubkey, + usdc_reserve.account.config.fee_receiver, + host_fee_receiver.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + user.keypair.pubkey(), + ), + ], + Some(&[&user.keypair]), + ) + .await + .unwrap_err() + .unwrap(); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( 0, InstructionError::Custom(LendingError::MultipleFlashBorrows as u32) @@ -360,193 +324,66 @@ async fn test_fail_double_borrow() { ); } -/// idk why anyone would do this but w/e #[tokio::test] async fn test_fail_double_repay() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(60_000); + let (mut test, lending_market, usdc_reserve, user, host_fee_receiver, _) = + setup(&ReserveConfig { + deposit_limit: u64::MAX, + borrow_limit: u64::MAX, + fees: ReserveFees { + borrow_fee_wad: 1, + host_fee_percentage: 20, + flash_loan_fee_wad: 1, + }, + ..test_reserve_config() + }) + .await; const FLASH_LOAN_AMOUNT: u64 = 3_000_000; - const LIQUIDITY_AMOUNT: u64 = 1_000 * FRACTIONAL_TO_USDC; - const FEE_AMOUNT: u64 = 3_000_000; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let mut reserve_config = test_reserve_config(); - reserve_config.fees.host_fee_percentage = 20; - reserve_config.fees.flash_loan_fee_wad = 3_000_000_000_000_000; - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - user_liquidity_amount: FEE_AMOUNT, - liquidity_amount: LIQUIDITY_AMOUNT, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - ..AddReserveArgs::default() - }, - ); - - let (mut banks_client, payer, recent_blockhash) = test.start().await; - let mut transaction = Transaction::new_with_payer( - &[ - flash_borrow_reserve_liquidity( - solend_program::id(), - FLASH_LOAN_AMOUNT, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - ), - flash_repay_reserve_liquidity( - solend_program::id(), - FLASH_LOAN_AMOUNT, - 0, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.config.fee_receiver, - usdc_test_reserve.liquidity_host_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - user_accounts_owner.pubkey(), - ), - flash_repay_reserve_liquidity( - solend_program::id(), - 0, - 0, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.config.fee_receiver, - usdc_test_reserve.liquidity_host_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - user_accounts_owner.pubkey(), - ), - ], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); - - assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError( - 0, - InstructionError::Custom(LendingError::MultipleFlashBorrows as u32) + let res = test + .process_transaction( + &[ + flash_borrow_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + usdc_reserve.account.liquidity.supply_pubkey, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + ), + flash_repay_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + 0, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.account.liquidity.supply_pubkey, + usdc_reserve.account.config.fee_receiver, + host_fee_receiver.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + user.keypair.pubkey(), + ), + flash_repay_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + 0, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.account.liquidity.supply_pubkey, + usdc_reserve.account.config.fee_receiver, + host_fee_receiver.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + user.keypair.pubkey(), + ), + ], + Some(&[&user.keypair]), ) - ); -} - -#[tokio::test] -async fn test_fail_only_one_flash_ix_pair_per_tx() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(60_000); - - const FLASH_LOAN_AMOUNT: u64 = 3_000_000; - const LIQUIDITY_AMOUNT: u64 = 1_000 * FRACTIONAL_TO_USDC; - const FEE_AMOUNT: u64 = 3_000_000; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let mut reserve_config = test_reserve_config(); - reserve_config.fees.host_fee_percentage = 20; - reserve_config.fees.flash_loan_fee_wad = 3_000_000_000_000_000; - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - user_liquidity_amount: FEE_AMOUNT, - liquidity_amount: LIQUIDITY_AMOUNT, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - ..AddReserveArgs::default() - }, - ); - - // eventually this will be valid. but for v1 implementation, we only let 1 flash ix pair per tx - let (mut banks_client, payer, recent_blockhash) = test.start().await; - let mut transaction = Transaction::new_with_payer( - &[ - flash_borrow_reserve_liquidity( - solend_program::id(), - FLASH_LOAN_AMOUNT, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - ), - flash_repay_reserve_liquidity( - solend_program::id(), - FLASH_LOAN_AMOUNT, - 0, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.config.fee_receiver, - usdc_test_reserve.liquidity_host_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - user_accounts_owner.pubkey(), - ), - flash_borrow_reserve_liquidity( - solend_program::id(), - FLASH_LOAN_AMOUNT, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - ), - flash_repay_reserve_liquidity( - solend_program::id(), - FLASH_LOAN_AMOUNT, - 2, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.config.fee_receiver, - usdc_test_reserve.liquidity_host_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - user_accounts_owner.pubkey(), - ), - ], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); + .await + .unwrap_err() + .unwrap(); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( 0, InstructionError::Custom(LendingError::MultipleFlashBorrows as u32) @@ -555,86 +392,130 @@ async fn test_fail_only_one_flash_ix_pair_per_tx() { } #[tokio::test] -async fn test_fail_invalid_repay_ix() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - let proxy_program_id = Pubkey::new_unique(); - test.prefer_bpf(false); - test.add_program( - "flash_loan_proxy", - proxy_program_id, - processor!(helpers::flash_loan_proxy::process_instruction), - ); +async fn test_fail_only_one_flash_ix_pair_per_tx() { + let (mut test, lending_market, usdc_reserve, user, host_fee_receiver, _) = + setup(&ReserveConfig { + deposit_limit: u64::MAX, + borrow_limit: u64::MAX, + fees: ReserveFees { + borrow_fee_wad: 1, + host_fee_percentage: 20, + flash_loan_fee_wad: 3_000_000_000_000_000, + }, + ..test_reserve_config() + }) + .await; const FLASH_LOAN_AMOUNT: u64 = 3_000_000; - const LIQUIDITY_AMOUNT: u64 = 1_000_000 * FRACTIONAL_TO_USDC; - const FEE_AMOUNT: u64 = 3_000_000; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let mut reserve_config = test_reserve_config(); - reserve_config.fees.host_fee_percentage = 20; - reserve_config.fees.flash_loan_fee_wad = 3_000_000_000_000_000; - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - user_liquidity_amount: FEE_AMOUNT, - liquidity_amount: LIQUIDITY_AMOUNT, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - ..AddReserveArgs::default() - }, - ); - - let (mut banks_client, payer, recent_blockhash) = test.start().await; - - // case 1: invalid reserve in repay - { - let mut transaction = Transaction::new_with_payer( + let res = test + .process_transaction( &[ flash_borrow_reserve_liquidity( solend_program::id(), FLASH_LOAN_AMOUNT, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, + usdc_reserve.account.liquidity.supply_pubkey, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, lending_market.pubkey, ), flash_repay_reserve_liquidity( solend_program::id(), FLASH_LOAN_AMOUNT, 0, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.config.fee_receiver, - usdc_test_reserve.liquidity_host_pubkey, - Pubkey::new_unique(), + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.account.liquidity.supply_pubkey, + usdc_reserve.account.config.fee_receiver, + host_fee_receiver.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + user.keypair.pubkey(), + ), + flash_borrow_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + usdc_reserve.account.liquidity.supply_pubkey, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, lending_market.pubkey, - user_accounts_owner.pubkey(), + ), + flash_repay_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + 2, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.account.liquidity.supply_pubkey, + usdc_reserve.account.config.fee_receiver, + host_fee_receiver.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + user.keypair.pubkey(), ), ], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); + Some(&[&user.keypair]), + ) + .await + .unwrap_err() + .unwrap(); + + assert_eq!( + res, + TransactionError::InstructionError( + 0, + InstructionError::Custom(LendingError::MultipleFlashBorrows as u32) + ) + ); +} + +#[tokio::test] +async fn test_fail_invalid_repay_ix() { + let (mut test, lending_market, usdc_reserve, user, host_fee_receiver, _) = + setup(&ReserveConfig { + deposit_limit: u64::MAX, + borrow_limit: u64::MAX, + fees: ReserveFees { + borrow_fee_wad: 1, + host_fee_percentage: 20, + flash_loan_fee_wad: 1, + }, + ..test_reserve_config() + }) + .await; + + const FLASH_LOAN_AMOUNT: u64 = 3_000_000; + // case 1: invalid reserve in repay + { + let res = test + .process_transaction( + &[ + flash_borrow_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + usdc_reserve.account.liquidity.supply_pubkey, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + ), + flash_repay_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + 0, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.account.liquidity.supply_pubkey, + usdc_reserve.account.config.fee_receiver, + host_fee_receiver.get_account(&usdc_mint::id()).unwrap(), + Pubkey::new_unique(), + lending_market.pubkey, + user.keypair.pubkey(), + ), + ], + Some(&[&user.keypair]), + ) + .await + .unwrap_err() + .unwrap(); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( 0, InstructionError::Custom(LendingError::InvalidFlashRepay as u32) @@ -644,39 +525,38 @@ async fn test_fail_invalid_repay_ix() { // case 2: invalid liquidity amount { - let mut transaction = Transaction::new_with_payer( - &[ - flash_borrow_reserve_liquidity( - solend_program::id(), - FLASH_LOAN_AMOUNT, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - ), - flash_repay_reserve_liquidity( - solend_program::id(), - FLASH_LOAN_AMOUNT - 1, - 0, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.config.fee_receiver, - usdc_test_reserve.liquidity_host_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - user_accounts_owner.pubkey(), - ), - ], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); + let res = test + .process_transaction( + &[ + flash_borrow_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + usdc_reserve.account.liquidity.supply_pubkey, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + ), + flash_repay_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT - 1, + 0, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.account.liquidity.supply_pubkey, + usdc_reserve.account.config.fee_receiver, + host_fee_receiver.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + user.keypair.pubkey(), + ), + ], + Some(&[&user.keypair]), + ) + .await + .unwrap_err() + .unwrap(); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( 0, InstructionError::Custom(LendingError::InvalidFlashRepay as u32) @@ -686,26 +566,24 @@ async fn test_fail_invalid_repay_ix() { // case 3: no repay { - let mut transaction = Transaction::new_with_payer( - &[flash_borrow_reserve_liquidity( - solend_program::id(), - FLASH_LOAN_AMOUNT, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - )], - Some(&payer.pubkey()), - ); - - transaction.sign(&[&payer], recent_blockhash); + let res = test + .process_transaction( + &[flash_borrow_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + usdc_reserve.account.liquidity.supply_pubkey, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + )], + None, + ) + .await + .unwrap_err() + .unwrap(); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( 0, InstructionError::Custom(LendingError::NoFlashRepayFound as u32) @@ -715,40 +593,39 @@ async fn test_fail_invalid_repay_ix() { // case 4: cpi repay { - let mut transaction = Transaction::new_with_payer( - &[ - flash_borrow_reserve_liquidity( - solend_program::id(), - FLASH_LOAN_AMOUNT, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - ), - helpers::flash_loan_proxy::repay_proxy( - proxy_program_id, - FLASH_LOAN_AMOUNT, - 0, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.config.fee_receiver, - usdc_test_reserve.liquidity_host_pubkey, - usdc_test_reserve.pubkey, - solend_program::id(), - lending_market.pubkey, - user_accounts_owner.pubkey(), - ), - ], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); + let res = test + .process_transaction( + &[ + flash_borrow_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + usdc_reserve.account.liquidity.supply_pubkey, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + ), + helpers::flash_loan_proxy::repay_proxy( + proxy_program::id(), + FLASH_LOAN_AMOUNT, + 0, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.account.liquidity.supply_pubkey, + usdc_reserve.account.config.fee_receiver, + host_fee_receiver.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + solend_program::id(), + lending_market.pubkey, + user.keypair.pubkey(), + ), + ], + Some(&[&user.keypair]), + ) + .await + .unwrap_err() + .unwrap(); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( 0, InstructionError::Custom(LendingError::NoFlashRepayFound as u32) @@ -756,38 +633,35 @@ async fn test_fail_invalid_repay_ix() { ); } - // case 5: insufficient funds to pay fees on repay. FEE_AMOUNT was calculated using - // FLASH_LOAN_AMOUNT, not LIQUIDITY_AMOUNT. + // case 5: insufficient funds to pay fees on repay. { - let mut transaction = Transaction::new_with_payer( - &[ - flash_borrow_reserve_liquidity( - solend_program::id(), - LIQUIDITY_AMOUNT, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - ), - flash_repay_reserve_liquidity( - solend_program::id(), - LIQUIDITY_AMOUNT, - 0, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.config.fee_receiver, - usdc_test_reserve.liquidity_host_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - user_accounts_owner.pubkey(), - ), - ], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); - - let res = banks_client - .process_transaction(transaction) + let new_user = User::new_with_balances(&mut test, &[(&usdc_mint::id(), 0)]).await; + let res = test + .process_transaction( + &[ + flash_borrow_reserve_liquidity( + solend_program::id(), + 100_000_000_000, + usdc_reserve.account.liquidity.supply_pubkey, + new_user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + ), + flash_repay_reserve_liquidity( + solend_program::id(), + 100_000_000_000, + 0, + new_user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.account.liquidity.supply_pubkey, + usdc_reserve.account.config.fee_receiver, + host_fee_receiver.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + new_user.keypair.pubkey(), + ), + ], + Some(&[&new_user.keypair]), + ) .await .unwrap_err() .unwrap(); @@ -807,29 +681,28 @@ async fn test_fail_invalid_repay_ix() { // case 6: Sole repay instruction { - let mut transaction = Transaction::new_with_payer( - &[flash_repay_reserve_liquidity( - solend_program::id(), - LIQUIDITY_AMOUNT, - 0, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.config.fee_receiver, - usdc_test_reserve.liquidity_host_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - user_accounts_owner.pubkey(), - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); + let res = test + .process_transaction( + &[flash_repay_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + 0, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.account.liquidity.supply_pubkey, + usdc_reserve.account.config.fee_receiver, + host_fee_receiver.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + user.keypair.pubkey(), + )], + Some(&[&user.keypair]), + ) + .await + .unwrap_err() + .unwrap(); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( 0, InstructionError::Custom(LendingError::InvalidFlashRepay as u32) @@ -839,39 +712,38 @@ async fn test_fail_invalid_repay_ix() { // case 7: Incorrect borrow instruction index -- points to itself { - let mut transaction = Transaction::new_with_payer( - &[ - flash_borrow_reserve_liquidity( - solend_program::id(), - LIQUIDITY_AMOUNT, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - ), - flash_repay_reserve_liquidity( - solend_program::id(), - LIQUIDITY_AMOUNT, - 1, // should be zero - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.config.fee_receiver, - usdc_test_reserve.liquidity_host_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - user_accounts_owner.pubkey(), - ), - ], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); + let res = test + .process_transaction( + &[ + flash_borrow_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + usdc_reserve.account.liquidity.supply_pubkey, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + ), + flash_repay_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + 1, // should be 0 + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.account.liquidity.supply_pubkey, + usdc_reserve.account.config.fee_receiver, + host_fee_receiver.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + user.keypair.pubkey(), + ), + ], + Some(&[&user.keypair]), + ) + .await + .unwrap_err() + .unwrap(); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( 0, InstructionError::Custom(LendingError::InvalidFlashRepay as u32) @@ -882,40 +754,47 @@ async fn test_fail_invalid_repay_ix() { // case 8: Incorrect borrow instruction index -- points to some other program { let user_transfer_authority = Keypair::new(); - let mut transaction = Transaction::new_with_payer( - &[ - approve( - &spl_token::id(), - &usdc_test_reserve.user_liquidity_pubkey, - &user_transfer_authority.pubkey(), - &user_accounts_owner.pubkey(), - &[], - 1, - ) - .unwrap(), - flash_repay_reserve_liquidity( - solend_program::id(), - LIQUIDITY_AMOUNT, - 0, // should be zero - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.config.fee_receiver, - usdc_test_reserve.liquidity_host_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - user_accounts_owner.pubkey(), - ), - ], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); + let res = test + .process_transaction( + &[ + approve( + &spl_token::id(), + &user.get_account(&usdc_mint::id()).unwrap(), + &user_transfer_authority.pubkey(), + &user.keypair.pubkey(), + &[], + 1, + ) + .unwrap(), + flash_borrow_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + usdc_reserve.account.liquidity.supply_pubkey, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + ), + flash_repay_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + 0, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.account.liquidity.supply_pubkey, + usdc_reserve.account.config.fee_receiver, + host_fee_receiver.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + user.keypair.pubkey(), + ), + ], + Some(&[&user.keypair]), + ) + .await + .unwrap_err() + .unwrap(); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( 1, InstructionError::Custom(LendingError::InvalidFlashRepay as u32) @@ -924,51 +803,50 @@ async fn test_fail_invalid_repay_ix() { } // case 9: Incorrect borrow instruction index -- points to a later borrow { - let mut transaction = Transaction::new_with_payer( - &[ - flash_repay_reserve_liquidity( - solend_program::id(), - FRACTIONAL_TO_USDC, - 1, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.config.fee_receiver, - usdc_test_reserve.liquidity_host_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - user_accounts_owner.pubkey(), - ), - flash_borrow_reserve_liquidity( - solend_program::id(), - FEE_AMOUNT, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - ), - flash_repay_reserve_liquidity( - solend_program::id(), - FEE_AMOUNT, - 1, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.config.fee_receiver, - usdc_test_reserve.liquidity_host_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - user_accounts_owner.pubkey(), - ), - ], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); + let res = test + .process_transaction( + &[ + flash_repay_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + 1, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.account.liquidity.supply_pubkey, + usdc_reserve.account.config.fee_receiver, + host_fee_receiver.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + user.keypair.pubkey(), + ), + flash_borrow_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + usdc_reserve.account.liquidity.supply_pubkey, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + ), + flash_repay_reserve_liquidity( + solend_program::id(), + FLASH_LOAN_AMOUNT, + 1, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.account.liquidity.supply_pubkey, + usdc_reserve.account.config.fee_receiver, + host_fee_receiver.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + user.keypair.pubkey(), + ), + ], + Some(&[&user.keypair]), + ) + .await + .unwrap_err() + .unwrap(); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( 0, InstructionError::Custom(LendingError::InvalidFlashRepay as u32) @@ -979,76 +857,50 @@ async fn test_fail_invalid_repay_ix() { #[tokio::test] async fn test_fail_insufficient_liquidity_for_borrow() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(60_000); - - const LIQUIDITY_AMOUNT: u64 = 1_000 * FRACTIONAL_TO_USDC; - const FEE_AMOUNT: u64 = 3_000_000; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let mut reserve_config = test_reserve_config(); - reserve_config.fees.host_fee_percentage = 20; - reserve_config.fees.flash_loan_fee_wad = 3_000_000_000_000_000; - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - user_liquidity_amount: FEE_AMOUNT, - liquidity_amount: LIQUIDITY_AMOUNT, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - ..AddReserveArgs::default() - }, - ); - - let (mut banks_client, payer, recent_blockhash) = test.start().await; - let mut transaction = Transaction::new_with_payer( - &[ - flash_borrow_reserve_liquidity( - solend_program::id(), - LIQUIDITY_AMOUNT + 1, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - ), - flash_repay_reserve_liquidity( - solend_program::id(), - LIQUIDITY_AMOUNT + 1, - 0, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.config.fee_receiver, - usdc_test_reserve.liquidity_host_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - user_accounts_owner.pubkey(), - ), - ], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); + let (mut test, lending_market, usdc_reserve, user, host_fee_receiver, _) = + setup(&ReserveConfig { + deposit_limit: u64::MAX, + fees: ReserveFees { + borrow_fee_wad: 100_000_000_000, + host_fee_percentage: 20, + flash_loan_fee_wad: 3_000_000_000_000_000, + }, + ..test_reserve_config() + }) + .await; + + let res = test + .process_transaction( + &[ + flash_borrow_reserve_liquidity( + solend_program::id(), + 1_000_000_000_000, + usdc_reserve.account.liquidity.supply_pubkey, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + ), + flash_repay_reserve_liquidity( + solend_program::id(), + 1_000_000_000_000, + 0, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.account.liquidity.supply_pubkey, + usdc_reserve.account.config.fee_receiver, + host_fee_receiver.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + user.keypair.pubkey(), + ), + ], + Some(&[&user.keypair]), + ) + .await + .unwrap_err() + .unwrap(); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( 0, InstructionError::Custom(LendingError::InsufficientLiquidity as u32) @@ -1058,73 +910,43 @@ async fn test_fail_insufficient_liquidity_for_borrow() { #[tokio::test] async fn test_fail_cpi_borrow() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - let proxy_program_id = Pubkey::new_unique(); - test.prefer_bpf(false); - test.add_program( - "flash_loan_proxy", - proxy_program_id, - processor!(helpers::flash_loan_proxy::process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(60_000); - - const FLASH_LOAN_AMOUNT: u64 = 3_000_000; - const LIQUIDITY_AMOUNT: u64 = 1_000 * FRACTIONAL_TO_USDC; - const FEE_AMOUNT: u64 = 3_000_000; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let mut reserve_config = test_reserve_config(); - reserve_config.fees.host_fee_percentage = 20; - reserve_config.fees.flash_loan_fee_wad = 3_000_000_000_000_000; - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - user_liquidity_amount: FEE_AMOUNT, - liquidity_amount: LIQUIDITY_AMOUNT, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - ..AddReserveArgs::default() + let (mut test, lending_market, usdc_reserve, user, _, _) = setup(&ReserveConfig { + deposit_limit: u64::MAX, + borrow_limit: u64::MAX, + fees: ReserveFees { + borrow_fee_wad: 1, + host_fee_percentage: 20, + flash_loan_fee_wad: 1, }, - ); + ..test_reserve_config() + }) + .await; - let (mut banks_client, payer, recent_blockhash) = test.start().await; - let mut transaction = Transaction::new_with_payer( - &[helpers::flash_loan_proxy::borrow_proxy( - proxy_program_id, - FLASH_LOAN_AMOUNT, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - solend_program::id(), - lending_market.pubkey, - lending_market.authority, - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer], recent_blockhash); + const FLASH_LOAN_AMOUNT: u64 = 3_000_000; + let res = test + .process_transaction( + &[helpers::flash_loan_proxy::borrow_proxy( + proxy_program::id(), + FLASH_LOAN_AMOUNT, + usdc_reserve.account.liquidity.supply_pubkey, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + solend_program::id(), + lending_market.pubkey, + Pubkey::find_program_address( + &[lending_market.pubkey.as_ref()], + &solend_program::id(), + ) + .0, + )], + None, + ) + .await + .unwrap_err() + .unwrap(); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( 0, InstructionError::Custom(LendingError::FlashBorrowCpi as u32) @@ -1134,76 +956,43 @@ async fn test_fail_cpi_borrow() { #[tokio::test] async fn test_fail_cpi_repay() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - let proxy_program_id = Pubkey::new_unique(); - test.prefer_bpf(false); - test.add_program( - "flash_loan_proxy", - proxy_program_id, - processor!(helpers::flash_loan_proxy::process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(60_000); + let (mut test, lending_market, usdc_reserve, user, host_fee_receiver, _) = + setup(&ReserveConfig { + deposit_limit: u64::MAX, + borrow_limit: u64::MAX, + fees: ReserveFees { + borrow_fee_wad: 1, + host_fee_percentage: 20, + flash_loan_fee_wad: 1, + }, + ..test_reserve_config() + }) + .await; const FLASH_LOAN_AMOUNT: u64 = 3_000_000; - const LIQUIDITY_AMOUNT: u64 = 1_000 * FRACTIONAL_TO_USDC; - const FEE_AMOUNT: u64 = 3_000_000; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let mut reserve_config = test_reserve_config(); - reserve_config.fees.host_fee_percentage = 20; - reserve_config.fees.flash_loan_fee_wad = 3_000_000_000_000_000; - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - user_liquidity_amount: FEE_AMOUNT, - liquidity_amount: LIQUIDITY_AMOUNT, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - ..AddReserveArgs::default() - }, - ); - - let (mut banks_client, payer, recent_blockhash) = test.start().await; - let mut transaction = Transaction::new_with_payer( - &[helpers::flash_loan_proxy::repay_proxy( - proxy_program_id, - FLASH_LOAN_AMOUNT, - 0, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.config.fee_receiver, - usdc_test_reserve.liquidity_host_pubkey, - usdc_test_reserve.pubkey, - solend_program::id(), - lending_market.pubkey, - user_accounts_owner.pubkey(), - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); + let res = test + .process_transaction( + &[helpers::flash_loan_proxy::repay_proxy( + proxy_program::id(), + FLASH_LOAN_AMOUNT, + 0, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.account.liquidity.supply_pubkey, + usdc_reserve.account.config.fee_receiver, + host_fee_receiver.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + solend_program::id(), + lending_market.pubkey, + user.keypair.pubkey(), + )], + Some(&[&user.keypair]), + ) + .await + .unwrap_err() + .unwrap(); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( 0, InstructionError::Custom(LendingError::FlashRepayCpi as u32) @@ -1213,99 +1002,73 @@ async fn test_fail_cpi_repay() { #[tokio::test] async fn test_fail_repay_from_diff_reserve() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(61_000); - - const FLASH_LOAN_AMOUNT: u64 = 1_000 * FRACTIONAL_TO_USDC; - const FEE_AMOUNT: u64 = 3_000_000; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let mut reserve_config = test_reserve_config(); - reserve_config.fees.host_fee_percentage = 20; - reserve_config.fees.flash_loan_fee_wad = 3_000_000_000_000_000; - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - user_liquidity_amount: FEE_AMOUNT, - liquidity_amount: FLASH_LOAN_AMOUNT, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - ..AddReserveArgs::default() - }, - ); - let another_usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - user_liquidity_amount: FEE_AMOUNT, - liquidity_amount: FLASH_LOAN_AMOUNT, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - ..AddReserveArgs::default() - }, - ); - - let (mut banks_client, payer, recent_blockhash) = test.start().await; + let (mut test, lending_market, usdc_reserve, user, host_fee_receiver, lending_market_owner) = + setup(&ReserveConfig { + deposit_limit: u64::MAX, + fees: ReserveFees { + borrow_fee_wad: 1, + host_fee_percentage: 20, + flash_loan_fee_wad: 1, + }, + ..test_reserve_config() + }) + .await; + + let another_usdc_reserve = test + .init_reserve( + &lending_market, + &lending_market_owner, + &usdc_mint::id(), + &test_reserve_config(), + &Keypair::new(), + 10, + None, + ) + .await + .unwrap(); // this transaction fails because the repay token transfers aren't signed by the // lending_market_authority PDA. - let mut transaction = Transaction::new_with_payer( - &[ - flash_borrow_reserve_liquidity( - solend_program::id(), - FLASH_LOAN_AMOUNT, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - ), - malicious_flash_repay_reserve_liquidity( - solend_program::id(), - FLASH_LOAN_AMOUNT, - 0, - another_usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.config.fee_receiver, - usdc_test_reserve.liquidity_host_pubkey, - usdc_test_reserve.pubkey, - lending_market.pubkey, - lending_market.authority, - ), - ], - Some(&payer.pubkey()), - ); - - transaction.sign(&[&payer], recent_blockhash); - // panics due to signer privilege escalation - let err = banks_client - .process_transaction(transaction) + let res = test + .process_transaction( + &[ + flash_borrow_reserve_liquidity( + solend_program::id(), + 1000, + usdc_reserve.account.liquidity.supply_pubkey, + user.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + ), + malicious_flash_repay_reserve_liquidity( + solend_program::id(), + 1000, + 0, + another_usdc_reserve.account.liquidity.supply_pubkey, + usdc_reserve.account.liquidity.supply_pubkey, + usdc_reserve.account.config.fee_receiver, + host_fee_receiver.get_account(&usdc_mint::id()).unwrap(), + usdc_reserve.pubkey, + lending_market.pubkey, + Pubkey::find_program_address( + &[lending_market.pubkey.as_ref()], + &solend_program::id(), + ) + .0, + ), + ], + None, // Some(&[&user.keypair]), + ) .await .unwrap_err(); - match err { + + match res { BanksClientError::RpcError(..) => (), BanksClientError::TransactionError(TransactionError::InstructionError( 1, InstructionError::PrivilegeEscalation, )) => (), - _ => panic!("Unexpected error: {:?}", err), + _ => panic!("Unexpected error: {:?}", res), }; } diff --git a/token-lending/program/tests/helpers/flash_loan_proxy.rs b/token-lending/program/tests/helpers/flash_loan_proxy.rs index e8dd1dea799..7058aafaea6 100644 --- a/token-lending/program/tests/helpers/flash_loan_proxy.rs +++ b/token-lending/program/tests/helpers/flash_loan_proxy.rs @@ -18,6 +18,11 @@ use solend_program::{ instruction::flash_borrow_reserve_liquidity, instruction::flash_repay_reserve_liquidity, }; +pub mod proxy_program { + use solana_sdk::declare_id; + declare_id!("proGcH2t31EsUC2bCZUqZDJ74V6LAB1DCjeYDLfrGYa"); +} + pub enum FlashLoanProxyInstruction { ProxyBorrow { liquidity_amount: u64, diff --git a/token-lending/program/tests/helpers/mock_pyth.rs b/token-lending/program/tests/helpers/mock_pyth.rs new file mode 100644 index 00000000000..9a0d680e195 --- /dev/null +++ b/token-lending/program/tests/helpers/mock_pyth.rs @@ -0,0 +1,238 @@ +use pyth_sdk_solana::state::{ + AccountType, PriceAccount, PriceStatus, ProductAccount, MAGIC, PROD_ACCT_SIZE, PROD_ATTR_SIZE, + VERSION_2, +}; +/// mock oracle prices in tests with this program. +use solana_program::{ + account_info::AccountInfo, + clock::Clock, + entrypoint::ProgramResult, + instruction::{AccountMeta, Instruction}, + msg, + pubkey::Pubkey, + sysvar::Sysvar, +}; +use std::cell::RefMut; +use switchboard_v2::{AggregatorAccountData, SwitchboardDecimal}; + +use borsh::{BorshDeserialize, BorshSerialize}; +use spl_token::solana_program::{account_info::next_account_info, program_error::ProgramError}; +use thiserror::Error; + +use super::{load_mut, QUOTE_CURRENCY}; + +pub mod mock_pyth_program { + solana_program::declare_id!("SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f"); +} + +#[derive(BorshSerialize, BorshDeserialize)] +pub enum MockPythInstruction { + /// Accounts: + /// 0: PriceAccount (uninitialized) + /// 1: ProductAccount (uninitialized) + Init, + + /// Accounts: + /// 0: PriceAccount + SetPrice { price: i64, conf: u64, expo: i32 }, + + /// Accounts: + /// 0: AggregatorAccount + InitSwitchboard, + + /// Accounts: + /// 0: AggregatorAccount + SetSwitchboardPrice { price: i64, expo: i32 }, +} + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + Processor::process(program_id, accounts, instruction_data) +} + +pub struct Processor; +impl Processor { + pub fn process( + _program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], + ) -> ProgramResult { + let instruction = MockPythInstruction::try_from_slice(instruction_data)?; + let account_info_iter = &mut accounts.iter().peekable(); + + match instruction { + MockPythInstruction::Init => { + msg!("Mock Pyth: Init"); + + let price_account_info = next_account_info(account_info_iter)?; + let product_account_info = next_account_info(account_info_iter)?; + + // write PriceAccount + let price_account = PriceAccount { + magic: MAGIC, + ver: VERSION_2, + atype: AccountType::Price as u32, + size: 240, // PC_PRICE_T_COMP_OFFSET from pyth_client repo + ..PriceAccount::default() + }; + + let mut data = price_account_info.try_borrow_mut_data()?; + data.copy_from_slice(bytemuck::bytes_of(&price_account)); + + // write ProductAccount + let attr = { + let mut attr: Vec = Vec::new(); + let quote_currency = b"quote_currency"; + attr.push(quote_currency.len() as u8); + attr.extend(quote_currency); + attr.push(QUOTE_CURRENCY.len() as u8); + attr.extend(QUOTE_CURRENCY); + + let mut buf = [0; PROD_ATTR_SIZE]; + buf[0..attr.len()].copy_from_slice(&attr); + + buf + }; + + let product_account = ProductAccount { + magic: MAGIC, + ver: VERSION_2, + atype: AccountType::Product as u32, + size: PROD_ACCT_SIZE as u32, + px_acc: *price_account_info.key, + attr, + }; + + let mut data = product_account_info.try_borrow_mut_data()?; + data.copy_from_slice(bytemuck::bytes_of(&product_account)); + + Ok(()) + } + MockPythInstruction::SetPrice { price, conf, expo } => { + msg!("Mock Pyth: Set price"); + let price_account_info = next_account_info(account_info_iter)?; + let data = &mut price_account_info.try_borrow_mut_data()?; + let mut price_account: &mut PriceAccount = load_mut(data).unwrap(); + + price_account.agg.price = price; + price_account.agg.conf = conf; + price_account.expo = expo; + + price_account.last_slot = Clock::get()?.slot; + price_account.agg.pub_slot = Clock::get()?.slot; + price_account.agg.status = PriceStatus::Trading; + + Ok(()) + } + MockPythInstruction::InitSwitchboard => { + msg!("Mock Pyth: Init Switchboard"); + let switchboard_feed = next_account_info(account_info_iter)?; + let mut data = switchboard_feed.try_borrow_mut_data()?; + + let discriminator = [217, 230, 65, 101, 201, 162, 27, 125]; + data[0..8].copy_from_slice(&discriminator); + Ok(()) + } + MockPythInstruction::SetSwitchboardPrice { price, expo } => { + msg!("Mock Pyth: Set Switchboard price"); + let switchboard_feed = next_account_info(account_info_iter)?; + let data = switchboard_feed.try_borrow_mut_data()?; + + let mut aggregator_account: RefMut = + RefMut::map(data, |data| { + bytemuck::from_bytes_mut( + &mut data[8..std::mem::size_of::() + 8], + ) + }); + + aggregator_account.min_oracle_results = 1; + aggregator_account.latest_confirmed_round.num_success = 1; + aggregator_account.latest_confirmed_round.result = SwitchboardDecimal { + mantissa: price as i128, + scale: expo as u32, + }; + aggregator_account.latest_confirmed_round.round_open_slot = Clock::get()?.slot; + + Ok(()) + } + } + } +} + +#[derive(Error, Debug, Copy, Clone)] +pub enum MockPythError { + /// Invalid instruction + #[error("Invalid Instruction")] + InvalidInstruction, + #[error("The account is not currently owned by the program")] + IncorrectProgramId, + #[error("Failed to deserialize")] + FailedToDeserialize, +} + +impl From for ProgramError { + fn from(e: MockPythError) -> Self { + ProgramError::Custom(e as u32) + } +} + +pub fn init( + program_id: Pubkey, + price_account_pubkey: Pubkey, + product_account_pubkey: Pubkey, +) -> Instruction { + let data = MockPythInstruction::Init.try_to_vec().unwrap(); + Instruction { + program_id, + accounts: vec![ + AccountMeta::new(price_account_pubkey, false), + AccountMeta::new(product_account_pubkey, false), + ], + data, + } +} + +pub fn set_price( + program_id: Pubkey, + price_account_pubkey: Pubkey, + price: i64, + conf: u64, + expo: i32, +) -> Instruction { + let data = MockPythInstruction::SetPrice { price, conf, expo } + .try_to_vec() + .unwrap(); + Instruction { + program_id, + accounts: vec![AccountMeta::new(price_account_pubkey, false)], + data, + } +} + +pub fn set_switchboard_price( + program_id: Pubkey, + switchboard_feed: Pubkey, + price: i64, + expo: i32, +) -> Instruction { + let data = MockPythInstruction::SetSwitchboardPrice { price, expo } + .try_to_vec() + .unwrap(); + Instruction { + program_id, + accounts: vec![AccountMeta::new(switchboard_feed, false)], + data, + } +} + +pub fn init_switchboard(program_id: Pubkey, switchboard_feed: Pubkey) -> Instruction { + let data = MockPythInstruction::InitSwitchboard.try_to_vec().unwrap(); + Instruction { + program_id, + accounts: vec![AccountMeta::new(switchboard_feed, false)], + data, + } +} diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index 17236e0719b..b1eda18ff6e 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -3,43 +3,29 @@ pub mod flash_loan_proxy; pub mod flash_loan_receiver; pub mod genesis; +pub mod mock_pyth; +pub mod solend_program_test; -use assert_matches::*; use bytemuck::{cast_slice_mut, from_bytes_mut, try_cast_slice_mut, Pod, PodCastError}; -use pyth_sdk_solana::state::PriceAccount; + use solana_program::{program_option::COption, program_pack::Pack, pubkey::Pubkey}; use solana_program_test::*; use solana_sdk::{ account::Account, - signature::{read_keypair_file, Keypair, Signer}, - system_instruction::create_account, - transaction::{Transaction, TransactionError}, + signature::{Keypair, Signer}, }; use solend_program::{ instruction::{ - borrow_obligation_liquidity, deposit_reserve_liquidity, - deposit_reserve_liquidity_and_obligation_collateral, init_lending_market, init_obligation, - init_reserve, liquidate_obligation, refresh_obligation, refresh_reserve, + borrow_obligation_liquidity, deposit_reserve_liquidity_and_obligation_collateral, + init_obligation, liquidate_obligation, refresh_obligation, refresh_reserve, withdraw_obligation_collateral_and_redeem_reserve_collateral, }, - math::{Decimal, Rate, TryAdd, TryMul}, - state::{ - InitLendingMarketParams, InitObligationParams, InitReserveParams, LendingMarket, - NewReserveCollateralParams, NewReserveLiquidityParams, Obligation, ObligationCollateral, - ObligationLiquidity, Reserve, ReserveCollateral, ReserveConfig, ReserveFees, - ReserveLiquidity, INITIAL_COLLATERAL_RATIO, PROGRAM_VERSION, - }, -}; -use solend_sdk::switchboard_v2_mainnet; -use spl_token::{ - instruction::approve, - state::{Account as Token, AccountState, Mint}, -}; -use std::{convert::TryInto, str::FromStr}; -use std::{ - mem::size_of, - time::{SystemTime, UNIX_EPOCH}, + state::{Obligation, ReserveConfig, ReserveFees}, }; + +use spl_token::state::Mint; + +use std::mem::size_of; use switchboard_v2::AggregatorAccountData; pub const QUOTE_CURRENCY: [u8; 32] = @@ -58,31 +44,26 @@ pub fn test_reserve_config() -> ReserveConfig { optimal_borrow_rate: 4, max_borrow_rate: 30, fees: ReserveFees { - borrow_fee_wad: 100_000_000_000, - flash_loan_fee_wad: 3_000_000_000_000_000, - host_fee_percentage: 20, + borrow_fee_wad: 0, + flash_loan_fee_wad: 0, + host_fee_percentage: 0, }, - deposit_limit: 100_000_000_000, + deposit_limit: u64::MAX, borrow_limit: u64::MAX, fee_receiver: Keypair::new().pubkey(), - protocol_liquidation_fee: 30, - protocol_take_rate: 10, + protocol_liquidation_fee: 0, + protocol_take_rate: 0, } } -pub const NULL_PUBKEY: &str = "nu11111111111111111111111111111111111111111"; - -pub const SOL_PYTH_PRODUCT: &str = "3Mnn2fX6rQyUsyELYms1sBJyChWofzSNRoqYzvgMVz5E"; -pub const SOL_PYTH_PRICE: &str = "J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix"; -pub const SOL_SWITCHBOARD_FEED: &str = "AdtRGGhmqvom3Jemp5YNrxd9q9unX36BZk1pujkkXijL"; -pub const SOL_SWITCHBOARDV2_FEED: &str = "GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR"; - -pub const SRM_PYTH_PRODUCT: &str = "6MEwdxe4g1NeAF9u6KDG14anJpFsVEa2cvr5H6iriFZ8"; -pub const SRM_PYTH_PRICE: &str = "992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs"; -pub const SRM_SWITCHBOARD_FEED: &str = "BAoygKcKN7wk8yKzLD6sxzUQUqLvhBV1rjMA4UJqfZuH"; -pub const SRM_SWITCHBOARDV2_FEED: &str = "CUgoqwiQ4wCt6Tthkrgx5saAEpLBjPCdHshVa4Pbfcx2"; +pub mod usdc_mint { + solana_program::declare_id!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); +} -pub const USDC_MINT: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; +pub mod wsol_mint { + // fake mint, not the real wsol bc i can't mint wsol programmatically + solana_program::declare_id!("So1m5eppzgokXLBt9Cg8KCMPWhHfTzVaGh26Y415MRG"); +} trait AddPacked { fn add_packable_account( @@ -108,1382 +89,29 @@ impl AddPacked for ProgramTest { } } -pub fn add_lending_market(test: &mut ProgramTest) -> TestLendingMarket { - let lending_market_pubkey = Pubkey::new_unique(); - let (lending_market_authority, bump_seed) = - Pubkey::find_program_address(&[lending_market_pubkey.as_ref()], &solend_program::id()); - - let lending_market_owner = - read_keypair_file("tests/fixtures/lending_market_owner.json").unwrap(); - let oracle_program_id = read_keypair_file("tests/fixtures/oracle_program_id.json") - .unwrap() - .pubkey(); - - test.add_packable_account( - lending_market_pubkey, - u32::MAX as u64, - &LendingMarket::new(InitLendingMarketParams { - bump_seed, - owner: lending_market_owner.pubkey(), - quote_currency: QUOTE_CURRENCY, - token_program_id: spl_token::id(), - oracle_program_id, - switchboard_oracle_program_id: oracle_program_id, - }), - &solend_program::id(), - ); - - TestLendingMarket { - pubkey: lending_market_pubkey, - owner: lending_market_owner, - authority: lending_market_authority, - quote_currency: QUOTE_CURRENCY, - oracle_program_id, - switchboard_oracle_program_id: oracle_program_id, - } -} - -#[derive(Default)] -pub struct AddObligationArgs<'a> { - pub deposits: &'a [(&'a TestReserve, u64)], - pub borrows: &'a [(&'a TestReserve, u64)], - pub mark_fresh: bool, - pub slots_elapsed: u64, -} - -pub fn add_obligation( - test: &mut ProgramTest, - lending_market: &TestLendingMarket, - user_accounts_owner: &Keypair, - args: AddObligationArgs, -) -> TestObligation { - let AddObligationArgs { - deposits, - borrows, - mark_fresh, - slots_elapsed, - } = args; - - let obligation_keypair = Keypair::new(); - let obligation_pubkey = obligation_keypair.pubkey(); - - let (obligation_deposits, test_deposits) = deposits - .iter() - .map(|(deposit_reserve, collateral_amount)| { - let mut collateral = ObligationCollateral::new(deposit_reserve.pubkey); - collateral.deposited_amount = *collateral_amount; - - ( - collateral, - TestObligationCollateral { - obligation_pubkey, - deposit_reserve: deposit_reserve.pubkey, - deposited_amount: *collateral_amount, - }, - ) - }) - .unzip(); - - let (obligation_borrows, test_borrows) = borrows - .iter() - .map(|(borrow_reserve, liquidity_amount)| { - let borrowed_amount_wads = Decimal::from(*liquidity_amount); - - let mut liquidity = ObligationLiquidity::new(borrow_reserve.pubkey, Decimal::one()); - liquidity.borrowed_amount_wads = borrowed_amount_wads; - - ( - liquidity, - TestObligationLiquidity { - obligation_pubkey, - borrow_reserve: borrow_reserve.pubkey, - borrowed_amount_wads, - }, - ) - }) - .unzip(); - - let current_slot = slots_elapsed + 1; - - let mut obligation = Obligation::new(InitObligationParams { - // intentionally wrapped to simulate elapsed slots - current_slot, - lending_market: lending_market.pubkey, - owner: user_accounts_owner.pubkey(), - deposits: obligation_deposits, - borrows: obligation_borrows, - }); - - if mark_fresh { - obligation.last_update.update_slot(current_slot); - } - - test.add_packable_account( - obligation_pubkey, - u32::MAX as u64, - &obligation, - &solend_program::id(), - ); - - TestObligation { - pubkey: obligation_pubkey, - keypair: obligation_keypair, - lending_market: lending_market.pubkey, - owner: user_accounts_owner.pubkey(), - deposits: test_deposits, - borrows: test_borrows, - } -} - -#[derive(Default)] -pub struct AddReserveArgs { - pub name: String, - pub config: ReserveConfig, - pub liquidity_amount: u64, - pub liquidity_mint_pubkey: Pubkey, - pub liquidity_mint_decimals: u8, - pub user_liquidity_amount: u64, - pub borrow_amount: u64, - pub initial_borrow_rate: u8, - pub collateral_amount: u64, - pub mark_fresh: bool, - pub slots_elapsed: u64, -} - -pub fn add_reserve( - test: &mut ProgramTest, - lending_market: &TestLendingMarket, - oracle: &TestOracle, - user_accounts_owner: &Keypair, - args: AddReserveArgs, -) -> TestReserve { - let AddReserveArgs { - name, - config, - liquidity_amount, - liquidity_mint_pubkey, - liquidity_mint_decimals, - user_liquidity_amount, - borrow_amount, - initial_borrow_rate, - collateral_amount, - mark_fresh, - slots_elapsed, - } = args; - - let is_native = if liquidity_mint_pubkey == spl_token::native_mint::id() { - COption::Some(2039280u64) - } else { - COption::None - }; - - let current_slot = slots_elapsed + 1; - - let collateral_mint_pubkey = Pubkey::new_unique(); - test.add_packable_account( - collateral_mint_pubkey, - u32::MAX as u64, - &Mint { - is_initialized: true, - decimals: liquidity_mint_decimals, - mint_authority: COption::Some(lending_market.authority), - supply: collateral_amount, - ..Mint::default() - }, - &spl_token::id(), - ); - - let collateral_supply_pubkey = Pubkey::new_unique(); - test.add_packable_account( - collateral_supply_pubkey, - u32::MAX as u64, - &Token { - mint: collateral_mint_pubkey, - owner: lending_market.authority, - amount: collateral_amount, - state: AccountState::Initialized, - ..Token::default() - }, - &spl_token::id(), - ); - - let amount = if let COption::Some(rent_reserve) = is_native { - liquidity_amount + rent_reserve - } else { - u32::MAX as u64 - }; - - let liquidity_supply_pubkey = Pubkey::new_unique(); - test.add_packable_account( - liquidity_supply_pubkey, - amount, - &Token { - mint: liquidity_mint_pubkey, - owner: lending_market.authority, - amount: liquidity_amount, - state: AccountState::Initialized, - is_native, - ..Token::default() - }, - &spl_token::id(), - ); - - let amount = if let COption::Some(rent_reserve) = is_native { - rent_reserve - } else { - u32::MAX as u64 - }; - - test.add_packable_account( - config.fee_receiver, - amount, - &Token { - mint: liquidity_mint_pubkey, - owner: lending_market.owner.pubkey(), - amount: 0, - is_native, - state: AccountState::Initialized, - ..Token::default() - }, - &spl_token::id(), - ); - - let liquidity_host_pubkey = Pubkey::new_unique(); - test.add_packable_account( - liquidity_host_pubkey, - u32::MAX as u64, - &Token { - mint: liquidity_mint_pubkey, - owner: user_accounts_owner.pubkey(), - amount: 0, - state: AccountState::Initialized, - ..Token::default() - }, - &spl_token::id(), - ); - - let reserve_keypair = Keypair::new(); - let reserve_pubkey = reserve_keypair.pubkey(); - let mut reserve = Reserve::new(InitReserveParams { - current_slot, - lending_market: lending_market.pubkey, - liquidity: ReserveLiquidity::new(NewReserveLiquidityParams { - mint_pubkey: liquidity_mint_pubkey, - mint_decimals: liquidity_mint_decimals, - supply_pubkey: liquidity_supply_pubkey, - pyth_oracle_pubkey: oracle.pyth_price_pubkey, - switchboard_oracle_pubkey: oracle.switchboard_feed_pubkey, - market_price: oracle.price, - }), - collateral: ReserveCollateral::new(NewReserveCollateralParams { - mint_pubkey: collateral_mint_pubkey, - supply_pubkey: collateral_supply_pubkey, - }), - config, - }); - reserve.deposit_liquidity(liquidity_amount).unwrap(); - reserve.liquidity.borrow(borrow_amount.into()).unwrap(); - let borrow_rate_multiplier = Rate::one() - .try_add(Rate::from_percent(initial_borrow_rate)) - .unwrap(); - reserve.liquidity.cumulative_borrow_rate_wads = - Decimal::one().try_mul(borrow_rate_multiplier).unwrap(); - - if mark_fresh { - reserve.last_update.update_slot(current_slot); - } - - test.add_packable_account( - reserve_pubkey, - u32::MAX as u64, - &reserve, - &solend_program::id(), - ); - - let amount = if let COption::Some(rent_reserve) = is_native { - user_liquidity_amount + rent_reserve - } else { - u32::MAX as u64 - }; - - let user_liquidity_pubkey = Pubkey::new_unique(); - test.add_packable_account( - user_liquidity_pubkey, - amount, - &Token { - mint: liquidity_mint_pubkey, - owner: user_accounts_owner.pubkey(), - amount: user_liquidity_amount, - state: AccountState::Initialized, - is_native, - ..Token::default() - }, - &spl_token::id(), - ); - let user_collateral_pubkey = Pubkey::new_unique(); - test.add_packable_account( - user_collateral_pubkey, - u32::MAX as u64, - &Token { - mint: collateral_mint_pubkey, - owner: user_accounts_owner.pubkey(), - amount: liquidity_amount * INITIAL_COLLATERAL_RATIO, - state: AccountState::Initialized, - ..Token::default() - }, - &spl_token::id(), - ); - - TestReserve { - name, - pubkey: reserve_pubkey, - lending_market_pubkey: lending_market.pubkey, - config, - liquidity_mint_pubkey, - liquidity_mint_decimals, - liquidity_supply_pubkey, - liquidity_host_pubkey, - liquidity_pyth_oracle_pubkey: oracle.pyth_price_pubkey, - liquidity_switchboard_oracle_pubkey: oracle.switchboard_feed_pubkey, - collateral_mint_pubkey, - collateral_supply_pubkey, - user_liquidity_pubkey, - user_collateral_pubkey, - market_price: oracle.price, - } -} - -pub fn add_account_for_program( - test: &mut ProgramTest, - program_derived_account: &Pubkey, - amount: u64, - mint_pubkey: &Pubkey, -) -> Pubkey { - let program_owned_token_account = Keypair::new(); - test.add_packable_account( - program_owned_token_account.pubkey(), - u32::MAX as u64, - &Token { - mint: *mint_pubkey, - owner: *program_derived_account, - amount, - state: AccountState::Initialized, - is_native: COption::None, - ..Token::default() - }, - &spl_token::id(), - ); - program_owned_token_account.pubkey() -} - -pub struct TestLendingMarket { - pub pubkey: Pubkey, - pub owner: Keypair, - pub authority: Pubkey, - pub quote_currency: [u8; 32], - pub oracle_program_id: Pubkey, - pub switchboard_oracle_program_id: Pubkey, -} - -pub struct BorrowArgs<'a> { - pub liquidity_amount: u64, - pub obligation: &'a TestObligation, - pub borrow_reserve: &'a TestReserve, - pub user_accounts_owner: &'a Keypair, -} - -pub struct LiquidateArgs<'a> { - pub liquidity_amount: u64, - pub obligation: &'a TestObligation, - pub repay_reserve: &'a TestReserve, - pub withdraw_reserve: &'a TestReserve, - pub user_accounts_owner: &'a Keypair, -} - -impl TestLendingMarket { - pub async fn init(banks_client: &mut BanksClient, payer: &Keypair) -> Self { - let lending_market_owner = - read_keypair_file("tests/fixtures/lending_market_owner.json").unwrap(); - let oracle_program_id = read_keypair_file("tests/fixtures/oracle_program_id.json") - .unwrap() - .pubkey(); - - let lending_market_keypair = Keypair::new(); - let lending_market_pubkey = lending_market_keypair.pubkey(); - let (lending_market_authority, _bump_seed) = Pubkey::find_program_address( - &[&lending_market_pubkey.to_bytes()[..32]], - &solend_program::id(), - ); - - let rent = banks_client.get_rent().await.unwrap(); - let mut transaction = Transaction::new_with_payer( - &[ - create_account( - &payer.pubkey(), - &lending_market_pubkey, - rent.minimum_balance(LendingMarket::LEN), - LendingMarket::LEN as u64, - &solend_program::id(), - ), - init_lending_market( - solend_program::id(), - lending_market_owner.pubkey(), - QUOTE_CURRENCY, - lending_market_pubkey, - oracle_program_id, - oracle_program_id, - ), - ], - Some(&payer.pubkey()), - ); - - let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - transaction.sign(&[payer, &lending_market_keypair], recent_blockhash); - assert_matches!(banks_client.process_transaction(transaction).await, Ok(())); - - TestLendingMarket { - owner: lending_market_owner, - pubkey: lending_market_pubkey, - authority: lending_market_authority, - quote_currency: QUOTE_CURRENCY, - oracle_program_id, - switchboard_oracle_program_id: oracle_program_id, - } - } - - pub async fn refresh_reserve( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - reserve: &TestReserve, - ) { - let mut transaction = Transaction::new_with_payer( - &[refresh_reserve( - solend_program::id(), - reserve.pubkey, - reserve.liquidity_pyth_oracle_pubkey, - reserve.liquidity_switchboard_oracle_pubkey, - )], - Some(&payer.pubkey()), - ); - - let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - transaction.sign(&[payer], recent_blockhash); - - assert_matches!(banks_client.process_transaction(transaction).await, Ok(())); - } - - pub async fn deposit( - &self, - banks_client: &mut BanksClient, - user_accounts_owner: &Keypair, - payer: &Keypair, - reserve: &TestReserve, - liquidity_amount: u64, - ) { - let user_transfer_authority = Keypair::new(); - let mut transaction = Transaction::new_with_payer( - &[ - approve( - &spl_token::id(), - &reserve.user_liquidity_pubkey, - &user_transfer_authority.pubkey(), - &user_accounts_owner.pubkey(), - &[], - liquidity_amount, - ) - .unwrap(), - deposit_reserve_liquidity( - solend_program::id(), - liquidity_amount, - reserve.user_liquidity_pubkey, - reserve.user_collateral_pubkey, - reserve.pubkey, - reserve.liquidity_supply_pubkey, - reserve.collateral_mint_pubkey, - self.pubkey, - user_transfer_authority.pubkey(), - ), - ], - Some(&payer.pubkey()), - ); - - let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - transaction.sign( - &[payer, user_accounts_owner, &user_transfer_authority], - recent_blockhash, - ); - - assert_matches!(banks_client.process_transaction(transaction).await, Ok(())); - } - - pub async fn deposit_obligation_and_collateral( - &self, - banks_client: &mut BanksClient, - user_accounts_owner: &Keypair, - payer: &Keypair, - reserve: &TestReserve, - obligation: &TestObligation, - liquidity_amount: u64, - ) { - let user_transfer_authority = Keypair::new(); - let mut transaction = Transaction::new_with_payer( - &[ - approve( - &spl_token::id(), - &reserve.user_liquidity_pubkey, - &user_transfer_authority.pubkey(), - &user_accounts_owner.pubkey(), - &[], - liquidity_amount, - ) - .unwrap(), - approve( - &spl_token::id(), - &reserve.user_collateral_pubkey, - &user_transfer_authority.pubkey(), - &user_accounts_owner.pubkey(), - &[], - liquidity_amount, - ) - .unwrap(), - deposit_reserve_liquidity_and_obligation_collateral( - solend_program::id(), - liquidity_amount, - reserve.user_liquidity_pubkey, - reserve.user_collateral_pubkey, - reserve.pubkey, - reserve.liquidity_supply_pubkey, - reserve.collateral_mint_pubkey, - self.pubkey, - reserve.collateral_supply_pubkey, - obligation.pubkey, - obligation.owner, - reserve.liquidity_pyth_oracle_pubkey, - reserve.liquidity_switchboard_oracle_pubkey, - user_transfer_authority.pubkey(), - ), - ], - Some(&payer.pubkey()), - ); - - let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - transaction.sign( - &[payer, user_accounts_owner, &user_transfer_authority], - recent_blockhash, - ); - - assert_matches!(banks_client.process_transaction(transaction).await, Ok(())); - } - - pub async fn withdraw_and_redeem_collateral( - &self, - banks_client: &mut BanksClient, - user_accounts_owner: &Keypair, - payer: &Keypair, - reserve: &TestReserve, - obligation: &TestObligation, - collateral_amount: u64, - ) { - let user_transfer_authority = Keypair::new(); - let mut transaction = Transaction::new_with_payer( - &[ - approve( - &spl_token::id(), - &reserve.user_collateral_pubkey, - &user_transfer_authority.pubkey(), - &user_accounts_owner.pubkey(), - &[], - collateral_amount, - ) - .unwrap(), - refresh_obligation( - solend_program::id(), - obligation.pubkey, - vec![reserve.pubkey], - ), - withdraw_obligation_collateral_and_redeem_reserve_collateral( - solend_program::id(), - collateral_amount, - reserve.collateral_supply_pubkey, - reserve.user_collateral_pubkey, - reserve.pubkey, - obligation.pubkey, - self.pubkey, - reserve.user_liquidity_pubkey, - reserve.collateral_mint_pubkey, - reserve.liquidity_supply_pubkey, - obligation.owner, - user_transfer_authority.pubkey(), - ), - ], - Some(&payer.pubkey()), - ); - let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - transaction.sign( - &[payer, user_accounts_owner, &user_transfer_authority], - recent_blockhash, - ); - - assert_matches!(banks_client.process_transaction(transaction).await, Ok(())); - } - pub async fn liquidate( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - args: LiquidateArgs<'_>, - ) { - let LiquidateArgs { - liquidity_amount, - obligation, - repay_reserve, - withdraw_reserve, - user_accounts_owner, - } = args; - - let user_transfer_authority = Keypair::new(); - let mut transaction = Transaction::new_with_payer( - &[ - approve( - &spl_token::id(), - &repay_reserve.user_liquidity_pubkey, - &user_transfer_authority.pubkey(), - &user_accounts_owner.pubkey(), - &[], - liquidity_amount, - ) - .unwrap(), - liquidate_obligation( - solend_program::id(), - liquidity_amount, - repay_reserve.user_liquidity_pubkey, - withdraw_reserve.user_collateral_pubkey, - repay_reserve.pubkey, - repay_reserve.liquidity_supply_pubkey, - withdraw_reserve.pubkey, - withdraw_reserve.collateral_supply_pubkey, - obligation.pubkey, - self.pubkey, - user_transfer_authority.pubkey(), - ), - ], - Some(&payer.pubkey()), - ); - - let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - transaction.sign( - &[payer, user_accounts_owner, &user_transfer_authority], - recent_blockhash, - ); - assert!(banks_client.process_transaction(transaction).await.is_ok()); - } - - pub async fn borrow( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - args: BorrowArgs<'_>, - ) { - let BorrowArgs { - liquidity_amount, - obligation, - borrow_reserve, - user_accounts_owner, - } = args; - - let mut transaction = Transaction::new_with_payer( - &[borrow_obligation_liquidity( - solend_program::id(), - liquidity_amount, - borrow_reserve.liquidity_supply_pubkey, - borrow_reserve.user_liquidity_pubkey, - borrow_reserve.pubkey, - borrow_reserve.config.fee_receiver, - obligation.pubkey, - self.pubkey, - obligation.owner, - Some(borrow_reserve.liquidity_host_pubkey), - )], - Some(&payer.pubkey()), - ); - - let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - transaction.sign(&vec![payer, user_accounts_owner], recent_blockhash); - - assert_matches!(banks_client.process_transaction(transaction).await, Ok(())); - } - - pub async fn get_state(&self, banks_client: &mut BanksClient) -> LendingMarket { - let lending_market_account: Account = banks_client - .get_account(self.pubkey) - .await - .unwrap() - .unwrap(); - LendingMarket::unpack(&lending_market_account.data[..]).unwrap() - } - - pub async fn validate_state(&self, banks_client: &mut BanksClient) { - let lending_market = self.get_state(banks_client).await; - assert_eq!(lending_market.version, PROGRAM_VERSION); - assert_eq!(lending_market.owner, self.owner.pubkey()); - assert_eq!(lending_market.quote_currency, self.quote_currency); - } -} - -#[derive(Debug)] -pub struct TestReserve { - pub name: String, - pub pubkey: Pubkey, - pub lending_market_pubkey: Pubkey, - pub config: ReserveConfig, - pub liquidity_mint_pubkey: Pubkey, - pub liquidity_mint_decimals: u8, - pub liquidity_supply_pubkey: Pubkey, - pub liquidity_host_pubkey: Pubkey, - pub liquidity_pyth_oracle_pubkey: Pubkey, - pub liquidity_switchboard_oracle_pubkey: Pubkey, - pub collateral_mint_pubkey: Pubkey, - pub collateral_supply_pubkey: Pubkey, - pub user_liquidity_pubkey: Pubkey, - pub user_collateral_pubkey: Pubkey, - pub market_price: Decimal, -} - -impl TestReserve { - #[allow(clippy::too_many_arguments)] - pub async fn init( - name: String, - banks_client: &mut BanksClient, - lending_market: &TestLendingMarket, - oracle: &TestOracle, - liquidity_amount: u64, - config: ReserveConfig, - liquidity_mint_pubkey: Pubkey, - user_liquidity_pubkey: Pubkey, - liquidity_fee_receiver_keypair: &Keypair, - payer: &Keypair, - user_accounts_owner: &Keypair, - ) -> Result { - let reserve_keypair = Keypair::new(); - let reserve_pubkey = reserve_keypair.pubkey(); - let collateral_mint_keypair = Keypair::new(); - let collateral_supply_keypair = Keypair::new(); - let liquidity_supply_keypair = Keypair::new(); - let liquidity_host_keypair = Keypair::new(); - let user_collateral_token_keypair = Keypair::new(); - let user_transfer_authority_keypair = Keypair::new(); - - let liquidity_mint_account = banks_client - .get_account(liquidity_mint_pubkey) - .await - .unwrap() - .unwrap(); - let liquidity_mint = Mint::unpack(&liquidity_mint_account.data[..]).unwrap(); - - let rent = banks_client.get_rent().await.unwrap(); - let mut transaction = Transaction::new_with_payer( - &[ - approve( - &spl_token::id(), - &user_liquidity_pubkey, - &user_transfer_authority_keypair.pubkey(), - &user_accounts_owner.pubkey(), - &[], - liquidity_amount, - ) - .unwrap(), - create_account( - &payer.pubkey(), - &collateral_mint_keypair.pubkey(), - rent.minimum_balance(Mint::LEN), - Mint::LEN as u64, - &spl_token::id(), - ), - create_account( - &payer.pubkey(), - &collateral_supply_keypair.pubkey(), - rent.minimum_balance(Token::LEN), - Token::LEN as u64, - &spl_token::id(), - ), - create_account( - &payer.pubkey(), - &liquidity_supply_keypair.pubkey(), - rent.minimum_balance(Token::LEN), - Token::LEN as u64, - &spl_token::id(), - ), - create_account( - &payer.pubkey(), - &liquidity_fee_receiver_keypair.pubkey(), - rent.minimum_balance(Token::LEN), - Token::LEN as u64, - &spl_token::id(), - ), - create_account( - &payer.pubkey(), - &liquidity_host_keypair.pubkey(), - rent.minimum_balance(Token::LEN), - Token::LEN as u64, - &spl_token::id(), - ), - create_account( - &payer.pubkey(), - &user_collateral_token_keypair.pubkey(), - rent.minimum_balance(Token::LEN), - Token::LEN as u64, - &spl_token::id(), - ), - create_account( - &payer.pubkey(), - &reserve_pubkey, - rent.minimum_balance(Reserve::LEN), - Reserve::LEN as u64, - &solend_program::id(), - ), - init_reserve( - solend_program::id(), - liquidity_amount, - config, - user_liquidity_pubkey, - user_collateral_token_keypair.pubkey(), - reserve_pubkey, - liquidity_mint_pubkey, - liquidity_supply_keypair.pubkey(), - collateral_mint_keypair.pubkey(), - collateral_supply_keypair.pubkey(), - oracle.pyth_product_pubkey, - oracle.pyth_price_pubkey, - oracle.switchboard_feed_pubkey, - lending_market.pubkey, - lending_market.owner.pubkey(), - user_transfer_authority_keypair.pubkey(), - ), - ], - Some(&payer.pubkey()), - ); - let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - transaction.sign( - &vec![ - payer, - user_accounts_owner, - &reserve_keypair, - &lending_market.owner, - &collateral_mint_keypair, - &collateral_supply_keypair, - &liquidity_supply_keypair, - liquidity_fee_receiver_keypair, - &liquidity_host_keypair, - &user_collateral_token_keypair, - &user_transfer_authority_keypair, - ], - recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map(|_| Self { - name, - pubkey: reserve_pubkey, - lending_market_pubkey: lending_market.pubkey, - config, - liquidity_mint_pubkey, - liquidity_mint_decimals: liquidity_mint.decimals, - liquidity_supply_pubkey: liquidity_supply_keypair.pubkey(), - liquidity_host_pubkey: liquidity_host_keypair.pubkey(), - liquidity_pyth_oracle_pubkey: oracle.pyth_price_pubkey, - liquidity_switchboard_oracle_pubkey: oracle.switchboard_feed_pubkey, - collateral_mint_pubkey: collateral_mint_keypair.pubkey(), - collateral_supply_pubkey: collateral_supply_keypair.pubkey(), - user_liquidity_pubkey, - user_collateral_pubkey: user_collateral_token_keypair.pubkey(), - market_price: oracle.price, - }) - .map_err(|e| e.unwrap()) - } - - pub async fn get_state(&self, banks_client: &mut BanksClient) -> Reserve { - let reserve_account: Account = banks_client - .get_account(self.pubkey) - .await - .unwrap() - .unwrap(); - Reserve::unpack(&reserve_account.data[..]).unwrap() - } - - pub async fn validate_state(&self, banks_client: &mut BanksClient) { - let reserve = self.get_state(banks_client).await; - assert!(reserve.last_update.slot > 0); - assert_eq!(PROGRAM_VERSION, reserve.version); - assert_eq!(self.lending_market_pubkey, reserve.lending_market); - assert_eq!(self.liquidity_mint_pubkey, reserve.liquidity.mint_pubkey); - assert_eq!( - self.liquidity_supply_pubkey, - reserve.liquidity.supply_pubkey - ); - assert_eq!(self.collateral_mint_pubkey, reserve.collateral.mint_pubkey); - assert_eq!( - self.collateral_supply_pubkey, - reserve.collateral.supply_pubkey - ); - assert_eq!(self.config, reserve.config); - - assert_eq!( - self.liquidity_pyth_oracle_pubkey, - reserve.liquidity.pyth_oracle_pubkey - ); - assert_eq!( - self.liquidity_switchboard_oracle_pubkey, - reserve.liquidity.switchboard_oracle_pubkey - ); - assert_eq!( - reserve.liquidity.cumulative_borrow_rate_wads, - Decimal::one() - ); - assert_eq!(reserve.liquidity.borrowed_amount_wads, Decimal::zero()); - assert!(reserve.liquidity.available_amount > 0); - assert!(reserve.collateral.mint_total_supply > 0); - } -} - -#[derive(Debug)] -pub struct TestObligation { - pub pubkey: Pubkey, - pub keypair: Keypair, - pub lending_market: Pubkey, - pub owner: Pubkey, - pub deposits: Vec, - pub borrows: Vec, -} - -impl TestObligation { - #[allow(clippy::too_many_arguments)] - pub async fn init( - banks_client: &mut BanksClient, - lending_market: &TestLendingMarket, - user_accounts_owner: &Keypair, - payer: &Keypair, - ) -> Result { - let obligation_keypair = Keypair::new(); - let obligation = TestObligation { - pubkey: obligation_keypair.pubkey(), - keypair: obligation_keypair, - lending_market: lending_market.pubkey, - owner: user_accounts_owner.pubkey(), - deposits: vec![], - borrows: vec![], - }; - - let rent = banks_client.get_rent().await.unwrap(); - let mut transaction = Transaction::new_with_payer( - &[ - create_account( - &payer.pubkey(), - &obligation.keypair.pubkey(), - rent.minimum_balance(Obligation::LEN), - Obligation::LEN as u64, - &solend_program::id(), - ), - init_obligation( - solend_program::id(), - obligation.pubkey, - lending_market.pubkey, - user_accounts_owner.pubkey(), - ), - ], - Some(&payer.pubkey()), - ); - - let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - transaction.sign( - &vec![payer, &obligation.keypair, user_accounts_owner], - recent_blockhash, - ); - - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.unwrap())?; - - Ok(obligation) - } - - pub async fn get_state(&self, banks_client: &mut BanksClient) -> Obligation { - let obligation_account: Account = banks_client - .get_account(self.pubkey) - .await - .unwrap() - .unwrap(); - Obligation::unpack(&obligation_account.data[..]).unwrap() - } - - pub async fn validate_state(&self, banks_client: &mut BanksClient) { - let obligation = self.get_state(banks_client).await; - assert_eq!(obligation.version, PROGRAM_VERSION); - assert_eq!(obligation.lending_market, self.lending_market); - assert_eq!(obligation.owner, self.owner); - } -} - -#[derive(Debug)] -pub struct TestObligationCollateral { - pub obligation_pubkey: Pubkey, - pub deposit_reserve: Pubkey, - pub deposited_amount: u64, -} - -impl TestObligationCollateral { - pub async fn get_state(&self, banks_client: &mut BanksClient) -> Obligation { - let obligation_account: Account = banks_client - .get_account(self.obligation_pubkey) - .await - .unwrap() - .unwrap(); - Obligation::unpack(&obligation_account.data[..]).unwrap() - } - - pub async fn validate_state(&self, banks_client: &mut BanksClient) { - let obligation = self.get_state(banks_client).await; - assert_eq!(obligation.version, PROGRAM_VERSION); - - let (collateral, _) = obligation - .find_collateral_in_deposits(self.deposit_reserve) - .unwrap(); - assert_eq!(collateral.deposited_amount, self.deposited_amount); - } -} - -#[derive(Debug)] -pub struct TestObligationLiquidity { - pub obligation_pubkey: Pubkey, - pub borrow_reserve: Pubkey, - pub borrowed_amount_wads: Decimal, -} - -impl TestObligationLiquidity { - pub async fn get_state(&self, banks_client: &mut BanksClient) -> Obligation { - let obligation_account: Account = banks_client - .get_account(self.obligation_pubkey) - .await - .unwrap() - .unwrap(); - Obligation::unpack(&obligation_account.data[..]).unwrap() - } - - pub async fn validate_state(&self, banks_client: &mut BanksClient) { - let obligation = self.get_state(banks_client).await; - assert_eq!(obligation.version, PROGRAM_VERSION); - let (liquidity, _) = obligation - .find_liquidity_in_borrows(self.borrow_reserve) - .unwrap(); - assert!(liquidity.cumulative_borrow_rate_wads >= Decimal::one()); - assert!(liquidity.borrowed_amount_wads >= self.borrowed_amount_wads); - } -} - pub struct TestMint { pub pubkey: Pubkey, pub authority: Keypair, pub decimals: u8, } -pub fn add_usdc_mint(test: &mut ProgramTest) -> TestMint { - let authority = Keypair::new(); - let pubkey = Pubkey::from_str(USDC_MINT).unwrap(); - let decimals = 6; +pub fn load_mut(data: &mut [u8]) -> Result<&mut T, PodCastError> { + let size = size_of::(); + Ok(from_bytes_mut(cast_slice_mut::( + try_cast_slice_mut(&mut data[0..size])?, + ))) +} + +fn add_mint(test: &mut ProgramTest, mint: Pubkey, decimals: u8, authority: Pubkey) { test.add_packable_account( - pubkey, + mint, u32::MAX as u64, &Mint { is_initialized: true, - mint_authority: COption::Some(authority.pubkey()), + mint_authority: COption::Some(authority), decimals, ..Mint::default() }, &spl_token::id(), ); - TestMint { - pubkey, - authority, - decimals, - } -} - -pub struct TestOracle { - pub pyth_product_pubkey: Pubkey, - pub pyth_price_pubkey: Pubkey, - pub switchboard_feed_pubkey: Pubkey, - pub price: Decimal, -} - -pub fn add_sol_oracle(test: &mut ProgramTest) -> TestOracle { - add_oracle( - test, - Pubkey::from_str(SOL_PYTH_PRODUCT).unwrap(), - Pubkey::from_str(SOL_PYTH_PRICE).unwrap(), - Pubkey::from_str(SOL_SWITCHBOARD_FEED).unwrap(), - // Set SOL price to $20 - Decimal::from(20u64), - 0, - ) -} - -pub fn add_sol_oracle_switchboardv2(test: &mut ProgramTest) -> TestOracle { - add_oracle( - test, - Pubkey::from_str(NULL_PUBKEY).unwrap(), - Pubkey::from_str(NULL_PUBKEY).unwrap(), - Pubkey::from_str(SOL_SWITCHBOARDV2_FEED).unwrap(), - // Set SOL price to $20 - Decimal::from(20u64), - 0, - ) -} - -pub fn add_usdc_oracle(test: &mut ProgramTest) -> TestOracle { - add_oracle( - test, - // Mock with SRM since Pyth doesn't have USDC yet - Pubkey::from_str(SRM_PYTH_PRODUCT).unwrap(), - Pubkey::from_str(SRM_PYTH_PRICE).unwrap(), - Pubkey::from_str(SRM_SWITCHBOARD_FEED).unwrap(), - // Set USDC price to $1 - Decimal::from(1u64), - 0, - ) -} - -pub fn add_usdc_oracle_switchboardv2(test: &mut ProgramTest) -> TestOracle { - add_oracle( - test, - // Mock with SRM since Pyth doesn't have USDC yet - Pubkey::from_str(NULL_PUBKEY).unwrap(), - Pubkey::from_str(NULL_PUBKEY).unwrap(), - Pubkey::from_str(SRM_SWITCHBOARDV2_FEED).unwrap(), - // Set USDC price to $1 - Decimal::from(1u64), - 0, - ) -} - -pub fn load_mut(data: &mut [u8]) -> Result<&mut T, PodCastError> { - let size = size_of::(); - Ok(from_bytes_mut(cast_slice_mut::( - try_cast_slice_mut(&mut data[0..size])?, - ))) -} - -pub fn add_oracle( - test: &mut ProgramTest, - pyth_product_pubkey: Pubkey, - pyth_price_pubkey: Pubkey, - switchboard_feed_pubkey: Pubkey, - price: Decimal, - valid_slot: u64, -) -> TestOracle { - let oracle_program_id = read_keypair_file("tests/fixtures/oracle_program_id.json").unwrap(); - - if pyth_price_pubkey.to_string() != NULL_PUBKEY { - // Add Pyth product account - test.add_account_with_file_data( - pyth_product_pubkey, - u32::MAX as u64, - oracle_program_id.pubkey(), - &format!("{}.bin", pyth_product_pubkey), - ); - } - if pyth_price_pubkey.to_string() != NULL_PUBKEY { - // Add Pyth price account after setting the price - let filename = &format!("{}.bin", pyth_price_pubkey); - let mut pyth_price_data = read_file(find_file(filename).unwrap_or_else(|| { - panic!("Unable to locate {}", filename); - })); - - let mut pyth_price = load_mut::(pyth_price_data.as_mut_slice()).unwrap(); - - let decimals = 10u64 - .checked_pow(pyth_price.expo.checked_abs().unwrap().try_into().unwrap()) - .unwrap(); - - pyth_price.valid_slot = valid_slot; - pyth_price.agg.price = price - .try_round_u64() - .unwrap() - .checked_mul(decimals) - .unwrap() - .try_into() - .unwrap(); - - pyth_price.agg.pub_slot = valid_slot; - pyth_price.timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() as i64; - - test.add_account( - pyth_price_pubkey, - Account { - lamports: u32::MAX as u64, - data: pyth_price_data, - owner: oracle_program_id.pubkey(), - executable: false, - rent_epoch: 0, - }, - ); - } - - // Add Switchboard price feed account after setting the price - let filename2 = &format!("{}.bin", switchboard_feed_pubkey); - // mut and set data here later - let mut switchboard_feed_data = read_file(find_file(filename2).unwrap_or_else(|| { - panic!("Unable tod locate {}", filename2); - })); - - let is_v2 = switchboard_feed_pubkey.to_string() == SOL_SWITCHBOARDV2_FEED - || switchboard_feed_pubkey.to_string() == SRM_SWITCHBOARDV2_FEED; - if is_v2 { - // let mut_switchboard_feed_data = &mut switchboard_feed_data[8..]; - let agg_state = - bytemuck::from_bytes_mut::(&mut switchboard_feed_data[8..]); - agg_state.latest_confirmed_round.round_open_slot = 0; - test.add_account( - switchboard_feed_pubkey, - Account { - lamports: u32::MAX as u64, - data: switchboard_feed_data, - owner: switchboard_v2_mainnet::id(), - executable: false, - rent_epoch: 0, - }, - ); - } else { - test.add_account( - switchboard_feed_pubkey, - Account { - lamports: u32::MAX as u64, - data: switchboard_feed_data, - owner: oracle_program_id.pubkey(), - executable: false, - rent_epoch: 0, - }, - ); - } - - TestOracle { - pyth_product_pubkey, - pyth_price_pubkey, - switchboard_feed_pubkey, - price, - } -} - -pub async fn create_and_mint_to_token_account( - banks_client: &mut BanksClient, - mint_pubkey: Pubkey, - mint_authority: Option<&Keypair>, - payer: &Keypair, - authority: Pubkey, - amount: u64, -) -> Pubkey { - if let Some(mint_authority) = mint_authority { - let account_pubkey = - create_token_account(banks_client, mint_pubkey, payer, Some(authority), None).await; - - mint_to( - banks_client, - mint_pubkey, - payer, - account_pubkey, - mint_authority, - amount, - ) - .await; - - account_pubkey - } else { - create_token_account( - banks_client, - mint_pubkey, - payer, - Some(authority), - Some(amount), - ) - .await - } -} - -pub async fn create_token_account( - banks_client: &mut BanksClient, - mint_pubkey: Pubkey, - payer: &Keypair, - authority: Option, - native_amount: Option, -) -> Pubkey { - let token_keypair = Keypair::new(); - let token_pubkey = token_keypair.pubkey(); - let authority_pubkey = authority.unwrap_or_else(|| payer.pubkey()); - - let rent = banks_client.get_rent().await.unwrap(); - let lamports = rent.minimum_balance(Token::LEN) + native_amount.unwrap_or_default(); - let mut transaction = Transaction::new_with_payer( - &[ - create_account( - &payer.pubkey(), - &token_pubkey, - lamports, - Token::LEN as u64, - &spl_token::id(), - ), - spl_token::instruction::initialize_account( - &spl_token::id(), - &token_pubkey, - &mint_pubkey, - &authority_pubkey, - ) - .unwrap(), - ], - Some(&payer.pubkey()), - ); - - let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - transaction.sign(&[payer, &token_keypair], recent_blockhash); - - assert_matches!(banks_client.process_transaction(transaction).await, Ok(())); - - token_pubkey -} - -pub async fn mint_to( - banks_client: &mut BanksClient, - mint_pubkey: Pubkey, - payer: &Keypair, - account_pubkey: Pubkey, - authority: &Keypair, - amount: u64, -) { - let mut transaction = Transaction::new_with_payer( - &[spl_token::instruction::mint_to( - &spl_token::id(), - &mint_pubkey, - &account_pubkey, - &authority.pubkey(), - &[], - amount, - ) - .unwrap()], - Some(&payer.pubkey()), - ); - - let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - transaction.sign(&[payer, authority], recent_blockhash); - - assert_matches!(banks_client.process_transaction(transaction).await, Ok(())); -} - -pub async fn get_token_balance(banks_client: &mut BanksClient, pubkey: Pubkey) -> u64 { - let token: Account = banks_client.get_account(pubkey).await.unwrap().unwrap(); - - spl_token::state::Account::unpack(&token.data[..]) - .unwrap() - .amount } diff --git a/token-lending/program/tests/helpers/solend_program_test.rs b/token-lending/program/tests/helpers/solend_program_test.rs new file mode 100644 index 00000000000..8626da96e13 --- /dev/null +++ b/token-lending/program/tests/helpers/solend_program_test.rs @@ -0,0 +1,1456 @@ +use super::{ + flash_loan_proxy::proxy_program, + mock_pyth::{init_switchboard, set_switchboard_price}, +}; +use crate::helpers::*; +use solana_program::native_token::LAMPORTS_PER_SOL; +use solend_sdk::{instruction::update_reserve_config, NULL_PUBKEY}; + +use pyth_sdk_solana::state::PROD_ACCT_SIZE; +use solana_program::{ + clock::Clock, + instruction::Instruction, + program_pack::{IsInitialized, Pack}, + pubkey::Pubkey, + rent::Rent, + system_instruction, sysvar, +}; +use solana_sdk::{ + compute_budget::ComputeBudgetInstruction, + signature::{Keypair, Signer}, + system_instruction::create_account, + transaction::Transaction, +}; +use solend_program::{ + instruction::{ + deposit_obligation_collateral, deposit_reserve_liquidity, init_lending_market, + init_reserve, liquidate_obligation_and_redeem_reserve_collateral, redeem_fees, + redeem_reserve_collateral, repay_obligation_liquidity, set_lending_market_owner, + withdraw_obligation_collateral, + }, + processor::process_instruction, + state::{LendingMarket, Reserve, ReserveConfig}, +}; + +use spl_token::state::{Account as Token, Mint}; +use std::{ + collections::{HashMap, HashSet}, + str::FromStr, +}; + +use super::mock_pyth::{init, mock_pyth_program, set_price}; + +pub struct SolendProgramTest { + pub context: ProgramTestContext, + rent: Rent, + + // authority of all mints + authority: Keypair, + + pub mints: HashMap>, +} + +#[derive(Debug, Clone, Copy)] +pub struct Oracle { + pub pyth_product_pubkey: Pubkey, + pub pyth_price_pubkey: Pubkey, + pub switchboard_feed_pubkey: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Info { + pub pubkey: Pubkey, + pub account: T, +} + +impl SolendProgramTest { + pub async fn start_new() -> Self { + let mut test = ProgramTest::new( + "solend_program", + solend_program::id(), + processor!(process_instruction), + ); + + test.prefer_bpf(false); + test.add_program( + "mock_pyth", + mock_pyth_program::id(), + processor!(mock_pyth::process_instruction), + ); + + test.add_program( + "flash_loan_proxy", + proxy_program::id(), + processor!(flash_loan_proxy::process_instruction), + ); + + let authority = Keypair::new(); + + add_mint(&mut test, usdc_mint::id(), 6, authority.pubkey()); + add_mint(&mut test, wsol_mint::id(), 9, authority.pubkey()); + + let mut context = test.start_with_context().await; + let rent = context.banks_client.get_rent().await.unwrap(); + + SolendProgramTest { + context, + rent, + authority, + mints: HashMap::from([(usdc_mint::id(), None), (wsol_mint::id(), None)]), + } + } + + pub async fn process_transaction( + &mut self, + instructions: &[Instruction], + signers: Option<&[&Keypair]>, + ) -> Result<(), BanksClientError> { + let mut transaction = + Transaction::new_with_payer(instructions, Some(&self.context.payer.pubkey())); + + let mut all_signers = vec![&self.context.payer]; + + if let Some(signers) = signers { + all_signers.extend_from_slice(signers); + } + + // This fails when warping is involved - https://gitmemory.com/issue/solana-labs/solana/18201/868325078 + // let recent_blockhash = self.context.banks_client.get_recent_blockhash().await.unwrap(); + + transaction.sign(&all_signers, self.context.last_blockhash); + + self.context + .banks_client + .process_transaction(transaction) + .await + } + + pub async fn load_optional_account( + &mut self, + acc_pk: Pubkey, + ) -> Info> { + self.context + .banks_client + .get_account(acc_pk) + .await + .unwrap() + .map(|acc| Info { + pubkey: acc_pk, + account: T::unpack(&acc.data).ok(), + }) + .unwrap() + } + + pub async fn load_account(&mut self, acc_pk: Pubkey) -> Info { + let acc = self + .context + .banks_client + .get_account(acc_pk) + .await + .unwrap() + .unwrap(); + + Info { + pubkey: acc_pk, + account: T::unpack(&acc.data).unwrap(), + } + } + + pub async fn get_bincode_account( + &mut self, + address: &Pubkey, + ) -> T { + self.context + .banks_client + .get_account(*address) + .await + .unwrap() + .map(|a| bincode::deserialize::(&a.data).unwrap()) + .unwrap_or_else(|| panic!("GET-TEST-ACCOUNT-ERROR")) + } + + #[allow(dead_code)] + pub async fn get_clock(&mut self) -> Clock { + self.get_bincode_account::(&sysvar::clock::id()) + .await + } + + /// Advances clock by x slots. note that transactions don't automatically increment the slot + /// value in Clock, so this function must be explicitly called whenever you want time to move + /// forward. + pub async fn advance_clock_by_slots(&mut self, slots: u64) { + let clock: Clock = self.get_clock().await; + self.context.warp_to_slot(clock.slot + slots).unwrap(); + } + + pub async fn create_account( + &mut self, + size: usize, + owner: &Pubkey, + keypair: Option<&Keypair>, + ) -> Pubkey { + let rent = self.rent.minimum_balance(size); + + let new_keypair = Keypair::new(); + let keypair = match keypair { + None => &new_keypair, + Some(kp) => kp, + }; + + let instructions = [system_instruction::create_account( + &self.context.payer.pubkey(), + &keypair.pubkey(), + rent as u64, + size as u64, + owner, + )]; + + self.process_transaction(&instructions, Some(&[keypair])) + .await + .unwrap(); + + keypair.pubkey() + } + + pub async fn create_mint(&mut self, mint_authority: &Pubkey) -> Pubkey { + let keypair = Keypair::new(); + let rent = self.rent.minimum_balance(Mint::LEN); + + let instructions = [ + system_instruction::create_account( + &self.context.payer.pubkey(), + &keypair.pubkey(), + rent, + Mint::LEN as u64, + &spl_token::id(), + ), + spl_token::instruction::initialize_mint( + &spl_token::id(), + &keypair.pubkey(), + mint_authority, + None, + 0, + ) + .unwrap(), + ]; + + self.process_transaction(&instructions, Some(&[&keypair])) + .await + .unwrap(); + + keypair.pubkey() + } + + pub async fn create_token_account(&mut self, owner: &Pubkey, mint: &Pubkey) -> Pubkey { + let keypair = Keypair::new(); + let instructions = [ + system_instruction::create_account( + &self.context.payer.pubkey(), + &keypair.pubkey(), + self.rent.minimum_balance(Token::LEN), + spl_token::state::Account::LEN as u64, + &spl_token::id(), + ), + spl_token::instruction::initialize_account( + &spl_token::id(), + &keypair.pubkey(), + mint, + owner, + ) + .unwrap(), + ]; + + self.process_transaction(&instructions, Some(&[&keypair])) + .await + .unwrap(); + + keypair.pubkey() + } + + pub async fn mint_to(&mut self, mint: &Pubkey, dst: &Pubkey, amount: u64) { + assert!(self.mints.contains_key(mint)); + + let instructions = [spl_token::instruction::mint_to( + &spl_token::id(), + mint, + dst, + &self.authority.pubkey(), + &[], + amount, + ) + .unwrap()]; + + let authority = Keypair::from_bytes(&self.authority.to_bytes()).unwrap(); // hack + self.process_transaction(&instructions, Some(&[&authority])) + .await + .unwrap(); + } + + // wrappers around solend instructions. these should be used to test logic things (eg you can't + // borrow more than the borrow limit, but these methods can't be used to test account-level + // security of an instruction (eg what happens if im not the lending market owner but i try to + // add a reserve anyways). + + pub async fn init_lending_market( + &mut self, + owner: &User, + lending_market_key: &Keypair, + ) -> Result, BanksClientError> { + let payer = self.context.payer.pubkey(); + let lamports = Rent::minimum_balance(&self.rent, LendingMarket::LEN); + + let res = self + .process_transaction( + &[ + create_account( + &payer, + &lending_market_key.pubkey(), + lamports, + LendingMarket::LEN as u64, + &solend_program::id(), + ), + init_lending_market( + solend_program::id(), + owner.keypair.pubkey(), + QUOTE_CURRENCY, + lending_market_key.pubkey(), + mock_pyth_program::id(), + mock_pyth_program::id(), // TODO suspicious + ), + ], + Some(&[lending_market_key]), + ) + .await; + + match res { + Ok(()) => Ok(self + .load_account::(lending_market_key.pubkey()) + .await), + Err(e) => Err(e), + } + } + + pub async fn init_pyth_feed(&mut self, mint: &Pubkey) { + let pyth_price_pubkey = self + .create_account(3312, &mock_pyth_program::id(), None) + .await; + let pyth_product_pubkey = self + .create_account(PROD_ACCT_SIZE, &mock_pyth_program::id(), None) + .await; + + self.process_transaction( + &[init( + mock_pyth_program::id(), + pyth_price_pubkey, + pyth_product_pubkey, + )], + None, + ) + .await + .unwrap(); + + self.mints.insert( + *mint, + Some(Oracle { + pyth_product_pubkey, + pyth_price_pubkey, + switchboard_feed_pubkey: None, + }), + ); + } + + pub async fn set_price(&mut self, mint: &Pubkey, price: PriceArgs) { + let oracle = self.mints.get(mint).unwrap().unwrap(); + self.process_transaction( + &[set_price( + mock_pyth_program::id(), + oracle.pyth_price_pubkey, + price.price, + price.conf, + price.expo, + )], + None, + ) + .await + .unwrap(); + } + + pub async fn init_switchboard_feed(&mut self, mint: &Pubkey) { + let switchboard_feed_pubkey = self + .create_account( + std::mem::size_of::() + 8, + &mock_pyth_program::id(), + None, + ) + .await; + + self.process_transaction( + &[init_switchboard( + mock_pyth_program::id(), + switchboard_feed_pubkey, + )], + None, + ) + .await + .unwrap(); + + let oracle = self.mints.get_mut(mint).unwrap(); + if let Some(ref mut oracle) = oracle { + oracle.switchboard_feed_pubkey = Some(switchboard_feed_pubkey); + } else { + panic!("oracle not initialized"); + } + } + + pub async fn set_switchboard_price(&mut self, mint: &Pubkey, price: PriceArgs) { + let oracle = self.mints.get(mint).unwrap().unwrap(); + self.process_transaction( + &[set_switchboard_price( + mock_pyth_program::id(), + oracle.switchboard_feed_pubkey.unwrap(), + price.price, + price.expo, + )], + None, + ) + .await + .unwrap(); + } + + #[allow(clippy::too_many_arguments)] + pub async fn init_reserve( + &mut self, + lending_market: &Info, + lending_market_owner: &User, + mint: &Pubkey, + reserve_config: &ReserveConfig, + reserve_keypair: &Keypair, + liquidity_amount: u64, + oracle: Option, + ) -> Result, BanksClientError> { + let destination_collateral_pubkey = self + .create_account(Token::LEN, &spl_token::id(), None) + .await; + let reserve_liquidity_supply_pubkey = self + .create_account(Token::LEN, &spl_token::id(), None) + .await; + let reserve_pubkey = self + .create_account(Reserve::LEN, &solend_program::id(), Some(reserve_keypair)) + .await; + + let reserve_liquidity_fee_receiver = self + .create_account(Token::LEN, &spl_token::id(), None) + .await; + + let reserve_collateral_mint_pubkey = + self.create_account(Mint::LEN, &spl_token::id(), None).await; + let reserve_collateral_supply_pubkey = self + .create_account(Token::LEN, &spl_token::id(), None) + .await; + + let oracle = if let Some(o) = oracle { + o + } else { + self.mints.get(mint).unwrap().unwrap() + }; + + let res = self + .process_transaction( + &[ + ComputeBudgetInstruction::set_compute_unit_limit(70_000), + init_reserve( + solend_program::id(), + liquidity_amount, + ReserveConfig { + fee_receiver: reserve_liquidity_fee_receiver, + ..*reserve_config + }, + lending_market_owner.get_account(mint).unwrap(), + destination_collateral_pubkey, + reserve_pubkey, + *mint, + reserve_liquidity_supply_pubkey, + reserve_collateral_mint_pubkey, + reserve_collateral_supply_pubkey, + oracle.pyth_product_pubkey, + oracle.pyth_price_pubkey, + Pubkey::from_str("nu11111111111111111111111111111111111111111").unwrap(), + lending_market.pubkey, + lending_market_owner.keypair.pubkey(), + lending_market_owner.keypair.pubkey(), + ), + ], + Some(&[&lending_market_owner.keypair]), + ) + .await; + + match res { + Ok(()) => Ok(self.load_account::(reserve_pubkey).await), + Err(e) => Err(e), + } + } +} + +/// 1 User holds many token accounts +#[derive(Debug)] +pub struct User { + pub keypair: Keypair, + pub token_accounts: Vec>, +} + +impl User { + pub fn new_with_keypair(keypair: Keypair) -> Self { + User { + keypair, + token_accounts: Vec::new(), + } + } + + /// Creates a user with specified token accounts and balances. This function only works if the + /// SolendProgramTest object owns the mint authorities. eg this won't work for native SOL. + pub async fn new_with_balances( + test: &mut SolendProgramTest, + mints_and_balances: &[(&Pubkey, u64)], + ) -> Self { + let mut user = User { + keypair: Keypair::new(), + token_accounts: Vec::new(), + }; + + for (mint, balance) in mints_and_balances { + let token_account = user.create_token_account(mint, test).await; + if *balance > 0 { + test.mint_to(mint, &token_account.pubkey, *balance).await; + } + } + + user + } + + pub fn get_account(&self, mint: &Pubkey) -> Option { + self.token_accounts.iter().find_map(|ta| { + if ta.account.mint == *mint { + Some(ta.pubkey) + } else { + None + } + }) + } + + pub async fn get_balance(&self, test: &mut SolendProgramTest, mint: &Pubkey) -> Option { + match self.get_account(mint) { + None => None, + Some(pubkey) => { + let token_account = test.load_account::(pubkey).await; + Some(token_account.account.amount) + } + } + } + + pub async fn create_token_account( + &mut self, + mint: &Pubkey, + test: &mut SolendProgramTest, + ) -> Info { + match self + .token_accounts + .iter() + .find(|ta| ta.account.mint == *mint) + { + None => { + let pubkey = test + .create_token_account(&self.keypair.pubkey(), mint) + .await; + let account = test.load_account::(pubkey).await; + + self.token_accounts.push(account.clone()); + + account + } + Some(_) => panic!("Token account already exists!"), + } + } + + pub async fn transfer( + &self, + mint: &Pubkey, + destination_pubkey: Pubkey, + amount: u64, + test: &mut SolendProgramTest, + ) { + let instruction = [spl_token::instruction::transfer( + &spl_token::id(), + &self.get_account(mint).unwrap(), + &destination_pubkey, + &self.keypair.pubkey(), + &[], + amount, + ) + .unwrap()]; + + test.process_transaction(&instruction, Some(&[&self.keypair])) + .await + .unwrap(); + } +} + +pub struct PriceArgs { + pub price: i64, + pub conf: u64, + pub expo: i32, +} + +impl Info { + pub async fn deposit( + &self, + test: &mut SolendProgramTest, + reserve: &Info, + user: &User, + liquidity_amount: u64, + ) -> Result<(), BanksClientError> { + let instructions = [deposit_reserve_liquidity( + solend_program::id(), + liquidity_amount, + user.get_account(&reserve.account.liquidity.mint_pubkey) + .unwrap(), + user.get_account(&reserve.account.collateral.mint_pubkey) + .unwrap(), + reserve.pubkey, + reserve.account.liquidity.supply_pubkey, + reserve.account.collateral.mint_pubkey, + self.pubkey, + user.keypair.pubkey(), + )]; + + test.process_transaction(&instructions, Some(&[&user.keypair])) + .await + } + + pub async fn update_reserve_config( + &self, + test: &mut SolendProgramTest, + lending_market_owner: &User, + reserve: &Info, + config: ReserveConfig, + oracle: Option<&Oracle>, + ) -> Result<(), BanksClientError> { + let default_oracle = test + .mints + .get(&reserve.account.liquidity.mint_pubkey) + .unwrap() + .unwrap(); + let oracle = oracle.unwrap_or(&default_oracle); + println!("{:?}", oracle); + + let instructions = [update_reserve_config( + solend_program::id(), + config, + reserve.pubkey, + self.pubkey, + lending_market_owner.keypair.pubkey(), + oracle.pyth_product_pubkey, + oracle.pyth_price_pubkey, + oracle.switchboard_feed_pubkey.unwrap_or(NULL_PUBKEY), + )]; + + test.process_transaction(&instructions, Some(&[&lending_market_owner.keypair])) + .await + } + + pub async fn deposit_reserve_liquidity_and_obligation_collateral( + &self, + test: &mut SolendProgramTest, + reserve: &Info, + obligation: &Info, + user: &User, + liquidity_amount: u64, + ) -> Result<(), BanksClientError> { + let instructions = [deposit_reserve_liquidity_and_obligation_collateral( + solend_program::id(), + liquidity_amount, + user.get_account(&reserve.account.liquidity.mint_pubkey) + .unwrap(), + user.get_account(&reserve.account.collateral.mint_pubkey) + .unwrap(), + reserve.pubkey, + reserve.account.liquidity.supply_pubkey, + reserve.account.collateral.mint_pubkey, + self.pubkey, + reserve.account.collateral.supply_pubkey, + obligation.pubkey, + user.keypair.pubkey(), + reserve.account.liquidity.pyth_oracle_pubkey, + reserve.account.liquidity.switchboard_oracle_pubkey, + user.keypair.pubkey(), + )]; + + test.process_transaction(&instructions, Some(&[&user.keypair])) + .await + } + + pub async fn redeem( + &self, + test: &mut SolendProgramTest, + reserve: &Info, + user: &User, + collateral_amount: u64, + ) -> Result<(), BanksClientError> { + let instructions = [redeem_reserve_collateral( + solend_program::id(), + collateral_amount, + user.get_account(&reserve.account.collateral.mint_pubkey) + .unwrap(), + user.get_account(&reserve.account.liquidity.mint_pubkey) + .unwrap(), + reserve.pubkey, + reserve.account.collateral.mint_pubkey, + reserve.account.liquidity.supply_pubkey, + self.pubkey, + user.keypair.pubkey(), + )]; + + test.process_transaction(&instructions, Some(&[&user.keypair])) + .await + } + + pub async fn init_obligation( + &self, + test: &mut SolendProgramTest, + obligation_keypair: Keypair, + user: &User, + ) -> Result, BanksClientError> { + let instructions = [ + system_instruction::create_account( + &test.context.payer.pubkey(), + &obligation_keypair.pubkey(), + Rent::minimum_balance(&Rent::default(), Obligation::LEN), + Obligation::LEN as u64, + &solend_program::id(), + ), + init_obligation( + solend_program::id(), + obligation_keypair.pubkey(), + self.pubkey, + user.keypair.pubkey(), + ), + ]; + + match test + .process_transaction(&instructions, Some(&[&obligation_keypair, &user.keypair])) + .await + { + Ok(()) => Ok(test + .load_account::(obligation_keypair.pubkey()) + .await), + Err(e) => Err(e), + } + } + + pub async fn deposit_obligation_collateral( + &self, + test: &mut SolendProgramTest, + reserve: &Info, + obligation: &Info, + user: &User, + collateral_amount: u64, + ) -> Result<(), BanksClientError> { + let instructions = [deposit_obligation_collateral( + solend_program::id(), + collateral_amount, + user.get_account(&reserve.account.collateral.mint_pubkey) + .unwrap(), + reserve.account.collateral.supply_pubkey, + reserve.pubkey, + obligation.pubkey, + self.pubkey, + user.keypair.pubkey(), + user.keypair.pubkey(), + )]; + + test.process_transaction(&instructions, Some(&[&user.keypair])) + .await + } + + pub async fn refresh_reserve( + &self, + test: &mut SolendProgramTest, + reserve: &Info, + ) -> Result<(), BanksClientError> { + test.process_transaction( + &[refresh_reserve( + solend_program::id(), + reserve.pubkey, + reserve.account.liquidity.pyth_oracle_pubkey, + reserve.account.liquidity.switchboard_oracle_pubkey, + )], + None, + ) + .await + } + + pub async fn build_refresh_instructions( + &self, + test: &mut SolendProgramTest, + obligation: &Info, + extra_reserve: Option<&Info>, + ) -> Vec { + let reserve_pubkeys: Vec = { + let mut r = HashSet::new(); + r.extend( + obligation + .account + .deposits + .iter() + .map(|d| d.deposit_reserve), + ); + r.extend(obligation.account.borrows.iter().map(|b| b.borrow_reserve)); + + if let Some(reserve) = extra_reserve { + r.insert(reserve.pubkey); + } + + r.into_iter().collect() + }; + + let mut reserves = Vec::new(); + for pubkey in reserve_pubkeys { + reserves.push(test.load_account::(pubkey).await); + } + + let mut instructions: Vec = reserves + .into_iter() + .map(|reserve| { + refresh_reserve( + solend_program::id(), + reserve.pubkey, + reserve.account.liquidity.pyth_oracle_pubkey, + reserve.account.liquidity.switchboard_oracle_pubkey, + ) + }) + .collect(); + + let reserve_pubkeys: Vec = { + let mut r = Vec::new(); + r.extend( + obligation + .account + .deposits + .iter() + .map(|d| d.deposit_reserve), + ); + r.extend(obligation.account.borrows.iter().map(|b| b.borrow_reserve)); + r + }; + + instructions.push(refresh_obligation( + solend_program::id(), + obligation.pubkey, + reserve_pubkeys, + )); + + instructions + } + + pub async fn refresh_obligation( + &self, + test: &mut SolendProgramTest, + obligation: &Info, + ) -> Result<(), BanksClientError> { + let instructions = self + .build_refresh_instructions(test, obligation, None) + .await; + + test.process_transaction(&instructions, None).await + } + + pub async fn borrow_obligation_liquidity( + &self, + test: &mut SolendProgramTest, + borrow_reserve: &Info, + obligation: &Info, + user: &User, + host_fee_receiver_pubkey: &Pubkey, + liquidity_amount: u64, + ) -> Result<(), BanksClientError> { + let mut instructions = self + .build_refresh_instructions(test, obligation, Some(borrow_reserve)) + .await; + + instructions.push(borrow_obligation_liquidity( + solend_program::id(), + liquidity_amount, + borrow_reserve.account.liquidity.supply_pubkey, + user.get_account(&borrow_reserve.account.liquidity.mint_pubkey) + .unwrap(), + borrow_reserve.pubkey, + borrow_reserve.account.config.fee_receiver, + obligation.pubkey, + self.pubkey, + user.keypair.pubkey(), + Some(*host_fee_receiver_pubkey), + )); + + test.process_transaction(&instructions, Some(&[&user.keypair])) + .await + } + + pub async fn repay_obligation_liquidity( + &self, + test: &mut SolendProgramTest, + repay_reserve: &Info, + obligation: &Info, + user: &User, + liquidity_amount: u64, + ) -> Result<(), BanksClientError> { + let instructions = [repay_obligation_liquidity( + solend_program::id(), + liquidity_amount, + user.get_account(&repay_reserve.account.liquidity.mint_pubkey) + .unwrap(), + repay_reserve.account.liquidity.supply_pubkey, + repay_reserve.pubkey, + obligation.pubkey, + self.pubkey, + user.keypair.pubkey(), + )]; + + test.process_transaction(&instructions, Some(&[&user.keypair])) + .await + } + + pub async fn redeem_fees( + &self, + test: &mut SolendProgramTest, + reserve: &Info, + ) -> Result<(), BanksClientError> { + let instructions = [ + refresh_reserve( + solend_program::id(), + reserve.pubkey, + reserve.account.liquidity.pyth_oracle_pubkey, + reserve.account.liquidity.switchboard_oracle_pubkey, + ), + redeem_fees( + solend_program::id(), + reserve.pubkey, + reserve.account.config.fee_receiver, + reserve.account.liquidity.supply_pubkey, + self.pubkey, + ), + ]; + + test.process_transaction(&instructions, None).await + } + + pub async fn liquidate_obligation_and_redeem_reserve_collateral( + &self, + test: &mut SolendProgramTest, + repay_reserve: &Info, + withdraw_reserve: &Info, + obligation: &Info, + user: &User, + liquidity_amount: u64, + ) -> Result<(), BanksClientError> { + let mut instructions = self + .build_refresh_instructions(test, obligation, None) + .await; + + instructions.push(liquidate_obligation_and_redeem_reserve_collateral( + solend_program::id(), + liquidity_amount, + user.get_account(&repay_reserve.account.liquidity.mint_pubkey) + .unwrap(), + user.get_account(&withdraw_reserve.account.collateral.mint_pubkey) + .unwrap(), + user.get_account(&withdraw_reserve.account.liquidity.mint_pubkey) + .unwrap(), + repay_reserve.pubkey, + repay_reserve.account.liquidity.supply_pubkey, + withdraw_reserve.pubkey, + withdraw_reserve.account.collateral.mint_pubkey, + withdraw_reserve.account.collateral.supply_pubkey, + withdraw_reserve.account.liquidity.supply_pubkey, + withdraw_reserve.account.config.fee_receiver, + obligation.pubkey, + self.pubkey, + user.keypair.pubkey(), + )); + + test.process_transaction(&instructions, Some(&[&user.keypair])) + .await + } + + pub async fn liquidate_obligation( + &self, + test: &mut SolendProgramTest, + repay_reserve: &Info, + withdraw_reserve: &Info, + obligation: &Info, + user: &User, + liquidity_amount: u64, + ) -> Result<(), BanksClientError> { + let mut instructions = self + .build_refresh_instructions(test, obligation, None) + .await; + + instructions.push(liquidate_obligation( + solend_program::id(), + liquidity_amount, + user.get_account(&repay_reserve.account.liquidity.mint_pubkey) + .unwrap(), + user.get_account(&withdraw_reserve.account.collateral.mint_pubkey) + .unwrap(), + repay_reserve.pubkey, + repay_reserve.account.liquidity.supply_pubkey, + withdraw_reserve.pubkey, + withdraw_reserve.account.collateral.supply_pubkey, + obligation.pubkey, + self.pubkey, + user.keypair.pubkey(), + )); + + test.process_transaction(&instructions, Some(&[&user.keypair])) + .await + } + + pub async fn withdraw_obligation_collateral_and_redeem_reserve_collateral( + &self, + test: &mut SolendProgramTest, + withdraw_reserve: &Info, + obligation: &Info, + user: &User, + collateral_amount: u64, + ) -> Result<(), BanksClientError> { + let mut instructions = self + .build_refresh_instructions(test, obligation, Some(withdraw_reserve)) + .await; + + instructions.push( + withdraw_obligation_collateral_and_redeem_reserve_collateral( + solend_program::id(), + collateral_amount, + withdraw_reserve.account.collateral.supply_pubkey, + user.get_account(&withdraw_reserve.account.collateral.mint_pubkey) + .unwrap(), + withdraw_reserve.pubkey, + obligation.pubkey, + self.pubkey, + user.get_account(&withdraw_reserve.account.liquidity.mint_pubkey) + .unwrap(), + withdraw_reserve.account.collateral.mint_pubkey, + withdraw_reserve.account.liquidity.supply_pubkey, + user.keypair.pubkey(), + user.keypair.pubkey(), + ), + ); + + test.process_transaction(&instructions, Some(&[&user.keypair])) + .await + } + + pub async fn withdraw_obligation_collateral( + &self, + test: &mut SolendProgramTest, + withdraw_reserve: &Info, + obligation: &Info, + user: &User, + collateral_amount: u64, + ) -> Result<(), BanksClientError> { + let mut instructions = self + .build_refresh_instructions(test, obligation, Some(withdraw_reserve)) + .await; + + instructions.push(withdraw_obligation_collateral( + solend_program::id(), + collateral_amount, + withdraw_reserve.account.collateral.supply_pubkey, + user.get_account(&withdraw_reserve.account.collateral.mint_pubkey) + .unwrap(), + withdraw_reserve.pubkey, + obligation.pubkey, + self.pubkey, + user.keypair.pubkey(), + )); + + test.process_transaction(&instructions, Some(&[&user.keypair])) + .await + } + + pub async fn set_lending_market_owner( + &self, + test: &mut SolendProgramTest, + lending_market_owner: &User, + new_owner: &Pubkey, + ) -> Result<(), BanksClientError> { + let instructions = [set_lending_market_owner( + solend_program::id(), + self.pubkey, + lending_market_owner.keypair.pubkey(), + *new_owner, + )]; + + test.process_transaction(&instructions, Some(&[&lending_market_owner.keypair])) + .await + } +} + +/// Track token balance changes across transactions. +pub struct BalanceChecker { + token_accounts: Vec>>, + mint_accounts: Vec>>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct TokenBalanceChange { + pub token_account: Pubkey, + pub mint: Pubkey, + pub diff: i128, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct MintSupplyChange { + pub mint: Pubkey, + pub diff: i128, +} + +impl BalanceChecker { + pub async fn start(test: &mut SolendProgramTest, objs: &[&dyn GetTokenAndMintPubkeys]) -> Self { + let mut refreshed_token_accounts = Vec::new(); + let mut refreshed_mint_accounts = Vec::new(); + + for obj in objs { + let (token_pubkeys, mint_pubkeys) = obj.get_token_and_mint_pubkeys(); + + for pubkey in token_pubkeys { + let refreshed_account = test.load_optional_account::(pubkey).await; + refreshed_token_accounts.push(refreshed_account); + } + + for pubkey in mint_pubkeys { + let refreshed_account = test.load_optional_account::(pubkey).await; + refreshed_mint_accounts.push(refreshed_account); + } + } + + BalanceChecker { + token_accounts: refreshed_token_accounts, + mint_accounts: refreshed_mint_accounts, + } + } + + pub async fn find_balance_changes( + &self, + test: &mut SolendProgramTest, + ) -> (HashSet, HashSet) { + let mut token_balance_changes = HashSet::new(); + let mut mint_supply_changes = HashSet::new(); + + for token_account in &self.token_accounts { + let refreshed_token_account = test.load_account::(token_account.pubkey).await; + match token_account.account { + None => { + if refreshed_token_account.account.amount > 0 { + token_balance_changes.insert(TokenBalanceChange { + token_account: refreshed_token_account.pubkey, + mint: refreshed_token_account.account.mint, + diff: refreshed_token_account.account.amount as i128, + }); + } + } + Some(token_account) => { + if refreshed_token_account.account.amount != token_account.amount { + token_balance_changes.insert(TokenBalanceChange { + token_account: refreshed_token_account.pubkey, + mint: token_account.mint, + diff: (refreshed_token_account.account.amount as i128) + - (token_account.amount as i128), + }); + } + } + }; + } + + for mint_account in &self.mint_accounts { + let refreshed_mint_account = test.load_account::(mint_account.pubkey).await; + match mint_account.account { + None => { + if refreshed_mint_account.account.supply > 0 { + mint_supply_changes.insert(MintSupplyChange { + mint: refreshed_mint_account.pubkey, + diff: refreshed_mint_account.account.supply as i128, + }); + } + } + Some(mint_account) => { + if refreshed_mint_account.account.supply != mint_account.supply { + mint_supply_changes.insert(MintSupplyChange { + mint: refreshed_mint_account.pubkey, + diff: (refreshed_mint_account.account.supply as i128) + - (mint_account.supply as i128), + }); + } + } + }; + } + + (token_balance_changes, mint_supply_changes) + } +} + +/// trait that tracks token and mint accounts associated with a specific struct +pub trait GetTokenAndMintPubkeys { + fn get_token_and_mint_pubkeys(&self) -> (Vec, Vec); +} + +impl GetTokenAndMintPubkeys for User { + fn get_token_and_mint_pubkeys(&self) -> (Vec, Vec) { + ( + self.token_accounts.iter().map(|a| a.pubkey).collect(), + vec![], + ) + } +} + +impl GetTokenAndMintPubkeys for Info { + fn get_token_and_mint_pubkeys(&self) -> (Vec, Vec) { + ( + vec![ + self.account.liquidity.supply_pubkey, + self.account.collateral.supply_pubkey, + self.account.config.fee_receiver, + ], + vec![ + self.account.liquidity.mint_pubkey, + self.account.collateral.mint_pubkey, + ], + ) + } +} + +pub struct MintAccount(pub Pubkey); +pub struct TokenAccount(pub Pubkey); + +impl GetTokenAndMintPubkeys for MintAccount { + fn get_token_and_mint_pubkeys(&self) -> (Vec, Vec) { + (vec![], vec![self.0]) + } +} + +impl GetTokenAndMintPubkeys for TokenAccount { + fn get_token_and_mint_pubkeys(&self) -> (Vec, Vec) { + (vec![self.0], vec![]) + } +} + +/// Init's a lending market with a usdc reserve and wsol reserve. +pub async fn setup_world( + usdc_reserve_config: &ReserveConfig, + wsol_reserve_config: &ReserveConfig, +) -> ( + SolendProgramTest, + Info, + Info, + Info, + User, + User, +) { + let mut test = SolendProgramTest::start_new().await; + + let lending_market_owner = User::new_with_balances( + &mut test, + &[ + (&usdc_mint::id(), 2_000_000), + (&wsol_mint::id(), 2 * LAMPORTS_TO_SOL), + ], + ) + .await; + + let lending_market = test + .init_lending_market(&lending_market_owner, &Keypair::new()) + .await + .unwrap(); + + test.advance_clock_by_slots(999).await; + + test.init_pyth_feed(&usdc_mint::id()).await; + test.set_price( + &usdc_mint::id(), + PriceArgs { + price: 1, + conf: 0, + expo: 0, + }, + ) + .await; + + test.init_pyth_feed(&wsol_mint::id()).await; + test.set_price( + &wsol_mint::id(), + PriceArgs { + price: 10, + conf: 0, + expo: 0, + }, + ) + .await; + + let usdc_reserve = test + .init_reserve( + &lending_market, + &lending_market_owner, + &usdc_mint::id(), + usdc_reserve_config, + &Keypair::new(), + 1_000_000, + None, + ) + .await + .unwrap(); + + let wsol_reserve = test + .init_reserve( + &lending_market, + &lending_market_owner, + &wsol_mint::id(), + wsol_reserve_config, + &Keypair::new(), + LAMPORTS_TO_SOL, + None, + ) + .await + .unwrap(); + + let user = User::new_with_balances( + &mut test, + &[ + (&usdc_mint::id(), 1_000_000_000_000), // 1M USDC + (&usdc_reserve.account.collateral.mint_pubkey, 0), // cUSDC + (&wsol_mint::id(), 10 * LAMPORTS_TO_SOL), + (&wsol_reserve.account.collateral.mint_pubkey, 0), // cSOL + ], + ) + .await; + + ( + test, + lending_market, + usdc_reserve, + wsol_reserve, + lending_market_owner, + user, + ) +} + +/// Scenario 1 +/// LendingMarket +/// - USDC Reserve +/// - WSOL Reserve +/// Obligation +/// - 100k USDC deposit +/// - 10 SOL borrowed +/// no interest has accrued on anything yet, ie: +/// - cUSDC/USDC = 1 +/// - cSOL/SOL = 1 +/// - Obligation owes _exactly_ 10 SOL +/// slot is 999, so the next tx that runs will be at slot 1000 +pub async fn scenario_1( + usdc_reserve_config: &ReserveConfig, + wsol_reserve_config: &ReserveConfig, +) -> ( + SolendProgramTest, + Info, + Info, + Info, + User, + Info, +) { + let (mut test, lending_market, usdc_reserve, wsol_reserve, lending_market_owner, user) = + setup_world(usdc_reserve_config, wsol_reserve_config).await; + + // init obligation + let obligation = lending_market + .init_obligation(&mut test, Keypair::new(), &user) + .await + .expect("This should succeed"); + + // deposit 100k USDC + lending_market + .deposit(&mut test, &usdc_reserve, &user, 100_000_000_000) + .await + .expect("This should succeed"); + + let usdc_reserve = test.load_account(usdc_reserve.pubkey).await; + + // deposit 100k cUSDC + lending_market + .deposit_obligation_collateral( + &mut test, + &usdc_reserve, + &obligation, + &user, + 100_000_000_000, + ) + .await + .expect("This should succeed"); + + let wsol_depositor = User::new_with_balances( + &mut test, + &[ + (&wsol_mint::id(), 9 * LAMPORTS_PER_SOL), + (&wsol_reserve.account.collateral.mint_pubkey, 0), + ], + ) + .await; + + // deposit 9 SOL. wSOL reserve now has 10 SOL. + lending_market + .deposit( + &mut test, + &wsol_reserve, + &wsol_depositor, + 9 * LAMPORTS_PER_SOL, + ) + .await + .unwrap(); + + // borrow 10 SOL against 100k cUSDC. + let obligation = test.load_account::(obligation.pubkey).await; + lending_market + .borrow_obligation_liquidity( + &mut test, + &wsol_reserve, + &obligation, + &user, + &lending_market_owner.get_account(&wsol_mint::id()).unwrap(), + u64::MAX, + ) + .await + .unwrap(); + + // populate market price correctly + lending_market + .refresh_reserve(&mut test, &wsol_reserve) + .await + .unwrap(); + + // populate deposit value correctly. + let obligation = test.load_account::(obligation.pubkey).await; + lending_market + .refresh_obligation(&mut test, &obligation) + .await + .unwrap(); + + let lending_market = test.load_account(lending_market.pubkey).await; + let usdc_reserve = test.load_account(usdc_reserve.pubkey).await; + let wsol_reserve = test.load_account(wsol_reserve.pubkey).await; + let obligation = test.load_account::(obligation.pubkey).await; + + ( + test, + lending_market, + usdc_reserve, + wsol_reserve, + user, + obligation, + ) +} diff --git a/token-lending/program/tests/init_lending_market.rs b/token-lending/program/tests/init_lending_market.rs index d12fc8040a2..89180a0f8bd 100644 --- a/token-lending/program/tests/init_lending_market.rs +++ b/token-lending/program/tests/init_lending_market.rs @@ -2,64 +2,71 @@ mod helpers; +use helpers::solend_program_test::{SolendProgramTest, User}; use helpers::*; +use mock_pyth::mock_pyth_program; +use solana_program::instruction::InstructionError; use solana_program_test::*; -use solana_sdk::{ - instruction::InstructionError, - signature::Signer, - transaction::{Transaction, TransactionError}, -}; -use solend_program::{ - error::LendingError, instruction::init_lending_market, processor::process_instruction, -}; +use solana_sdk::signature::Keypair; +use solana_sdk::signer::Signer; +use solana_sdk::transaction::TransactionError; +use solend_program::error::LendingError; +use solend_program::instruction::init_lending_market; +use solend_program::state::{LendingMarket, PROGRAM_VERSION}; #[tokio::test] async fn test_success() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(17_000); - - let (mut banks_client, payer, _recent_blockhash) = test.start().await; + let mut test = SolendProgramTest::start_new().await; + let lending_market_owner = User::new_with_balances(&mut test, &[]).await; - let test_lending_market = TestLendingMarket::init(&mut banks_client, &payer).await; - - test_lending_market.validate_state(&mut banks_client).await; + let lending_market = test + .init_lending_market(&lending_market_owner, &Keypair::new()) + .await + .unwrap(); + assert_eq!( + lending_market.account, + LendingMarket { + version: PROGRAM_VERSION, + bump_seed: lending_market.account.bump_seed, // TODO test this field + owner: lending_market_owner.keypair.pubkey(), + quote_currency: QUOTE_CURRENCY, + token_program_id: spl_token::id(), + oracle_program_id: mock_pyth_program::id(), + switchboard_oracle_program_id: mock_pyth_program::id(), + } + ); } #[tokio::test] async fn test_already_initialized() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); + let mut test = SolendProgramTest::start_new().await; + let lending_market_owner = User::new_with_balances(&mut test, &[]).await; - let existing_market = add_lending_market(&mut test); - let (mut banks_client, payer, recent_blockhash) = test.start().await; + let keypair = Keypair::new(); + test.init_lending_market(&lending_market_owner, &keypair) + .await + .unwrap(); + + test.advance_clock_by_slots(1).await; + + let res = test + .process_transaction( + &[init_lending_market( + solend_program::id(), + lending_market_owner.keypair.pubkey(), + QUOTE_CURRENCY, + keypair.pubkey(), + mock_pyth_program::id(), + mock_pyth_program::id(), + )], + None, + ) + .await + .unwrap_err() + .unwrap(); - let mut transaction = Transaction::new_with_payer( - &[init_lending_market( - solend_program::id(), - existing_market.owner.pubkey(), - existing_market.quote_currency, - existing_market.pubkey, - existing_market.oracle_program_id, - existing_market.switchboard_oracle_program_id, - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer], recent_blockhash); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( 0, InstructionError::Custom(LendingError::AlreadyInitialized as u32) diff --git a/token-lending/program/tests/init_obligation.rs b/token-lending/program/tests/init_obligation.rs index 2986bb4aeb1..ad102bf1a7a 100644 --- a/token-lending/program/tests/init_obligation.rs +++ b/token-lending/program/tests/init_obligation.rs @@ -2,83 +2,85 @@ mod helpers; +use helpers::solend_program_test::{setup_world, Info, SolendProgramTest, User}; use helpers::*; +use solana_program::instruction::InstructionError; use solana_program_test::*; -use solana_sdk::{ - instruction::InstructionError, - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, -}; -use solend_program::{ - error::LendingError, instruction::init_obligation, processor::process_instruction, -}; +use solana_sdk::signature::Keypair; -#[tokio::test] -async fn test_success() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); +use solana_sdk::signer::Signer; +use solana_sdk::transaction::TransactionError; +use solend_program::error::LendingError; +use solend_program::instruction::init_obligation; +use solend_program::math::Decimal; +use solend_program::state::{LastUpdate, LendingMarket, Obligation, PROGRAM_VERSION}; - // limit to track compute unit increase - test.set_compute_max_units(8_000); +async fn setup() -> (SolendProgramTest, Info, User) { + let (test, lending_market, _, _, _, user) = + setup_world(&test_reserve_config(), &test_reserve_config()).await; - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); + (test, lending_market, user) +} + +#[tokio::test] +async fn test_success() { + let (mut test, lending_market, user) = setup().await; - let (mut banks_client, payer, _recent_blockhash) = test.start().await; - let obligation = TestObligation::init( - &mut banks_client, - &lending_market, - &user_accounts_owner, - &payer, - ) - .await - .unwrap(); + let obligation = lending_market + .init_obligation(&mut test, Keypair::new(), &user) + .await + .expect("This should succeed"); - obligation.validate_state(&mut banks_client).await; + assert_eq!( + obligation.account, + Obligation { + version: PROGRAM_VERSION, + last_update: LastUpdate { + slot: 1000, + stale: true + }, + lending_market: lending_market.pubkey, + owner: user.keypair.pubkey(), + deposits: Vec::new(), + borrows: Vec::new(), + deposited_value: Decimal::zero(), + borrowed_value: Decimal::zero(), + allowed_borrow_value: Decimal::zero(), + unhealthy_borrow_value: Decimal::zero() + } + ); } #[tokio::test] async fn test_already_initialized() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); + let (mut test, lending_market, user) = setup().await; - // limit to track compute unit increase - test.set_compute_max_units(13_000); + let keypair = Keypair::new(); + let keypair_clone = Keypair::from_bytes(&keypair.to_bytes().clone()).unwrap(); - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); + lending_market + .init_obligation(&mut test, keypair, &user) + .await + .expect("This should succeed"); - let usdc_obligation = add_obligation( - &mut test, - &lending_market, - &user_accounts_owner, - AddObligationArgs::default(), - ); + test.advance_clock_by_slots(1).await; - let (mut banks_client, payer, recent_blockhash) = test.start().await; - let mut transaction = Transaction::new_with_payer( - &[init_obligation( - solend_program::id(), - usdc_obligation.pubkey, - lending_market.pubkey, - user_accounts_owner.pubkey(), - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); + let res = test + .process_transaction( + &[init_obligation( + solend_program::id(), + keypair_clone.pubkey(), + lending_market.pubkey, + user.keypair.pubkey(), + )], + Some(&[&user.keypair]), + ) + .await + .unwrap_err() + .unwrap(); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( 0, InstructionError::Custom(LendingError::AlreadyInitialized as u32) diff --git a/token-lending/program/tests/init_reserve.rs b/token-lending/program/tests/init_reserve.rs index f3acb00f1ae..f54578c702a 100644 --- a/token-lending/program/tests/init_reserve.rs +++ b/token-lending/program/tests/init_reserve.rs @@ -1,293 +1,248 @@ #![cfg(feature = "test-bpf")] +use crate::solend_program_test::BalanceChecker; +use crate::solend_program_test::MintAccount; +use crate::solend_program_test::MintSupplyChange; +use crate::solend_program_test::Oracle; +use crate::solend_program_test::TokenAccount; +use crate::solend_program_test::TokenBalanceChange; +use std::collections::HashSet; +use std::str::FromStr; mod helpers; +use crate::solend_program_test::setup_world; +use crate::solend_program_test::Info; +use crate::solend_program_test::SolendProgramTest; +use crate::solend_program_test::User; use helpers::*; +use solana_program::example_mocks::solana_sdk::Pubkey; +use solana_program::program_pack::Pack; use solana_program_test::*; use solana_sdk::{ instruction::InstructionError, signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, + transaction::TransactionError, }; +use solend_program::state::LastUpdate; +use solend_program::state::Reserve; +use solend_program::state::ReserveCollateral; +use solend_program::state::ReserveLiquidity; +use solend_program::state::PROGRAM_VERSION; +use solend_program::NULL_PUBKEY; use solend_program::{ error::LendingError, - instruction::{init_reserve, update_reserve_config}, + instruction::init_reserve, math::Decimal, - processor::process_instruction, - state::{ReserveConfig, ReserveFees, INITIAL_COLLATERAL_RATIO}, + state::{ReserveConfig, ReserveFees}, }; +use solend_sdk::state::LendingMarket; +use spl_token::state::{Account as Token, Mint}; -#[tokio::test] -async fn test_success() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(70_000); - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - let sol_oracle = add_sol_oracle(&mut test); - - let mut test_context = test.start_with_context().await; - test_context.warp_to_slot(240).unwrap(); // clock.slot = 240 +async fn setup() -> (SolendProgramTest, Info, User) { + let (test, lending_market, _, _, lending_market_owner, _) = + setup_world(&test_reserve_config(), &test_reserve_config()).await; - let ProgramTestContext { - mut banks_client, - payer, - last_blockhash: _recent_blockhash, - .. - } = test_context; + (test, lending_market, lending_market_owner) +} - const RESERVE_AMOUNT: u64 = 42; +#[tokio::test] +async fn test_success() { + let (mut test, lending_market, lending_market_owner) = setup().await; + + // create required pubkeys + let reserve_keypair = Keypair::new(); + let destination_collateral_pubkey = test + .create_account(Token::LEN, &spl_token::id(), None) + .await; + let reserve_liquidity_supply_pubkey = test + .create_account(Token::LEN, &spl_token::id(), None) + .await; + let reserve_pubkey = test + .create_account(Reserve::LEN, &solend_program::id(), Some(&reserve_keypair)) + .await; + let reserve_liquidity_fee_receiver = test + .create_account(Token::LEN, &spl_token::id(), None) + .await; + let reserve_collateral_mint_pubkey = + test.create_account(Mint::LEN, &spl_token::id(), None).await; + let reserve_collateral_supply_pubkey = test + .create_account(Token::LEN, &spl_token::id(), None) + .await; + + test.advance_clock_by_slots(1).await; + + let oracle = test.mints.get(&wsol_mint::id()).unwrap().unwrap(); + let reserve_config = ReserveConfig { + fee_receiver: reserve_liquidity_fee_receiver, + ..test_reserve_config() + }; - let sol_user_liquidity_account = create_and_mint_to_token_account( - &mut banks_client, - spl_token::native_mint::id(), - None, - &payer, - user_accounts_owner.pubkey(), - RESERVE_AMOUNT, + let balance_checker = BalanceChecker::start( + &mut test, + &[ + &lending_market_owner, + &TokenAccount(destination_collateral_pubkey), + &TokenAccount(reserve_liquidity_supply_pubkey), + &TokenAccount(reserve_liquidity_fee_receiver), + &TokenAccount(reserve_collateral_supply_pubkey), + &MintAccount(reserve_collateral_mint_pubkey), + ], ) .await; - let mut config = test_reserve_config(); - let fee_receiver_keypair = Keypair::new(); - config.fee_receiver = fee_receiver_keypair.pubkey(); - - let sol_reserve = TestReserve::init( - "sol".to_owned(), - &mut banks_client, - &lending_market, - &sol_oracle, - RESERVE_AMOUNT, - config, - spl_token::native_mint::id(), - sol_user_liquidity_account, - &fee_receiver_keypair, - &payer, - &user_accounts_owner, + test.process_transaction( + &[init_reserve( + solend_program::id(), + 1000, + reserve_config, + lending_market_owner.get_account(&wsol_mint::id()).unwrap(), + destination_collateral_pubkey, + reserve_pubkey, + wsol_mint::id(), + reserve_liquidity_supply_pubkey, + reserve_collateral_mint_pubkey, + reserve_collateral_supply_pubkey, + oracle.pyth_product_pubkey, + oracle.pyth_price_pubkey, + Pubkey::from_str("nu11111111111111111111111111111111111111111").unwrap(), + lending_market.pubkey, + lending_market_owner.keypair.pubkey(), + lending_market_owner.keypair.pubkey(), + )], + Some(&[&lending_market_owner.keypair]), ) .await .unwrap(); - sol_reserve.validate_state(&mut banks_client).await; + // check token balances + let (balance_changes, mint_supply_changes) = + balance_checker.find_balance_changes(&mut test).await; + let expected_balance_changes = HashSet::from([ + TokenBalanceChange { + token_account: lending_market_owner.get_account(&wsol_mint::id()).unwrap(), + mint: wsol_mint::id(), + diff: -1000, + }, + TokenBalanceChange { + token_account: destination_collateral_pubkey, + mint: reserve_collateral_mint_pubkey, + diff: 1000, + }, + TokenBalanceChange { + token_account: reserve_liquidity_supply_pubkey, + mint: wsol_mint::id(), + diff: 1000, + }, + ]); + assert_eq!(balance_changes, expected_balance_changes); + + assert_eq!( + mint_supply_changes, + HashSet::from([MintSupplyChange { + mint: reserve_collateral_mint_pubkey, + diff: 1000, + }]) + ); - let sol_liquidity_supply = - get_token_balance(&mut banks_client, sol_reserve.liquidity_supply_pubkey).await; - assert_eq!(sol_liquidity_supply, RESERVE_AMOUNT); - let user_sol_balance = - get_token_balance(&mut banks_client, sol_reserve.user_liquidity_pubkey).await; - assert_eq!(user_sol_balance, 0); - let user_sol_collateral_balance = - get_token_balance(&mut banks_client, sol_reserve.user_collateral_pubkey).await; + // check program state + let wsol_reserve = test.load_account::(reserve_pubkey).await; assert_eq!( - user_sol_collateral_balance, - RESERVE_AMOUNT * INITIAL_COLLATERAL_RATIO + wsol_reserve.account, + Reserve { + version: PROGRAM_VERSION, + last_update: LastUpdate { + slot: 1001, + stale: true + }, + lending_market: lending_market.pubkey, + liquidity: ReserveLiquidity { + mint_pubkey: wsol_mint::id(), + mint_decimals: 9, + supply_pubkey: reserve_liquidity_supply_pubkey, + pyth_oracle_pubkey: oracle.pyth_price_pubkey, + switchboard_oracle_pubkey: NULL_PUBKEY, + available_amount: 1000, + borrowed_amount_wads: Decimal::zero(), + cumulative_borrow_rate_wads: Decimal::one(), + accumulated_protocol_fees_wads: Decimal::zero(), + market_price: Decimal::from(10u64), + }, + collateral: ReserveCollateral { + mint_pubkey: reserve_collateral_mint_pubkey, + mint_total_supply: 1000, + supply_pubkey: reserve_collateral_supply_pubkey, + }, + config: reserve_config + } ); } #[tokio::test] async fn test_init_reserve_null_oracles() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(70_000); - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - let all_null_oracles = TestOracle { - pyth_product_pubkey: solend_program::NULL_PUBKEY, - pyth_price_pubkey: solend_program::NULL_PUBKEY, - switchboard_feed_pubkey: solend_program::NULL_PUBKEY, - price: Decimal::from(1u64), - }; - - let (mut banks_client, payer, _recent_blockhash) = test.start().await; - - const RESERVE_AMOUNT: u64 = 42; + let (mut test, lending_market, lending_market_owner) = setup().await; - let sol_user_liquidity_account = create_and_mint_to_token_account( - &mut banks_client, - spl_token::native_mint::id(), - None, - &payer, - user_accounts_owner.pubkey(), - RESERVE_AMOUNT, - ) - .await; - - let mut config = test_reserve_config(); - let fee_receiver_keypair = Keypair::new(); - config.fee_receiver = fee_receiver_keypair.pubkey(); - - assert_eq!( - TestReserve::init( - "sol".to_owned(), - &mut banks_client, + let res = test + .init_reserve( &lending_market, - &all_null_oracles, - RESERVE_AMOUNT, - config, - spl_token::native_mint::id(), - sol_user_liquidity_account, - &fee_receiver_keypair, - &payer, - &user_accounts_owner, + &lending_market_owner, + &wsol_mint::id(), + &test_reserve_config(), + &Keypair::new(), + 1000, + Some(Oracle { + pyth_product_pubkey: NULL_PUBKEY, + pyth_price_pubkey: NULL_PUBKEY, + switchboard_feed_pubkey: Some(NULL_PUBKEY), + }), ) .await - .unwrap_err(), + .unwrap_err() + .unwrap(); + + assert_eq!( + res, TransactionError::InstructionError( - 8, + 1, InstructionError::Custom(LendingError::InvalidOracleConfig as u32) ) ); } #[tokio::test] -async fn test_null_switchboard() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(75_000); - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - let mut sol_oracle = add_sol_oracle(&mut test); - sol_oracle.switchboard_feed_pubkey = solend_program::NULL_PUBKEY; - - let mut test_context = test.start_with_context().await; - test_context.warp_to_slot(240).unwrap(); // clock.slot = 240 - - let ProgramTestContext { - mut banks_client, - payer, - last_blockhash: _recent_blockhash, - .. - } = test_context; - - const RESERVE_AMOUNT: u64 = 42; - - let sol_user_liquidity_account = create_and_mint_to_token_account( - &mut banks_client, - spl_token::native_mint::id(), - None, - &payer, - user_accounts_owner.pubkey(), - RESERVE_AMOUNT, - ) - .await; - - let mut config = test_reserve_config(); - let fee_receiver_keypair = Keypair::new(); - config.fee_receiver = fee_receiver_keypair.pubkey(); +async fn test_already_initialized() { + let (mut test, lending_market, lending_market_owner) = setup().await; - let sol_reserve = TestReserve::init( - "sol".to_owned(), - &mut banks_client, + let keypair = Keypair::new(); + test.init_reserve( &lending_market, - &sol_oracle, - RESERVE_AMOUNT, - config, - spl_token::native_mint::id(), - sol_user_liquidity_account, - &fee_receiver_keypair, - &payer, - &user_accounts_owner, + &lending_market_owner, + &wsol_mint::id(), + &test_reserve_config(), + &keypair, + 1000, + None, ) .await .unwrap(); - sol_reserve.validate_state(&mut banks_client).await; - - let sol_liquidity_supply = - get_token_balance(&mut banks_client, sol_reserve.liquidity_supply_pubkey).await; - assert_eq!(sol_liquidity_supply, RESERVE_AMOUNT); - let user_sol_balance = - get_token_balance(&mut banks_client, sol_reserve.user_liquidity_pubkey).await; - assert_eq!(user_sol_balance, 0); - let user_sol_collateral_balance = - get_token_balance(&mut banks_client, sol_reserve.user_collateral_pubkey).await; - assert_eq!( - user_sol_collateral_balance, - RESERVE_AMOUNT * INITIAL_COLLATERAL_RATIO - ); -} - -#[tokio::test] -async fn test_already_initialized() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - let user_accounts_owner = Keypair::new(); - let user_transfer_authority = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - liquidity_amount: 42, - liquidity_mint_decimals: usdc_mint.decimals, - liquidity_mint_pubkey: usdc_mint.pubkey, - config: test_reserve_config(), - ..AddReserveArgs::default() - }, - ); - - let (mut banks_client, payer, recent_blockhash) = test.start().await; + let res = test + .init_reserve( + &lending_market, + &lending_market_owner, + &wsol_mint::id(), + &test_reserve_config(), + &keypair, + 1000, + None, + ) + .await + .unwrap_err() + .unwrap(); - let mut transaction = Transaction::new_with_payer( - &[init_reserve( - solend_program::id(), - 42, - usdc_test_reserve.config, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.user_collateral_pubkey, - usdc_test_reserve.pubkey, - usdc_test_reserve.liquidity_mint_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.collateral_mint_pubkey, - usdc_test_reserve.collateral_supply_pubkey, - usdc_oracle.pyth_product_pubkey, - usdc_oracle.pyth_price_pubkey, - usdc_oracle.switchboard_feed_pubkey, - lending_market.pubkey, - lending_market.owner.pubkey(), - user_transfer_authority.pubkey(), - )], - Some(&payer.pubkey()), - ); - transaction.sign( - &[&payer, &lending_market.owner, &user_transfer_authority], - recent_blockhash, - ); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( - 0, + 1, InstructionError::Custom(LendingError::AlreadyInitialized as u32) ) ); @@ -295,94 +250,45 @@ async fn test_already_initialized() { #[tokio::test] async fn test_invalid_fees() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - let sol_oracle = add_sol_oracle(&mut test); - - let (mut banks_client, payer, _recent_blockhash) = test.start().await; + let (mut test, lending_market, lending_market_owner) = setup().await; - const RESERVE_AMOUNT: u64 = 42; - - let sol_user_liquidity_account = create_and_mint_to_token_account( - &mut banks_client, - spl_token::native_mint::id(), - None, - &payer, - user_accounts_owner.pubkey(), - RESERVE_AMOUNT, - ) - .await; - - // fee above 100% - { - let mut config = test_reserve_config(); - config.fees = ReserveFees { + let invalid_fees = [ + // borrow fee over 100% + ReserveFees { borrow_fee_wad: 1_000_000_000_000_000_001, flash_loan_fee_wad: 1_000_000_000_000_000_001, host_fee_percentage: 0, - }; - - let fee_receiver_keypair = Keypair::new(); - config.fee_receiver = fee_receiver_keypair.pubkey(); - - assert_eq!( - TestReserve::init( - "sol".to_owned(), - &mut banks_client, - &lending_market, - &sol_oracle, - RESERVE_AMOUNT, - config, - spl_token::native_mint::id(), - sol_user_liquidity_account, - &fee_receiver_keypair, - &payer, - &user_accounts_owner, - ) - .await - .unwrap_err(), - TransactionError::InstructionError( - 8, - InstructionError::Custom(LendingError::InvalidConfig as u32) - ) - ); - } - - // host fee above 100% - { - let mut config = test_reserve_config(); - config.fees = ReserveFees { + }, + // host fee pct over 100% + ReserveFees { borrow_fee_wad: 10_000_000_000_000_000, flash_loan_fee_wad: 10_000_000_000_000_000, host_fee_percentage: 101, - }; - let fee_receiver_keypair = Keypair::new(); - config.fee_receiver = fee_receiver_keypair.pubkey(); + }, + ]; - assert_eq!( - TestReserve::init( - "sol".to_owned(), - &mut banks_client, + for fees in invalid_fees { + let res = test + .init_reserve( &lending_market, - &sol_oracle, - RESERVE_AMOUNT, - config, - spl_token::native_mint::id(), - sol_user_liquidity_account, - &fee_receiver_keypair, - &payer, - &user_accounts_owner, + &lending_market_owner, + &usdc_mint::id(), + &ReserveConfig { + fees, + ..test_reserve_config() + }, + &Keypair::new(), + 1000, + None, ) .await - .unwrap_err(), + .unwrap_err() + .unwrap(); + + assert_eq!( + res, TransactionError::InstructionError( - 8, + 1, InstructionError::Custom(LendingError::InvalidConfig as u32) ) ); @@ -391,180 +297,82 @@ async fn test_invalid_fees() { #[tokio::test] async fn test_update_reserve_config() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); + let (mut test, lending_market, lending_market_owner) = setup().await; - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let mint = add_usdc_mint(&mut test); - let oracle = add_usdc_oracle(&mut test); - let test_reserve = add_reserve( - &mut test, - &lending_market, - &oracle, - &user_accounts_owner, - AddReserveArgs { - liquidity_amount: 42, - liquidity_mint_decimals: mint.decimals, - liquidity_mint_pubkey: mint.pubkey, - config: test_reserve_config(), - ..AddReserveArgs::default() - }, - ); - - // Update the reserve config - let new_config: ReserveConfig = ReserveConfig { - optimal_utilization_rate: 75, - loan_to_value_ratio: 45, - liquidation_bonus: 10, - liquidation_threshold: 65, - min_borrow_rate: 1, - optimal_borrow_rate: 5, - max_borrow_rate: 45, - fees: ReserveFees { - borrow_fee_wad: 200_000_000_000, - flash_loan_fee_wad: 5_000_000_000_000_000, - host_fee_percentage: 15, - }, - deposit_limit: 1_000_000, - borrow_limit: 300_000, - fee_receiver: Keypair::new().pubkey(), - protocol_liquidation_fee: 30, - protocol_take_rate: 10, - }; + let wsol_reserve = test + .init_reserve( + &lending_market, + &lending_market_owner, + &wsol_mint::id(), + &test_reserve_config(), + &Keypair::new(), + 1000, + None, + ) + .await + .unwrap(); + + let new_reserve_config = test_reserve_config(); + lending_market + .update_reserve_config( + &mut test, + &lending_market_owner, + &wsol_reserve, + new_reserve_config, + None, + ) + .await + .unwrap(); - let (mut banks_client, payer, recent_blockhash) = test.start().await; - let mut transaction = Transaction::new_with_payer( - &[update_reserve_config( - solend_program::id(), - new_config, - test_reserve.pubkey, - lending_market.pubkey, - lending_market.owner.pubkey(), - oracle.pyth_product_pubkey, - oracle.pyth_price_pubkey, - oracle.switchboard_feed_pubkey, - )], - Some(&payer.pubkey()), + let wsol_reserve_post = test.load_account::(wsol_reserve.pubkey).await; + assert_eq!( + wsol_reserve_post.account, + Reserve { + config: new_reserve_config, + ..wsol_reserve.account + } ); - transaction.sign(&[&payer, &lending_market.owner], recent_blockhash); - assert!(banks_client.process_transaction(transaction).await.is_ok()); - - let updated_reserve = test_reserve.get_state(&mut banks_client).await; - assert_eq!(updated_reserve.config, new_config); } #[tokio::test] async fn test_update_invalid_oracle_config() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); + let (mut test, lending_market, lending_market_owner) = setup().await; + let wsol_reserve = test + .init_reserve( + &lending_market, + &lending_market_owner, + &wsol_mint::id(), + &test_reserve_config(), + &Keypair::new(), + 1000, + None, + ) + .await + .unwrap(); - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - let config = test_reserve_config(); - let mint = add_usdc_mint(&mut test); - let oracle = add_usdc_oracle(&mut test); - let test_reserve = add_reserve( - &mut test, - &lending_market, - &oracle, - &user_accounts_owner, - AddReserveArgs { - liquidity_amount: 42, - liquidity_mint_decimals: mint.decimals, - liquidity_mint_pubkey: mint.pubkey, - config, - ..AddReserveArgs::default() - }, - ); + let oracle = test.mints.get(&wsol_mint::id()).unwrap().unwrap(); - let (mut banks_client, payer, recent_blockhash) = test.start().await; + let new_reserve_config = test_reserve_config(); // Try setting both of the oracles to null: Should fail - let mut transaction = Transaction::new_with_payer( - &[update_reserve_config( - solend_program::id(), - config, - test_reserve.pubkey, - lending_market.pubkey, - lending_market.owner.pubkey(), - solend_program::NULL_PUBKEY, - solend_program::NULL_PUBKEY, - solend_program::NULL_PUBKEY, - )], - Some(&payer.pubkey()), - ); - - transaction.sign(&[&payer, &lending_market.owner], recent_blockhash); - assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError( - 0, - InstructionError::Custom(LendingError::InvalidOracleConfig as u32) + let res = lending_market + .update_reserve_config( + &mut test, + &lending_market_owner, + &wsol_reserve, + new_reserve_config, + Some(&Oracle { + pyth_product_pubkey: oracle.pyth_product_pubkey, + pyth_price_pubkey: NULL_PUBKEY, + switchboard_feed_pubkey: Some(NULL_PUBKEY), + }), ) - ); - - // Set one of the oracles to null - let mut transaction = Transaction::new_with_payer( - &[update_reserve_config( - solend_program::id(), - config, - test_reserve.pubkey, - lending_market.pubkey, - lending_market.owner.pubkey(), - oracle.pyth_product_pubkey, - oracle.pyth_price_pubkey, - solend_program::NULL_PUBKEY, - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &lending_market.owner], recent_blockhash); - assert!(banks_client.process_transaction(transaction).await.is_ok()); - let updated_reserve = test_reserve.get_state(&mut banks_client).await; - assert_eq!(updated_reserve.config, config); - assert_eq!( - updated_reserve.liquidity.pyth_oracle_pubkey, - oracle.pyth_price_pubkey - ); - assert_eq!( - updated_reserve.liquidity.switchboard_oracle_pubkey, - solend_program::NULL_PUBKEY - ); - - // Setting both oracles to null still fails, even if one is - // already null - let mut transaction = Transaction::new_with_payer( - &[update_reserve_config( - solend_program::id(), - config, - test_reserve.pubkey, - lending_market.pubkey, - lending_market.owner.pubkey(), - solend_program::NULL_PUBKEY, - solend_program::NULL_PUBKEY, - solend_program::NULL_PUBKEY, - )], - Some(&payer.pubkey()), - ); + .await + .unwrap_err() + .unwrap(); - transaction.sign(&[&payer, &lending_market.owner], recent_blockhash); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( 0, InstructionError::Custom(LendingError::InvalidOracleConfig as u32) diff --git a/token-lending/program/tests/liquidate_obligation.rs b/token-lending/program/tests/liquidate_obligation.rs index 6e8317e7b61..4261628cfc3 100644 --- a/token-lending/program/tests/liquidate_obligation.rs +++ b/token-lending/program/tests/liquidate_obligation.rs @@ -2,137 +2,39 @@ mod helpers; +use crate::solend_program_test::scenario_1; + use helpers::*; +use solana_program::instruction::InstructionError; + use solana_program_test::*; -use solana_sdk::{ - signature::{Keypair, Signer}, - transaction::Transaction, -}; -use solend_program::{ - instruction::{liquidate_obligation, refresh_obligation}, - processor::process_instruction, - state::{INITIAL_COLLATERAL_RATIO, LIQUIDATION_CLOSE_FACTOR}, -}; -use spl_token::instruction::approve; + +use solana_sdk::transaction::TransactionError; +use solend_program::error::LendingError; #[tokio::test] async fn test_fail_deprecated() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(62_000); - - // 100 SOL collateral - const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; - // 100 SOL * 80% LTV -> 80 SOL * 20 USDC -> 1600 USDC borrow - const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = 1_600 * FRACTIONAL_TO_USDC; - // 1600 USDC * 20% -> 320 USDC liquidation - const USDC_LIQUIDATION_AMOUNT_FRACTIONAL: u64 = - USDC_BORROW_AMOUNT_FRACTIONAL * (LIQUIDATION_CLOSE_FACTOR as u64) / 100; - // 320 USDC / 20 USDC per SOL -> 16 SOL + 10% bonus -> 17.6 SOL (88/5) - // const SOL_LIQUIDATION_AMOUNT_LAMPORTS: u64 = - // LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO * 88 * (LIQUIDATION_CLOSE_FACTOR as u64) / 100; - - const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS; - const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 2 * USDC_BORROW_AMOUNT_FRACTIONAL; - - let user_accounts_owner = Keypair::new(); - let user_transfer_authority = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let mut reserve_config = test_reserve_config(); - reserve_config.loan_to_value_ratio = 50; - reserve_config.liquidation_threshold = 80; - reserve_config.liquidation_bonus = 10; - - let sol_oracle = add_sol_oracle(&mut test); - let sol_test_reserve = add_reserve( - &mut test, - &lending_market, - &sol_oracle, - &user_accounts_owner, - AddReserveArgs { - collateral_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, - liquidity_mint_pubkey: spl_token::native_mint::id(), - liquidity_mint_decimals: 9, - config: reserve_config, - mark_fresh: true, - ..AddReserveArgs::default() - }, - ); - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - borrow_amount: USDC_BORROW_AMOUNT_FRACTIONAL, - user_liquidity_amount: USDC_BORROW_AMOUNT_FRACTIONAL, - liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - mark_fresh: true, - ..AddReserveArgs::default() - }, - ); - - let test_obligation = add_obligation( - &mut test, - &lending_market, - &user_accounts_owner, - AddObligationArgs { - deposits: &[(&sol_test_reserve, SOL_DEPOSIT_AMOUNT_LAMPORTS)], - borrows: &[(&usdc_test_reserve, USDC_BORROW_AMOUNT_FRACTIONAL)], - ..AddObligationArgs::default() - }, - ); - - let (mut banks_client, payer, recent_blockhash) = test.start().await; - - let mut transaction = Transaction::new_with_payer( - &[ - approve( - &spl_token::id(), - &usdc_test_reserve.user_liquidity_pubkey, - &user_transfer_authority.pubkey(), - &user_accounts_owner.pubkey(), - &[], - USDC_LIQUIDATION_AMOUNT_FRACTIONAL, - ) - .unwrap(), - refresh_obligation( - solend_program::id(), - test_obligation.pubkey, - vec![sol_test_reserve.pubkey, usdc_test_reserve.pubkey], - ), - liquidate_obligation( - solend_program::id(), - USDC_LIQUIDATION_AMOUNT_FRACTIONAL, - usdc_test_reserve.user_liquidity_pubkey, - sol_test_reserve.user_collateral_pubkey, - usdc_test_reserve.pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - sol_test_reserve.pubkey, - sol_test_reserve.collateral_supply_pubkey, - test_obligation.pubkey, - lending_market.pubkey, - user_transfer_authority.pubkey(), - ), - ], - Some(&payer.pubkey()), - ); - - transaction.sign( - &[&payer, &user_accounts_owner, &user_transfer_authority], - recent_blockhash, + let (mut test, lending_market, usdc_reserve, wsol_reserve, user, obligation) = + scenario_1(&test_reserve_config(), &test_reserve_config()).await; + + let res = lending_market + .liquidate_obligation( + &mut test, + &wsol_reserve, + &usdc_reserve, + &obligation, + &user, + 1, + ) + .await + .unwrap_err() + .unwrap(); + + assert_eq!( + res, + TransactionError::InstructionError( + 3, + InstructionError::Custom(LendingError::DeprecatedInstruction as u32) + ) ); - assert!(banks_client.process_transaction(transaction).await.is_err()); } diff --git a/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs b/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs index 3d321fed750..4f3dc983170 100644 --- a/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs +++ b/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs @@ -1,391 +1,385 @@ #![cfg(feature = "test-bpf")] +use crate::solend_program_test::MintSupplyChange; +use solend_program::math::TrySub; +use solend_program::state::LastUpdate; +use solend_program::state::ObligationCollateral; +use solend_program::state::ObligationLiquidity; +use solend_program::state::ReserveConfig; mod helpers; +use crate::solend_program_test::scenario_1; +use crate::solend_program_test::BalanceChecker; +use crate::solend_program_test::PriceArgs; +use crate::solend_program_test::TokenBalanceChange; +use crate::solend_program_test::User; use helpers::*; use solana_program_test::*; -use solana_sdk::{ - signature::{Keypair, Signer}, - transaction::Transaction, -}; -use solend_program::{ - instruction::{liquidate_obligation_and_redeem_reserve_collateral, refresh_obligation}, - processor::process_instruction, - state::{INITIAL_COLLATERAL_RATIO, LIQUIDATION_CLOSE_FACTOR}, -}; -use std::cmp::{max, min}; +use solana_sdk::signature::Keypair; +use solend_program::math::Decimal; +use solend_program::state::LendingMarket; +use solend_program::state::Obligation; +use solend_program::state::Reserve; +use solend_program::state::ReserveCollateral; +use solend_program::state::ReserveLiquidity; +use solend_program::state::LIQUIDATION_CLOSE_FACTOR; -#[tokio::test] -async fn test_success() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); +use std::collections::HashSet; - // limit to track compute unit increase - test.set_compute_max_units(101_000); - - // 100 SOL collateral - const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; - // 100 SOL * 80% LTV -> 80 SOL * 20 USDC -> 1600 USDC borrow - const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = 1_600 * FRACTIONAL_TO_USDC; - // 1600 USDC * 20% -> 320 USDC liquidation - const USDC_LIQUIDATION_AMOUNT_FRACTIONAL: u64 = - USDC_BORROW_AMOUNT_FRACTIONAL * (LIQUIDATION_CLOSE_FACTOR as u64) / 100; - // 320 USDC / 20 USDC per SOL -> 16 SOL + 10% bonus -> 17.6 SOL (88/5) - const SOL_LIQUIDATION_AMOUNT_LAMPORTS: u64 = - LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO * 88 * (LIQUIDATION_CLOSE_FACTOR as u64) / 100; - - const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS; - const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 2 * USDC_BORROW_AMOUNT_FRACTIONAL; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let mut reserve_config = test_reserve_config(); - reserve_config.loan_to_value_ratio = 50; - reserve_config.liquidation_threshold = 80; - reserve_config.liquidation_bonus = 10; - - let sol_oracle = add_sol_oracle(&mut test); - let sol_test_reserve = add_reserve( - &mut test, - &lending_market, - &sol_oracle, - &user_accounts_owner, - AddReserveArgs { - collateral_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, - liquidity_amount: SOL_DEPOSIT_AMOUNT_LAMPORTS / INITIAL_COLLATERAL_RATIO, - liquidity_mint_pubkey: spl_token::native_mint::id(), - liquidity_mint_decimals: 9, - config: reserve_config, - mark_fresh: true, - ..AddReserveArgs::default() +#[tokio::test] +async fn test_success_new() { + let (mut test, lending_market, usdc_reserve, wsol_reserve, user, obligation) = scenario_1( + &ReserveConfig { + protocol_liquidation_fee: 30, + ..test_reserve_config() }, - ); + &test_reserve_config(), + ) + .await; - let mut reserve_config = test_reserve_config(); - reserve_config.loan_to_value_ratio = 50; - reserve_config.liquidation_threshold = 80; - reserve_config.liquidation_bonus = 10; - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( + let liquidator = User::new_with_balances( &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - borrow_amount: USDC_BORROW_AMOUNT_FRACTIONAL, - user_liquidity_amount: USDC_BORROW_AMOUNT_FRACTIONAL, - liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - mark_fresh: true, - ..AddReserveArgs::default() - }, - ); + &[ + (&wsol_mint::id(), 100 * LAMPORTS_TO_SOL), + (&usdc_reserve.account.collateral.mint_pubkey, 0), + (&usdc_mint::id(), 0), + ], + ) + .await; - let test_obligation = add_obligation( + let balance_checker = BalanceChecker::start( &mut test, - &lending_market, - &user_accounts_owner, - AddObligationArgs { - deposits: &[(&sol_test_reserve, SOL_DEPOSIT_AMOUNT_LAMPORTS)], - borrows: &[(&usdc_test_reserve, USDC_BORROW_AMOUNT_FRACTIONAL)], - ..AddObligationArgs::default() - }, - ); - - let (mut banks_client, payer, recent_blockhash) = test.start().await; - - let initial_user_liquidity_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; - let initial_liquidity_supply_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; - let initial_user_collateral_balance = - get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; - let initial_collateral_supply_balance = - get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; - let initial_user_withdraw_liquidity_balance = - get_token_balance(&mut banks_client, sol_test_reserve.user_liquidity_pubkey).await; - let initial_fee_receiver_withdraw_liquidity_balance = - get_token_balance(&mut banks_client, sol_test_reserve.config.fee_receiver).await; - - let mut transaction = Transaction::new_with_payer( &[ - refresh_obligation( - solend_program::id(), - test_obligation.pubkey, - vec![sol_test_reserve.pubkey, usdc_test_reserve.pubkey], - ), - liquidate_obligation_and_redeem_reserve_collateral( - solend_program::id(), - USDC_LIQUIDATION_AMOUNT_FRACTIONAL, - usdc_test_reserve.user_liquidity_pubkey, - sol_test_reserve.user_collateral_pubkey, - sol_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - sol_test_reserve.pubkey, - sol_test_reserve.collateral_mint_pubkey, - sol_test_reserve.collateral_supply_pubkey, - sol_test_reserve.liquidity_supply_pubkey, - sol_test_reserve.config.fee_receiver, - test_obligation.pubkey, - lending_market.pubkey, - user_accounts_owner.pubkey(), - ), + &usdc_reserve, + &user, + &wsol_reserve, + &usdc_reserve, + &liquidator, ], - Some(&payer.pubkey()), - ); - - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); - assert!(banks_client.process_transaction(transaction).await.is_ok()); - - let user_liquidity_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; - assert_eq!( - user_liquidity_balance, - initial_user_liquidity_balance - USDC_LIQUIDATION_AMOUNT_FRACTIONAL - ); - - let liquidity_supply_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; + ) + .await; + + // close LTV is 0.55, we've deposited 100k USDC and borrowed 10 SOL. + // obligation gets liquidated if 100k * 0.55 = 10 SOL * sol_price => sol_price = 5.5k + test.set_price( + &wsol_mint::id(), + PriceArgs { + price: 5500, + conf: 0, + expo: 0, + }, + ) + .await; + + lending_market + .liquidate_obligation_and_redeem_reserve_collateral( + &mut test, + &wsol_reserve, + &usdc_reserve, + &obligation, + &liquidator, + u64::MAX, + ) + .await + .unwrap(); + + let (balance_changes, mint_supply_changes) = + balance_checker.find_balance_changes(&mut test).await; + + let bonus = usdc_reserve.account.config.liquidation_bonus as u64; + let protocol_liquidation_fee_pct = usdc_reserve.account.config.protocol_liquidation_fee as u64; + + let expected_borrow_repaid = 10 * (LIQUIDATION_CLOSE_FACTOR as u64) / 100; + let expected_usdc_withdrawn = expected_borrow_repaid * 5500 * (100 + bonus) / 100; + + let expected_total_bonus = expected_usdc_withdrawn - expected_borrow_repaid * 5500; + let expected_protocol_liquidation_fee = + expected_total_bonus * protocol_liquidation_fee_pct / 100; + + let expected_balance_changes = HashSet::from([ + // liquidator + TokenBalanceChange { + token_account: liquidator.get_account(&usdc_mint::id()).unwrap(), + mint: usdc_mint::id(), + diff: ((expected_usdc_withdrawn - expected_protocol_liquidation_fee) + * FRACTIONAL_TO_USDC) as i128, + }, + TokenBalanceChange { + token_account: liquidator.get_account(&wsol_mint::id()).unwrap(), + mint: wsol_mint::id(), + diff: -((expected_borrow_repaid * LAMPORTS_TO_SOL) as i128), + }, + // usdc reserve + TokenBalanceChange { + token_account: usdc_reserve.account.collateral.supply_pubkey, + mint: usdc_reserve.account.collateral.mint_pubkey, + diff: -((expected_usdc_withdrawn * FRACTIONAL_TO_USDC) as i128), + }, + TokenBalanceChange { + token_account: usdc_reserve.account.liquidity.supply_pubkey, + mint: usdc_mint::id(), + diff: -((expected_usdc_withdrawn * FRACTIONAL_TO_USDC) as i128), + }, + TokenBalanceChange { + token_account: usdc_reserve.account.config.fee_receiver, + mint: usdc_mint::id(), + diff: (expected_protocol_liquidation_fee * FRACTIONAL_TO_USDC) as i128, + }, + // wsol reserve + TokenBalanceChange { + token_account: wsol_reserve.account.liquidity.supply_pubkey, + mint: wsol_mint::id(), + diff: (expected_borrow_repaid * LAMPORTS_TO_SOL) as i128, + }, + ]); + assert_eq!(balance_changes, expected_balance_changes); assert_eq!( - liquidity_supply_balance, - initial_liquidity_supply_balance + USDC_LIQUIDATION_AMOUNT_FRACTIONAL + mint_supply_changes, + HashSet::from([MintSupplyChange { + mint: usdc_reserve.account.collateral.mint_pubkey, + diff: -((expected_usdc_withdrawn * FRACTIONAL_TO_USDC) as i128) + }]) ); - let user_collateral_balance = - get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; - assert_eq!(user_collateral_balance, initial_user_collateral_balance); + // check program state + let lending_market_post = test + .load_account::(lending_market.pubkey) + .await; + assert_eq!(lending_market_post.account, lending_market.account); - let user_withdraw_liquidity_balance = - get_token_balance(&mut banks_client, sol_test_reserve.user_liquidity_pubkey).await; - let fee_receiver_withdraw_liquidity_balance = - get_token_balance(&mut banks_client, sol_test_reserve.config.fee_receiver).await; + let usdc_reserve_post = test.load_account::(usdc_reserve.pubkey).await; assert_eq!( - user_withdraw_liquidity_balance + fee_receiver_withdraw_liquidity_balance, - initial_user_withdraw_liquidity_balance - + initial_fee_receiver_withdraw_liquidity_balance - + SOL_LIQUIDATION_AMOUNT_LAMPORTS + usdc_reserve_post.account, + Reserve { + liquidity: ReserveLiquidity { + available_amount: usdc_reserve.account.liquidity.available_amount + - expected_usdc_withdrawn * FRACTIONAL_TO_USDC, + ..usdc_reserve.account.liquidity + }, + collateral: ReserveCollateral { + mint_total_supply: usdc_reserve.account.collateral.mint_total_supply + - expected_usdc_withdrawn * FRACTIONAL_TO_USDC, + ..usdc_reserve.account.collateral + }, + ..usdc_reserve.account + } ); + let wsol_reserve_post = test.load_account::(wsol_reserve.pubkey).await; assert_eq!( - // 30% of the bonus - max(SOL_LIQUIDATION_AMOUNT_LAMPORTS * 3 / 10 / 11, 1), - (fee_receiver_withdraw_liquidity_balance - initial_fee_receiver_withdraw_liquidity_balance) + wsol_reserve_post.account, + Reserve { + liquidity: ReserveLiquidity { + available_amount: wsol_reserve.account.liquidity.available_amount + + expected_borrow_repaid * LAMPORTS_TO_SOL, + borrowed_amount_wads: wsol_reserve + .account + .liquidity + .borrowed_amount_wads + .try_sub(Decimal::from(expected_borrow_repaid * LAMPORTS_TO_SOL)) + .unwrap(), + market_price: Decimal::from(5500u64), + ..wsol_reserve.account.liquidity + }, + ..wsol_reserve.account + } ); - let collateral_supply_balance = - get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; + let obligation_post = test.load_account::(obligation.pubkey).await; assert_eq!( - collateral_supply_balance, - initial_collateral_supply_balance - SOL_LIQUIDATION_AMOUNT_LAMPORTS + obligation_post.account, + Obligation { + last_update: LastUpdate { + slot: 1000, + stale: true + }, + deposits: [ObligationCollateral { + deposit_reserve: usdc_reserve.pubkey, + deposited_amount: (100_000 - expected_usdc_withdrawn) * FRACTIONAL_TO_USDC, + market_value: Decimal::from(100_000u64) // old value + }] + .to_vec(), + borrows: [ObligationLiquidity { + borrow_reserve: wsol_reserve.pubkey, + cumulative_borrow_rate_wads: Decimal::one(), + borrowed_amount_wads: Decimal::from(10 * LAMPORTS_TO_SOL) + .try_sub(Decimal::from(expected_borrow_repaid * LAMPORTS_TO_SOL)) + .unwrap(), + market_value: Decimal::from(55_000u64), + }] + .to_vec(), + deposited_value: Decimal::from(100_000u64), + borrowed_value: Decimal::from(55_000u64), + allowed_borrow_value: Decimal::from(50_000u64), + unhealthy_borrow_value: Decimal::from(55_000u64), + ..obligation.account + } ); - - let obligation = test_obligation.get_state(&mut banks_client).await; - assert_eq!( - obligation.deposits[0].deposited_amount, - SOL_DEPOSIT_AMOUNT_LAMPORTS - SOL_LIQUIDATION_AMOUNT_LAMPORTS - ); - assert_eq!( - obligation.borrows[0].borrowed_amount_wads, - (USDC_BORROW_AMOUNT_FRACTIONAL - USDC_LIQUIDATION_AMOUNT_FRACTIONAL).into() - ) } #[tokio::test] -async fn test_success_insufficent_liquidity() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(101_000); - - // 100 SOL collateral - const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; - // 100 SOL * 80% LTV -> 80 SOL * 20 USDC -> 1600 USDC borrow - const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = 1_600 * FRACTIONAL_TO_USDC; - // 1600 USDC * 20% -> 320 USDC liquidation - const USDC_LIQUIDATION_AMOUNT_FRACTIONAL: u64 = - USDC_BORROW_AMOUNT_FRACTIONAL * (LIQUIDATION_CLOSE_FACTOR as u64) / 100; - // 320 USDC / 20 USDC per SOL -> 16 SOL + 10% bonus -> 17.6 SOL (88/5) - const SOL_LIQUIDATION_AMOUNT_LAMPORTS: u64 = - LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO * 88 * (LIQUIDATION_CLOSE_FACTOR as u64) / 100; - - const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS; - const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 2 * USDC_BORROW_AMOUNT_FRACTIONAL; - const AVAILABLE_SOL_LIQUIDITY: u64 = LAMPORTS_TO_SOL / 2; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let mut reserve_config = test_reserve_config(); - reserve_config.loan_to_value_ratio = 50; - reserve_config.liquidation_threshold = 80; - reserve_config.liquidation_bonus = 10; - - let sol_oracle = add_sol_oracle(&mut test); - let sol_test_reserve = add_reserve( +async fn test_success_insufficient_liquidity() { + let (mut test, lending_market, usdc_reserve, wsol_reserve, user, obligation) = + scenario_1(&test_reserve_config(), &test_reserve_config()).await; + + // basically the same test as above, but now someone borrows a lot of USDC so the liquidatior + // partially receives USDC and cUSDC + { + let usdc_borrower = User::new_with_balances( + &mut test, + &[ + (&usdc_mint::id(), 0), + (&wsol_mint::id(), 20_000 * LAMPORTS_TO_SOL), + (&wsol_reserve.account.collateral.mint_pubkey, 0), + ], + ) + .await; + + let obligation = lending_market + .init_obligation(&mut test, Keypair::new(), &usdc_borrower) + .await + .unwrap(); + + lending_market + .deposit_reserve_liquidity_and_obligation_collateral( + &mut test, + &wsol_reserve, + &obligation, + &usdc_borrower, + 20_000 * LAMPORTS_TO_SOL, + ) + .await + .unwrap(); + + let obligation = test.load_account::(obligation.pubkey).await; + lending_market + .borrow_obligation_liquidity( + &mut test, + &usdc_reserve, + &obligation, + &usdc_borrower, + &usdc_borrower.get_account(&usdc_mint::id()).unwrap(), + u64::MAX, + ) + .await + .unwrap() + } + + let liquidator = User::new_with_balances( &mut test, - &lending_market, - &sol_oracle, - &user_accounts_owner, - AddReserveArgs { - collateral_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, - liquidity_amount: SOL_DEPOSIT_AMOUNT_LAMPORTS / INITIAL_COLLATERAL_RATIO, - borrow_amount: SOL_DEPOSIT_AMOUNT_LAMPORTS - AVAILABLE_SOL_LIQUIDITY, - liquidity_mint_pubkey: spl_token::native_mint::id(), - liquidity_mint_decimals: 9, - config: reserve_config, - mark_fresh: true, - ..AddReserveArgs::default() - }, - ); + &[ + (&wsol_mint::id(), 100 * LAMPORTS_TO_SOL), + (&usdc_reserve.account.collateral.mint_pubkey, 0), + (&usdc_mint::id(), 0), + ], + ) + .await; - let mut reserve_config = test_reserve_config(); - reserve_config.loan_to_value_ratio = 50; - reserve_config.liquidation_threshold = 80; - reserve_config.liquidation_bonus = 10; - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( + let balance_checker = BalanceChecker::start( &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - borrow_amount: USDC_BORROW_AMOUNT_FRACTIONAL, - user_liquidity_amount: USDC_BORROW_AMOUNT_FRACTIONAL, - liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - mark_fresh: true, - ..AddReserveArgs::default() + &[&usdc_reserve, &user, &wsol_reserve, &liquidator], + ) + .await; + + // close LTV is 0.55, we've deposited 100k USDC and borrowed 10 SOL. + // obligation gets liquidated if 100k * 0.55 = 10 SOL * sol_price => sol_price == 5.5k + test.set_price( + &wsol_mint::id(), + PriceArgs { + price: 5500, + conf: 0, + expo: 0, }, - ); - - let test_obligation = add_obligation( - &mut test, - &lending_market, - &user_accounts_owner, - AddObligationArgs { - deposits: &[(&sol_test_reserve, SOL_DEPOSIT_AMOUNT_LAMPORTS)], - borrows: &[(&usdc_test_reserve, USDC_BORROW_AMOUNT_FRACTIONAL)], - ..AddObligationArgs::default() + ) + .await; + + let lending_market = test + .load_account::(lending_market.pubkey) + .await; + let usdc_reserve = test.load_account::(usdc_reserve.pubkey).await; + let wsol_reserve = test.load_account::(wsol_reserve.pubkey).await; + + let available_amount = usdc_reserve.account.liquidity.available_amount / FRACTIONAL_TO_USDC; + + lending_market + .liquidate_obligation_and_redeem_reserve_collateral( + &mut test, + &wsol_reserve, + &usdc_reserve, + &obligation, + &liquidator, + u64::MAX, + ) + .await + .unwrap(); + + let (balance_changes, mint_supply_changes) = + balance_checker.find_balance_changes(&mut test).await; + + let bonus = usdc_reserve.account.config.liquidation_bonus as u64; + + let expected_borrow_repaid = 10 * (LIQUIDATION_CLOSE_FACTOR as u64) / 100; + let expected_cusdc_withdrawn = + expected_borrow_repaid * 5500 * (100 + bonus) / 100 - available_amount; + let expected_protocol_liquidation_fee = usdc_reserve + .account + .calculate_protocol_liquidation_fee(available_amount * FRACTIONAL_TO_USDC) + .unwrap(); + + let expected_balance_changes = HashSet::from([ + // liquidator + TokenBalanceChange { + token_account: liquidator.get_account(&usdc_mint::id()).unwrap(), + mint: usdc_mint::id(), + diff: (available_amount * FRACTIONAL_TO_USDC - expected_protocol_liquidation_fee) + as i128, }, - ); - - let (mut banks_client, payer, recent_blockhash) = test.start().await; - - let initial_user_liquidity_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; - let initial_liquidity_supply_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; - let initial_user_collateral_balance = - get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; - let initial_collateral_supply_balance = - get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; - let initial_user_withdraw_liquidity_balance = - get_token_balance(&mut banks_client, sol_test_reserve.user_liquidity_pubkey).await; - let initial_fee_reciever_withdraw_liquidity_balance = - get_token_balance(&mut banks_client, sol_test_reserve.config.fee_receiver).await; - - let mut transaction = Transaction::new_with_payer( - &[ - refresh_obligation( - solend_program::id(), - test_obligation.pubkey, - vec![sol_test_reserve.pubkey, usdc_test_reserve.pubkey], - ), - liquidate_obligation_and_redeem_reserve_collateral( - solend_program::id(), - USDC_LIQUIDATION_AMOUNT_FRACTIONAL, - usdc_test_reserve.user_liquidity_pubkey, - sol_test_reserve.user_collateral_pubkey, - sol_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - sol_test_reserve.pubkey, - sol_test_reserve.collateral_mint_pubkey, - sol_test_reserve.collateral_supply_pubkey, - sol_test_reserve.liquidity_supply_pubkey, - sol_test_reserve.config.fee_receiver, - test_obligation.pubkey, - lending_market.pubkey, - user_accounts_owner.pubkey(), - ), - ], - Some(&payer.pubkey()), - ); - - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); - assert!(banks_client.process_transaction(transaction).await.is_ok()); - - let user_liquidity_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; - assert_eq!( - user_liquidity_balance, - initial_user_liquidity_balance - USDC_LIQUIDATION_AMOUNT_FRACTIONAL - ); - - let liquidity_supply_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; - assert_eq!( - liquidity_supply_balance, - initial_liquidity_supply_balance + USDC_LIQUIDATION_AMOUNT_FRACTIONAL - ); - - // assert_eq!(USDC_LIQUIDATION_AMOUNT_FRACTIONAL, 0); - let user_collateral_balance = - get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; - assert_eq!( - user_collateral_balance, - initial_user_collateral_balance + SOL_LIQUIDATION_AMOUNT_LAMPORTS - AVAILABLE_SOL_LIQUIDITY - ); - - let user_withdraw_liquidity_balance = - get_token_balance(&mut banks_client, sol_test_reserve.user_liquidity_pubkey).await; - let fee_reciever_withdraw_liquidity_balance = - get_token_balance(&mut banks_client, sol_test_reserve.config.fee_receiver).await; - assert_eq!( - user_withdraw_liquidity_balance + fee_reciever_withdraw_liquidity_balance, - initial_user_withdraw_liquidity_balance - + initial_fee_reciever_withdraw_liquidity_balance - + AVAILABLE_SOL_LIQUIDITY - ); - - assert_eq!( - // 30% of the bonus (math looks stupid because need to divide but round up so x/y -> (x-1)/y+1 ) - max( - (min(SOL_LIQUIDATION_AMOUNT_LAMPORTS, AVAILABLE_SOL_LIQUIDITY) * 3 - 1) / (10 * 11) + 1, - 1 - ), - (fee_reciever_withdraw_liquidity_balance - initial_fee_reciever_withdraw_liquidity_balance) - ); - - let collateral_supply_balance = - get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; + TokenBalanceChange { + token_account: liquidator + .get_account(&usdc_reserve.account.collateral.mint_pubkey) + .unwrap(), + mint: usdc_reserve.account.collateral.mint_pubkey, + diff: (expected_cusdc_withdrawn * FRACTIONAL_TO_USDC) as i128, + }, + TokenBalanceChange { + token_account: liquidator.get_account(&wsol_mint::id()).unwrap(), + mint: wsol_mint::id(), + diff: -((expected_borrow_repaid * LAMPORTS_TO_SOL) as i128), + }, + // usdc reserve + TokenBalanceChange { + token_account: usdc_reserve.account.collateral.supply_pubkey, + mint: usdc_reserve.account.collateral.mint_pubkey, + diff: -(((expected_cusdc_withdrawn + available_amount) * FRACTIONAL_TO_USDC) as i128), + }, + TokenBalanceChange { + token_account: usdc_reserve.account.liquidity.supply_pubkey, + mint: usdc_mint::id(), + diff: -((available_amount * FRACTIONAL_TO_USDC) as i128), + }, + TokenBalanceChange { + token_account: usdc_reserve.account.config.fee_receiver, + mint: usdc_mint::id(), + diff: expected_protocol_liquidation_fee as i128, + }, + // wsol reserve + TokenBalanceChange { + token_account: wsol_reserve.account.liquidity.supply_pubkey, + mint: wsol_mint::id(), + diff: (expected_borrow_repaid * LAMPORTS_TO_SOL) as i128, + }, + ]); assert_eq!( - collateral_supply_balance, - initial_collateral_supply_balance - SOL_LIQUIDATION_AMOUNT_LAMPORTS + balance_changes, expected_balance_changes, + "{:#?} {:#?}", + balance_changes, expected_balance_changes ); - let obligation = test_obligation.get_state(&mut banks_client).await; assert_eq!( - obligation.deposits[0].deposited_amount, - SOL_DEPOSIT_AMOUNT_LAMPORTS - SOL_LIQUIDATION_AMOUNT_LAMPORTS + mint_supply_changes, + HashSet::from([MintSupplyChange { + mint: usdc_reserve.account.collateral.mint_pubkey, + diff: -((available_amount * FRACTIONAL_TO_USDC) as i128) + }]) ); - assert_eq!( - obligation.borrows[0].borrowed_amount_wads, - (USDC_BORROW_AMOUNT_FRACTIONAL - USDC_LIQUIDATION_AMOUNT_FRACTIONAL).into() - ) } diff --git a/token-lending/program/tests/obligation_end_to_end.rs b/token-lending/program/tests/obligation_end_to_end.rs index 4a1cbbbe386..00700cc4057 100644 --- a/token-lending/program/tests/obligation_end_to_end.rs +++ b/token-lending/program/tests/obligation_end_to_end.rs @@ -1,549 +1,140 @@ #![cfg(feature = "test-bpf")] +use crate::solend_program_test::TokenBalanceChange; +use solend_program::math::TryMul; +use solend_program::math::TrySub; +use solend_program::state::ReserveConfig; +use solend_program::state::ReserveFees; mod helpers; +use std::collections::HashSet; + +use crate::solend_program_test::setup_world; +use crate::solend_program_test::BalanceChecker; +use crate::solend_program_test::Info; +use crate::solend_program_test::SolendProgramTest; +use crate::solend_program_test::User; use helpers::*; use solana_program_test::*; -use solana_sdk::{ - account::Account, - signature::{Keypair, Signer}, - system_instruction::create_account, - transaction::Transaction, -}; -use solend_program::{ - instruction::{ - borrow_obligation_liquidity, deposit_obligation_collateral, init_obligation, - refresh_obligation, refresh_reserve, repay_obligation_liquidity, - withdraw_obligation_collateral, - }, - math::Decimal, - processor::process_instruction, - state::{Obligation, INITIAL_COLLATERAL_RATIO}, -}; -use spl_token::{instruction::approve, solana_program::program_pack::Pack}; - -#[tokio::test] -async fn test_success() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(163_000); - - const FEE_AMOUNT: u64 = 100; - const HOST_FEE_AMOUNT: u64 = 20; - - const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; - const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = SOL_DEPOSIT_AMOUNT_LAMPORTS; - - const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 1_000 * FRACTIONAL_TO_USDC; - const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = USDC_RESERVE_LIQUIDITY_FRACTIONAL - FEE_AMOUNT; - const USDC_REPAY_AMOUNT_FRACTIONAL: u64 = USDC_RESERVE_LIQUIDITY_FRACTIONAL; - - let user_accounts_owner = Keypair::new(); - let user_accounts_owner_pubkey = user_accounts_owner.pubkey(); - - let user_transfer_authority = Keypair::new(); - let user_transfer_authority_pubkey = user_transfer_authority.pubkey(); - - let obligation_keypair = Keypair::new(); - let obligation_pubkey = obligation_keypair.pubkey(); - - let lending_market = add_lending_market(&mut test); - - let mut reserve_config = test_reserve_config(); - reserve_config.loan_to_value_ratio = 50; - - let sol_oracle = add_sol_oracle(&mut test); - let sol_test_reserve = add_reserve( - &mut test, - &lending_market, - &sol_oracle, - &user_accounts_owner, - AddReserveArgs { - user_liquidity_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, - liquidity_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, - liquidity_mint_pubkey: spl_token::native_mint::id(), - liquidity_mint_decimals: 9, - config: reserve_config, - ..AddReserveArgs::default() - }, - ); - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - user_liquidity_amount: FEE_AMOUNT, - liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - ..AddReserveArgs::default() +use solana_sdk::signature::Keypair; +use solend_program::math::Decimal; +use solend_program::state::LendingMarket; +use solend_program::state::Reserve; + +async fn setup() -> ( + SolendProgramTest, + Info, + Info, + Info, + User, +) { + let (test, lending_market, usdc_reserve, wsol_reserve, _, user) = setup_world( + &test_reserve_config(), + &ReserveConfig { + fees: ReserveFees { + borrow_fee_wad: 100_000_000_000, + flash_loan_fee_wad: 0, + host_fee_percentage: 20, + }, + ..test_reserve_config() }, - ); - - let mut test_context = test.start_with_context().await; - test_context.warp_to_slot(240).unwrap(); // clock.slot = 240 - - let ProgramTestContext { - mut banks_client, - payer, - last_blockhash: recent_blockhash, - .. - } = test_context; - let payer_pubkey = payer.pubkey(); - - let initial_collateral_supply_balance = - get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; - let initial_user_collateral_balance = - get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; - let initial_liquidity_supply = - get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; - let initial_user_liquidity_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; - - let rent = banks_client.get_rent().await.unwrap(); - - let mut transaction = Transaction::new_with_payer( - &[ - // 0 - create_account( - &payer.pubkey(), - &obligation_keypair.pubkey(), - rent.minimum_balance(Obligation::LEN), - Obligation::LEN as u64, - &solend_program::id(), - ), - // 1 - init_obligation( - solend_program::id(), - obligation_pubkey, - lending_market.pubkey, - user_accounts_owner_pubkey, - ), - // 2 - approve( - &spl_token::id(), - &sol_test_reserve.user_collateral_pubkey, - &user_transfer_authority_pubkey, - &user_accounts_owner_pubkey, - &[], - SOL_DEPOSIT_AMOUNT_LAMPORTS, - ) - .unwrap(), - // 3 - deposit_obligation_collateral( - solend_program::id(), - SOL_DEPOSIT_AMOUNT_LAMPORTS, - sol_test_reserve.user_collateral_pubkey, - sol_test_reserve.collateral_supply_pubkey, - sol_test_reserve.pubkey, - obligation_pubkey, - lending_market.pubkey, - user_accounts_owner_pubkey, - user_transfer_authority_pubkey, - ), - // 4 - refresh_reserve( - solend_program::id(), - usdc_test_reserve.pubkey, - usdc_oracle.pyth_price_pubkey, - usdc_oracle.switchboard_feed_pubkey, - ), - // 5 - refresh_reserve( - solend_program::id(), - sol_test_reserve.pubkey, - sol_oracle.pyth_price_pubkey, - sol_oracle.switchboard_feed_pubkey, - ), - // 6 - refresh_obligation( - solend_program::id(), - obligation_pubkey, - vec![sol_test_reserve.pubkey], - ), - // 7 - borrow_obligation_liquidity( - solend_program::id(), - USDC_BORROW_AMOUNT_FRACTIONAL, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - usdc_test_reserve.config.fee_receiver, - obligation_pubkey, - lending_market.pubkey, - user_accounts_owner_pubkey, - Some(usdc_test_reserve.liquidity_host_pubkey), - ), - // 8 - approve( - &spl_token::id(), - &usdc_test_reserve.user_liquidity_pubkey, - &user_transfer_authority_pubkey, - &user_accounts_owner_pubkey, - &[], - USDC_REPAY_AMOUNT_FRACTIONAL, - ) - .unwrap(), - // 9 - repay_obligation_liquidity( - solend_program::id(), - USDC_REPAY_AMOUNT_FRACTIONAL, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.pubkey, - obligation_pubkey, - lending_market.pubkey, - user_transfer_authority_pubkey, - ), - // 10 - refresh_reserve( - solend_program::id(), - usdc_test_reserve.pubkey, - usdc_oracle.pyth_price_pubkey, - usdc_oracle.switchboard_feed_pubkey, - ), - // 11 - refresh_obligation( - solend_program::id(), - obligation_pubkey, - vec![sol_test_reserve.pubkey], - ), - // 12 - withdraw_obligation_collateral( - solend_program::id(), - SOL_DEPOSIT_AMOUNT_LAMPORTS, - sol_test_reserve.collateral_supply_pubkey, - sol_test_reserve.user_collateral_pubkey, - sol_test_reserve.pubkey, - obligation_pubkey, - lending_market.pubkey, - user_accounts_owner_pubkey, - ), - ], - Some(&payer_pubkey), - ); - - transaction.sign( - &vec![ - &payer, - &obligation_keypair, - &user_accounts_owner, - &user_transfer_authority, - ], - recent_blockhash, - ); - assert!(banks_client.process_transaction(transaction).await.is_ok()); - - let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; - - let obligation = { - let obligation_account: Account = banks_client - .get_account(obligation_pubkey) - .await - .unwrap() - .unwrap(); - Obligation::unpack(&obligation_account.data[..]).unwrap() - }; - - let collateral_supply_balance = - get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; - let user_collateral_balance = - get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; - assert_eq!(collateral_supply_balance, initial_collateral_supply_balance); - assert_eq!(user_collateral_balance, initial_user_collateral_balance); + ) + .await; - let liquidity_supply = - get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; - let user_liquidity_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; - assert_eq!(liquidity_supply, initial_liquidity_supply); - assert_eq!( - user_liquidity_balance, - initial_user_liquidity_balance - FEE_AMOUNT - ); - assert_eq!(usdc_reserve.liquidity.borrowed_amount_wads, Decimal::zero()); - assert_eq!( - usdc_reserve.liquidity.available_amount, - initial_liquidity_supply - ); - - assert_eq!(obligation.deposits.len(), 0); - assert_eq!(obligation.borrows.len(), 0); - - let fee_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.config.fee_receiver).await; - assert_eq!(fee_balance, FEE_AMOUNT - HOST_FEE_AMOUNT); - - let host_fee_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_host_pubkey).await; - assert_eq!(host_fee_balance, HOST_FEE_AMOUNT); + (test, lending_market, usdc_reserve, wsol_reserve, user) } #[tokio::test] -async fn test_success2() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(158_000); - - const FEE_AMOUNT: u64 = 100; - const HOST_FEE_AMOUNT: u64 = 20; - - const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; - const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = SOL_DEPOSIT_AMOUNT_LAMPORTS; - - const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 1_000 * FRACTIONAL_TO_USDC; - const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = USDC_RESERVE_LIQUIDITY_FRACTIONAL - FEE_AMOUNT; - const USDC_REPAY_AMOUNT_FRACTIONAL: u64 = USDC_RESERVE_LIQUIDITY_FRACTIONAL; - - let user_accounts_owner = Keypair::new(); - let user_accounts_owner_pubkey = user_accounts_owner.pubkey(); - - let user_transfer_authority = Keypair::new(); - let user_transfer_authority_pubkey = user_transfer_authority.pubkey(); - - let obligation_keypair = Keypair::new(); - let obligation_pubkey = obligation_keypair.pubkey(); - - let lending_market = add_lending_market(&mut test); +async fn test_success() { + let (mut test, lending_market, usdc_reserve, wsol_reserve, user) = setup().await; - let mut reserve_config = test_reserve_config(); - reserve_config.loan_to_value_ratio = 50; + let host_fee_receiver = User::new_with_balances(&mut test, &[(&wsol_mint::id(), 0)]).await; + let obligation = lending_market + .init_obligation(&mut test, Keypair::new(), &user) + .await + .unwrap(); - let sol_oracle = add_sol_oracle_switchboardv2(&mut test); - let sol_test_reserve = add_reserve( + let balance_checker = BalanceChecker::start( &mut test, - &lending_market, - &sol_oracle, - &user_accounts_owner, - AddReserveArgs { - user_liquidity_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, - liquidity_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, - liquidity_mint_pubkey: spl_token::native_mint::id(), - liquidity_mint_decimals: 9, - config: reserve_config, - ..AddReserveArgs::default() + &[&usdc_reserve, &wsol_reserve, &user, &host_fee_receiver], + ) + .await; + + lending_market + .deposit_reserve_liquidity_and_obligation_collateral( + &mut test, + &usdc_reserve, + &obligation, + &user, + 100 * FRACTIONAL_TO_USDC, + ) + .await + .unwrap(); + + let obligation = test.load_account(obligation.pubkey).await; + lending_market + .borrow_obligation_liquidity( + &mut test, + &wsol_reserve, + &obligation, + &user, + &host_fee_receiver.get_account(&wsol_mint::id()).unwrap(), + LAMPORTS_TO_SOL / 2, + ) + .await + .unwrap(); + + lending_market + .repay_obligation_liquidity(&mut test, &wsol_reserve, &obligation, &user, u64::MAX) + .await + .unwrap(); + + let obligation = test.load_account(obligation.pubkey).await; + lending_market + .withdraw_obligation_collateral_and_redeem_reserve_collateral( + &mut test, + &usdc_reserve, + &obligation, + &user, + 100 * FRACTIONAL_TO_USDC, + ) + .await + .unwrap(); + + let (balance_changes, mint_supply_changes) = + balance_checker.find_balance_changes(&mut test).await; + let borrow_fee = Decimal::from(LAMPORTS_TO_SOL / 2) + .try_mul(Decimal::from_scaled_val( + wsol_reserve.account.config.fees.borrow_fee_wad as u128, + )) + .unwrap(); + let host_fee = borrow_fee + .try_mul(Decimal::from_percent( + wsol_reserve.account.config.fees.host_fee_percentage, + )) + .unwrap(); + + let expected_balance_changes = HashSet::from([ + TokenBalanceChange { + token_account: user.get_account(&wsol_mint::id()).unwrap(), + mint: wsol_mint::id(), + diff: -(borrow_fee.try_round_u64().unwrap() as i128), }, - ); - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle_switchboardv2(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - user_liquidity_amount: FEE_AMOUNT, - liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - ..AddReserveArgs::default() + TokenBalanceChange { + token_account: host_fee_receiver.get_account(&wsol_mint::id()).unwrap(), + mint: wsol_mint::id(), + diff: host_fee.try_round_u64().unwrap() as i128, }, - ); - - let (mut banks_client, payer, recent_blockhash) = test.start().await; - let payer_pubkey = payer.pubkey(); - - let initial_collateral_supply_balance = - get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; - let initial_user_collateral_balance = - get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; - let initial_liquidity_supply = - get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; - let initial_user_liquidity_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; - - let rent = banks_client.get_rent().await.unwrap(); - - let mut transaction = Transaction::new_with_payer( - &[ - // 0 - create_account( - &payer.pubkey(), - &obligation_keypair.pubkey(), - rent.minimum_balance(Obligation::LEN), - Obligation::LEN as u64, - &solend_program::id(), - ), - // 1 - init_obligation( - solend_program::id(), - obligation_pubkey, - lending_market.pubkey, - user_accounts_owner_pubkey, - ), - // 2 - approve( - &spl_token::id(), - &sol_test_reserve.user_collateral_pubkey, - &user_transfer_authority_pubkey, - &user_accounts_owner_pubkey, - &[], - SOL_DEPOSIT_AMOUNT_LAMPORTS, - ) - .unwrap(), - // 3 - deposit_obligation_collateral( - solend_program::id(), - SOL_DEPOSIT_AMOUNT_LAMPORTS, - sol_test_reserve.user_collateral_pubkey, - sol_test_reserve.collateral_supply_pubkey, - sol_test_reserve.pubkey, - obligation_pubkey, - lending_market.pubkey, - user_accounts_owner_pubkey, - user_transfer_authority_pubkey, - ), - // 4 - refresh_reserve( - solend_program::id(), - usdc_test_reserve.pubkey, - usdc_oracle.pyth_price_pubkey, - usdc_oracle.switchboard_feed_pubkey, - ), - // 5 - refresh_reserve( - solend_program::id(), - sol_test_reserve.pubkey, - sol_oracle.pyth_price_pubkey, - sol_oracle.switchboard_feed_pubkey, - ), - // 6 - refresh_obligation( - solend_program::id(), - obligation_pubkey, - vec![sol_test_reserve.pubkey], - ), - // 7 - borrow_obligation_liquidity( - solend_program::id(), - USDC_BORROW_AMOUNT_FRACTIONAL, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - usdc_test_reserve.config.fee_receiver, - obligation_pubkey, - lending_market.pubkey, - user_accounts_owner_pubkey, - Some(usdc_test_reserve.liquidity_host_pubkey), - ), - // 8 - approve( - &spl_token::id(), - &usdc_test_reserve.user_liquidity_pubkey, - &user_transfer_authority_pubkey, - &user_accounts_owner_pubkey, - &[], - USDC_REPAY_AMOUNT_FRACTIONAL, - ) - .unwrap(), - // 9 - repay_obligation_liquidity( - solend_program::id(), - USDC_REPAY_AMOUNT_FRACTIONAL, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.pubkey, - obligation_pubkey, - lending_market.pubkey, - user_transfer_authority_pubkey, - ), - // 10 - refresh_reserve( - solend_program::id(), - usdc_test_reserve.pubkey, - usdc_oracle.pyth_price_pubkey, - usdc_oracle.switchboard_feed_pubkey, - ), - // 11 - refresh_obligation( - solend_program::id(), - obligation_pubkey, - vec![sol_test_reserve.pubkey], - ), - // 12 - withdraw_obligation_collateral( - solend_program::id(), - SOL_DEPOSIT_AMOUNT_LAMPORTS, - sol_test_reserve.collateral_supply_pubkey, - sol_test_reserve.user_collateral_pubkey, - sol_test_reserve.pubkey, - obligation_pubkey, - lending_market.pubkey, - user_accounts_owner_pubkey, - ), - ], - Some(&payer_pubkey), - ); - - transaction.sign( - &vec![ - &payer, - &obligation_keypair, - &user_accounts_owner, - &user_transfer_authority, - ], - recent_blockhash, - ); - assert!(banks_client.process_transaction(transaction).await.is_ok()); - - let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; - - let obligation = { - let obligation_account: Account = banks_client - .get_account(obligation_pubkey) - .await - .unwrap() - .unwrap(); - Obligation::unpack(&obligation_account.data[..]).unwrap() - }; - - let collateral_supply_balance = - get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; - let user_collateral_balance = - get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; - assert_eq!(collateral_supply_balance, initial_collateral_supply_balance); - assert_eq!(user_collateral_balance, initial_user_collateral_balance); - - let liquidity_supply = - get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; - let user_liquidity_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; - assert_eq!(liquidity_supply, initial_liquidity_supply); - assert_eq!( - user_liquidity_balance, - initial_user_liquidity_balance - FEE_AMOUNT - ); - assert_eq!(usdc_reserve.liquidity.borrowed_amount_wads, Decimal::zero()); - assert_eq!( - usdc_reserve.liquidity.available_amount, - initial_liquidity_supply - ); - - assert_eq!(obligation.deposits.len(), 0); - assert_eq!(obligation.borrows.len(), 0); - - let fee_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.config.fee_receiver).await; - assert_eq!(fee_balance, FEE_AMOUNT - HOST_FEE_AMOUNT); - - let host_fee_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_host_pubkey).await; - assert_eq!(host_fee_balance, HOST_FEE_AMOUNT); + TokenBalanceChange { + token_account: wsol_reserve.account.config.fee_receiver, + mint: wsol_mint::id(), + diff: borrow_fee + .try_sub(host_fee) + .unwrap() + .try_round_u64() + .unwrap() as i128, + }, + ]); + assert_eq!(balance_changes, expected_balance_changes); + assert_eq!(mint_supply_changes, HashSet::new()); } diff --git a/token-lending/program/tests/redeem_fees.rs b/token-lending/program/tests/redeem_fees.rs index fbca749e697..adcd98330ec 100644 --- a/token-lending/program/tests/redeem_fees.rs +++ b/token-lending/program/tests/redeem_fees.rs @@ -2,294 +2,107 @@ mod helpers; -use std::str::FromStr; +use crate::solend_program_test::scenario_1; +use crate::solend_program_test::BalanceChecker; +use crate::solend_program_test::PriceArgs; +use crate::solend_program_test::TokenBalanceChange; +use solana_program::native_token::LAMPORTS_PER_SOL; +use solend_program::state::LastUpdate; +use solend_program::state::ReserveLiquidity; +use solend_program::state::{Reserve, ReserveConfig}; +use std::collections::HashSet; use helpers::*; use solana_program_test::*; -use solana_sdk::{ - pubkey::Pubkey, - signature::{Keypair, Signer}, - transaction::Transaction, -}; use solend_program::{ - instruction::{redeem_fees, refresh_reserve}, - math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub}, - processor::process_instruction, + math::{Decimal, TrySub}, state::SLOTS_PER_YEAR, }; #[tokio::test] async fn test_success() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(228_000); - - const SOL_RESERVE_LIQUIDITY_LAMPORTS: u64 = 100000000 * LAMPORTS_TO_SOL; - const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 100000 * FRACTIONAL_TO_USDC; - const BORROW_AMOUNT: u64 = 100000; - const SLOTS_ELAPSED: u64 = 69420; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let mut usdc_reserve_config = test_reserve_config(); - usdc_reserve_config.loan_to_value_ratio = 80; - - // Configure reserve to a fixed borrow rate of 200% - const BORROW_RATE: u8 = 250; - usdc_reserve_config.min_borrow_rate = BORROW_RATE; - usdc_reserve_config.optimal_borrow_rate = BORROW_RATE; - usdc_reserve_config.optimal_utilization_rate = 100; - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_oracle( - &mut test, - Pubkey::from_str(SRM_PYTH_PRODUCT).unwrap(), - Pubkey::from_str(SRM_PYTH_PRICE).unwrap(), - Pubkey::from_str(SRM_SWITCHBOARD_FEED).unwrap(), - // Set USDC price to $1 - Decimal::from(1u64), - SLOTS_ELAPSED, - ); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - borrow_amount: BORROW_AMOUNT, - liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, - liquidity_mint_decimals: usdc_mint.decimals, - liquidity_mint_pubkey: usdc_mint.pubkey, - config: usdc_reserve_config, - slots_elapsed: 1, // elapsed from 1; clock.slot = 2 - ..AddReserveArgs::default() + let (mut test, lending_market, _, wsol_reserve, user, _) = scenario_1( + &test_reserve_config(), + &ReserveConfig { + protocol_take_rate: 10, + ..test_reserve_config() }, - ); + ) + .await; - let mut sol_reserve_config = test_reserve_config(); - sol_reserve_config.loan_to_value_ratio = 80; + test.advance_clock_by_slots(SLOTS_PER_YEAR).await; - // Configure reserve to a fixed borrow rate of 1% - sol_reserve_config.min_borrow_rate = BORROW_RATE; - sol_reserve_config.optimal_borrow_rate = BORROW_RATE; - sol_reserve_config.optimal_utilization_rate = 100; - let sol_oracle = add_oracle( - &mut test, - Pubkey::from_str(SOL_PYTH_PRODUCT).unwrap(), - Pubkey::from_str(SOL_PYTH_PRICE).unwrap(), - Pubkey::from_str(SOL_SWITCHBOARD_FEED).unwrap(), - // Set SOL price to $20 - Decimal::from(20u64), - SLOTS_ELAPSED, - ); - let sol_test_reserve = add_reserve( - &mut test, - &lending_market, - &sol_oracle, - &user_accounts_owner, - AddReserveArgs { - borrow_amount: BORROW_AMOUNT, - liquidity_amount: SOL_RESERVE_LIQUIDITY_LAMPORTS, - liquidity_mint_decimals: 9, - liquidity_mint_pubkey: spl_token::native_mint::id(), - config: sol_reserve_config, - slots_elapsed: 1, // elapsed from 1; clock.slot = 2 - ..AddReserveArgs::default() + test.set_price( + &wsol_mint::id(), + PriceArgs { + price: 10, + expo: 0, + conf: 0, }, - ); - - let mut test_context = test.start_with_context().await; - test_context.warp_to_slot(2 + SLOTS_ELAPSED).unwrap(); // clock.slot = 100 - - let ProgramTestContext { - mut banks_client, - payer, - last_blockhash: recent_blockhash, - .. - } = test_context; - - let mut transaction = Transaction::new_with_payer( - &[ - refresh_reserve( - solend_program::id(), - usdc_test_reserve.pubkey, - usdc_oracle.pyth_price_pubkey, - usdc_oracle.switchboard_feed_pubkey, - ), - refresh_reserve( - solend_program::id(), - sol_test_reserve.pubkey, - sol_oracle.pyth_price_pubkey, - sol_oracle.switchboard_feed_pubkey, - ), - ], - Some(&payer.pubkey()), - ); + ) + .await; - transaction.sign(&[&payer], recent_blockhash); - assert!(banks_client.process_transaction(transaction).await.is_ok()); - - let sol_reserve_before = sol_test_reserve.get_state(&mut banks_client).await; - let usdc_reserve_before = usdc_test_reserve.get_state(&mut banks_client).await; - let sol_balance_before = - get_token_balance(&mut banks_client, sol_reserve_before.config.fee_receiver).await; - let usdc_balance_before = - get_token_balance(&mut banks_client, usdc_reserve_before.config.fee_receiver).await; - - let mut transaction2 = Transaction::new_with_payer( - &[ - redeem_fees( - solend_program::id(), - usdc_test_reserve.pubkey, - usdc_test_reserve.config.fee_receiver, - usdc_test_reserve.liquidity_supply_pubkey, - lending_market.pubkey, - ), - redeem_fees( - solend_program::id(), - sol_test_reserve.pubkey, - sol_test_reserve.config.fee_receiver, - sol_test_reserve.liquidity_supply_pubkey, - lending_market.pubkey, - ), - ], - Some(&payer.pubkey()), - ); + lending_market + .refresh_reserve(&mut test, &wsol_reserve) + .await + .unwrap(); - transaction2.sign(&[&payer], recent_blockhash); - assert!(banks_client.process_transaction(transaction2).await.is_ok()); + // deposit some liquidity so we can actually redeem the fees later + lending_market + .deposit(&mut test, &wsol_reserve, &user, LAMPORTS_PER_SOL) + .await + .unwrap(); - let sol_reserve = sol_test_reserve.get_state(&mut banks_client).await; - let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; - let sol_balance_after = - get_token_balance(&mut banks_client, sol_reserve.config.fee_receiver).await; - let usdc_balance_after = - get_token_balance(&mut banks_client, usdc_reserve.config.fee_receiver).await; + let wsol_reserve = test.load_account::(wsol_reserve.pubkey).await; - let slot_rate = Rate::from_percent(BORROW_RATE) - .try_div(SLOTS_PER_YEAR) - .unwrap(); - let compound_rate = Rate::one() - .try_add(slot_rate) - .unwrap() - .try_pow(SLOTS_ELAPSED) - .unwrap(); - let compound_borrow = Decimal::from(BORROW_AMOUNT).try_mul(compound_rate).unwrap(); + // redeem fees + let balance_checker = BalanceChecker::start(&mut test, &[&wsol_reserve]).await; - let net_new_debt = compound_borrow - .try_sub(Decimal::from(BORROW_AMOUNT)) + lending_market + .redeem_fees(&mut test, &wsol_reserve) + .await .unwrap(); - let protocol_take_rate = Rate::from_percent(sol_test_reserve.config.protocol_take_rate); - let delta_accumulated_protocol_fees = net_new_debt.try_mul(protocol_take_rate).unwrap(); - assert_eq!( - usdc_reserve_before.liquidity.total_supply(), - usdc_reserve.liquidity.total_supply(), - ); - assert_eq!( - sol_reserve_before.liquidity.total_supply(), - sol_reserve.liquidity.total_supply(), - ); - assert_eq!( - Rate::from(usdc_reserve_before.collateral_exchange_rate().unwrap()), - Rate::from(usdc_reserve.collateral_exchange_rate().unwrap()), - ); - assert_eq!( - Rate::from(sol_reserve_before.collateral_exchange_rate().unwrap()), - Rate::from(sol_reserve.collateral_exchange_rate().unwrap()), - ); + let expected_fees = wsol_reserve.account.calculate_redeem_fees().unwrap(); - // utilization increases because redeeming adds to borrows and takes from availible - assert_eq!( - usdc_reserve_before.liquidity.utilization_rate().unwrap(), - usdc_reserve.liquidity.utilization_rate().unwrap(), - ); - assert_eq!( - sol_reserve_before.liquidity.utilization_rate().unwrap(), - sol_reserve.liquidity.utilization_rate().unwrap(), - ); - assert_eq!( - sol_reserve.liquidity.cumulative_borrow_rate_wads, - compound_rate.into() - ); - assert_eq!( - sol_reserve.liquidity.cumulative_borrow_rate_wads, - usdc_reserve.liquidity.cumulative_borrow_rate_wads - ); - assert_eq!(sol_reserve.liquidity.borrowed_amount_wads, compound_borrow); - assert_eq!(usdc_reserve.liquidity.borrowed_amount_wads, compound_borrow); - assert_eq!( - Decimal::from(delta_accumulated_protocol_fees.try_floor_u64().unwrap()), - usdc_reserve_before - .liquidity - .accumulated_protocol_fees_wads - .try_sub(usdc_reserve.liquidity.accumulated_protocol_fees_wads) - .unwrap() - ); - assert_eq!( - Decimal::from(delta_accumulated_protocol_fees.try_floor_u64().unwrap()), - sol_reserve_before - .liquidity - .accumulated_protocol_fees_wads - .try_sub(sol_reserve.liquidity.accumulated_protocol_fees_wads) - .unwrap() - ); - assert_eq!( - usdc_reserve_before.liquidity.accumulated_protocol_fees_wads, - delta_accumulated_protocol_fees - ); - assert_eq!( - sol_reserve_before.liquidity.accumulated_protocol_fees_wads, - delta_accumulated_protocol_fees - ); - assert_eq!( - usdc_reserve_before - .liquidity - .accumulated_protocol_fees_wads - .try_floor_u64() - .unwrap(), - usdc_balance_after - usdc_balance_before - ); - assert_eq!( - usdc_reserve.liquidity.accumulated_protocol_fees_wads, - usdc_reserve_before - .liquidity - .accumulated_protocol_fees_wads - .try_sub(Decimal::from(usdc_balance_after - usdc_balance_before)) - .unwrap() - ); - assert_eq!( - sol_reserve_before - .liquidity - .accumulated_protocol_fees_wads - .try_floor_u64() - .unwrap(), - sol_balance_after - sol_balance_before - ); - assert_eq!( - sol_reserve.liquidity.accumulated_protocol_fees_wads, - sol_reserve_before - .liquidity - .accumulated_protocol_fees_wads - .try_sub(Decimal::from(sol_balance_after - sol_balance_before)) - .unwrap() - ); - assert_eq!( - sol_reserve.liquidity.borrowed_amount_wads, - usdc_reserve.liquidity.borrowed_amount_wads - ); - assert_eq!( - sol_reserve.liquidity.market_price, - sol_test_reserve.market_price - ); - assert_eq!( - usdc_reserve.liquidity.market_price, - usdc_test_reserve.market_price + // check token balances + let (balance_changes, mint_supply_changes) = + balance_checker.find_balance_changes(&mut test).await; + let expected_balance_changes = HashSet::from([ + TokenBalanceChange { + token_account: wsol_reserve.account.config.fee_receiver, + mint: wsol_mint::id(), + diff: expected_fees as i128, + }, + TokenBalanceChange { + token_account: wsol_reserve.account.liquidity.supply_pubkey, + mint: wsol_mint::id(), + diff: -(expected_fees as i128), + }, + ]); + assert_eq!(balance_changes, expected_balance_changes); + assert_eq!(mint_supply_changes, HashSet::new()); + + // check program state + let wsol_reserve_post = test.load_account::(wsol_reserve.pubkey).await; + assert_eq!( + wsol_reserve_post.account, + Reserve { + last_update: LastUpdate { + slot: 1000 + SLOTS_PER_YEAR, + stale: true + }, + liquidity: ReserveLiquidity { + available_amount: wsol_reserve.account.liquidity.available_amount - expected_fees, + accumulated_protocol_fees_wads: wsol_reserve + .account + .liquidity + .accumulated_protocol_fees_wads + .try_sub(Decimal::from(expected_fees)) + .unwrap(), + ..wsol_reserve.account.liquidity + }, + ..wsol_reserve.account + } ); } diff --git a/token-lending/program/tests/redeem_reserve_collateral.rs b/token-lending/program/tests/redeem_reserve_collateral.rs index 03a9b63febb..68b885c6ecf 100644 --- a/token-lending/program/tests/redeem_reserve_collateral.rs +++ b/token-lending/program/tests/redeem_reserve_collateral.rs @@ -2,103 +2,130 @@ mod helpers; +use crate::solend_program_test::MintSupplyChange; +use std::collections::HashSet; + +use helpers::solend_program_test::{ + setup_world, BalanceChecker, Info, SolendProgramTest, TokenBalanceChange, User, +}; use helpers::*; +use solana_program::instruction::InstructionError; use solana_program_test::*; -use solana_sdk::{ - signature::{Keypair, Signer}, - transaction::Transaction, -}; -use solend_program::{ - instruction::redeem_reserve_collateral, processor::process_instruction, - state::INITIAL_COLLATERAL_RATIO, +use solana_sdk::transaction::TransactionError; +use solend_program::state::{ + LastUpdate, LendingMarket, Reserve, ReserveCollateral, ReserveLiquidity, }; -use spl_token::instruction::approve; + +pub async fn setup() -> (SolendProgramTest, Info, Info, User) { + let (mut test, lending_market, usdc_reserve, _, _, user) = + setup_world(&test_reserve_config(), &test_reserve_config()).await; + + lending_market + .deposit(&mut test, &usdc_reserve, &user, 1_000_000) + .await + .expect("this should succeed"); + + let lending_market = test + .load_account::(lending_market.pubkey) + .await; + + let usdc_reserve = test.load_account::(usdc_reserve.pubkey).await; + + (test, lending_market, usdc_reserve, user) +} #[tokio::test] async fn test_success() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); + let (mut test, lending_market, usdc_reserve, user) = setup().await; - // limit to track compute unit increase - test.set_compute_max_units(48_000); - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 10 * FRACTIONAL_TO_USDC; - const COLLATERAL_AMOUNT: u64 = USDC_RESERVE_LIQUIDITY_FRACTIONAL * INITIAL_COLLATERAL_RATIO; - const BORROWED_AMOUNT: u64 = FRACTIONAL_TO_USDC; - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - collateral_amount: COLLATERAL_AMOUNT, - liquidity_amount: 2 * USDC_RESERVE_LIQUIDITY_FRACTIONAL, - liquidity_mint_decimals: usdc_mint.decimals, - liquidity_mint_pubkey: usdc_mint.pubkey, - borrow_amount: BORROWED_AMOUNT, - config: test_reserve_config(), - mark_fresh: true, - ..AddReserveArgs::default() - }, - ); + let balance_checker = BalanceChecker::start(&mut test, &[&usdc_reserve, &user]).await; + + lending_market + .redeem(&mut test, &usdc_reserve, &user, 1_000_000) + .await + .expect("This should succeed"); - let mut test_context = test.start_with_context().await; - test_context.warp_to_slot(300).unwrap(); // clock.slot = 300 - - let ProgramTestContext { - mut banks_client, - payer, - last_blockhash: recent_blockhash, - .. - } = test_context; - - let pre_usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; - let old_borrow_rate = pre_usdc_reserve.liquidity.cumulative_borrow_rate_wads; - - let user_transfer_authority = Keypair::new(); - let mut transaction = Transaction::new_with_payer( - &[ - approve( - &spl_token::id(), - &usdc_test_reserve.user_collateral_pubkey, - &user_transfer_authority.pubkey(), - &user_accounts_owner.pubkey(), - &[], - COLLATERAL_AMOUNT, - ) - .unwrap(), - redeem_reserve_collateral( - solend_program::id(), - COLLATERAL_AMOUNT, - usdc_test_reserve.user_collateral_pubkey, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.pubkey, - usdc_test_reserve.collateral_mint_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - lending_market.pubkey, - user_transfer_authority.pubkey(), - ), - ], - Some(&payer.pubkey()), + // check token balances + let (balance_changes, mint_supply_changes) = + balance_checker.find_balance_changes(&mut test).await; + + assert_eq!( + balance_changes, + HashSet::from([ + TokenBalanceChange { + token_account: user.get_account(&usdc_mint::id()).unwrap(), + mint: usdc_mint::id(), + diff: 1_000_000, + }, + TokenBalanceChange { + token_account: user + .get_account(&usdc_reserve.account.collateral.mint_pubkey) + .unwrap(), + mint: usdc_reserve.account.collateral.mint_pubkey, + diff: -1_000_000, + }, + TokenBalanceChange { + token_account: usdc_reserve.account.liquidity.supply_pubkey, + mint: usdc_reserve.account.liquidity.mint_pubkey, + diff: -1_000_000, + }, + ]), + "{:#?}", + balance_changes + ); + assert_eq!( + mint_supply_changes, + HashSet::from([MintSupplyChange { + mint: usdc_reserve.account.collateral.mint_pubkey, + diff: -1_000_000, + },]), + "{:#?}", + mint_supply_changes ); - transaction.sign( - &[&payer, &user_accounts_owner, &user_transfer_authority], - recent_blockhash, + // check program state changes + let lending_market_post = test + .load_account::(lending_market.pubkey) + .await; + assert_eq!(lending_market.account, lending_market_post.account); + + let usdc_reserve_post = test.load_account::(usdc_reserve.pubkey).await; + assert_eq!( + usdc_reserve_post.account, + Reserve { + last_update: LastUpdate { + slot: 1000, + stale: true + }, + liquidity: ReserveLiquidity { + available_amount: usdc_reserve.account.liquidity.available_amount - 1_000_000, + ..usdc_reserve.account.liquidity + }, + collateral: ReserveCollateral { + mint_total_supply: usdc_reserve.account.collateral.mint_total_supply - 1_000_000, + ..usdc_reserve.account.collateral + }, + ..usdc_reserve.account + } ); - assert!(banks_client.process_transaction(transaction).await.is_ok()); +} + +#[tokio::test] +async fn test_fail_redeem_too_much() { + let (mut test, lending_market, usdc_reserve, user) = setup().await; - let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; - assert!(usdc_reserve.last_update.stale); + let res = lending_market + .redeem(&mut test, &usdc_reserve, &user, 1_000_001) + .await + .err() + .unwrap() + .unwrap(); - assert!(usdc_reserve.liquidity.cumulative_borrow_rate_wads > old_borrow_rate); + match res { + // TokenError::Insufficient Funds + TransactionError::InstructionError(0, InstructionError::Custom(1)) => (), + // LendingError::TokenBurnFailed + TransactionError::InstructionError(0, InstructionError::Custom(19)) => (), + _ => panic!("Unexpected error: {:#?}", res), + }; } diff --git a/token-lending/program/tests/refresh_obligation.rs b/token-lending/program/tests/refresh_obligation.rs index 0b8c0b0b056..e057a08558c 100644 --- a/token-lending/program/tests/refresh_obligation.rs +++ b/token-lending/program/tests/refresh_obligation.rs @@ -2,182 +2,227 @@ mod helpers; +use std::collections::HashSet; + +use helpers::solend_program_test::{setup_world, BalanceChecker, Info, SolendProgramTest, User}; use helpers::*; +use solana_program::native_token::LAMPORTS_PER_SOL; use solana_program_test::*; -use solana_sdk::{ - signature::{Keypair, Signer}, - transaction::Transaction, -}; -use solend_program::math::{Rate, TryAdd, TryMul, TrySub}; +use solana_sdk::signature::Keypair; use solend_program::state::SLOTS_PER_YEAR; +use solend_program::state::{LastUpdate, ObligationLiquidity, ReserveFees, ReserveLiquidity}; + use solend_program::{ - instruction::{refresh_obligation, refresh_reserve}, - math::{Decimal, TryDiv}, - processor::process_instruction, - state::INITIAL_COLLATERAL_RATIO, + math::{Decimal, TryAdd, TryDiv, TryMul}, + state::{LendingMarket, Obligation, Reserve, ReserveConfig}, }; -#[tokio::test] -async fn test_success() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); +async fn setup() -> ( + SolendProgramTest, + Info, + Info, + Info, + User, + Info, +) { + let (mut test, lending_market, usdc_reserve, wsol_reserve, lending_market_owner, user) = + setup_world( + &ReserveConfig { + deposit_limit: u64::MAX, + ..test_reserve_config() + }, + &ReserveConfig { + fees: ReserveFees { + borrow_fee_wad: 0, + host_fee_percentage: 0, + flash_loan_fee_wad: 0, + }, + protocol_take_rate: 0, + ..test_reserve_config() + }, + ) + .await; + + // init obligation + let obligation = lending_market + .init_obligation(&mut test, Keypair::new(), &user) + .await + .expect("This should succeed"); + + // deposit 100k USDC + lending_market + .deposit(&mut test, &usdc_reserve, &user, 100_000_000_000) + .await + .expect("This should succeed"); + + let usdc_reserve = test.load_account(usdc_reserve.pubkey).await; + + // deposit 100k cUSDC + lending_market + .deposit_obligation_collateral( + &mut test, + &usdc_reserve, + &obligation, + &user, + 100_000_000_000, + ) + .await + .expect("This should succeed"); + + let wsol_depositor = User::new_with_balances( + &mut test, + &[ + (&wsol_mint::id(), 5 * LAMPORTS_PER_SOL), + (&wsol_reserve.account.collateral.mint_pubkey, 0), + ], + ) + .await; + + // deposit 5SOL. wSOL reserve now has 6 SOL. + lending_market + .deposit( + &mut test, + &wsol_reserve, + &wsol_depositor, + 5 * LAMPORTS_PER_SOL, + ) + .await + .unwrap(); - // limit to track compute unit increase - test.set_compute_max_units(45_000); + // borrow 6 SOL against 100k cUSDC. + let obligation = test.load_account::(obligation.pubkey).await; + lending_market + .borrow_obligation_liquidity( + &mut test, + &wsol_reserve, + &obligation, + &user, + &lending_market_owner.get_account(&wsol_mint::id()).unwrap(), + u64::MAX, + ) + .await + .unwrap(); - const SOL_DEPOSIT_AMOUNT: u64 = 100; - const USDC_BORROW_AMOUNT: u64 = 1_000; - const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = - SOL_DEPOSIT_AMOUNT * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; - const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = USDC_BORROW_AMOUNT * FRACTIONAL_TO_USDC; - const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS; - const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 2 * USDC_BORROW_AMOUNT_FRACTIONAL; + // populate market price correctly + lending_market + .refresh_reserve(&mut test, &wsol_reserve) + .await + .unwrap(); - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); + // populate deposit value correctly. + let obligation = test.load_account::(obligation.pubkey).await; + lending_market + .refresh_obligation(&mut test, &obligation) + .await + .unwrap(); - let mut reserve_config = test_reserve_config(); - reserve_config.loan_to_value_ratio = 50; + let lending_market = test.load_account(lending_market.pubkey).await; + let usdc_reserve = test.load_account(usdc_reserve.pubkey).await; + let wsol_reserve = test.load_account(wsol_reserve.pubkey).await; + let obligation = test.load_account::(obligation.pubkey).await; + + ( + test, + lending_market, + usdc_reserve, + wsol_reserve, + user, + obligation, + ) +} - // Configure reserve to a fixed borrow rate of 1% - const BORROW_RATE: u8 = 1; - reserve_config.min_borrow_rate = BORROW_RATE; - reserve_config.optimal_borrow_rate = BORROW_RATE; - reserve_config.optimal_utilization_rate = 100; +#[tokio::test] +async fn test_success() { + let (mut test, lending_market, usdc_reserve, wsol_reserve, user, obligation) = setup().await; - let sol_oracle = add_sol_oracle(&mut test); - let sol_test_reserve = add_reserve( - &mut test, - &lending_market, - &sol_oracle, - &user_accounts_owner, - AddReserveArgs { - collateral_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, - liquidity_mint_decimals: 9, - liquidity_mint_pubkey: spl_token::native_mint::id(), - config: reserve_config, - slots_elapsed: 238, // elapsed from 1; clock.slot = 239 - ..AddReserveArgs::default() - }, - ); + test.advance_clock_by_slots(1).await; - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - borrow_amount: USDC_BORROW_AMOUNT_FRACTIONAL, - liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, - liquidity_mint_decimals: usdc_mint.decimals, - liquidity_mint_pubkey: usdc_mint.pubkey, - config: reserve_config, - slots_elapsed: 238, // elapsed from 1; clock.slot = 239 - ..AddReserveArgs::default() - }, - ); + let balance_checker = + BalanceChecker::start(&mut test, &[&usdc_reserve, &user, &wsol_reserve]).await; - let test_obligation = add_obligation( - &mut test, - &lending_market, - &user_accounts_owner, - AddObligationArgs { - deposits: &[(&sol_test_reserve, SOL_DEPOSIT_AMOUNT_LAMPORTS)], - borrows: &[(&usdc_test_reserve, USDC_BORROW_AMOUNT_FRACTIONAL)], - slots_elapsed: 238, // elapsed from 1; clock.slot = 239 - ..AddObligationArgs::default() - }, - ); + lending_market + .refresh_obligation(&mut test, &obligation) + .await + .unwrap(); - let mut test_context = test.start_with_context().await; - test_context.warp_to_slot(240).unwrap(); // clock.slot = 240 + // check token balances + let (balance_changes, mint_supply_changes) = + balance_checker.find_balance_changes(&mut test).await; + assert_eq!(balance_changes, HashSet::new()); + assert_eq!(mint_supply_changes, HashSet::new()); - let ProgramTestContext { - mut banks_client, - payer, - last_blockhash: recent_blockhash, - .. - } = test_context; + // check program state + let lending_market_post = test + .load_account::(lending_market.pubkey) + .await; + assert_eq!(lending_market_post, lending_market); - let mut transaction = Transaction::new_with_payer( - &[ - refresh_reserve( - solend_program::id(), - usdc_test_reserve.pubkey, - usdc_oracle.pyth_price_pubkey, - usdc_oracle.switchboard_feed_pubkey, - ), - refresh_reserve( - solend_program::id(), - sol_test_reserve.pubkey, - sol_oracle.pyth_price_pubkey, - sol_oracle.switchboard_feed_pubkey, - ), - refresh_obligation( - solend_program::id(), - test_obligation.pubkey, - vec![sol_test_reserve.pubkey, usdc_test_reserve.pubkey], - ), - ], - Some(&payer.pubkey()), + let usdc_reserve_post = test.load_account::(usdc_reserve.pubkey).await; + assert_eq!( + usdc_reserve_post.account, + Reserve { + last_update: LastUpdate { + slot: 1001, + stale: false + }, + ..usdc_reserve.account + } ); - transaction.sign(&[&payer], recent_blockhash); - assert!(banks_client.process_transaction(transaction).await.is_ok()); - - let sol_reserve = sol_test_reserve.get_state(&mut banks_client).await; - let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; - let obligation = test_obligation.get_state(&mut banks_client).await; - - let collateral = &obligation.deposits[0]; - let liquidity = &obligation.borrows[0]; + let wsol_reserve_post = test.load_account::(wsol_reserve.pubkey).await; - let collateral_price = collateral.market_value.try_div(SOL_DEPOSIT_AMOUNT).unwrap(); - - let slot_rate = Rate::from_percent(BORROW_RATE) - .try_div(SLOTS_PER_YEAR) - .unwrap(); - let compound_rate = Rate::one().try_add(slot_rate).unwrap(); - let compound_borrow = Decimal::from(USDC_BORROW_AMOUNT) - .try_mul(compound_rate) + // 1 + 0.3/SLOTS_PER_YEAR + let new_cumulative_borrow_rate = Decimal::one() + .try_add( + Decimal::from_percent(wsol_reserve.account.config.max_borrow_rate) + .try_div(Decimal::from(SLOTS_PER_YEAR)) + .unwrap(), + ) .unwrap(); - let compound_borrow_wads = Decimal::from(USDC_BORROW_AMOUNT_FRACTIONAL) - .try_mul(compound_rate) + let new_borrowed_amount_wads = new_cumulative_borrow_rate + .try_mul(Decimal::from(6 * LAMPORTS_PER_SOL)) .unwrap(); - let net_new_debt = compound_borrow_wads - .try_sub(Decimal::from(USDC_BORROW_AMOUNT_FRACTIONAL)) - .unwrap(); - let protocol_take_rate = Rate::from_percent(usdc_reserve.config.protocol_take_rate); - let delta_accumulated_protocol_fees = net_new_debt.try_mul(protocol_take_rate).unwrap(); - let new_borrow_amount_wads = Decimal::from(USDC_BORROW_AMOUNT_FRACTIONAL) - .try_add(net_new_debt) - .unwrap(); - - let liquidity_price = liquidity.market_value.try_div(compound_borrow).unwrap(); assert_eq!( - usdc_reserve.liquidity.cumulative_borrow_rate_wads, - liquidity.cumulative_borrow_rate_wads - ); - assert_eq!(liquidity.cumulative_borrow_rate_wads, compound_rate.into()); - assert_eq!( - usdc_reserve.liquidity.borrowed_amount_wads, - liquidity.borrowed_amount_wads - ); - assert_eq!(liquidity.borrowed_amount_wads, new_borrow_amount_wads); - assert_eq!( - usdc_reserve.liquidity.accumulated_protocol_fees_wads, - delta_accumulated_protocol_fees + wsol_reserve_post.account, + Reserve { + last_update: LastUpdate { + slot: 1001, + stale: true + }, + liquidity: ReserveLiquidity { + available_amount: 0, + borrowed_amount_wads: new_borrowed_amount_wads, + cumulative_borrow_rate_wads: new_cumulative_borrow_rate, + ..wsol_reserve.account.liquidity + }, + ..wsol_reserve.account + } ); + + let obligation_post = test.load_account::(obligation.pubkey).await; + let new_borrow_value = new_borrowed_amount_wads + .try_mul(Decimal::from(10u64)) + .unwrap() + .try_div(Decimal::from(LAMPORTS_PER_SOL)) + .unwrap(); + assert_eq!( - sol_reserve.liquidity.accumulated_protocol_fees_wads, - Decimal::from(0u64) + obligation_post.account, + Obligation { + last_update: LastUpdate { + slot: 1001, + stale: false + }, + borrows: [ObligationLiquidity { + borrow_reserve: wsol_reserve.pubkey, + cumulative_borrow_rate_wads: new_cumulative_borrow_rate, + borrowed_amount_wads: new_borrowed_amount_wads, + market_value: new_borrow_value + }] + .to_vec(), + borrowed_value: new_borrow_value, + ..obligation.account + } ); - assert_eq!(sol_reserve.liquidity.market_price, collateral_price,); - assert_eq!(usdc_reserve.liquidity.market_price, liquidity_price,); } diff --git a/token-lending/program/tests/refresh_reserve.rs b/token-lending/program/tests/refresh_reserve.rs index e18374c55be..84ffedd084d 100644 --- a/token-lending/program/tests/refresh_reserve.rs +++ b/token-lending/program/tests/refresh_reserve.rs @@ -2,389 +2,252 @@ mod helpers; +use crate::solend_program_test::setup_world; +use crate::solend_program_test::BalanceChecker; +use crate::solend_program_test::Info; +use crate::solend_program_test::PriceArgs; +use crate::solend_program_test::SolendProgramTest; +use crate::solend_program_test::User; use helpers::*; -use solana_program::{ - instruction::{AccountMeta, Instruction, InstructionError}, - pubkey::Pubkey, - sysvar, -}; +use solana_program::instruction::InstructionError; +use solana_program::native_token::LAMPORTS_PER_SOL; use solana_program_test::*; -use solana_sdk::{ - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, -}; +use solana_sdk::{signature::Keypair, transaction::TransactionError}; +use solend_program::state::LastUpdate; +use solend_program::state::LendingMarket; +use solend_program::state::Obligation; +use solend_program::state::Reserve; +use solend_program::state::ReserveConfig; +use solend_program::state::ReserveFees; +use solend_program::state::ReserveLiquidity; use solend_program::{ error::LendingError, - instruction::{refresh_reserve, LendingInstruction}, math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub}, - processor::process_instruction, state::SLOTS_PER_YEAR, }; -use std::str::FromStr; - -#[tokio::test] -async fn test_success() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(31_000); - - const SOL_RESERVE_LIQUIDITY_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL; - const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 100 * FRACTIONAL_TO_USDC; - const BORROW_AMOUNT: u64 = 100; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let mut reserve_config = test_reserve_config(); - reserve_config.loan_to_value_ratio = 80; - - // Configure reserve to a fixed borrow rate of 1% - const BORROW_RATE: u8 = 1; - reserve_config.min_borrow_rate = BORROW_RATE; - reserve_config.optimal_borrow_rate = BORROW_RATE; - reserve_config.optimal_utilization_rate = 100; - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( +use std::collections::HashSet; + +async fn setup() -> ( + SolendProgramTest, + Info, + Info, + Info, + User, + Info, +) { + let (mut test, lending_market, usdc_reserve, wsol_reserve, lending_market_owner, user) = + setup_world( + &ReserveConfig { + deposit_limit: u64::MAX, + ..test_reserve_config() + }, + &ReserveConfig { + fees: ReserveFees { + borrow_fee_wad: 0, + host_fee_percentage: 0, + flash_loan_fee_wad: 0, + }, + protocol_take_rate: 10, + ..test_reserve_config() + }, + ) + .await; + + // init obligation + let obligation = lending_market + .init_obligation(&mut test, Keypair::new(), &user) + .await + .expect("This should succeed"); + + // deposit 100k USDC + lending_market + .deposit(&mut test, &usdc_reserve, &user, 100_000_000_000) + .await + .expect("This should succeed"); + + let usdc_reserve = test.load_account(usdc_reserve.pubkey).await; + + // deposit 100k cUSDC + lending_market + .deposit_obligation_collateral( + &mut test, + &usdc_reserve, + &obligation, + &user, + 100_000_000_000, + ) + .await + .expect("This should succeed"); + + let wsol_depositor = User::new_with_balances( &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - borrow_amount: BORROW_AMOUNT, - liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, - liquidity_mint_decimals: usdc_mint.decimals, - liquidity_mint_pubkey: usdc_mint.pubkey, - config: reserve_config, - slots_elapsed: 238, // elapsed from 1; clock.slot = 239 - ..AddReserveArgs::default() - }, - ); - - let sol_oracle = add_sol_oracle(&mut test); - let sol_test_reserve = add_reserve( - &mut test, - &lending_market, - &sol_oracle, - &user_accounts_owner, - AddReserveArgs { - borrow_amount: BORROW_AMOUNT, - liquidity_amount: SOL_RESERVE_LIQUIDITY_LAMPORTS, - liquidity_mint_decimals: 9, - liquidity_mint_pubkey: spl_token::native_mint::id(), - config: reserve_config, - slots_elapsed: 238, // elapsed from 1; clock.slot = 239 - ..AddReserveArgs::default() - }, - ); - - let mut test_context = test.start_with_context().await; - test_context.warp_to_slot(240).unwrap(); // clock.slot = 240 - - let ProgramTestContext { - mut banks_client, - payer, - last_blockhash: recent_blockhash, - .. - } = test_context; - - let mut transaction = Transaction::new_with_payer( &[ - refresh_reserve( - solend_program::id(), - usdc_test_reserve.pubkey, - usdc_oracle.pyth_price_pubkey, - usdc_oracle.switchboard_feed_pubkey, - ), - refresh_reserve( - solend_program::id(), - sol_test_reserve.pubkey, - sol_oracle.pyth_price_pubkey, - sol_oracle.switchboard_feed_pubkey, - ), + (&wsol_mint::id(), 5 * LAMPORTS_PER_SOL), + (&wsol_reserve.account.collateral.mint_pubkey, 0), ], - Some(&payer.pubkey()), - ); - - transaction.sign(&[&payer], recent_blockhash); - assert!(banks_client.process_transaction(transaction).await.is_ok()); + ) + .await; + + // deposit 5SOL. wSOL reserve now has 6 SOL. + lending_market + .deposit( + &mut test, + &wsol_reserve, + &wsol_depositor, + 5 * LAMPORTS_PER_SOL, + ) + .await + .unwrap(); - let sol_reserve = sol_test_reserve.get_state(&mut banks_client).await; - let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; + // borrow 6 SOL against 100k cUSDC. All sol is borrowed, so the borrow rate should be at max. + let obligation = test.load_account::(obligation.pubkey).await; + lending_market + .borrow_obligation_liquidity( + &mut test, + &wsol_reserve, + &obligation, + &user, + &lending_market_owner.get_account(&wsol_mint::id()).unwrap(), + u64::MAX, + ) + .await + .unwrap(); - let slot_rate = Rate::from_percent(BORROW_RATE) - .try_div(SLOTS_PER_YEAR) + // populate market price correctly + lending_market + .refresh_reserve(&mut test, &wsol_reserve) + .await .unwrap(); - let compound_rate = Rate::one().try_add(slot_rate).unwrap(); - let compound_borrow = Decimal::from(BORROW_AMOUNT).try_mul(compound_rate).unwrap(); - let net_new_debt = compound_borrow - .try_sub(Decimal::from(BORROW_AMOUNT)) + + // populate deposit value correctly. + let obligation = test.load_account::(obligation.pubkey).await; + lending_market + .refresh_obligation(&mut test, &obligation) + .await .unwrap(); - let protocol_take_rate = Rate::from_percent(usdc_reserve.config.protocol_take_rate); - let delta_accumulated_protocol_fees = net_new_debt.try_mul(protocol_take_rate).unwrap(); - assert_eq!( - sol_reserve.liquidity.cumulative_borrow_rate_wads, - compound_rate.into() - ); - assert_eq!( - sol_reserve.liquidity.cumulative_borrow_rate_wads, - usdc_reserve.liquidity.cumulative_borrow_rate_wads - ); - assert_eq!(sol_reserve.liquidity.borrowed_amount_wads, compound_borrow); - assert_eq!( - sol_reserve.liquidity.borrowed_amount_wads, - usdc_reserve.liquidity.borrowed_amount_wads - ); - assert_eq!( - sol_reserve.liquidity.market_price, - sol_test_reserve.market_price - ); - assert_eq!( - usdc_reserve.liquidity.market_price, - usdc_test_reserve.market_price - ); - assert_eq!( - delta_accumulated_protocol_fees, - usdc_reserve.liquidity.accumulated_protocol_fees_wads - ); - assert_eq!( - delta_accumulated_protocol_fees, - sol_reserve.liquidity.accumulated_protocol_fees_wads - ); + let lending_market = test.load_account(lending_market.pubkey).await; + let usdc_reserve = test.load_account(usdc_reserve.pubkey).await; + let wsol_reserve = test.load_account(wsol_reserve.pubkey).await; + let obligation = test.load_account::(obligation.pubkey).await; + + ( + test, + lending_market, + usdc_reserve, + wsol_reserve, + lending_market_owner, + obligation, + ) } #[tokio::test] -async fn test_success_no_switchboard() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(31_000); - - const SOL_RESERVE_LIQUIDITY_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL; - const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 100 * FRACTIONAL_TO_USDC; - const BORROW_AMOUNT: u64 = 100; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let mut reserve_config = test_reserve_config(); - reserve_config.loan_to_value_ratio = 80; - - // Configure reserve to a fixed borrow rate of 1% - const BORROW_RATE: u8 = 1; - reserve_config.min_borrow_rate = BORROW_RATE; - reserve_config.optimal_borrow_rate = BORROW_RATE; - reserve_config.optimal_utilization_rate = 100; - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - borrow_amount: BORROW_AMOUNT, - liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, - liquidity_mint_decimals: usdc_mint.decimals, - liquidity_mint_pubkey: usdc_mint.pubkey, - config: reserve_config, - slots_elapsed: 238, // elapsed from 1; clock.slot = 239 - ..AddReserveArgs::default() - }, - ); +async fn test_success() { + let (mut test, lending_market, _, wsol_reserve, _, _) = setup().await; - let sol_oracle = add_sol_oracle(&mut test); - let sol_test_reserve = add_reserve( - &mut test, - &lending_market, - &sol_oracle, - &user_accounts_owner, - AddReserveArgs { - borrow_amount: BORROW_AMOUNT, - liquidity_amount: SOL_RESERVE_LIQUIDITY_LAMPORTS, - liquidity_mint_decimals: 9, - liquidity_mint_pubkey: spl_token::native_mint::id(), - config: reserve_config, - slots_elapsed: 238, // elapsed from 1; clock.slot = 239 - ..AddReserveArgs::default() - }, - ); + // should be maxed out at 30% + let borrow_rate = wsol_reserve.account.current_borrow_rate().unwrap(); - let mut test_context = test.start_with_context().await; - test_context.warp_to_slot(240).unwrap(); // clock.slot = 240 + test.advance_clock_by_slots(1).await; + let balance_checker = BalanceChecker::start(&mut test, &[&wsol_reserve]).await; - let ProgramTestContext { - mut banks_client, - payer, - last_blockhash: recent_blockhash, - .. - } = test_context; + lending_market + .refresh_reserve(&mut test, &wsol_reserve) + .await + .unwrap(); - let mut transaction = Transaction::new_with_payer( - &[ - refresh_reserve_no_switchboard( - solend_program::id(), - usdc_test_reserve.pubkey, - usdc_oracle.pyth_price_pubkey, - false, - ), - refresh_reserve_no_switchboard( - solend_program::id(), - sol_test_reserve.pubkey, - sol_oracle.pyth_price_pubkey, - true, - ), - ], - Some(&payer.pubkey()), + // check balances + assert_eq!( + balance_checker.find_balance_changes(&mut test).await, + (HashSet::new(), HashSet::new()) ); - transaction.sign(&[&payer], recent_blockhash); - assert!(banks_client.process_transaction(transaction).await.is_ok()); - - let sol_reserve = sol_test_reserve.get_state(&mut banks_client).await; - let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; + // check program state + let wsol_reserve_post = test.load_account::(wsol_reserve.pubkey).await; - let slot_rate = Rate::from_percent(BORROW_RATE) - .try_div(SLOTS_PER_YEAR) - .unwrap(); + let slot_rate = borrow_rate.try_div(SLOTS_PER_YEAR).unwrap(); let compound_rate = Rate::one().try_add(slot_rate).unwrap(); - let compound_borrow = Decimal::from(BORROW_AMOUNT).try_mul(compound_rate).unwrap(); + let compound_borrow = Decimal::from(6 * LAMPORTS_PER_SOL) + .try_mul(compound_rate) + .unwrap(); let net_new_debt = compound_borrow - .try_sub(Decimal::from(BORROW_AMOUNT)) + .try_sub(Decimal::from(6 * LAMPORTS_PER_SOL)) .unwrap(); - let protocol_take_rate = Rate::from_percent(usdc_reserve.config.protocol_take_rate); + let protocol_take_rate = Rate::from_percent(wsol_reserve.account.config.protocol_take_rate); let delta_accumulated_protocol_fees = net_new_debt.try_mul(protocol_take_rate).unwrap(); assert_eq!( - sol_reserve.liquidity.cumulative_borrow_rate_wads, - compound_rate.into() - ); - assert_eq!( - sol_reserve.liquidity.cumulative_borrow_rate_wads, - usdc_reserve.liquidity.cumulative_borrow_rate_wads - ); - assert_eq!(sol_reserve.liquidity.borrowed_amount_wads, compound_borrow); - assert_eq!( - sol_reserve.liquidity.borrowed_amount_wads, - usdc_reserve.liquidity.borrowed_amount_wads - ); - assert_eq!( - sol_reserve.liquidity.market_price, - sol_test_reserve.market_price - ); - assert_eq!( - usdc_reserve.liquidity.market_price, - usdc_test_reserve.market_price + wsol_reserve_post.account, + Reserve { + last_update: LastUpdate { + slot: 1001, + stale: false + }, + liquidity: ReserveLiquidity { + borrowed_amount_wads: compound_borrow, + cumulative_borrow_rate_wads: compound_rate.into(), + accumulated_protocol_fees_wads: delta_accumulated_protocol_fees, + ..wsol_reserve.account.liquidity + }, + ..wsol_reserve.account + } ); - assert_eq!( - delta_accumulated_protocol_fees, - usdc_reserve.liquidity.accumulated_protocol_fees_wads - ); - assert_eq!( - delta_accumulated_protocol_fees, - sol_reserve.liquidity.accumulated_protocol_fees_wads - ); -} - -/// Creates a `RefreshReserve` instruction -pub fn refresh_reserve_no_switchboard( - program_id: Pubkey, - reserve_pubkey: Pubkey, - reserve_liquidity_pyth_oracle_pubkey: Pubkey, - with_clock: bool, -) -> Instruction { - let mut accounts = vec![ - AccountMeta::new(reserve_pubkey, false), - AccountMeta::new_readonly(reserve_liquidity_pyth_oracle_pubkey, false), - ]; - if with_clock { - accounts.push(AccountMeta::new_readonly(sysvar::clock::id(), false)) - } - Instruction { - program_id, - accounts, - data: LendingInstruction::RefreshReserve.pack(), - } } #[tokio::test] -async fn test_pyth_price_stale() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(31_000); - - const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 100 * FRACTIONAL_TO_USDC; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let reserve_config = test_reserve_config(); - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - borrow_amount: 100, - liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, - liquidity_mint_decimals: usdc_mint.decimals, - liquidity_mint_pubkey: usdc_mint.pubkey, - config: reserve_config, - slots_elapsed: 238, // elapsed from 1; clock.slot = 239 - ..AddReserveArgs::default() - }, - ); +async fn test_fail_pyth_price_stale() { + let (mut test, lending_market, _usdc_reserve, wsol_reserve, _user, _obligation) = setup().await; - let mut test_context = test.start_with_context().await; - test_context.warp_to_slot(241).unwrap(); // clock.slot = 241 + test.advance_clock_by_slots(241).await; - let ProgramTestContext { - mut banks_client, - payer, - last_blockhash: recent_blockhash, - .. - } = test_context; - - let mut transaction = Transaction::new_with_payer( - &[refresh_reserve( - solend_program::id(), - usdc_test_reserve.pubkey, - usdc_oracle.pyth_price_pubkey, - Pubkey::from_str(NULL_PUBKEY).unwrap(), - )], - Some(&payer.pubkey()), - ); + let res = lending_market + .refresh_reserve(&mut test, &wsol_reserve) + .await + .unwrap_err() + .unwrap(); + println!("{:?}", res); - transaction.sign(&[&payer], recent_blockhash); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( 0, - InstructionError::Custom(LendingError::InvalidOracleConfig as u32), + InstructionError::Custom(LendingError::NullOracleConfig as u32), ), ); } + +#[tokio::test] +async fn test_success_pyth_price_stale_switchboard_valid() { + let (mut test, lending_market, _, wsol_reserve, lending_market_owner, _) = setup().await; + + test.advance_clock_by_slots(241).await; + + test.init_switchboard_feed(&wsol_mint::id()).await; + test.set_switchboard_price( + &wsol_mint::id(), + PriceArgs { + price: 10, + expo: 0, + conf: 0, + }, + ) + .await; + + // update reserve so the switchboard feed is not NULL_PUBKEY + lending_market + .update_reserve_config( + &mut test, + &lending_market_owner, + &wsol_reserve, + wsol_reserve.account.config, + None, + ) + .await + .unwrap(); + + let wsol_reserve = test.load_account::(wsol_reserve.pubkey).await; + lending_market + .refresh_reserve(&mut test, &wsol_reserve) + .await + .unwrap() +} diff --git a/token-lending/program/tests/repay_obligation_liquidity.rs b/token-lending/program/tests/repay_obligation_liquidity.rs index 4d708da8aab..a94f24cd21f 100644 --- a/token-lending/program/tests/repay_obligation_liquidity.rs +++ b/token-lending/program/tests/repay_obligation_liquidity.rs @@ -2,157 +2,111 @@ mod helpers; +use crate::solend_program_test::scenario_1; +use std::collections::HashSet; + +use helpers::solend_program_test::{BalanceChecker, TokenBalanceChange}; use helpers::*; +use solana_program::native_token::LAMPORTS_PER_SOL; use solana_program_test::*; -use solana_sdk::{ - signature::{Keypair, Signer}, - transaction::Transaction, -}; + +use solend_program::math::TryDiv; +use solend_program::state::{LastUpdate, ObligationLiquidity, ReserveLiquidity, SLOTS_PER_YEAR}; use solend_program::{ - instruction::repay_obligation_liquidity, - math::{Decimal, Rate, TryAdd, TryMul, TrySub}, - processor::process_instruction, - state::INITIAL_COLLATERAL_RATIO, + math::{Decimal, TryAdd, TryMul, TrySub}, + state::{Obligation, Reserve}, }; -use spl_token::instruction::approve; #[tokio::test] async fn test_success() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(27_000); - - const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; - const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = 1_000 * FRACTIONAL_TO_USDC; - const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS; - const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 2 * USDC_BORROW_AMOUNT_FRACTIONAL; - const USDC_RESERVE_BORROW_RATE: u8 = 110; - - let user_accounts_owner = Keypair::new(); - let user_transfer_authority = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let mut reserve_config = test_reserve_config(); - reserve_config.loan_to_value_ratio = 50; + let (mut test, lending_market, usdc_reserve, wsol_reserve, user, obligation) = + scenario_1(&test_reserve_config(), &test_reserve_config()).await; + + test.advance_clock_by_slots(1).await; + + let balance_checker = + BalanceChecker::start(&mut test, &[&usdc_reserve, &user, &wsol_reserve]).await; + + lending_market + .repay_obligation_liquidity( + &mut test, + &wsol_reserve, + &obligation, + &user, + 10 * LAMPORTS_PER_SOL, + ) + .await + .unwrap(); - let sol_oracle = add_sol_oracle(&mut test); - let sol_test_reserve = add_reserve( - &mut test, - &lending_market, - &sol_oracle, - &user_accounts_owner, - AddReserveArgs { - collateral_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, - liquidity_mint_pubkey: spl_token::native_mint::id(), - liquidity_mint_decimals: 9, - config: reserve_config, - mark_fresh: true, - ..AddReserveArgs::default() + // check token balances + let (balance_changes, mint_supply_changes) = + balance_checker.find_balance_changes(&mut test).await; + let expected_balance_changes = HashSet::from([ + TokenBalanceChange { + token_account: user.get_account(&wsol_mint::id()).unwrap(), + mint: wsol_mint::id(), + diff: -(10 * LAMPORTS_PER_SOL as i128), }, - ); - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - borrow_amount: USDC_BORROW_AMOUNT_FRACTIONAL, - user_liquidity_amount: USDC_BORROW_AMOUNT_FRACTIONAL, - liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - initial_borrow_rate: USDC_RESERVE_BORROW_RATE, - config: reserve_config, - mark_fresh: true, - ..AddReserveArgs::default() + TokenBalanceChange { + token_account: wsol_reserve.account.liquidity.supply_pubkey, + mint: wsol_mint::id(), + diff: (10 * LAMPORTS_PER_SOL as i128), }, - ); - - let test_obligation = add_obligation( - &mut test, - &lending_market, - &user_accounts_owner, - AddObligationArgs { - deposits: &[(&sol_test_reserve, SOL_DEPOSIT_AMOUNT_LAMPORTS)], - borrows: &[(&usdc_test_reserve, USDC_BORROW_AMOUNT_FRACTIONAL)], - ..AddObligationArgs::default() - }, - ); - - let (mut banks_client, payer, recent_blockhash) = test.start().await; - - let initial_user_liquidity_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; - let initial_liquidity_supply_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; - - let mut transaction = Transaction::new_with_payer( - &[ - approve( - &spl_token::id(), - &usdc_test_reserve.user_liquidity_pubkey, - &user_transfer_authority.pubkey(), - &user_accounts_owner.pubkey(), - &[], - USDC_BORROW_AMOUNT_FRACTIONAL, - ) - .unwrap(), - repay_obligation_liquidity( - solend_program::id(), - USDC_BORROW_AMOUNT_FRACTIONAL, - usdc_test_reserve.user_liquidity_pubkey, - usdc_test_reserve.liquidity_supply_pubkey, - usdc_test_reserve.pubkey, - test_obligation.pubkey, - lending_market.pubkey, - user_transfer_authority.pubkey(), - ), - ], - Some(&payer.pubkey()), - ); - - transaction.sign( - &[&payer, &user_accounts_owner, &user_transfer_authority], - recent_blockhash, - ); - assert!(banks_client.process_transaction(transaction).await.is_ok()); - - let user_liquidity_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; - assert_eq!( - user_liquidity_balance, - initial_user_liquidity_balance - USDC_BORROW_AMOUNT_FRACTIONAL - ); + ]); + assert_eq!(balance_changes, expected_balance_changes); + assert_eq!(mint_supply_changes, HashSet::new()); + + // check program state + let wsol_reserve_post = test.load_account::(wsol_reserve.pubkey).await; + + // 1 + 0.3/SLOTS_PER_YEAR + let new_cumulative_borrow_rate = Decimal::one() + .try_add( + Decimal::from_percent(wsol_reserve.account.config.max_borrow_rate) + .try_div(Decimal::from(SLOTS_PER_YEAR)) + .unwrap(), + ) + .unwrap(); + let new_borrowed_amount_wads = new_cumulative_borrow_rate + .try_mul(Decimal::from(10 * LAMPORTS_PER_SOL)) + .unwrap() + .try_sub(Decimal::from(10 * LAMPORTS_TO_SOL)) + .unwrap(); - let liquidity_supply_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; assert_eq!( - liquidity_supply_balance, - initial_liquidity_supply_balance + USDC_BORROW_AMOUNT_FRACTIONAL + wsol_reserve_post.account, + Reserve { + last_update: LastUpdate { + slot: 1001, + stale: true + }, + liquidity: ReserveLiquidity { + available_amount: 10 * LAMPORTS_PER_SOL, + borrowed_amount_wads: new_borrowed_amount_wads, + cumulative_borrow_rate_wads: new_cumulative_borrow_rate, + ..wsol_reserve.account.liquidity + }, + ..wsol_reserve.account + } ); - let obligation = test_obligation.get_state(&mut banks_client).await; - assert_eq!(obligation.borrows.len(), 1); - let new_rate = Rate::one() - .try_add(Rate::from_percent(USDC_RESERVE_BORROW_RATE)) - .unwrap(); - let new_rate_decmial = Decimal::one().try_mul(new_rate).unwrap(); - let balance_due = new_rate_decmial - .try_mul(USDC_BORROW_AMOUNT_FRACTIONAL) - .unwrap(); - let expected_balance_after_repay = balance_due - .try_sub(Decimal::from(USDC_BORROW_AMOUNT_FRACTIONAL)) - .unwrap(); + let obligation_post = test.load_account::(obligation.pubkey).await; assert_eq!( - obligation.borrows[0].borrowed_amount_wads, - expected_balance_after_repay + obligation_post.account, + Obligation { + // we don't require obligation to be refreshed for repay + last_update: LastUpdate { + slot: 1000, + stale: true + }, + borrows: [ObligationLiquidity { + borrow_reserve: wsol_reserve.pubkey, + cumulative_borrow_rate_wads: new_cumulative_borrow_rate, + borrowed_amount_wads: new_borrowed_amount_wads, + ..obligation.account.borrows[0] + }] + .to_vec(), + ..obligation.account + } ); } diff --git a/token-lending/program/tests/set_lending_market_owner.rs b/token-lending/program/tests/set_lending_market_owner.rs index f675e875e2c..aed837d5d21 100644 --- a/token-lending/program/tests/set_lending_market_owner.rs +++ b/token-lending/program/tests/set_lending_market_owner.rs @@ -2,6 +2,10 @@ mod helpers; +use crate::solend_program_test::setup_world; +use crate::solend_program_test::Info; +use crate::solend_program_test::SolendProgramTest; +use crate::solend_program_test::User; use helpers::*; use solana_program::instruction::{AccountMeta, Instruction}; use solana_program_test::*; @@ -9,82 +13,48 @@ use solana_sdk::{ instruction::InstructionError, pubkey::Pubkey, signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, -}; -use solend_program::{ - error::LendingError, - instruction::{set_lending_market_owner, LendingInstruction}, - processor::process_instruction, + transaction::TransactionError, }; +use solend_program::state::LendingMarket; -#[tokio::test] -async fn test_success() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); +use solend_program::{error::LendingError, instruction::LendingInstruction}; - // limit to track compute unit increase - test.set_compute_max_units(4_000); +async fn setup() -> (SolendProgramTest, Info, User) { + let (test, lending_market, _usdc_reserve, _, lending_market_owner, _user) = + setup_world(&test_reserve_config(), &test_reserve_config()).await; - let lending_market = add_lending_market(&mut test); - let (mut banks_client, payer, recent_blockhash) = test.start().await; - - let new_owner = Pubkey::new_unique(); - let mut transaction = Transaction::new_with_payer( - &[set_lending_market_owner( - solend_program::id(), - lending_market.pubkey, - lending_market.owner.pubkey(), - new_owner, - )], - Some(&payer.pubkey()), - ); - - transaction.sign(&[&payer, &lending_market.owner], recent_blockhash); + (test, lending_market, lending_market_owner) +} - banks_client - .process_transaction(transaction) +#[tokio::test] +async fn test_success() { + let (mut test, lending_market, lending_market_owner) = setup().await; + let new_owner = Keypair::new(); + lending_market + .set_lending_market_owner(&mut test, &lending_market_owner, &new_owner.pubkey()) .await - .map_err(|e| e.unwrap()) .unwrap(); - let lending_market_info = lending_market.get_state(&mut banks_client).await; - assert_eq!(lending_market_info.owner, new_owner); + let lending_market = test + .load_account::(lending_market.pubkey) + .await; + assert_eq!(lending_market.account.owner, new_owner.pubkey()); } #[tokio::test] async fn test_invalid_owner() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - let lending_market = add_lending_market(&mut test); - let (mut banks_client, payer, recent_blockhash) = test.start().await; + let (mut test, lending_market, _lending_market_owner) = setup().await; + let invalid_owner = User::new_with_keypair(Keypair::new()); + let new_owner = Keypair::new(); - let invalid_owner = Keypair::new(); - let new_owner = Pubkey::new_unique(); - let mut transaction = Transaction::new_with_payer( - &[set_lending_market_owner( - solend_program::id(), - lending_market.pubkey, - invalid_owner.pubkey(), - new_owner, - )], - Some(&payer.pubkey()), - ); - - transaction.sign(&[&payer, &invalid_owner], recent_blockhash); + let res = lending_market + .set_lending_market_owner(&mut test, &invalid_owner, &new_owner.pubkey()) + .await + .unwrap_err() + .unwrap(); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( 0, InstructionError::Custom(LendingError::InvalidMarketOwner as u32) @@ -94,36 +64,26 @@ async fn test_invalid_owner() { #[tokio::test] async fn test_owner_not_signer() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - let lending_market = add_lending_market(&mut test); - let (mut banks_client, payer, recent_blockhash) = test.start().await; - + let (mut test, lending_market, _lending_market_owner) = setup().await; let new_owner = Pubkey::new_unique(); - let mut transaction = Transaction::new_with_payer( - &[Instruction { - program_id: solend_program::id(), - accounts: vec![ - AccountMeta::new(lending_market.pubkey, false), - AccountMeta::new_readonly(lending_market.owner.pubkey(), false), - ], - data: LendingInstruction::SetLendingMarketOwner { new_owner }.pack(), - }], - Some(&payer.pubkey()), - ); - - transaction.sign(&[&payer], recent_blockhash); + let res = test + .process_transaction( + &[Instruction { + program_id: solend_program::id(), + accounts: vec![ + AccountMeta::new(lending_market.pubkey, false), + AccountMeta::new_readonly(lending_market.account.owner, false), + ], + data: LendingInstruction::SetLendingMarketOwner { new_owner }.pack(), + }], + None, + ) + .await + .unwrap_err() + .unwrap(); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( 0, InstructionError::Custom(LendingError::InvalidSigner as u32) diff --git a/token-lending/program/tests/withdraw_obligation_collateral.rs b/token-lending/program/tests/withdraw_obligation_collateral.rs index 6f189f35a55..bbf9ba5b98f 100644 --- a/token-lending/program/tests/withdraw_obligation_collateral.rs +++ b/token-lending/program/tests/withdraw_obligation_collateral.rs @@ -2,355 +2,156 @@ mod helpers; +use crate::solend_program_test::scenario_1; +use helpers::solend_program_test::{BalanceChecker, TokenBalanceChange}; use helpers::*; + use solana_program_test::*; -use solana_sdk::{ - instruction::InstructionError, - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, -}; -use solend_program::{ - error::LendingError, - instruction::{refresh_obligation, withdraw_obligation_collateral}, - processor::process_instruction, - state::INITIAL_COLLATERAL_RATIO, -}; +use solana_sdk::{instruction::InstructionError, transaction::TransactionError}; +use solend_program::error::LendingError; +use solend_program::state::{LastUpdate, Obligation, ObligationCollateral, Reserve}; +use std::collections::HashSet; use std::u64; #[tokio::test] -async fn test_withdraw_fixed_amount() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(40_000); - - const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 200 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; - const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = 1_000 * FRACTIONAL_TO_USDC; - const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS; - const WITHDRAW_AMOUNT: u64 = 100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let mut reserve_config = test_reserve_config(); - reserve_config.loan_to_value_ratio = 50; - - let sol_oracle = add_sol_oracle(&mut test); - let sol_test_reserve = add_reserve( - &mut test, - &lending_market, - &sol_oracle, - &user_accounts_owner, - AddReserveArgs { - collateral_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, - liquidity_mint_pubkey: spl_token::native_mint::id(), - liquidity_mint_decimals: 9, - config: reserve_config, - mark_fresh: true, - ..AddReserveArgs::default() +async fn test_success_withdraw_fixed_amount() { + let (mut test, lending_market, usdc_reserve, wsol_reserve, user, obligation) = + scenario_1(&test_reserve_config(), &test_reserve_config()).await; + + let balance_checker = + BalanceChecker::start(&mut test, &[&usdc_reserve, &user, &wsol_reserve]).await; + + lending_market + .withdraw_obligation_collateral(&mut test, &usdc_reserve, &obligation, &user, 1_000_000) + .await + .unwrap(); + + let (balance_changes, mint_supply_changes) = + balance_checker.find_balance_changes(&mut test).await; + let expected_balance_changes = HashSet::from([ + TokenBalanceChange { + token_account: user + .get_account(&usdc_reserve.account.collateral.mint_pubkey) + .unwrap(), + mint: usdc_reserve.account.collateral.mint_pubkey, + diff: 1_000_000, }, - ); - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - borrow_amount: USDC_BORROW_AMOUNT_FRACTIONAL, - liquidity_amount: USDC_BORROW_AMOUNT_FRACTIONAL, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - mark_fresh: true, - ..AddReserveArgs::default() - }, - ); - - let test_obligation = add_obligation( - &mut test, - &lending_market, - &user_accounts_owner, - AddObligationArgs { - deposits: &[(&sol_test_reserve, SOL_DEPOSIT_AMOUNT_LAMPORTS)], - borrows: &[(&usdc_test_reserve, USDC_BORROW_AMOUNT_FRACTIONAL)], - ..AddObligationArgs::default() + TokenBalanceChange { + token_account: usdc_reserve.account.collateral.supply_pubkey, + mint: usdc_reserve.account.collateral.mint_pubkey, + diff: -1_000_000, }, - ); - - let test_collateral = &test_obligation.deposits[0]; - let test_liquidity = &test_obligation.borrows[0]; - - let (mut banks_client, payer, recent_blockhash) = test.start().await; + ]); + assert_eq!(balance_changes, expected_balance_changes); + assert_eq!(mint_supply_changes, HashSet::new()); - test_obligation.validate_state(&mut banks_client).await; - test_collateral.validate_state(&mut banks_client).await; - test_liquidity.validate_state(&mut banks_client).await; + let usdc_reserve_post = test.load_account::(usdc_reserve.pubkey).await; + assert_eq!(usdc_reserve_post.account, usdc_reserve.account); - let initial_collateral_supply_balance = - get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; - let initial_user_collateral_balance = - get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; - - let mut transaction = Transaction::new_with_payer( - &[ - refresh_obligation( - solend_program::id(), - test_obligation.pubkey, - vec![sol_test_reserve.pubkey, usdc_test_reserve.pubkey], - ), - withdraw_obligation_collateral( - solend_program::id(), - WITHDRAW_AMOUNT, - sol_test_reserve.collateral_supply_pubkey, - sol_test_reserve.user_collateral_pubkey, - sol_test_reserve.pubkey, - test_obligation.pubkey, - lending_market.pubkey, - test_obligation.owner, - ), - ], - Some(&payer.pubkey()), - ); - - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); - assert!(banks_client.process_transaction(transaction).await.is_ok()); - - // check that collateral tokens were transferred - let collateral_supply_balance = - get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; + let obligation_post = test.load_account::(obligation.pubkey).await; assert_eq!( - collateral_supply_balance, - initial_collateral_supply_balance - WITHDRAW_AMOUNT - ); - let user_collateral_balance = - get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; - assert_eq!( - user_collateral_balance, - initial_user_collateral_balance + WITHDRAW_AMOUNT - ); - - let obligation = test_obligation.get_state(&mut banks_client).await; - let collateral = &obligation.deposits[0]; - assert_eq!( - collateral.deposited_amount, - SOL_DEPOSIT_AMOUNT_LAMPORTS - WITHDRAW_AMOUNT + obligation_post.account, + Obligation { + last_update: LastUpdate { + slot: 1000, + stale: true + }, + deposits: [ObligationCollateral { + deposit_reserve: usdc_reserve.pubkey, + deposited_amount: 100_000_000_000 - 1_000_000, + ..obligation.account.deposits[0] + }] + .to_vec(), + ..obligation.account + } ); } #[tokio::test] -async fn test_withdraw_max_amount() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(33_000); - - const USDC_DEPOSIT_AMOUNT_FRACTIONAL: u64 = - 1_000 * FRACTIONAL_TO_USDC * INITIAL_COLLATERAL_RATIO; - const USDC_RESERVE_COLLATERAL_FRACTIONAL: u64 = 2 * USDC_DEPOSIT_AMOUNT_FRACTIONAL; - const WITHDRAW_AMOUNT: u64 = u64::MAX; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let mut reserve_config = test_reserve_config(); - reserve_config.loan_to_value_ratio = 50; - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - collateral_amount: USDC_RESERVE_COLLATERAL_FRACTIONAL, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - mark_fresh: true, - ..AddReserveArgs::default() +async fn test_success_withdraw_max() { + let (mut test, lending_market, usdc_reserve, wsol_reserve, user, obligation) = + scenario_1(&test_reserve_config(), &test_reserve_config()).await; + + let balance_checker = + BalanceChecker::start(&mut test, &[&usdc_reserve, &user, &wsol_reserve]).await; + + lending_market + .withdraw_obligation_collateral(&mut test, &usdc_reserve, &obligation, &user, u64::MAX) + .await + .unwrap(); + + // we are borrowing 10 SOL @ $10 with an ltv of 0.5, so the debt has to be collateralized by + // exactly 200cUSDC. + let sol_borrowed = obligation.account.borrows[0] + .borrowed_amount_wads + .try_ceil_u64() + .unwrap() + / LAMPORTS_TO_SOL; + let expected_remaining_collateral = sol_borrowed * 10 * 2 * FRACTIONAL_TO_USDC; + + let (balance_changes, mint_supply_changes) = + balance_checker.find_balance_changes(&mut test).await; + let expected_balance_changes = HashSet::from([ + TokenBalanceChange { + token_account: user + .get_account(&usdc_reserve.account.collateral.mint_pubkey) + .unwrap(), + mint: usdc_reserve.account.collateral.mint_pubkey, + diff: (100_000 * FRACTIONAL_TO_USDC - expected_remaining_collateral) as i128, }, - ); - - let test_obligation = add_obligation( - &mut test, - &lending_market, - &user_accounts_owner, - AddObligationArgs { - deposits: &[(&usdc_test_reserve, USDC_DEPOSIT_AMOUNT_FRACTIONAL)], - ..AddObligationArgs::default() + TokenBalanceChange { + token_account: usdc_reserve.account.collateral.supply_pubkey, + mint: usdc_reserve.account.collateral.mint_pubkey, + diff: -((100_000_000_000 - expected_remaining_collateral) as i128), }, - ); - - let test_collateral = &test_obligation.deposits[0]; + ]); + assert_eq!(balance_changes, expected_balance_changes); + assert_eq!(mint_supply_changes, HashSet::new()); - let (mut banks_client, payer, recent_blockhash) = test.start().await; - - test_obligation.validate_state(&mut banks_client).await; - test_collateral.validate_state(&mut banks_client).await; - - let initial_collateral_supply_balance = get_token_balance( - &mut banks_client, - usdc_test_reserve.collateral_supply_pubkey, - ) - .await; - let initial_user_collateral_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.user_collateral_pubkey).await; - - let mut transaction = Transaction::new_with_payer( - &[ - refresh_obligation( - solend_program::id(), - test_obligation.pubkey, - vec![usdc_test_reserve.pubkey], - ), - withdraw_obligation_collateral( - solend_program::id(), - WITHDRAW_AMOUNT, - usdc_test_reserve.collateral_supply_pubkey, - usdc_test_reserve.user_collateral_pubkey, - usdc_test_reserve.pubkey, - test_obligation.pubkey, - lending_market.pubkey, - test_obligation.owner, - ), - ], - Some(&payer.pubkey()), - ); + let usdc_reserve_post = test.load_account::(usdc_reserve.pubkey).await; + assert_eq!(usdc_reserve_post.account, usdc_reserve.account); - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); - assert!(banks_client.process_transaction(transaction).await.is_ok()); - - // check that collateral tokens were transferred - let collateral_supply_balance = get_token_balance( - &mut banks_client, - usdc_test_reserve.collateral_supply_pubkey, - ) - .await; - assert_eq!( - collateral_supply_balance, - initial_collateral_supply_balance - USDC_DEPOSIT_AMOUNT_FRACTIONAL - ); - let user_collateral_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.user_collateral_pubkey).await; + let obligation_post = test.load_account::(obligation.pubkey).await; assert_eq!( - user_collateral_balance, - initial_user_collateral_balance + USDC_DEPOSIT_AMOUNT_FRACTIONAL + obligation_post.account, + Obligation { + last_update: LastUpdate { + slot: 1000, + stale: true + }, + deposits: [ObligationCollateral { + deposit_reserve: usdc_reserve.pubkey, + deposited_amount: expected_remaining_collateral, + ..obligation.account.deposits[0] + }] + .to_vec(), + ..obligation.account + } ); - - let obligation = test_obligation.get_state(&mut banks_client).await; - assert_eq!(obligation.deposits.len(), 0); } #[tokio::test] -async fn test_withdraw_too_large() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 200 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; - const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = 1_000 * FRACTIONAL_TO_USDC; - const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS; - const WITHDRAW_AMOUNT: u64 = (100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO) + 1; - - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); - - let mut reserve_config = test_reserve_config(); - reserve_config.loan_to_value_ratio = 50; - - let sol_oracle = add_sol_oracle(&mut test); - let sol_test_reserve = add_reserve( - &mut test, - &lending_market, - &sol_oracle, - &user_accounts_owner, - AddReserveArgs { - collateral_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, - liquidity_mint_pubkey: spl_token::native_mint::id(), - liquidity_mint_decimals: 9, - config: reserve_config, - mark_fresh: true, - ..AddReserveArgs::default() - }, - ); - - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - borrow_amount: USDC_BORROW_AMOUNT_FRACTIONAL, - liquidity_amount: USDC_BORROW_AMOUNT_FRACTIONAL, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - mark_fresh: true, - ..AddReserveArgs::default() - }, - ); - - let test_obligation = add_obligation( - &mut test, - &lending_market, - &user_accounts_owner, - AddObligationArgs { - deposits: &[(&sol_test_reserve, SOL_DEPOSIT_AMOUNT_LAMPORTS)], - borrows: &[(&usdc_test_reserve, USDC_BORROW_AMOUNT_FRACTIONAL)], - ..AddObligationArgs::default() - }, - ); - - let (mut banks_client, payer, recent_blockhash) = test.start().await; - - let mut transaction = Transaction::new_with_payer( - &[ - refresh_obligation( - solend_program::id(), - test_obligation.pubkey, - vec![sol_test_reserve.pubkey, usdc_test_reserve.pubkey], - ), - withdraw_obligation_collateral( - solend_program::id(), - WITHDRAW_AMOUNT, - sol_test_reserve.collateral_supply_pubkey, - sol_test_reserve.user_collateral_pubkey, - sol_test_reserve.pubkey, - test_obligation.pubkey, - lending_market.pubkey, - test_obligation.owner, - ), - ], - Some(&payer.pubkey()), - ); - - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); +async fn test_fail_withdraw_too_much() { + let (mut test, lending_market, usdc_reserve, _wsol_reserve, user, obligation) = + scenario_1(&test_reserve_config(), &test_reserve_config()).await; + + let res = lending_market + .withdraw_obligation_collateral( + &mut test, + &usdc_reserve, + &obligation, + &user, + 100_000_000_000 - 200_000_000 + 1, + ) + .await + .err() + .unwrap() + .unwrap(); - // check that transaction fails assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( - 1, + 3, InstructionError::Custom(LendingError::WithdrawTooLarge as u32) ) ); diff --git a/token-lending/program/tests/withdraw_obligation_collateral_and_redeem_reserve_collateral.rs b/token-lending/program/tests/withdraw_obligation_collateral_and_redeem_reserve_collateral.rs index b3ad3aa7df2..8ca403e41b0 100644 --- a/token-lending/program/tests/withdraw_obligation_collateral_and_redeem_reserve_collateral.rs +++ b/token-lending/program/tests/withdraw_obligation_collateral_and_redeem_reserve_collateral.rs @@ -2,105 +2,112 @@ mod helpers; +use crate::solend_program_test::MintSupplyChange; +use solend_sdk::state::ObligationCollateral; +use solend_sdk::state::ReserveCollateral; +use std::collections::HashSet; + +use crate::solend_program_test::scenario_1; +use crate::solend_program_test::BalanceChecker; +use crate::solend_program_test::TokenBalanceChange; use helpers::*; + use solana_program_test::*; -use solana_sdk::signature::Keypair; -use solend_program::processor::process_instruction; + +use solend_sdk::state::LastUpdate; +use solend_sdk::state::Obligation; + +use solend_sdk::state::Reserve; +use solend_sdk::state::ReserveLiquidity; #[tokio::test] async fn test_success() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - // limit to track compute unit increase - test.set_compute_max_units(70_000); + let (mut test, lending_market, usdc_reserve, wsol_reserve, user, obligation) = + scenario_1(&test_reserve_config(), &test_reserve_config()).await; - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); + let balance_checker = + BalanceChecker::start(&mut test, &[&usdc_reserve, &user, &wsol_reserve]).await; - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( - &mut test, - &lending_market, - &usdc_oracle, - &user_accounts_owner, - AddReserveArgs { - user_liquidity_amount: 100 * FRACTIONAL_TO_USDC, - liquidity_amount: 10_000 * FRACTIONAL_TO_USDC, - liquidity_mint_decimals: usdc_mint.decimals, - liquidity_mint_pubkey: usdc_mint.pubkey, - config: test_reserve_config(), - mark_fresh: true, - ..AddReserveArgs::default() + lending_market + .withdraw_obligation_collateral_and_redeem_reserve_collateral( + &mut test, + &usdc_reserve, + &obligation, + &user, + u64::MAX, + ) + .await + .unwrap(); + + // check token balances + let (balance_changes, mint_supply_changes) = + balance_checker.find_balance_changes(&mut test).await; + let withdraw_amount = (100_000 * FRACTIONAL_TO_USDC - 200 * FRACTIONAL_TO_USDC) as i128; + + let expected_balance_changes = HashSet::from([ + TokenBalanceChange { + token_account: user.get_account(&usdc_mint::id()).unwrap(), + mint: usdc_mint::id(), + diff: withdraw_amount, + }, + TokenBalanceChange { + token_account: usdc_reserve.account.liquidity.supply_pubkey, + mint: usdc_mint::id(), + diff: -withdraw_amount, + }, + TokenBalanceChange { + token_account: usdc_reserve.account.collateral.supply_pubkey, + mint: usdc_reserve.account.collateral.mint_pubkey, + diff: -withdraw_amount, }, + ]); + assert_eq!(balance_changes, expected_balance_changes); + assert_eq!( + mint_supply_changes, + HashSet::from([MintSupplyChange { + mint: usdc_reserve.account.collateral.mint_pubkey, + diff: -withdraw_amount + }]) ); - let test_obligation = add_obligation( - &mut test, - &lending_market, - &user_accounts_owner, - AddObligationArgs::default(), + // check program state + let usdc_reserve_post = test.load_account::(usdc_reserve.pubkey).await; + assert_eq!( + usdc_reserve_post.account, + Reserve { + last_update: LastUpdate { + slot: 1000, + stale: true + }, + liquidity: ReserveLiquidity { + available_amount: usdc_reserve.account.liquidity.available_amount + - withdraw_amount as u64, + ..usdc_reserve.account.liquidity + }, + collateral: ReserveCollateral { + mint_total_supply: usdc_reserve.account.collateral.mint_total_supply + - withdraw_amount as u64, + ..usdc_reserve.account.collateral + }, + ..usdc_reserve.account + } ); - let mut test_context = test.start_with_context().await; - test_context.warp_to_slot(240).unwrap(); // clock.slot = 240 - - let ProgramTestContext { - mut banks_client, - payer, - last_blockhash: _recent_blockhash, - .. - } = test_context; - - test_obligation.validate_state(&mut banks_client).await; - - lending_market - .deposit_obligation_and_collateral( - &mut banks_client, - &user_accounts_owner, - &payer, - &usdc_test_reserve, - &test_obligation, - 100 * FRACTIONAL_TO_USDC, - ) - .await; - - let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; - assert!(usdc_reserve.last_update.stale); - - let user_liquidity_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; - assert_eq!(user_liquidity_balance, 0); - let liquidity_supply = - get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; - assert_eq!(liquidity_supply, 10_100 * FRACTIONAL_TO_USDC); - - lending_market - .refresh_reserve(&mut banks_client, &payer, &usdc_test_reserve) - .await; - - lending_market - .withdraw_and_redeem_collateral( - &mut banks_client, - &user_accounts_owner, - &payer, - &usdc_test_reserve, - &test_obligation, - 50 * FRACTIONAL_TO_USDC, - ) - .await; - - let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; - assert!(usdc_reserve.last_update.stale); - - let user_liquidity_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; - assert_eq!(user_liquidity_balance, 50 * FRACTIONAL_TO_USDC); - let liquidity_supply = - get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; - assert_eq!(liquidity_supply, 10_050 * FRACTIONAL_TO_USDC); + let obligation_post = test.load_account::(obligation.pubkey).await; + assert_eq!( + obligation_post.account, + Obligation { + last_update: LastUpdate { + slot: 1000, + stale: true + }, + deposits: [ObligationCollateral { + deposit_reserve: usdc_reserve.pubkey, + deposited_amount: 200 * FRACTIONAL_TO_USDC, + ..obligation.account.deposits[0] + }] + .to_vec(), + ..obligation.account + } + ); }