From 0430a4bc731bbe5ac2c1f56d526e32d186107ec0 Mon Sep 17 00:00:00 2001 From: kampung-tech <82401405+codewithgun@users.noreply.github.com> Date: Tue, 11 Apr 2023 19:45:32 +0800 Subject: [PATCH] Update solend v2 upcoming (#2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * allow loan_to_value_ratio == liquidation_threshold (#85) * high rate map (#86) * high rate map * prop test exception * adding more high rate cases * fix to support higher exponent for switchboard v2 (#90) * trying take rate other way (#89) * started take rate cleaning up all the boilerplate adding reserve config take rate claim fees config check + fixing/adding tests fix comment in instruction.rs fixed tests for native token and added pr feedback properly keeping track of availible amount when redeeming fees add in fees when compounding try other method * change to accumulating and redeem when can also added staleness checks * fixed tests * cleanup * sliding down borrow limit and 1 percent close factor (#91) * sliding down borrow limit and 1 percent close factor * fix tests * final dates set * only redeem as much collateral as liquidity allows during liquidate (#88) * only redeem as much collateral as liquidity allows * added test for where not enough liquidity to fully redeem and liquidation * ignore redeem if no collateral to withdraw * rebase and fix tests * undo borrow limit and close factor change and cap liquidation amount (#92) * undo borrow limit and close factor change, but keep caps on borrow limits * fml changed the wrong variable * add new liquidation value cap * test coverage github action (#94) * test coverage github action * move actions to another job * moving stuff around * grr * artifact error * try cobertura format with codecov * add decimal test * formatting * tests for decimal * rate tests * format * tests for calculate liquidation * enable github actions on commits to upcoming branch (#96) * turn on liquidation fees and deprecate old liquidation instruction (#100) * turn on liquidation fees and deprecate old liquidation instruction * fix tests * clippy * bump solana to 1.9.18 (#97) * bump t o 1.9.18 * Solve clippy warnings after bump update * bump solana version in script * fixing various warnings * fix all clippy warnings in the tests * fmt Co-authored-by: Andrei Hrs * flash loans (#95) * copying kiplet's code * make flash loans more conservative * happy case test * adding more tests, fixed a bug * moar tests * clippy lints * fix assert * remove clock * remove old flash loan instruction, but keep the instruction packing code * saner flash loan fee calculation * add flash borrow ix arg to flash repay * cargo fmt * add cpi repay test * bump compute units, no idea why github actions is failing * test for malicious use case * remove unused variable * fix coverage, check reserve borrow limit in flash borrow * mark reserves as stale in flash borrow and repay * revert back to old nightly version, install grcov with stable * fix for [repay, borrow, repay] * refactor CPI * recheck stuff in flash repay * explicitly check for out of bounds in flash borrow loop * 0xripleys remove clock (#99) * make clock sysvar optional * remove clock from instructions * add stack height check * fmt * bump cargo version (#103) * make cli update only update if actually changing something and add an… (#101) * make cli update only update if actually changing something and add anchor.toml * fix clippy * allow 0 ltv assets to be deposited + minor cleanup (#104) * Obligation_owner should be readonly (#87) Issue: As in description `WithdrawObligationCollateralAndRedeemReserveCollateral` combines `WithdrawObligationCollateral` and `RedeemReserveCollateral`. While `obligation_owner` in `WithdrawObligationCollateral` is readonly, it is writable in `WithdrawObligationCollateralAndRedeemReserveCollateral` is writable. Solution: Update `obligation_owner` readonly in `WithdrawObligationCollateralAndRedeemReserveCollateral` * allow passing just pyth to save bytes (#105) * oallow passing just pyth to save bytes * fixing to check that there is a next account and 1 other check * use option instead * added a test * update debt limit (#107) * 0xripleys manual liquidate (#112) * kinda working cli liquidation * dynamically create atas if they don't exist * adding compute budget request * fix clippy * liquidate scripts * withdraw ctokens command * add redeem reserve collateral function * fix cli for updating oracle (#114) * Use pyth sdk + tests (#113) * Create a crate, relax solana dependencies in crate (#118) * bump solana version to 1.14 (#116) * (rfc) bump solana version to 1.14 * fix build * remove flag * update solana version * fix clippy * fix clippy * pin serde version * Refactor and improve tests (#121) - use Reserve, LendingMarket, Obligation types directly instead of TestReserve, TestLendingMarket, TestObligation - more helper functions - check account state after every successful program call - check _all_ token accounts and mint accounts after every successful program call * changing util rate calc to exclude non-claimed fees (#132) * Solend v2.0.1 (#131) * 0xripleys outflow limits (#125) Use a sliding window rate limiter to limit borrows and withdraws at the lending pool owner's discretion. * 0xripleys borrow coefficient (#127) Add a borrow weight to the Reserve * Two Prices PR (#129) - Add a smoothed_market_price to Reserve that is used to limit borrows and withdraws in cases where smoothed price and spot price diverge. - allowed_borrow_value now uses the min(smoothed_market_price, current spot price) - new field on obligation called borrowed_value_upper_bound that uses max(smoothed_market_price, current spot price) * audit nits * audit fixes pt 2 * disable rate limiter if window duration == 0 * cli changes for v2.0.1 (#133) * div by zero fix (#138) * div by zero fix * fmt * use cur slot instead of 0 * fix for withdraw case (#140) --------- Co-authored-by: nope <83512286+nope-finance@users.noreply.github.com> Co-authored-by: Nope X Co-authored-by: 0xripleys <105607696+0xripleys@users.noreply.github.com> Co-authored-by: Andrei Hrs Co-authored-by: andrewsource147 <31321699+andrewsource147@users.noreply.github.com> --- .github/workflows/pull-request.yml | 48 +- Anchor.toml | 13 + Cargo.lock | 2414 +++++++++++------ Cargo.toml | 1 + ci/cargo-test-bpf.sh | 4 +- ci/install-build-deps.sh | 2 - ci/install-program-deps.sh | 2 + ci/rust-version.sh | 10 +- ci/solana-version.sh | 2 +- coverage.sh | 90 +- token-lending/cli/Cargo.toml | 18 +- token-lending/cli/scripts/liquidate.sh | 30 + token-lending/cli/scripts/withdraw.sh | 25 + token-lending/cli/src/lending_state.rs | 135 + token-lending/cli/src/main.rs | 772 +++++- token-lending/program/Cargo.toml | 25 +- token-lending/program/src/lib.rs | 6 +- token-lending/program/src/processor.rs | 1309 +++++---- token-lending/program/src/pyth.rs | 135 - .../tests/borrow_obligation_liquidity.rs | 922 +++---- token-lending/program/tests/borrow_weight.rs | 382 +++ .../tests/deposit_obligation_collateral.rs | 217 +- .../tests/deposit_reserve_liquidity.rs | 200 +- ...rve_liquidity_and_obligation_collateral.rs | 171 +- ...X6rQyUsyELYms1sBJyChWofzSNRoqYzvgMVz5E.bin | Bin 3312 -> 3312 bytes ...e4g1NeAF9u6KDG14anJpFsVEa2cvr5H6iriFZ8.bin | Bin 3312 -> 3312 bytes ...MQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs.bin | Bin 3312 -> 3312 bytes .../program/tests/flash_borrow_repay.rs | 1108 ++++++++ token-lending/program/tests/flash_loan.rs | 227 -- .../program/tests/helpers/flash_loan_proxy.rs | 285 ++ .../program/tests/helpers/genesis.rs | 2 +- .../program/tests/helpers/mock_pyth.rs | 271 ++ token-lending/program/tests/helpers/mod.rs | 1422 +--------- .../tests/helpers/solend_program_test.rs | 1639 +++++++++++ .../program/tests/init_lending_market.rs | 101 +- .../program/tests/init_obligation.rs | 124 +- token-lending/program/tests/init_reserve.rs | 753 ++--- .../program/tests/liquidate_obligation.rs | 201 +- ...uidate_obligation_and_redeem_collateral.rs | 520 ++-- .../program/tests/obligation_end_to_end.rs | 648 +---- .../program/tests/outflow_rate_limits.rs | 213 ++ token-lending/program/tests/redeem_fees.rs | 110 + .../tests/redeem_reserve_collateral.rs | 217 +- .../program/tests/refresh_obligation.rs | 376 ++- .../program/tests/refresh_reserve.rs | 419 ++- .../tests/repay_obligation_liquidity.rs | 229 +- .../program/tests/set_lending_market_owner.rs | 167 +- token-lending/program/tests/two_prices.rs | 487 ++++ .../tests/withdraw_obligation_collateral.rs | 438 +-- ...ollateral_and_redeem_reserve_collateral.rs | 209 +- token-lending/sdk/Cargo.toml | 41 + token-lending/{program => sdk}/src/error.rs | 37 + .../{program => sdk}/src/instruction.rs | 289 +- token-lending/sdk/src/lib.rs | 39 + .../{program => sdk}/src/math/common.rs | 2 + .../{program => sdk}/src/math/decimal.rs | 85 +- .../{program => sdk}/src/math/mod.rs | 0 .../{program => sdk}/src/math/rate.rs | 46 + token-lending/sdk/src/oracles.rs | 415 +++ .../{program => sdk}/src/state/last_update.rs | 0 .../src/state/lending_market.rs | 15 +- .../{program => sdk}/src/state/mod.rs | 2 + .../{program => sdk}/src/state/obligation.rs | 378 ++- token-lending/sdk/src/state/rate_limiter.rs | 236 ++ .../{program => sdk}/src/state/reserve.rs | 512 +++- 65 files changed, 12706 insertions(+), 6490 deletions(-) create mode 100644 Anchor.toml create mode 100755 token-lending/cli/scripts/liquidate.sh create mode 100755 token-lending/cli/scripts/withdraw.sh create mode 100644 token-lending/cli/src/lending_state.rs delete mode 100644 token-lending/program/src/pyth.rs create mode 100644 token-lending/program/tests/borrow_weight.rs create mode 100644 token-lending/program/tests/flash_borrow_repay.rs delete mode 100644 token-lending/program/tests/flash_loan.rs create mode 100644 token-lending/program/tests/helpers/flash_loan_proxy.rs create mode 100644 token-lending/program/tests/helpers/mock_pyth.rs create mode 100644 token-lending/program/tests/helpers/solend_program_test.rs create mode 100644 token-lending/program/tests/outflow_rate_limits.rs create mode 100644 token-lending/program/tests/redeem_fees.rs create mode 100644 token-lending/program/tests/two_prices.rs create mode 100644 token-lending/sdk/Cargo.toml rename token-lending/{program => sdk}/src/error.rs (83%) rename token-lending/{program => sdk}/src/instruction.rs (87%) create mode 100644 token-lending/sdk/src/lib.rs rename token-lending/{program => sdk}/src/math/common.rs (92%) rename token-lending/{program => sdk}/src/math/decimal.rs (71%) rename token-lending/{program => sdk}/src/math/mod.rs (100%) rename token-lending/{program => sdk}/src/math/rate.rs (79%) create mode 100644 token-lending/sdk/src/oracles.rs rename token-lending/{program => sdk}/src/state/last_update.rs (100%) rename token-lending/{program => sdk}/src/state/lending_market.rs (91%) rename token-lending/{program => sdk}/src/state/mod.rs (97%) rename token-lending/{program => sdk}/src/state/obligation.rs (61%) create mode 100644 token-lending/sdk/src/state/rate_limiter.rs rename token-lending/{program => sdk}/src/state/reserve.rs (73%) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index f9ef786e5d6..cea10a574cf 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -5,7 +5,7 @@ on: paths-ignore: - 'docs/**' push: - branches: [master] + branches: [master, upcoming] paths-ignore: - 'docs/**' @@ -50,11 +50,11 @@ jobs: - name: Set env vars run: | source ci/rust-version.sh - echo "RUST_NIGHTLY=$rust_nightly" >> $GITHUB_ENV + echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV - uses: actions-rs/toolchain@v1 with: - toolchain: ${{ env.RUST_NIGHTLY }} + toolchain: ${{ env.RUST_STABLE }} override: true profile: minimal components: clippy @@ -76,7 +76,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: clippy - args: -Zunstable-options --workspace --all-targets -- --deny=warnings + args: --workspace --all-targets -- --deny=warnings cargo-build-test: runs-on: ubuntu-latest @@ -124,3 +124,43 @@ jobs: - name: Build and test run: ./ci/cargo-build-test.sh + + cargo-coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set env vars + run: | + 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_STABLE }} + override: true + profile: minimal + + - uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + # target # Removed due to build dependency caching conflicts + key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE }} + + - name: Install dependencies + run: | + ./ci/install-build-deps.sh + ./ci/install-program-deps.sh + echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH + + - name: run test coverage + run: ./coverage.sh + + - name: Codecov + uses: codecov/codecov-action@v3.1.0 + with: + directory: target/coverage/ diff --git a/Anchor.toml b/Anchor.toml new file mode 100644 index 00000000000..cba6c367aa7 --- /dev/null +++ b/Anchor.toml @@ -0,0 +1,13 @@ +anchor_version = "0.13.2" + +[workspace] +members = [ + "token-lending/program", +] + +[provider] +cluster = "mainnet" +wallet = "~/.config/solana/id.json" + +[programs.mainnet] +spl_token_lending = "So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo" diff --git a/Cargo.lock b/Cargo.lock index 1c03ff39b80..d81c02a6b69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,16 +60,16 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom 0.2.4", + "getrandom 0.2.9", "once_cell", "version_check", ] [[package]] name = "aho-corasick" -version = "0.7.18" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] @@ -80,129 +80,144 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "anchor-attribute-access-control" -version = "0.19.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86cf179906fc142ba02424665a0ba0f3fcc1dec20a95d07e3359379cfdfb5715" +checksum = "cf7d535e1381be3de2c0716c0a1c1e32ad9df1042cddcf7bc18d743569e53319" dependencies = [ "anchor-syn", "anyhow", - "proc-macro2 1.0.47", - "quote 1.0.15", + "proc-macro2 1.0.56", + "quote 1.0.26", "regex", - "syn 1.0.103", + "syn 1.0.109", ] [[package]] name = "anchor-attribute-account" -version = "0.19.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c144eac802153bf4533aa50d37fc9aa73d5d64387c6b5248eab51543db6692dd" +checksum = "c3bcd731f21048a032be27c7791701120e44f3f6371358fc4261a7f716283d29" dependencies = [ "anchor-syn", "anyhow", "bs58 0.4.0", - "proc-macro2 1.0.47", - "quote 1.0.15", + "proc-macro2 1.0.56", + "quote 1.0.26", "rustversion", - "syn 1.0.103", + "syn 1.0.109", ] [[package]] name = "anchor-attribute-constant" -version = "0.19.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57b3019d1bddc493dad84829f43b6450048f8f0091bdc9dcee20b555a99937a2" +checksum = "e1be64a48e395fe00b8217287f226078be2cf32dae42fdf8a885b997945c3d28" dependencies = [ "anchor-syn", - "proc-macro2 1.0.47", - "syn 1.0.103", + "proc-macro2 1.0.56", + "syn 1.0.109", ] [[package]] name = "anchor-attribute-error" -version = "0.19.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5c6ba5011fbc310554fa0052e964f2f0a2d620bf17512c244bc2cef6d35e3d5" +checksum = "38ea6713d1938c0da03656ff8a693b17dc0396da66d1ba320557f07e86eca0d4" dependencies = [ "anchor-syn", - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", ] [[package]] name = "anchor-attribute-event" -version = "0.19.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608ce44a5c10a7b76dff241b680250cd4e995e1d22ad0247008b596b8cc22950" +checksum = "d401f11efb3644285685f8339829a9786d43ed7490bb1699f33c478d04d5a582" dependencies = [ "anchor-syn", "anyhow", - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", ] [[package]] name = "anchor-attribute-interface" -version = "0.19.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb0c8b0cbc9cd6e2d7f060ec60ec3920471b04ccdeebd2ff2549d1de564c35ef" +checksum = "c6700a6f5c888a9c33fe8afc0c64fd8575fa28d05446037306d0f96102ae4480" dependencies = [ "anchor-syn", "anyhow", - "heck", - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "heck 0.3.3", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", ] [[package]] name = "anchor-attribute-program" -version = "0.19.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a6c07c3d524b2e9fa3dcc4e5bc9e660bd8e47984503a9860b5f2f368e0ce8f7" +checksum = "6ad769993b5266714e8939e47fbdede90e5c030333c7522d99a4d4748cf26712" dependencies = [ "anchor-syn", "anyhow", - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", ] [[package]] name = "anchor-attribute-state" -version = "0.19.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6146252bc46d04b3a7db1510f6793fec9650546c796bbf15d12f9d2ab9f9f208" +checksum = "4e677fae4a016a554acdd0e3b7f178d3acafaa7e7ffac6b8690cf4e171f1c116" dependencies = [ "anchor-syn", "anyhow", - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", ] [[package]] name = "anchor-derive-accounts" -version = "0.19.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25596c2dba67c76b69ef8309e2ede97b18315f1f424003d0facd4e4fae71fd9" +checksum = "340beef6809d1c3fcc7ae219153d981e95a8a277ff31985bd7050e32645dc9a8" dependencies = [ "anchor-syn", "anyhow", - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", ] [[package]] name = "anchor-lang" -version = "0.19.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e65418ea57a3cbe3815eaa379278a650b8e3c34e32f4a1b8e99d966d4ad147" +checksum = "662ceafe667448ee4199a4be2ee83b6bb76da28566eee5cea05f96ab38255af8" dependencies = [ "anchor-attribute-access-control", "anchor-attribute-account", @@ -213,7 +228,8 @@ dependencies = [ "anchor-attribute-program", "anchor-attribute-state", "anchor-derive-accounts", - "base64 0.13.0", + "arrayref", + "base64 0.13.1", "bincode", "borsh", "bytemuck", @@ -221,25 +237,46 @@ dependencies = [ "thiserror", ] +[[package]] +name = "anchor-spl" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f32390ce8356f54c0f0245ea156f8190717e37285b8bf4f406a613dc4b954cde" +dependencies = [ + "anchor-lang", + "solana-program", + "spl-associated-token-account", + "spl-token", +] + [[package]] name = "anchor-syn" -version = "0.19.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e825987fd005baa5d58f8456bac83de019109576752467c04bbbecf44c9e288f" +checksum = "0418bcb5daac3b8cb1b60d8fdb1d468ca36f5509f31fb51179326fae1028fdcc" dependencies = [ "anyhow", "bs58 0.3.1", - "heck", - "proc-macro2 1.0.47", + "heck 0.3.3", + "proc-macro2 1.0.56", "proc-macro2-diagnostics", - "quote 1.0.15", + "quote 1.0.26", "serde", "serde_json", "sha2 0.9.9", - "syn 1.0.103", + "syn 1.0.109", "thiserror", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -251,15 +288,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.53" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0" +checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" [[package]] name = "arrayref" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" [[package]] name = "arrayvec" @@ -273,12 +310,65 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" +[[package]] +name = "asn1-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time 0.3.20", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" +dependencies = [ + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +dependencies = [ + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", +] + [[package]] name = "assert_matches" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" +[[package]] +name = "async-compression" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a" +dependencies = [ + "brotli", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-mutex" version = "1.4.0" @@ -290,13 +380,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.52" +version = "0.1.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 2.0.14", ] [[package]] @@ -305,16 +395,16 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] [[package]] name = "autocfg" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base64" @@ -324,15 +414,21 @@ checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" [[package]] name = "base64" -version = "0.13.0" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" [[package]] name = "base64ct" -version = "1.5.3" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bincode" @@ -345,9 +441,9 @@ dependencies = [ [[package]] name = "bit-set" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ "bit-vec", ] @@ -375,9 +471,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.3.2" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "895adc16c8b3273fbbc32685a7d55227705eda08c01e77704020f3491924b44b" +checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef" dependencies = [ "arrayref", "arrayvec", @@ -399,9 +495,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] @@ -431,8 +527,8 @@ dependencies = [ "borsh-derive-internal", "borsh-schema-derive-internal", "proc-macro-crate 0.1.5", - "proc-macro2 1.0.47", - "syn 1.0.103", + "proc-macro2 1.0.56", + "syn 1.0.109", ] [[package]] @@ -441,9 +537,9 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", ] [[package]] @@ -452,9 +548,30 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", +] + +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", ] [[package]] @@ -471,9 +588,9 @@ checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" [[package]] name = "bumpalo" -version = "3.9.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" [[package]] name = "bv" @@ -487,22 +604,22 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.12.3" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.3.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fe233b960f12f8007e3db2d136e3cb1c291bfd7396e384ee76025fc1a3932b4" +checksum = "fdde5c9cd29ebd706ce1b35600920a33550e402fc998a2e53ad3b42c3c47a192" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 2.0.14", ] [[package]] @@ -513,15 +630,15 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[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", @@ -540,20 +657,19 @@ dependencies = [ [[package]] name = "caps" -version = "0.5.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61bf7211aad104ce2769ec05efcdfabf85ee84ac92461d142f22cf8badd0e54c" +checksum = "190baaad529bcfbde9e1a19022c42781bdb6ff9de25721abdb8fd98c0807730b" dependencies = [ - "errno", "libc", "thiserror", ] [[package]] name = "cc" -version = "1.0.72" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" dependencies = [ "jobserver", ] @@ -566,23 +682,25 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.19" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" dependencies = [ - "libc", + "iana-time-zone", + "js-sys", "num-integer", "num-traits", "serde", - "time 0.1.44", + "time 0.1.45", + "wasm-bindgen", "winapi", ] [[package]] name = "chrono-humanize" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eddc119501d583fd930cb92144e605f44e0252c38dd89d9247fffa1993375cb" +checksum = "32dce1ea1988dbdf9f9815ff11425828523bd2a134ec0805d2ac8af26ee6096e" dependencies = [ "chrono", ] @@ -598,9 +716,9 @@ dependencies = [ [[package]] name = "cipher" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", @@ -615,12 +733,47 @@ dependencies = [ "ansi_term", "atty", "bitflags", - "strsim", - "textwrap", + "strsim 0.8.0", + "textwrap 0.11.0", "unicode-width", "vec_map", ] +[[package]] +name = "clap" +version = "3.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +dependencies = [ + "atty", + "bitflags", + "clap_lex", + "indexmap", + "once_cell", + "strsim 0.10.0", + "termcolor", + "textwrap 0.16.0", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + [[package]] name = "combine" version = "3.8.1" @@ -636,17 +789,15 @@ dependencies = [ [[package]] name = "console" -version = "0.15.0" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31" +checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60" dependencies = [ "encode_unicode", + "lazy_static", "libc", - "once_cell", - "regex", - "terminal_size", "unicode-width", - "winapi", + "windows-sys 0.42.0", ] [[package]] @@ -661,9 +812,9 @@ dependencies = [ [[package]] name = "console_log" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501a375961cef1a0d44767200e66e4a559283097e91d0730b1d75dfb2f8a1494" +checksum = "e89f72f65e8501878b8a004d5a1afb780987e2ce2b4532c562e367a72c57499f" dependencies = [ "log", "web-sys", @@ -677,15 +828,15 @@ checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" [[package]] name = "constant_time_eq" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3ad85c1f65dc7b37604eb0e89748faf0b9653065f2a8ef69f96a687ec1e9279" +checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" [[package]] name = "core-foundation" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ "core-foundation-sys", "libc", @@ -693,33 +844,33 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.1" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.3.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2209c310e29876f7f0b2721e7e26b84aff178aa3da5d091f9bfbf47669e60e3" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.2" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ "cfg-if", "crossbeam-utils", @@ -727,9 +878,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ "cfg-if", "crossbeam-epoch", @@ -738,25 +889,24 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.6" +version = "0.9.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97242a70df9b89a65d0b6df3c4bf5b9ce03c5b7309019777fbde37e7537f8762" +checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" dependencies = [ + "autocfg", "cfg-if", "crossbeam-utils", - "lazy_static", - "memoffset", + "memoffset 0.8.0", "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.6" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" dependencies = [ "cfg-if", - "lazy_static", ] [[package]] @@ -808,6 +958,50 @@ dependencies = [ "zeroize", ] +[[package]] +name = "cxx" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2 1.0.56", + "quote 1.0.26", + "scratch", + "syn 2.0.14", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" +dependencies = [ + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 2.0.14", +] + [[package]] name = "dashmap" version = "4.0.2" @@ -819,6 +1013,12 @@ dependencies = [ "rayon", ] +[[package]] +name = "data-encoding" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" + [[package]] name = "der" version = "0.5.1" @@ -828,6 +1028,20 @@ dependencies = [ "const-oid", ] +[[package]] +name = "der-parser" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint 0.4.3", + "num-traits", + "rusticata-macros", +] + [[package]] name = "derivation-path" version = "0.2.0" @@ -836,11 +1050,12 @@ checksum = "6e5c37193a1db1d8ed868c03ec7b152175f26160a5b740e5e484143877e0adf0" [[package]] name = "dialoguer" -version = "0.10.2" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92e7e37ecef6857fdc0c0c5d42fd5b0938e46590c2183cc92dd310a6d078eb1" +checksum = "59c6f2989294b9a498d3ad5491a79c6deb604617378e1cdc4bfc1c1361fe2f87" dependencies = [ "console", + "shell-words", "tempfile", "zeroize", ] @@ -860,7 +1075,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ - "block-buffer 0.10.3", + "block-buffer 0.10.4", "crypto-common", "subtle", ] @@ -895,6 +1110,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "displaydoc" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886" +dependencies = [ + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", +] + [[package]] name = "dlopen" version = "0.1.8" @@ -918,11 +1144,23 @@ dependencies = [ "syn 0.15.44", ] +[[package]] +name = "dyn-clone" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" + +[[package]] +name = "eager" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe71d579d1812060163dff96056261deb5bf6729b100fa2e36a68b9649ba3d3" + [[package]] name = "ed25519" -version = "1.3.0" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74e1069e39f1454367eb2de793ed062fac4c35c2934b76a81d90dd9abcd28816" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" dependencies = [ "signature", ] @@ -955,21 +1193,21 @@ dependencies = [ [[package]] name = "educe" -version = "0.4.18" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86b50932a01e7ec5c06160492ab660fb19b6bb2a7878030dd6cd68d21df9d4d" +checksum = "4af7804abe0786a9b69375115821fedc9995f21ab63ae285184b96b01ec50b1a" dependencies = [ "enum-ordinalize", - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", ] [[package]] name = "either" -version = "1.6.1" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" [[package]] name = "encode_unicode" @@ -979,44 +1217,57 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.30" +version = "0.8.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" dependencies = [ "cfg-if", ] [[package]] name = "enum-iterator" -version = "0.7.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eeac5c5edb79e4e39fe8439ef35207780a11f69c52cbe424ce3dfad4cb78de6" +checksum = "2953d1df47ac0eb70086ccabf0275aa8da8591a28bd358ee2b52bd9f9e3ff9e9" dependencies = [ "enum-iterator-derive", ] [[package]] name = "enum-iterator-derive" -version = "0.7.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c134c37760b27a871ba422106eedbb8247da973a09e82558bf26d619c882b159" +checksum = "8958699f9359f0b04e691a13850d48b7de329138023876d07cbd024c2c820598" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", ] [[package]] name = "enum-ordinalize" -version = "3.1.10" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b166c9e378360dd5a6666a9604bb4f54ae0cac39023ffbac425e917a2a04fef" +checksum = "a62bb1df8b45ecb7ffa78dca1c17a438fb193eb083db0b1b494d2a61bcb5096a" dependencies = [ - "num-bigint", + "num-bigint 0.4.3", "num-traits", - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "enum_dispatch" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f36e95862220b211a6e2aa5eca09b4fa391b13cd52ceb8035a24bf65a79de2" +dependencies = [ + "once_cell", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", ] [[package]] @@ -1034,13 +1285,13 @@ dependencies = [ [[package]] name = "errno" -version = "0.2.8" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -1061,9 +1312,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "fastrand" -version = "1.7.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] @@ -1076,25 +1327,23 @@ checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" [[package]] name = "filetime" -version = "0.2.15" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" +checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" dependencies = [ "cfg-if", "libc", - "redox_syscall", - "winapi", + "redox_syscall 0.2.16", + "windows-sys 0.48.0", ] [[package]] name = "flate2" -version = "1.0.22" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" dependencies = [ - "cfg-if", "crc32fast", - "libc", "miniz_oxide", ] @@ -1106,19 +1355,18 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" dependencies = [ - "matches", "percent-encoding", ] [[package]] name = "futures" -version = "0.3.19" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", @@ -1131,9 +1379,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", "futures-sink", @@ -1141,15 +1389,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-executor" -version = "0.3.19" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" dependencies = [ "futures-core", "futures-task", @@ -1158,38 +1406,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] name = "futures-macro" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 2.0.14", ] [[package]] name = "futures-sink" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-channel", "futures-core", @@ -1214,9 +1462,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.5" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "serde", "typenum", @@ -1248,20 +1496,22 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.4" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" dependencies = [ "cfg-if", + "js-sys", "libc", - "wasi 0.10.0+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] name = "goblin" -version = "0.4.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32401e89c6446dcd28185931a01b1093726d0356820ac744023e6850689bf926" +checksum = "a7666983ed0dd8d21a6f6576ee00053ca0926fb281a5522577a4dbd0f1b54143" dependencies = [ "log", "plain", @@ -1270,9 +1520,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.11" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e" +checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d" dependencies = [ "bytes", "fnv", @@ -1283,15 +1533,15 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util", + "tokio-util 0.7.2", "tracing", ] [[package]] name = "hash32" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" dependencies = [ "byteorder", ] @@ -1323,6 +1573,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1332,11 +1588,29 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "histogram" @@ -1376,9 +1650,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.6" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes", "fnv", @@ -1387,9 +1661,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", "http", @@ -1416,9 +1690,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.23" +version = "0.14.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" +checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899" dependencies = [ "bytes", "futures-channel", @@ -1440,9 +1714,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.23.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" +checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" dependencies = [ "http", "hyper", @@ -1452,24 +1726,47 @@ dependencies = [ ] [[package]] -name = "idna" -version = "0.2.3" +name = "iana-time-zone" +version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", ] [[package]] -name = "im" -version = "15.1.0" +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "im" +version = "15.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" dependencies = [ "bitmaps", - "rand_core 0.6.3", + "rand_core 0.6.4", "rand_xoshiro", "rayon", "serde", @@ -1486,12 +1783,12 @@ checksum = "5a9d968042a4902e08810946fc7cd5851eb75e80301342305af755ca06cb82ce" [[package]] name = "indexmap" -version = "1.8.0" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown 0.11.2", + "hashbrown 0.12.3", ] [[package]] @@ -1524,41 +1821,52 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "io-lifetimes" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "ipnet" -version = "2.3.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" +checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" [[package]] name = "itertools" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.1" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "jobserver" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.56" +version = "0.3.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" dependencies = [ "wasm-bindgen", ] @@ -1580,9 +1888,12 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" +checksum = "3afef3b6eff9ce9d8ff9b3601125eec7f0c8cbac7abd14f355d053fa56c98768" +dependencies = [ + "cpufeatures", +] [[package]] name = "lazy_static" @@ -1592,9 +1903,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.137" +version = "0.2.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" [[package]] name = "libloading" @@ -1606,6 +1917,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" + [[package]] name = "libsecp256k1" version = "0.6.0" @@ -1654,56 +1971,77 @@ dependencies = [ "libsecp256k1-core", ] +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + [[package]] name = "linked-hash-map" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" [[package]] name = "lock_api" -version = "0.4.6" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ + "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.14" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if", ] [[package]] -name = "lru" -version = "0.7.8" +name = "lz4" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" +checksum = "7e9e2dd86df36ce760a60f6ff6ad526f7ba1f14ba0356f8254fb6905e6494df1" dependencies = [ - "hashbrown 0.12.3", + "libc", + "lz4-sys", ] [[package]] -name = "matches" -version = "0.1.9" +name = "lz4-sys" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +checksum = "57d27b317e207b10f69f5e75494119e391a96f48861ae870d1da6edac98ca900" +dependencies = [ + "cc", + "libc", +] [[package]] name = "memchr" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memmap2" -version = "0.5.8" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b182332558b18d807c4ce1ca8ca983b34c3ee32765e47b3f0f69b90355cc1dc" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" dependencies = [ "libc", ] @@ -1717,6 +2055,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +dependencies = [ + "autocfg", +] + [[package]] name = "merlin" version = "3.0.0" @@ -1725,24 +2072,29 @@ checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" dependencies = [ "byteorder", "keccak", - "rand_core 0.6.3", + "rand_core 0.6.4", "zeroize", ] [[package]] name = "mime" -version = "0.3.16" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.4.4" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" dependencies = [ "adler", - "autocfg", ] [[package]] @@ -1783,33 +2135,67 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a7d5f7076603ebc68de2dc6a650ec331a062a13abaa346975be747bbfa4b789" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", ] [[package]] name = "nix" -version = "0.23.1" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" dependencies = [ "bitflags", - "cc", "cfg-if", "libc", - "memoffset", + "memoffset 0.6.5", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", ] [[package]] name = "ntapi" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +dependencies = [ + "num-bigint 0.2.6", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.3" @@ -1821,65 +2207,99 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg", + "num-traits", +] + [[package]] name = "num-derive" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", ] [[package]] name = "num-integer" -version = "0.1.44" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" dependencies = [ "autocfg", + "num-bigint 0.2.6", + "num-integer", "num-traits", ] [[package]] name = "num-traits" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", + "libm", ] [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi", + "hermit-abi 0.2.6", "libc", ] [[package]] name = "num_enum" -version = "0.5.6" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "720d3ea1055e4e4574c0c0b0f8c3fd4f24c4cdaf465948206dea090b57b526ad" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.5.6" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d992b768490d7fe0d8586d9b5745f6c49f557da6d81dc982b1d167ad4edbb21" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" dependencies = [ - "proc-macro-crate 1.1.0", - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro-crate 1.3.1", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", ] [[package]] @@ -1888,11 +2308,20 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +[[package]] +name = "oid-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" -version = "1.9.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "opaque-debug" @@ -1908,43 +2337,50 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "opentelemetry" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf9b1c4e9a6c4de793c632496fa490bdc0e1eea73f0c91394f7b6990935d22" +checksum = "6105e89802af13fdf48c49d7646d3b533a70e536d818aae7e78ba0433d01acb8" dependencies = [ "async-trait", "crossbeam-channel", - "futures", + "futures-channel", + "futures-executor", + "futures-util", "js-sys", "lazy_static", "percent-encoding", "pin-project", - "rand 0.8.4", + "rand 0.8.5", "thiserror", ] +[[package]] +name = "os_str_bytes" +version = "6.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" + [[package]] name = "ouroboros" -version = "0.14.2" +version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71643f290d126e18ac2598876d01e1d57aed164afc78fdb6e2a0c6589a1f6662" +checksum = "e1358bd1558bd2a083fed428ffeda486fbfb323e698cdda7794259d592ca72db" dependencies = [ "aliasable", "ouroboros_macro", - "stable_deref_trait", ] [[package]] name = "ouroboros_macro" -version = "0.14.2" +version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9a247206016d424fe8497bc611e510887af5c261fbbf977877c4bb55ca4d82" +checksum = "5f7d21ccd03305a674437ee1248f3ab5d4b1db095cf1caf49f1713ddf61956b7" dependencies = [ "Inflector", "proc-macro-error", - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", ] [[package]] @@ -1955,7 +2391,7 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", - "parking_lot_core 0.8.5", + "parking_lot_core 0.8.6", ] [[package]] @@ -1965,34 +2401,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.4", + "parking_lot_core 0.9.7", ] [[package]] name = "parking_lot_core" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" dependencies = [ "cfg-if", "instant", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", "winapi", ] [[package]] name = "parking_lot_core" -version = "0.9.4" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", - "windows-sys", + "windows-sys 0.45.0", ] [[package]] @@ -2006,53 +2442,62 @@ dependencies = [ [[package]] name = "pbkdf2" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271779f35b581956db91a3e55737327a03aa051e90b1c47aeb189508533adfd7" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ "digest 0.10.6", ] [[package]] name = "pem" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", ] [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "percentage" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "2fd23b938276f14057220b707937bcb42fa76dda7560e57a2da30cb52d557937" +dependencies = [ + "num", +] [[package]] name = "pin-project" -version = "1.0.10" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.10" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", ] [[package]] name = "pin-project-lite" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] name = "pin-utils" @@ -2073,9 +2518,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.24" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" [[package]] name = "plain" @@ -2097,9 +2542,9 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-crate" @@ -2112,12 +2557,12 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.1.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ - "thiserror", - "toml", + "once_cell", + "toml_edit", ] [[package]] @@ -2127,9 +2572,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", "version_check", ] @@ -2139,8 +2584,8 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.15", + "proc-macro2 1.0.56", + "quote 1.0.26", "version_check", ] @@ -2155,9 +2600,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.47" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ "unicode-ident", ] @@ -2168,18 +2613,18 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bf29726d67464d49fa6224a1d07936a8c08bb3fba727c7493f6cf1616fdaada" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", "version_check", "yansi", ] [[package]] name = "proptest" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0d9cc07f18492d879586c92b485def06bc850da3118075cd45d50e9c95b0e5" +checksum = "29f1b898011ce9595050a68e60f90bad083ff2987a695a42357134c8381fba70" dependencies = [ "bit-set", "bitflags", @@ -2187,12 +2632,43 @@ dependencies = [ "lazy_static", "num-traits", "quick-error 2.0.1", - "rand 0.8.4", + "rand 0.8.5", "rand_chacha 0.3.1", "rand_xorshift", "regex-syntax", "rusty-fork", "tempfile", + "unarray", +] + +[[package]] +name = "pyth-sdk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bf2540203ca3c7a5712fdb8b5897534b7f6a0b6e7b0923ff00466c5f9efcb3" +dependencies = [ + "borsh", + "borsh-derive", + "hex", + "schemars", + "serde", +] + +[[package]] +name = "pyth-sdk-solana" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bc0e0ab39d0543220dcba7c248161aab70e25916b2c1585057abc0856ff4e0c" +dependencies = [ + "borsh", + "borsh-derive", + "bytemuck", + "num-derive", + "num-traits", + "pyth-sdk", + "serde", + "solana-program", + "thiserror", ] [[package]] @@ -2252,7 +2728,7 @@ checksum = "3fce546b9688f767a57530652488420d419a8b1f44a478b451c3d1ab6d992a55" dependencies = [ "bytes", "fxhash", - "rand 0.8.4", + "rand 0.8.5", "ring", "rustls", "rustls-native-certs", @@ -2289,11 +2765,11 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.15" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ - "proc-macro2 1.0.47", + "proc-macro2 1.0.56", ] [[package]] @@ -2306,20 +2782,18 @@ dependencies = [ "libc", "rand_chacha 0.2.2", "rand_core 0.5.1", - "rand_hc 0.2.0", - "rand_pcg", + "rand_hc", ] [[package]] name = "rand" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", - "rand_core 0.6.3", - "rand_hc 0.3.1", + "rand_core 0.6.4", ] [[package]] @@ -2339,7 +2813,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -2353,11 +2827,11 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.4", + "getrandom 0.2.9", ] [[package]] @@ -2369,31 +2843,13 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "rand_hc" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" -dependencies = [ - "rand_core 0.6.3", -] - -[[package]] -name = "rand_pcg" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" -dependencies = [ - "rand_core 0.5.1", -] - [[package]] name = "rand_xorshift" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -2402,31 +2858,28 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" dependencies = [ - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] name = "rayon" -version = "1.5.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" dependencies = [ - "autocfg", - "crossbeam-deque", "either", "rayon-core", ] [[package]] name = "rayon-core" -version = "1.9.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" dependencies = [ "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "lazy_static", "num_cpus", ] @@ -2438,34 +2891,44 @@ checksum = "6413f3de1edee53342e6138e75b56d32e7bc6e332b3bd62d497b1929d4cfbcdd" dependencies = [ "pem", "ring", - "time 0.3.17", + "time 0.3.20", "yasna", ] [[package]] name = "redox_syscall" -version = "0.2.10" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ "bitflags", ] [[package]] name = "redox_users" -version = "0.4.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.4", - "redox_syscall", + "getrandom 0.2.9", + "redox_syscall 0.2.16", + "thiserror", ] [[package]] name = "regex" -version = "1.5.4" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" dependencies = [ "aho-corasick", "memchr", @@ -2474,26 +2937,18 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" - -[[package]] -name = "remove_dir_all" -version = "0.5.3" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "reqwest" -version = "0.11.13" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" +checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254" dependencies = [ - "base64 0.13.0", + "async-compression", + "base64 0.21.0", "bytes", "encoding_rs", "futures-core", @@ -2511,12 +2966,13 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls", - "rustls-pemfile 1.0.1", + "rustls-pemfile 1.0.2", "serde", "serde_json", "serde_urlencoded", "tokio", "tokio-rustls", + "tokio-util 0.7.2", "tower-service", "url", "wasm-bindgen", @@ -2555,9 +3011,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.21.0" +version = "1.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4214023b1223d02a4aad9f0bb9828317634a56530870a2eaf7200a99c0c10f68" +checksum = "ee9164faf726e4f3ece4978b25ca877ddc6802fa77f38cdccb32c7f805ecd70c" dependencies = [ "arrayvec", "num-traits", @@ -2566,19 +3022,19 @@ dependencies = [ [[package]] name = "rust_decimal_macros" -version = "1.21.0" +version = "1.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af2967752cced8cb034149d939f5624452a78ed2faaf97c4eaa8e335c2680f2" +checksum = "4903d8db81d2321699ca8318035d6ff805c548868df435813968795a802171b2" dependencies = [ - "quote 1.0.15", + "quote 1.0.26", "rust_decimal", ] [[package]] name = "rustc-demangle" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +checksum = "d4a36c42d1873f9a77c53bde094f9664d9891bc604a45b4798fd2c389ed12e5b" [[package]] name = "rustc-hash" @@ -2595,11 +3051,34 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "0.37.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85597d61f83914ddeba6a47b3b8ffe7365107221c2e557ed94426489fefb5f77" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + [[package]] name = "rustls" -version = "0.20.7" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c" +checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" dependencies = [ "log", "ring", @@ -2614,7 +3093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" dependencies = [ "openssl-probe", - "rustls-pemfile 1.0.1", + "rustls-pemfile 1.0.2", "schannel", "security-framework", ] @@ -2625,23 +3104,23 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", ] [[package]] name = "rustls-pemfile" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" +checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64 0.13.0", + "base64 0.21.0", ] [[package]] name = "rustversion" -version = "1.0.6" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" +checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" [[package]] name = "rusty-fork" @@ -2657,9 +3136,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.9" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" [[package]] name = "same-file" @@ -2672,12 +3151,35 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.19" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" dependencies = [ - "lazy_static", - "winapi", + "windows-sys 0.42.0", +] + +[[package]] +name = "schemars" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c" +dependencies = [ + "proc-macro2 1.0.56", + "quote 1.0.26", + "serde_derive_internals", + "syn 1.0.109", ] [[package]] @@ -2686,24 +3188,30 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "scratch" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" + [[package]] name = "scroll" -version = "0.10.2" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda28d4b4830b807a8b43f7b0e6b5df875311b3e7621d84577188c175b6ec1ec" +checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da" dependencies = [ "scroll_derive", ] [[package]] name = "scroll_derive" -version = "0.10.5" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaaae8f38bb311444cfb7f1979af0bc9240d95795f75f9ceddf6a59b79ceffa0" +checksum = "bdbda6ac5cd1321e724fa9cee216f3a61885889b896f073b8f82322789c5250e" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", ] [[package]] @@ -2718,9 +3226,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.3.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467" +checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" dependencies = [ "bitflags", "core-foundation", @@ -2731,9 +3239,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a57321bf8bc2362081b2599912d2961fe899c0efadf1b4b2f8d48b3e253bb96c" +checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" dependencies = [ "core-foundation-sys", "libc", @@ -2741,44 +3249,55 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.14" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.136" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +checksum = "fc855a42c7967b7c369eb5860f7164ef1f6f81c20c7cc1141f2a604e18723b03" dependencies = [ "serde_derive", ] [[package]] name = "serde_bytes" -version = "0.11.5" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16ae07dd2f88a366f15bd0632ba725227018c69a1c8550a927324f8eb8368bb9" +checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.136" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +checksum = "6f2122636b9fe3b81f1cb25099fcf2d3f542cdb1d45940d56c713158884a05da" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", +] + +[[package]] +name = "serde_derive_internals" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +dependencies = [ + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", ] [[package]] name = "serde_json" -version = "1.0.89" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" +checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" dependencies = [ "itoa", "ryu", @@ -2799,9 +3318,9 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.8.23" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a521f2940385c165a24ee286aa8599633d162077a54bdcae2a6fd5a7bfa7a0" +checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" dependencies = [ "indexmap", "ryu", @@ -2811,9 +3330,9 @@ dependencies = [ [[package]] name = "sha-1" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" dependencies = [ "cfg-if", "cpufeatures", @@ -2875,20 +3394,26 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "signal-hook-registry" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] [[package]] name = "signature" -version = "1.5.0" +version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f054c6c1a6e95179d6f23ed974060dcefb2d9388bb7256900badad682c499de4" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" [[package]] name = "sized-chunks" @@ -2902,21 +3427,24 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.5" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] [[package]] name = "smallvec" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "socket2" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi", @@ -2924,12 +3452,12 @@ dependencies = [ [[package]] name = "solana-account-decoder" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4348b49e8e47cb9f03f3db32638b489b22940867154fcacd14db2974950da2" +checksum = "8e319617cd926e48d3521849c38b1a9b282a37d87c74e7524d796f4fd64bb050" dependencies = [ "Inflector", - "base64 0.13.0", + "base64 0.13.1", "bincode", "bs58 0.4.0", "bv", @@ -2937,19 +3465,21 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "solana-address-lookup-table-program", "solana-config-program", "solana-sdk", "solana-vote-program", "spl-token", + "spl-token-2022", "thiserror", "zstd", ] [[package]] name = "solana-address-lookup-table-program" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a686cba166abc2cc6b3b62325018b663299976fd426160dd9b5a805f1c04abb" +checksum = "df996750c5a514d36b8aa35f63661ae743c52a95960c9cec4600a45d181f46c7" dependencies = [ "bincode", "bytemuck", @@ -2968,9 +3498,9 @@ dependencies = [ [[package]] name = "solana-banks-client" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b0519ca5c90ed27b15f5c12661609a1ca11c8752523993018eb92ac7468853" +checksum = "a03d21338c579b621b26cb3c8ef05a417d3852891cef46312cb4df00574b8371" dependencies = [ "borsh", "futures", @@ -2985,9 +3515,9 @@ dependencies = [ [[package]] name = "solana-banks-interface" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0057cf11cf1e62e472f1e377b69792f1be7b451f81a934c4b1203a01c8ae972" +checksum = "a1b83bf0d8b1cac6f7f82d872f660e5b1a54c1c8698d4706972237391fc7eff2" dependencies = [ "serde", "solana-sdk", @@ -2996,14 +3526,15 @@ dependencies = [ [[package]] name = "solana-banks-server" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "affdde46e9c99c5c9f2a756ab9b1224d36a9f26b25f73f9a53aa939f6bb32ece" +checksum = "1c6d523f852b125d700e797d6a9517adf36c867369311fef1cf6b343e2fa797e" dependencies = [ "bincode", "crossbeam-channel", "futures", "solana-banks-interface", + "solana-client", "solana-runtime", "solana-sdk", "solana-send-transaction-service", @@ -3015,9 +3546,9 @@ dependencies = [ [[package]] name = "solana-bpf-loader-program" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fe3f96c2ff6394397aeef3f561ecc6a460101ef1716b7935cbc637955950ff9" +checksum = "197e44180b0b5fdba6814a03d8ee3692325b579465286389bc85f693f507b403" dependencies = [ "bincode", "byteorder", @@ -3034,9 +3565,9 @@ dependencies = [ [[package]] name = "solana-bucket-map" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f0fb8a2c8ddd776b3faf7c7c8ac4202c6d9d0284fabdba31bebf5a48c6ad13f" +checksum = "8161bbbdaf7f0c21e2af497e436851c2a10ff17721ef1b6b53dc0b2a83883abe" dependencies = [ "log", "memmap2", @@ -3049,12 +3580,12 @@ dependencies = [ [[package]] name = "solana-clap-utils" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0819bcf2707ab6ca3632ddbeed21c5bcdfdda71b185e8a100a330a6592d6f247" +checksum = "9f01fb8496fdd7f6c5994182b55b7d3a29b94a0ff09ce25fe6e810743996914a" dependencies = [ "chrono", - "clap", + "clap 2.34.0", "rpassword", "solana-perf", "solana-remote-wallet", @@ -3067,9 +3598,9 @@ dependencies = [ [[package]] name = "solana-cli-config" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65b1ebf314662f6f9bf7b7d0f03eccf44ae0ef77108709ca9f1001525533a86d" +checksum = "3dc0c843ad3db6f791b8279aeae0d61f0151709d574edde56d17fa0f7f2230d5" dependencies = [ "dirs-next", "lazy_static", @@ -3083,26 +3614,27 @@ dependencies = [ [[package]] name = "solana-client" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "646e73474695b4b906efa045533ccc9d9abbdaa93bf55e3bd0cb6be460c2dc05" +checksum = "67dd2fd7ba13f301d953073463a479890f21d930819794d7a9e80ace61dc8904" dependencies = [ "async-mutex", "async-trait", - "base64 0.13.0", + "base64 0.13.1", "bincode", "bs58 0.4.0", "bytes", - "clap", + "clap 2.34.0", "crossbeam-channel", + "enum_dispatch", "futures", "futures-util", + "indexmap", "indicatif", "itertools", "jsonrpc-core", "lazy_static", "log", - "lru", "quinn", "quinn-proto", "rand 0.7.3", @@ -3125,6 +3657,7 @@ dependencies = [ "solana-transaction-status", "solana-version", "solana-vote-program", + "spl-token-2022", "thiserror", "tokio", "tokio-stream", @@ -3135,9 +3668,9 @@ dependencies = [ [[package]] name = "solana-compute-budget-program" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb085fe05c8a42a4283ba4e9d1bc970259b28903d9c85e27be760e2e66f07b0b" +checksum = "6f2de1835ca9bb54d759f42bcbafe4db251bd211217c5f7d4d164ea21e6e9b14" dependencies = [ "solana-program-runtime", "solana-sdk", @@ -3145,9 +3678,9 @@ dependencies = [ [[package]] name = "solana-config-program" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22102a47604c6e7baa50faea8ae942304c79d5a5ad9d4759b59f91750f4d0681" +checksum = "3fdfe7c2946d9f552cd91fffb8a991eb40465f70586d5fb71f9a49dc0cd296f5" dependencies = [ "bincode", "chrono", @@ -3159,13 +3692,13 @@ dependencies = [ [[package]] name = "solana-faucet" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3dffea98692d533c3aab77a6c594fb9bf8d80fd81434ee37e0169467dac74f" +checksum = "ce450d5c9114569329f0e6900a53be424f77ba07ed932b5d57a3bfb55afefe41" dependencies = [ "bincode", "byteorder", - "clap", + "clap 2.34.0", "crossbeam-channel", "log", "serde", @@ -3183,43 +3716,55 @@ dependencies = [ [[package]] name = "solana-frozen-abi" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55e139feda2afd510dea2027584cc35a9ee9ebac0bfef1ab4d23647bf5c091a6" +checksum = "a73da3a286cf0d1ab25d669c17a3c2b5fe1334f8262b9673cb22912d92a94b14" dependencies = [ + "ahash", + "blake3", + "block-buffer 0.9.0", "bs58 0.4.0", "bv", + "byteorder", + "cc", + "either", "generic-array", + "getrandom 0.1.16", + "hashbrown 0.12.3", "im", "lazy_static", "log", "memmap2", + "once_cell", + "rand_core 0.6.4", "rustc_version", "serde", "serde_bytes", "serde_derive", + "serde_json", "sha2 0.10.6", "solana-frozen-abi-macro", + "subtle", "thiserror", ] [[package]] name = "solana-frozen-abi-macro" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "798b851026ff2366b318f7818a64846dd1ebc7e613f7893fba07a61cd55e6a48" +checksum = "c88a0446927b49aee9b40ec1c6a96be562a9de543a0c58483a8520f99f454f36" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.15", + "proc-macro2 1.0.56", + "quote 1.0.26", "rustc_version", - "syn 1.0.103", + "syn 1.0.109", ] [[package]] name = "solana-logger" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63d0e97c25935253d93bf9c4740a990264185d1cf59a638a87066278ed817f0" +checksum = "48ec3aec81a83a876c68b6225d7eaf465b97e2d88ff33b2426e77ba08eded7ce" dependencies = [ "env_logger", "lazy_static", @@ -3228,9 +3773,9 @@ dependencies = [ [[package]] name = "solana-measure" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77b68f328ec2748982a591fd49979948dc46f807e0400b7fd396cb898c60713d" +checksum = "cecc0ddf9b0db68e2e92664b6e0432acf9d1739b3a6bc76a466c910d88d0ba98" dependencies = [ "log", "solana-sdk", @@ -3238,9 +3783,9 @@ dependencies = [ [[package]] name = "solana-metrics" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b44056221da6cb2efba4a7c53b16a199265ea15a560f68eb704af26a99901fcb" +checksum = "684c01d65b3b5a546afaff2fd83e9117d0842a1e805a47acba26b461a8b26a4b" dependencies = [ "crossbeam-channel", "gethostname", @@ -3252,12 +3797,12 @@ dependencies = [ [[package]] name = "solana-net-utils" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcaf30fc3e636a8d6336ad46612deca0c880c9b6d42f703fa9e8266585f2b8c2" +checksum = "dc95e2746f871dc2fa7e115a05158148b1522e9c1c99f3e7e3cc02f68dad8a19" dependencies = [ "bincode", - "clap", + "clap 3.2.23", "crossbeam-channel", "log", "nix", @@ -3274,9 +3819,9 @@ dependencies = [ [[package]] name = "solana-perf" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e5434082959d6cbb387d4d821547ff83f88cde9128e92b132ca98ba391695a" +checksum = "94c36a9572ac81be290f006a09aa53d14ce5fb8634345e7bc4fc3c89c0596bfe" dependencies = [ "ahash", "bincode", @@ -3301,11 +3846,11 @@ dependencies = [ [[package]] name = "solana-program" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e41ae6eb080e3ab14e9eda4474b676861af63fb0871db8b9149b0c69aada4d" +checksum = "927d3d7e49093e601811a89ede4a9698059fb819871b3eba88a6cb0c964040fe" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "bincode", "bitflags", "blake3", @@ -3314,41 +3859,49 @@ dependencies = [ "bs58 0.4.0", "bv", "bytemuck", + "cc", "console_error_panic_hook", "console_log", "curve25519-dalek", - "getrandom 0.1.16", + "getrandom 0.2.9", "itertools", "js-sys", "lazy_static", + "libc", "libsecp256k1", "log", + "memoffset 0.6.5", "num-derive", "num-traits", "parking_lot 0.12.1", "rand 0.7.3", + "rand_chacha 0.2.2", "rustc_version", "rustversion", "serde", "serde_bytes", "serde_derive", + "serde_json", "sha2 0.10.6", "sha3 0.10.6", "solana-frozen-abi", "solana-frozen-abi-macro", "solana-sdk-macro", "thiserror", + "tiny-bip39", "wasm-bindgen", + "zeroize", ] [[package]] name = "solana-program-runtime" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516bcd667bc879dadf622ba0b84cbdc7b6618b9e123433998b6c37ba03da81f8" +checksum = "7ddadda3f8b3944188ca93988033cbe5decf569271b5fcc0cd9338282115a47d" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "bincode", + "eager", "enum-iterator", "itertools", "libc", @@ -3356,23 +3909,26 @@ dependencies = [ "log", "num-derive", "num-traits", + "rand 0.7.3", "rustc_version", "serde", "solana-frozen-abi", "solana-frozen-abi-macro", "solana-measure", + "solana-metrics", "solana-sdk", "thiserror", ] [[package]] name = "solana-program-test" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c08e73075d9bf03e5a28972159e392e24359b31c54f144fdd47ec79288c66e" +checksum = "417a2ce701c6c65593a1ae4d654998a7aef9ab69abc7087dc2b999d42eff14da" dependencies = [ + "assert_matches", "async-trait", - "base64 0.13.0", + "base64 0.13.1", "bincode", "chrono-humanize", "log", @@ -3391,9 +3947,9 @@ dependencies = [ [[package]] name = "solana-rayon-threadlimit" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f279d5755275501f5bdcf109fb773d835546265ed3e2ffb90a4e6ab191b7db64" +checksum = "8bd7d70fdf385e1b67d8d43a7d2c5db60e0dc667de4cfee1471cead6563e6878" dependencies = [ "lazy_static", "num_cpus", @@ -3401,9 +3957,9 @@ dependencies = [ [[package]] name = "solana-remote-wallet" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ee4a9f757a8b5943576c1ae2369f17da700813406e40164d22406d8eea7eda" +checksum = "3b5ebbd2a1790e6cd1b594027bdb75da17b410958773fc0f521ff3ee20791dfa" dependencies = [ "console", "dialoguer", @@ -3420,9 +3976,9 @@ dependencies = [ [[package]] name = "solana-runtime" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765eb1373e053d5513eab207f9b9d2fe30d370b6e629c7cdfcced0272cd7eae7" +checksum = "a9bc515c9119a108e67aacb4b8241bddf5fdcaea9a404cfdca75b69418d9be04" dependencies = [ "arrayref", "bincode", @@ -3441,10 +3997,12 @@ dependencies = [ "itertools", "lazy_static", "log", + "lz4", "memmap2", "num-derive", "num-traits", "num_cpus", + "once_cell", "ouroboros", "rand 0.7.3", "rayon", @@ -3467,6 +4025,8 @@ dependencies = [ "solana-vote-program", "solana-zk-token-proof-program", "solana-zk-token-sdk", + "strum", + "strum_macros", "symlink", "tar", "tempfile", @@ -3476,12 +4036,12 @@ dependencies = [ [[package]] name = "solana-sdk" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef63e9db435bcc173d37073d769871379bb90c0fc60b20616f87edc0b06d890" +checksum = "6c925686af7b3235245997acdac126e53c78bab8b924b11434ca5ec45259114d" dependencies = [ "assert_matches", - "base64 0.13.0", + "base64 0.13.1", "bincode", "bitflags", "borsh", @@ -3503,7 +4063,7 @@ dependencies = [ "memmap2", "num-derive", "num-traits", - "pbkdf2 0.10.1", + "pbkdf2 0.11.0", "qstring", "rand 0.7.3", "rand_chacha 0.2.2", @@ -3527,22 +4087,22 @@ dependencies = [ [[package]] name = "solana-sdk-macro" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4d6c73b2dea5705afd28266015a00e7a9c3496a98a45104733fa81e8dabbb5" +checksum = "f511aecadeab3ebc0db10e78d9e7b571dffe1744c0003d6602f537581c3448cf" dependencies = [ "bs58 0.4.0", - "proc-macro2 1.0.47", - "quote 1.0.15", + "proc-macro2 1.0.56", + "quote 1.0.26", "rustversion", - "syn 1.0.103", + "syn 1.0.109", ] [[package]] name = "solana-send-transaction-service" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49662cc361f31b211f292b8e9b228a2a8badb7874ce3d5b43e412d8c4ccb02c8" +checksum = "c4caef6a83ebb24b78b19f5a894d6ab10c196322217ccceeec5d4fd2b594b36d" dependencies = [ "crossbeam-channel", "log", @@ -3555,9 +4115,9 @@ dependencies = [ [[package]] name = "solana-stake-program" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b0c454ae9cdc884fcb964ef4b5e5053d2bfce09fed45d0dbf3ed09742e6ba7" +checksum = "2c794a81a68d12192fc08064431b32a0bc9976c7df67c6921fda99604d7bea6e" dependencies = [ "bincode", "log", @@ -3578,18 +4138,20 @@ dependencies = [ [[package]] name = "solana-streamer" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fea5ffd8c2af4cea444af5f8ada7e88fb004bb49179bbbb741b783894d8373" +checksum = "971e56ca8c6bcd2f36992dd04b6bf65f0786bba55e458bdaa42ddc9c948f39dd" dependencies = [ "crossbeam-channel", "futures-util", "histogram", + "indexmap", "itertools", "libc", "log", "nix", "pem", + "percentage", "pkcs8", "quinn", "rand 0.7.3", @@ -3600,17 +4162,19 @@ dependencies = [ "solana-sdk", "thiserror", "tokio", + "x509-parser", ] [[package]] name = "solana-transaction-status" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3cff388aa40e46921e99f98dc2b82d138588cbbcdcc7cff1212d8aa9d5918a8" +checksum = "58138ee0d2c3f0b3be7e7d8a5bfafdd3fafe66e108b8934169b8b7ecfe8ac60e" dependencies = [ "Inflector", - "base64 0.13.0", + "base64 0.13.1", "bincode", + "borsh", "bs58 0.4.0", "lazy_static", "log", @@ -3618,25 +4182,27 @@ dependencies = [ "serde_derive", "serde_json", "solana-account-decoder", + "solana-address-lookup-table-program", "solana-measure", "solana-metrics", - "solana-runtime", "solana-sdk", "solana-vote-program", "spl-associated-token-account", "spl-memo", "spl-token", + "spl-token-2022", "thiserror", ] [[package]] name = "solana-version" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c331921cb9b9d3e620cdcd19822b29a80a5360e352aaf08b9da043b2124566bf" +checksum = "d976c2590fb565b2e07ff3659deb94774f3a7edf90ddcaa62078164740b8fee5" dependencies = [ "log", "rustc_version", + "semver", "serde", "serde_derive", "solana-frozen-abi", @@ -3646,9 +4212,9 @@ dependencies = [ [[package]] name = "solana-vote-program" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf37814a77d89b07a00c75e7976b99a97ded6ae1037c1e5a36c23a66a7158fc6" +checksum = "ffc47706ca644433d7681f3fe3e0b30094260065ae86a53ae4f92078a7cd4bf4" dependencies = [ "bincode", "log", @@ -3667,9 +4233,9 @@ dependencies = [ [[package]] name = "solana-zk-token-proof-program" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f6c9d71e341399d2b1f6ed5cb72bc2897a749006750e04ba1a6504d7bdd51f" +checksum = "d7704396dcd9338e6ac72137908ad5781edd023767d6e6d6b0a68938b8d86fb5" dependencies = [ "bytemuck", "getrandom 0.1.16", @@ -3682,19 +4248,20 @@ dependencies = [ [[package]] name = "solana-zk-token-sdk" -version = "1.10.9" +version = "1.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b83cd4399de06ca899a36eb62f69c50abad6265648501d52d3e7b05cf96bae7a" +checksum = "facf969af237320649c2ea99be5f75e98cba9b6e3217d9ddc5cbf3497c0282f9" dependencies = [ "aes-gcm-siv", "arrayref", - "base64 0.13.0", + "base64 0.13.1", "bincode", "bytemuck", "byteorder", - "cipher 0.4.3", + "cipher 0.4.4", "curve25519-dalek", "getrandom 0.1.16", + "itertools", "lazy_static", "merlin", "num-derive", @@ -3712,9 +4279,9 @@ dependencies = [ [[package]] name = "solana_rbpf" -version = "0.2.24" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41e138f6d6d4eb6a65f8e9f01ca620bc9907d79648d5038a69dd3f07b6ed3f1f" +checksum = "80a28c5dfe7e8af38daa39d6561c8e8b9ed7a2f900951ebe7362ad6348d36c73" dependencies = [ "byteorder", "combine", @@ -3722,42 +4289,42 @@ dependencies = [ "hash32", "libc", "log", - "rand 0.7.3", + "rand 0.8.5", "rustc-demangle", "scroll", "thiserror", - "time 0.1.44", ] [[package]] name = "solend-program" version = "0.1.0" dependencies = [ - "arrayref", "assert_matches", - "base64 0.13.0", + "base64 0.13.1", + "bincode", + "borsh", "bytemuck", "log", - "num-derive", - "num-traits", "proptest", + "pyth-sdk-solana", "serde", "serde_yaml", "solana-program", "solana-program-test", "solana-sdk", + "solend-sdk", "spl-token", + "static_assertions", "switchboard-program", "switchboard-v2", "thiserror", - "uint", ] [[package]] name = "solend-program-cli" version = "0.1.0" dependencies = [ - "clap", + "clap 2.34.0", "solana-clap-utils", "solana-cli-config", "solana-client", @@ -3765,7 +4332,32 @@ dependencies = [ "solana-program", "solana-sdk", "solend-program", + "solend-sdk", + "spl-associated-token-account", + "spl-token", +] + +[[package]] +name = "solend-sdk" +version = "0.1.0" +dependencies = [ + "arrayref", + "assert_matches", + "base64 0.13.1", + "bytemuck", + "log", + "num-derive", + "num-traits", + "proptest", + "pyth-sdk-solana", + "serde", + "serde_yaml", + "solana-program", + "solana-sdk", "spl-token", + "static_assertions", + "thiserror", + "uint", ] [[package]] @@ -3786,12 +4378,18 @@ dependencies = [ [[package]] name = "spl-associated-token-account" -version = "1.0.3" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "393e2240d521c3dd770806bff25c2c00d761ac962be106e14e22dd912007f428" +checksum = "fbc000f0fdf1f12f99d77d398137c1751345b18c88258ce0f99b7872cf6c9bd6" dependencies = [ + "assert_matches", + "borsh", + "num-derive", + "num-traits", "solana-program", "spl-token", + "spl-token-2022", + "thiserror", ] [[package]] @@ -3805,11 +4403,12 @@ dependencies = [ [[package]] name = "spl-token" -version = "3.2.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93bfdd5bd7c869cb565c7d7635c4fafe189b988a0bdef81063cd9585c6b8dc01" +checksum = "8e85e168a785e82564160dcb87b2a8e04cee9bfd1f4d488c729d53d6a4bd300d" dependencies = [ "arrayref", + "bytemuck", "num-derive", "num-traits", "num_enum", @@ -3818,10 +4417,22 @@ dependencies = [ ] [[package]] -name = "stable_deref_trait" -version = "1.2.0" +name = "spl-token-2022" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "0edb869dbe159b018f17fb9bfa67118c30f232d7f54a73742bc96794dff77ed8" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-program", + "solana-zk-token-sdk", + "spl-memo", + "spl-token", + "thiserror", +] [[package]] name = "static_assertions" @@ -3835,26 +4446,60 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck 0.4.1", + "proc-macro2 1.0.56", + "quote 1.0.26", + "rustversion", + "syn 1.0.109", +] + [[package]] name = "subtle" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +[[package]] +name = "superslice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16ced94dbd8a46c82fd81e3ed9a8727dac2977ea869d217bcc4ea1f122e81f" + [[package]] name = "switchboard-program" -version = "0.1.59" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57445d78029687264e245eb949e5c928e3aecd64a0afea770fc4fb2a826e60e0" +checksum = "534d4b2d45907427fc8d2cd151465cfaee3709c4742491734bc34e5a458ebd09" dependencies = [ "bincode", "borsh", + "bytemuck", "byteorder", "quick-protobuf", "solana-program", "switchboard-protos", "switchboard-utils", - "zerocopy", ] [[package]] @@ -3871,9 +4516,9 @@ dependencies = [ [[package]] name = "switchboard-utils" -version = "0.1.37" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b18fece38368c160cb28b6f038c29aabd6936a13728b64537af90271384205" +checksum = "4ac1d68193aa1669e34d16087db0f96e6597d2f78868378aabc1387b8b29172e" dependencies = [ "bincode", "borsh", @@ -3888,15 +4533,17 @@ dependencies = [ [[package]] name = "switchboard-v2" -version = "0.1.3" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b81ecf3120ae0e6d5518c4a0d88c0e4f6a7be24caaf46a48a566cf149bad3458" +checksum = "8abae8f9cce6c361940bf09fdff5772f32c9d24f3144c0767a10b1109bea7f26" dependencies = [ "anchor-lang", - "borsh", + "anchor-spl", "bytemuck", "rust_decimal", "solana-program", + "spl-token", + "superslice", ] [[package]] @@ -3918,12 +4565,23 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.103" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.15", + "proc-macro2 1.0.56", + "quote 1.0.26", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf316d5356ed6847742d036f8a39c3b8435cac10bd528a4bd461928a6ab34d5" +dependencies = [ + "proc-macro2 1.0.56", + "quote 1.0.26", "unicode-ident", ] @@ -3933,10 +4591,10 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", - "unicode-xid 0.2.2", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", + "unicode-xid 0.2.4", ] [[package]] @@ -3952,9 +4610,9 @@ dependencies = [ [[package]] name = "tarpc" -version = "0.27.2" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b85d0a9369a919ba0db919b142a2b704cd207dfc676f7a43c2d105d0bc225487" +checksum = "1c38a012bed6fb9681d3bf71ffaa4f88f3b4b9ed3198cda6e4c8462d24d4bb80" dependencies = [ "anyhow", "fnv", @@ -3962,14 +4620,14 @@ dependencies = [ "humantime", "opentelemetry", "pin-project", - "rand 0.8.4", + "rand 0.8.5", "serde", "static_assertions", "tarpc-plugins", "thiserror", "tokio", "tokio-serde", - "tokio-util", + "tokio-util 0.6.10", "tracing", "tracing-opentelemetry", ] @@ -3980,44 +4638,33 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee42b4e559f17bce0385ebf511a7beb67d5cc33c12c96b7f4e9789919d9c10f" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", ] [[package]] name = "tempfile" -version = "3.3.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" dependencies = [ "cfg-if", "fastrand", - "libc", - "redox_syscall", - "remove_dir_all", - "winapi", + "redox_syscall 0.3.5", + "rustix", + "windows-sys 0.45.0", ] [[package]] name = "termcolor" -version = "1.1.2" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" dependencies = [ "winapi-util", ] -[[package]] -name = "terminal_size" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "textwrap" version = "0.11.0" @@ -4027,40 +4674,47 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + [[package]] name = "thiserror" -version = "1.0.30" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.30" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 2.0.14", ] [[package]] name = "thread_local" -version = "1.1.4" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ + "cfg-if", "once_cell", ] [[package]] name = "time" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" dependencies = [ "libc", "wasi 0.10.0+wasi-snapshot-preview1", @@ -4069,12 +4723,14 @@ dependencies = [ [[package]] name = "time" -version = "0.3.17" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" dependencies = [ + "itoa", "serde", "time-core", + "time-macros", ] [[package]] @@ -4083,6 +4739,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +[[package]] +name = "time-macros" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +dependencies = [ + "time-core", +] + [[package]] name = "tiny-bip39" version = "0.8.2" @@ -4104,25 +4769,26 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.16.1" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a" +checksum = "b9d0183f6f6001549ab68f8c7585093bb732beefbcf6d23a10b9b95c73a1dd49" dependencies = [ + "autocfg", "bytes", "libc", "memchr", @@ -4138,20 +4804,20 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.7.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", ] [[package]] name = "tokio-rustls" -version = "0.23.2" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27d5f2b839802bd8267fa19b0530f5a08b9c08cd417976be2a65d130fe1c11b" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" dependencies = [ "rustls", "tokio", @@ -4176,9 +4842,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.8" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" +checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313" dependencies = [ "futures-core", "pin-project-lite", @@ -4203,9 +4869,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.9" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" +checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" dependencies = [ "bytes", "futures-core", @@ -4216,26 +4882,57 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + [[package]] name = "toml" -version = "0.5.8" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" + +[[package]] +name = "toml_edit" +version = "0.19.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "tower-service" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.29" +version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", "log", @@ -4250,26 +4947,28 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", ] [[package]] name = "tracing-core" -version = "0.1.21" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ - "lazy_static", + "once_cell", + "valuable", ] [[package]] name = "tracing-opentelemetry" -version = "0.15.0" +version = "0.17.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "599f388ecb26b28d9c1b2e4437ae019a7b336018b45ed911458cd9ebf91129f6" +checksum = "fbbe89715c1dbbb790059e2565353978564924ee85017b5fff365c872ff6721f" dependencies = [ + "once_cell", "opentelemetry", "tracing", "tracing-core", @@ -4278,9 +4977,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.2.25" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" dependencies = [ "sharded-slab", "thread_local", @@ -4289,9 +4988,9 @@ dependencies = [ [[package]] name = "try-lock" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "tungstenite" @@ -4299,13 +4998,13 @@ version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "byteorder", "bytes", "http", "httparse", "log", - "rand 0.8.4", + "rand 0.8.5", "rustls", "sha-1", "thiserror", @@ -4317,15 +5016,15 @@ dependencies = [ [[package]] name = "typenum" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "uint" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11fe9a9348741cf134085ad57c249508345fe16411b3d7fb4ff2da2f1d6382e" +checksum = "6470ab50f482bde894a037a57064480a246dbfdd5960bd65a44824693f08da5f" dependencies = [ "byteorder", "crunchy", @@ -4333,38 +5032,44 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-bidi" -version = "0.3.7" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.5" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" [[package]] name = "unicode-normalization" -version = "0.1.19" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "unicode-width" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] name = "unicode-xid" @@ -4374,9 +5079,9 @@ checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" [[package]] name = "unicode-xid" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "universal-hash" @@ -4405,9 +5110,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "uriparse" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e515b1ada404168e145ac55afba3c42f04cf972201a8552d42e2abb17c1b7221" +checksum = "0200d0fc04d809396c2ad43f3c95da3582a2556eba8d453c1087f4120ee352ff" dependencies = [ "fnv", "lazy_static", @@ -4415,13 +5120,12 @@ dependencies = [ [[package]] name = "url" -version = "2.2.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", ] @@ -4431,6 +5135,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vec_map" version = "0.8.2" @@ -4460,12 +5170,11 @@ dependencies = [ [[package]] name = "walkdir" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" dependencies = [ "same-file", - "winapi", "winapi-util", ] @@ -4491,11 +5200,17 @@ version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" -version = "0.2.79" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -4503,24 +5218,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.79" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" dependencies = [ "bumpalo", - "lazy_static", "log", - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "once_cell", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.29" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb6ec270a31b1d3c7e266b999739109abce8b6c87e4b31fcfcd788b65267395" +checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" dependencies = [ "cfg-if", "js-sys", @@ -4530,38 +5245,38 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.79" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" dependencies = [ - "quote 1.0.15", + "quote 1.0.26", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.79" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.79" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" [[package]] name = "web-sys" -version = "0.3.56" +version = "0.3.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" +checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" dependencies = [ "js-sys", "wasm-bindgen", @@ -4579,9 +5294,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.22.2" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552ceb903e957524388c4d3475725ff2c8b7960922063af6ce53c9a43da07449" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" dependencies = [ "webpki", ] @@ -4617,62 +5332,170 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.0", +] + [[package]] name = "windows-sys" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] name = "windows_aarch64_msvc" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_i686_gnu" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_msvc" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" -version = "0.42.0" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_msvc" -version = "0.42.0" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "winnow" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" +dependencies = [ + "memchr", +] [[package]] name = "winreg" @@ -4683,11 +5506,29 @@ dependencies = [ "winapi", ] +[[package]] +name = "x509-parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0ecbeb7b67ce215e40e3cc7f2ff902f94a223acf44995934763467e7b1febc8" +dependencies = [ + "asn1-rs", + "base64 0.13.1", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror", + "time 0.3.20", +] + [[package]] name = "xattr" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" +checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" dependencies = [ "libc", ] @@ -4703,38 +5544,17 @@ dependencies = [ [[package]] name = "yansi" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "yasna" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346d34a236c9d3e5f3b9b74563f238f955bbd05fa0b8b4efa53c130c43982f4c" -dependencies = [ - "time 0.3.17", -] - -[[package]] -name = "zerocopy" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6580539ad917b7c026220c4b3f2c08d52ce54d6ce0dc491e66002e35388fab46" -dependencies = [ - "byteorder", - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.2.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d498dbd1fd7beb83c86709ae1c33ca50942889473473d287d56ce4770a18edfb" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" dependencies = [ - "proc-macro2 1.0.47", - "syn 1.0.103", - "synstructure", + "time 0.3.20", ] [[package]] @@ -4748,14 +5568,13 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.3.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81e8f13fef10b63c06356d65d416b070798ddabcadc10d3ece0c5be9b3c7eddb" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.15", - "syn 1.0.103", - "synstructure", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 2.0.14", ] [[package]] @@ -4779,10 +5598,11 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.3+zstd.1.5.2" +version = "2.0.8+zstd.1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44ccf97612ac95f3ccb89b2d7346b345e52f1c3019be4984f0455fb4ba991f8a" +checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" dependencies = [ "cc", "libc", + "pkg-config", ] diff --git a/Cargo.toml b/Cargo.toml index 091493833da..5916528a22d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "token-lending/cli", "token-lending/program", + "token-lending/sdk", ] [profile.dev] 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-build-deps.sh b/ci/install-build-deps.sh index 30c967dde45..cd9c815df08 100755 --- a/ci/install-build-deps.sh +++ b/ci/install-build-deps.sh @@ -5,11 +5,9 @@ set -ex wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - sudo apt-add-repository "deb http://apt.llvm.org/bionic/ llvm-toolchain-bionic-10 main" sudo apt-get update -sudo apt-get install -y clang-7 --allow-unauthenticated sudo apt-get install -y openssl --allow-unauthenticated sudo apt-get install -y libssl-dev --allow-unauthenticated sudo apt-get install -y libssl1.1 --allow-unauthenticated sudo apt-get install -y libudev-dev sudo apt-get install -y binutils-dev sudo apt-get install -y libunwind-dev -clang-7 --version diff --git a/ci/install-program-deps.sh b/ci/install-program-deps.sh index 8c076bf8b8e..ccfa61a9098 100755 --- a/ci/install-program-deps.sh +++ b/ci/install-program-deps.sh @@ -10,5 +10,7 @@ set -x cargo --version cargo install rustfilt || true 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 150ef983fd5..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.58.1 + 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 @@ -51,9 +51,9 @@ export rust_nightly_docker_image=solanalabs/rust-nightly:"$nightly_version" stable) rustup_install "$rust_stable" ;; - # nightly) - # rustup_install "$rust_nightly" - # ;; + nightly) + rustup_install "$rust_nightly" + ;; all) rustup_install "$rust_stable" rustup_install "$rust_nightly" diff --git a/ci/solana-version.sh b/ci/solana-version.sh index 0d0ea6b2254..f6bf113686e 100755 --- a/ci/solana-version.sh +++ b/ci/solana-version.sh @@ -14,7 +14,7 @@ if [[ -n $SOLANA_VERSION ]]; then solana_version="$SOLANA_VERSION" else - solana_version=v1.8.14 + solana_version=v1.14.10 fi export solana_version="$solana_version" diff --git a/coverage.sh b/coverage.sh index e21e0fc92fb..3bef941b59a 100755 --- a/coverage.sh +++ b/coverage.sh @@ -2,8 +2,8 @@ # # Runs all program tests and builds a code coverage report # +set -ex -set -e cd "$(dirname "$0")" if ! which grcov; then @@ -11,84 +11,20 @@ if ! which grcov; then exit 1 fi -if [[ ! "$(grcov --version)" =~ "0.6.1" ]]; then - echo Error: Required grcov version not installed - 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 +nightly test --target-dir $here/target/cov - ) -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 --llvm -t html -o target/cov/$reportName - grcov target/cov/tmp --llvm -t lcov -o target/cov/lcov.info +# 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/$reportName/index.html -ln -sfT $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/cli/Cargo.toml b/token-lending/cli/Cargo.toml index 3b8cdf941be..4a93c7a10fe 100644 --- a/token-lending/cli/Cargo.toml +++ b/token-lending/cli/Cargo.toml @@ -9,15 +9,17 @@ repository = "https://github.com/solendprotocol/solana-program-library" version = "0.1.0" [dependencies] -clap = "2.34.0" -solana-clap-utils = "1.10.0" -solana-cli-config = "1.10.0" -solana-client = "1.10.0" -solana-logger = "1.10.0" -solana-sdk = "1.10.0" -solana-program = "1.10.0" +clap = "=2.34.0" +solana-clap-utils = "1.14.10" +solana-cli-config = "1.14.10" +solana-client = "1.14.10" +solana-logger = "1.14.10" +solana-sdk = "1.14.10" +solana-program = "1.14.10" +solend-sdk = { path="../sdk" } solend-program = { path="../program", features = [ "no-entrypoint" ] } -spl-token = { version = "3.2.0", features=["no-entrypoint"] } +spl-token = { version = "3.3.0", features=["no-entrypoint"] } +spl-associated-token-account = "1.0" [[bin]] name = "solend-program" diff --git a/token-lending/cli/scripts/liquidate.sh b/token-lending/cli/scripts/liquidate.sh new file mode 100755 index 00000000000..44b9fd27654 --- /dev/null +++ b/token-lending/cli/scripts/liquidate.sh @@ -0,0 +1,30 @@ +set -ex + +ETH_RESERVE=CPDiKagfozERtJ33p7HHhEfJERjvfk1VAjMXAFLrvrKP +SOL_RESERVE=8PbodeaosQP19SjYFx855UMqWxH2HynZLdBXmsrbac36 +STSOL_RESERVE=5sjkv6HD8wycocJ4tC4U36HHbvgcXYqcyiPRUkncnwWs +MSOL_RESERVE=CCpirWrgNuBVLdkP2haxLTbD6XqEgaYuVXixbbpxUB6 +USDC_RESERVE=BgxfHJDzm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw +USDT_RESERVE=8K9WC8xoh2rtQNY7iEGXtPvfbDCi563SdWhCAhuMP2xE +BTC_RESERVE=GYzjMCXTDue12eUGKKWAqtF5jcBYNmewr6Db6LaguEaX +RAY_RESERVE=9n2exoMQwMTzfw6NFoFFujxYPndWVLtKREJePssrKb36 +SLND_RESERVE=CviGNzD2C9ZCMmjDt5DKCce5cLV4Emrcm3NFvwudBFKA + +USDC_ATA=Bqn9qMjFEHRNS4wBRVAs3Uc52Dr3vm2AXJ5GaMkepBiQ +USDT_ATA=2bEeupwb9eC5R9LjCCrfetPm5yLwdGVLYng6XhNtue9H +BTC_ATA=A6Fu8DtnUqeYpzUMbZnnDpUFE5URnNUd8toZzcNBMkJ4 + +OBLIGATION_PUBKEY=HLRd6Dn4RUs4XbVzYhdp6UswQMCJTqWc9PgJ6VxvsyXu +# OBLIGATION_PUBKEY=3ErCznFWTRmhZE8C1mAQCkcneqcZQedB5ACqAwbbWUAP +REPAY_RESERVE=$USDC_RESERVE +WITHDRAW_RESERVE=$SOL_RESERVE +LIQUIDITY_AMOUNT=1000000000 +SOURCE_LIQUIDITY=$USDC_ATA + + +cargo run liquidate-obligation \ + --obligation $OBLIGATION_PUBKEY \ + --repay-reserve $REPAY_RESERVE \ + --withdraw-reserve $WITHDRAW_RESERVE \ + --liquidity-amount $LIQUIDITY_AMOUNT \ + --source-liquidity $SOURCE_LIQUIDITY diff --git a/token-lending/cli/scripts/withdraw.sh b/token-lending/cli/scripts/withdraw.sh new file mode 100755 index 00000000000..ccd4e3a3577 --- /dev/null +++ b/token-lending/cli/scripts/withdraw.sh @@ -0,0 +1,25 @@ +set -ex + +ETH_RESERVE=CPDiKagfozERtJ33p7HHhEfJERjvfk1VAjMXAFLrvrKP +SOL_RESERVE=8PbodeaosQP19SjYFx855UMqWxH2HynZLdBXmsrbac36 +STSOL_RESERVE=5sjkv6HD8wycocJ4tC4U36HHbvgcXYqcyiPRUkncnwWs +MSOL_RESERVE=CCpirWrgNuBVLdkP2haxLTbD6XqEgaYuVXixbbpxUB6 +USDC_RESERVE=BgxfHJDzm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw +USDT_RESERVE=8K9WC8xoh2rtQNY7iEGXtPvfbDCi563SdWhCAhuMP2xE +BTC_RESERVE=GYzjMCXTDue12eUGKKWAqtF5jcBYNmewr6Db6LaguEaX +RAY_RESERVE=9n2exoMQwMTzfw6NFoFFujxYPndWVLtKREJePssrKb36 +SLND_RESERVE=CviGNzD2C9ZCMmjDt5DKCce5cLV4Emrcm3NFvwudBFKA + +USDC_ATA=Bqn9qMjFEHRNS4wBRVAs3Uc52Dr3vm2AXJ5GaMkepBiQ +USDT_ATA=2bEeupwb9eC5R9LjCCrfetPm5yLwdGVLYng6XhNtue9H +BTC_ATA=A6Fu8DtnUqeYpzUMbZnnDpUFE5URnNUd8toZzcNBMkJ4 + +OBLIGATION_PUBKEY=9XQ18M6VvB9X9QVXxj1bCvKifAW2RWDePEfBwi6fsLhq +WITHDRAW_RESERVE=$SOL_RESERVE +COLLATERAL_AMOUNT=1000 + + +cargo run withdraw-collateral \ + --obligation $OBLIGATION_PUBKEY \ + --withdraw-reserve $WITHDRAW_RESERVE \ + --withdraw-amount $COLLATERAL_AMOUNT \ diff --git a/token-lending/cli/src/lending_state.rs b/token-lending/cli/src/lending_state.rs new file mode 100644 index 00000000000..5ed1e23e9e6 --- /dev/null +++ b/token-lending/cli/src/lending_state.rs @@ -0,0 +1,135 @@ +use solana_program::instruction::Instruction; +use solend_sdk::instruction::{ + refresh_obligation, refresh_reserve, withdraw_obligation_collateral, +}; +use solend_sdk::state::{Obligation, Reserve}; + +use solana_client::rpc_client::RpcClient; +use solana_program::program_pack::Pack; +use solana_program::pubkey::Pubkey; +use spl_associated_token_account::get_associated_token_address; +use std::collections::HashSet; + +pub struct SolendState { + lending_program_id: Pubkey, + obligation_pubkey: Pubkey, + obligation: Obligation, + reserves: Vec<(Pubkey, Reserve)>, +} + +impl SolendState { + pub fn new( + lending_program_id: Pubkey, + obligation_pubkey: Pubkey, + rpc_client: &RpcClient, + ) -> Self { + let obligation = { + let data = rpc_client.get_account(&obligation_pubkey).unwrap(); + Obligation::unpack(&data.data).unwrap() + }; + + // get reserve pubkeys + let reserve_pubkeys: Vec = { + let mut r = HashSet::new(); + r.extend(obligation.deposits.iter().map(|d| d.deposit_reserve)); + r.extend(obligation.borrows.iter().map(|b| b.borrow_reserve)); + r.into_iter().collect() + }; + + // get reserve accounts + let reserves: Vec<(Pubkey, Reserve)> = rpc_client + .get_multiple_accounts(&reserve_pubkeys) + .unwrap() + .into_iter() + .zip(reserve_pubkeys.iter()) + .map(|(account, pubkey)| (*pubkey, Reserve::unpack(&account.unwrap().data).unwrap())) + .collect(); + + assert!(reserve_pubkeys.len() == reserves.len()); + + Self { + lending_program_id, + obligation_pubkey, + obligation, + reserves, + } + } + + pub fn find_reserve_by_key(&self, pubkey: Pubkey) -> Option<&Reserve> { + self.reserves.iter().find_map( + |(p, reserve)| { + if pubkey == *p { + Some(reserve) + } else { + None + } + }, + ) + } + + fn get_refresh_instructions(&self) -> Vec { + let mut instructions = Vec::new(); + instructions.extend(self.reserves.iter().map(|(pubkey, reserve)| { + refresh_reserve( + self.lending_program_id, + *pubkey, + reserve.liquidity.pyth_oracle_pubkey, + reserve.liquidity.switchboard_oracle_pubkey, + ) + })); + + let reserve_pubkeys: Vec = { + let mut r = Vec::new(); + r.extend(self.obligation.deposits.iter().map(|d| d.deposit_reserve)); + r.extend(self.obligation.borrows.iter().map(|b| b.borrow_reserve)); + r + }; + + // refresh obligation + instructions.push(refresh_obligation( + self.lending_program_id, + self.obligation_pubkey, + reserve_pubkeys, + )); + + instructions + } + + /// withdraw obligation ctokens to owner's ata + pub fn withdraw( + &self, + withdraw_reserve_pubkey: &Pubkey, + collateral_amount: u64, + ) -> Vec { + let mut instructions = self.get_refresh_instructions(); + + // find repay, withdraw reserve states + let withdraw_reserve = self + .reserves + .iter() + .find_map(|(pubkey, reserve)| { + if withdraw_reserve_pubkey == pubkey { + Some(reserve) + } else { + None + } + }) + .unwrap(); + + instructions.push(withdraw_obligation_collateral( + self.lending_program_id, + collateral_amount, + withdraw_reserve.collateral.supply_pubkey, + get_associated_token_address( + &self.obligation.owner, + &withdraw_reserve.collateral.mint_pubkey, + ), + *withdraw_reserve_pubkey, + self.obligation_pubkey, + withdraw_reserve.lending_market, + self.obligation.owner, + )); + + instructions + } +} diff --git a/token-lending/cli/src/main.rs b/token-lending/cli/src/main.rs index 4ff58f5ba9b..efd3b899473 100644 --- a/token-lending/cli/src/main.rs +++ b/token-lending/cli/src/main.rs @@ -1,3 +1,17 @@ +use lending_state::SolendState; +use solana_client::rpc_config::RpcSendTransactionConfig; +use solana_sdk::{commitment_config::CommitmentLevel, compute_budget::ComputeBudgetInstruction}; +use solend_program::{instruction::set_lending_market_owner_and_config, state::RateLimiterConfig}; +use solend_sdk::{ + instruction::{ + liquidate_obligation_and_redeem_reserve_collateral, redeem_reserve_collateral, + refresh_obligation, refresh_reserve, + }, + state::Obligation, +}; + +mod lending_state; + use { clap::{ crate_description, crate_name, crate_version, value_t, App, AppSettings, Arg, ArgMatches, @@ -10,14 +24,16 @@ use { keypair::signer_from_path, }, solana_client::rpc_client::RpcClient, - solana_program::{native_token::lamports_to_sol, program_pack::Pack, pubkey::Pubkey}, + solana_program::{ + message::Message, native_token::lamports_to_sol, program_pack::Pack, pubkey::Pubkey, + }, solana_sdk::{ commitment_config::CommitmentConfig, signature::{Keypair, Signer}, system_instruction, transaction::Transaction, }, - solend_program::{ + solend_sdk::{ self, instruction::{init_lending_market, init_reserve, update_reserve_config}, math::WAD, @@ -33,6 +49,9 @@ use { system_instruction::create_account, }; +use spl_associated_token_account::get_associated_token_address; +use spl_associated_token_account::instruction::create_associated_token_account; + struct Config { rpc_client: RpcClient, fee_payer: Box, @@ -68,6 +87,14 @@ struct PartialReserveConfig { pub fee_receiver: Option, /// Cut of the liquidation bonus that the protocol receives, as a percentage pub protocol_liquidation_fee: Option, + /// Protocol take rate is the amount borrowed interest protocol recieves, as a percentage + pub protocol_take_rate: Option, + /// Rate Limiter's max window size + pub rate_limiter_window_duration: Option, + /// Rate Limiter's max outflow per window + pub rate_limiter_max_outflow: Option, + /// Added borrow weight in basis points + pub added_borrow_weight_bps: Option, } /// Reserve Fees with optional fields @@ -90,7 +117,7 @@ const SWITCHBOARD_PROGRAM_ID_DEV: &str = "7azgmy1pFXHikv36q1zZASvFq5vFa39TT9NweV fn main() { solana_logger::setup_with_default("solana=info"); - let default_lending_program_id: &str = &solend_program::id().to_string(); + let default_lending_program_id: &str = &solend_sdk::solend_mainnet::id().to_string(); let matches = App::new(crate_name!()) .about(crate_description!()) @@ -190,6 +217,101 @@ fn main() { .help("Currency market prices are quoted in"), ), ) + .subcommand( + SubCommand::with_name("liquidate-obligation") + .about("Liquidate Obligation and redeem reserve collateral") + // @TODO: use is_valid_signer + .arg( + Arg::with_name("obligation") + .long("obligation") + .value_name("OBLIGATION_PUBKEY") + .takes_value(true) + .required(true) + .help("obligation pubkey"), + ) + .arg( + Arg::with_name("repay-reserve") + .long("repay-reserve") + .value_name("RESERVE_PUBKEY") + .takes_value(true) + .required(true) + .help("repay reserve"), + ) + .arg( + Arg::with_name("source-liquidity") + .long("source-liquidity") + .value_name("PUBKEY") + .takes_value(true) + .required(true) + .help("Token account that repays the obligation's debt"), + ) + .arg( + Arg::with_name("withdraw-reserve") + .long("withdraw-reserve") + .value_name("RESERVE_PUBKEY") + .takes_value(true) + .required(true) + .help("withdraw reserve"), + ) + .arg( + Arg::with_name("liquidity-amount") + .long("liquidity-amount") + .value_name("AMOUNT") + .takes_value(true) + .required(true) + .help("amount of tokens to repay"), + ) + ) + .subcommand( + SubCommand::with_name("withdraw-collateral") + .about("Withdraw obligation collateral") + // @TODO: use is_valid_signer + .arg( + Arg::with_name("obligation") + .long("obligation") + .value_name("OBLIGATION_PUBKEY") + .takes_value(true) + .required(true) + .help("obligation pubkey"), + ) + .arg( + Arg::with_name("withdraw-reserve") + .long("withdraw-reserve") + .value_name("RESERVE_PUBKEY") + .takes_value(true) + .required(true) + .help("reserve that you want to withdraw ctokens from"), + ) + .arg( + Arg::with_name("collateral-amount") + .long("withdraw-amount") + .value_name("AMOUNT") + .takes_value(true) + .required(true) + .help("amount of ctokens to withdraw"), + ) + ) + .subcommand( + SubCommand::with_name("redeem-collateral") + .about("Redeem ctokens for tokens") + // @TODO: use is_valid_signer + .arg( + Arg::with_name("redeem-reserve") + .long("redeem-reserve") + .value_name("RESERVE_PUBKEY") + .takes_value(true) + .required(true) + .help("reserve pubkey"), + ) + .arg( + Arg::with_name("collateral-amount") + .long("redeem-amount") + .value_name("AMOUNT") + .takes_value(true) + .required(true) + .help("amount of ctokens to redeem"), + ) + ) .subcommand( SubCommand::with_name("add-reserve") .about("Add a reserve to a lending market") @@ -376,7 +498,16 @@ fn main() { .takes_value(true) .required(false) .default_value("30") - .help("Amount of liquidation bonus going to fee reciever: [0, 100]"), + .help("Amount of liquidation bonus going to fee receiver: [0, 100]"), + ) + .arg( + Arg::with_name("protocol_take_rate") + .long("protocol-take-rate") + .validator(is_parsable::) + .value_name("INTEGER_PERCENT") + .takes_value(true) + .required(false) + .help("Amount of interest spread going to fee receiver: [0, 100]"), ) .arg( Arg::with_name("deposit_limit") @@ -399,6 +530,55 @@ fn main() { .help("Borrow limit"), ) ) + .subcommand( + SubCommand::with_name("set-lending-market-owner-and-config") + .about("Set lending market owner and config") + .arg( + Arg::with_name("lending_market_owner") + .long("market-owner") + .validator(is_keypair) + .value_name("KEYPAIR") + .takes_value(true) + .required(true) + .help("Owner of the lending market"), + ) + .arg( + Arg::with_name("lending_market") + .long("market") + .validator(is_pubkey) + .value_name("PUBKEY") + .takes_value(true) + .required(true) + .help("Lending market address"), + ) + .arg( + Arg::with_name("new_lending_market_owner") + .long("new-lending-market-owner") + .validator(is_keypair) + .value_name("KEYPAIR") + .takes_value(true) + .required(false) + .help("Owner of the lending market"), + ) + .arg( + Arg::with_name("rate_limiter_window_duration") + .long("rate-limiter-window-duration") + .validator(is_parsable::) + .value_name("INTEGER") + .takes_value(true) + .required(false) + .help("Rate Limiter Window Duration in Slots"), + ) + .arg( + Arg::with_name("rate_limiter_max_outflow") + .long("rate-limiter-max-outflow") + .validator(is_parsable::) + .value_name("INTEGER") + .takes_value(true) + .required(false) + .help("Rate Limiter max outflow denominated in dollars within 1 window"), + ) + ) .subcommand( SubCommand::with_name("update-reserve") .about("Update a reserve config") @@ -527,7 +707,16 @@ fn main() { .value_name("INTEGER_PERCENT") .takes_value(true) .required(false) - .help("Amount of liquidation bonus going to fee reciever: [0, 100]"), + .help("Amount of liquidation bonus going to fee receiver: [0, 100]"), + ) + .arg( + Arg::with_name("protocol_take_rate") + .long("protocol-take-rate") + .validator(is_parsable::) + .value_name("INTEGER_PERCENT") + .takes_value(true) + .required(false) + .help("Amount of interest spread going to fee receiver: [0, 100]"), ) .arg( Arg::with_name("deposit_limit") @@ -583,6 +772,33 @@ fn main() { .required(false) .help("Switchboard price feed account: https://switchboard.xyz/#/explorer"), ) + .arg( + Arg::with_name("rate_limiter_window_duration") + .long("rate-limiter-window-duration") + .validator(is_parsable::) + .value_name("INTEGER") + .takes_value(true) + .required(false) + .help("Rate Limiter Window Duration in Slots"), + ) + .arg( + Arg::with_name("rate_limiter_max_outflow") + .long("rate-limiter-max-outflow") + .validator(is_parsable::) + .value_name("INTEGER") + .takes_value(true) + .required(false) + .help("Rate Limiter max outflow of token amounts within 1 window"), + ) + .arg( + Arg::with_name("added_borrow_weight_bps") + .long("added-borrow-weight-bps") + .validator(is_parsable::) + .value_name("INTEGER") + .takes_value(true) + .required(false) + .help("Added borrow weight in basis points"), + ) ) .get_matches(); @@ -638,6 +854,35 @@ fn main() { switchboard_oracle_program_id, ) } + ("liquidate-obligation", Some(arg_matches)) => { + let obligation = pubkey_of(arg_matches, "obligation").unwrap(); + let repay_reserve = pubkey_of(arg_matches, "repay-reserve").unwrap(); + let source_liquidity = pubkey_of(arg_matches, "source-liquidity").unwrap(); + let withdraw_reserve = pubkey_of(arg_matches, "withdraw-reserve").unwrap(); + let liquidity_amount = value_of(arg_matches, "liquidity-amount").unwrap(); + + command_liquidate_obligation( + &config, + obligation, + repay_reserve, + source_liquidity, + withdraw_reserve, + liquidity_amount, + ) + } + ("withdraw-collateral", Some(arg_matches)) => { + let obligation = pubkey_of(arg_matches, "obligation").unwrap(); + let withdraw_reserve = pubkey_of(arg_matches, "withdraw-reserve").unwrap(); + let collateral_amount = value_of(arg_matches, "collateral-amount").unwrap(); + + command_withdraw_collateral(&config, obligation, withdraw_reserve, collateral_amount) + } + ("redeem-collateral", Some(arg_matches)) => { + let redeem_reserve = pubkey_of(arg_matches, "redeem-reserve").unwrap(); + let collateral_amount = value_of(arg_matches, "collateral-amount").unwrap(); + + command_redeem_collateral(&config, &redeem_reserve, collateral_amount) + } ("add-reserve", Some(arg_matches)) => { let lending_market_owner_keypair = keypair_of(arg_matches, "lending_market_owner").unwrap(); @@ -669,6 +914,7 @@ fn main() { let liquidity_fee_receiver_keypair = Keypair::new(); let protocol_liquidation_fee = value_of(arg_matches, "protocol_liquidation_fee").unwrap(); + let protocol_take_rate = value_of(arg_matches, "protocol_take_rate").unwrap(); let source_liquidity_account = config .rpc_client @@ -707,6 +953,8 @@ fn main() { borrow_limit, fee_receiver: liquidity_fee_receiver_keypair.pubkey(), protocol_liquidation_fee, + protocol_take_rate, + added_borrow_weight_bps: 10000, }, source_liquidity_pubkey, source_liquidity_owner_keypair, @@ -719,6 +967,24 @@ fn main() { source_liquidity, ) } + ("set-lending-market-owner-and-config", Some(arg_matches)) => { + let lending_market_owner_keypair = + keypair_of(arg_matches, "lending_market_owner").unwrap(); + let lending_market_pubkey = pubkey_of(arg_matches, "lending_market").unwrap(); + let new_lending_market_owner_keypair = + keypair_of(arg_matches, "new_lending_market_owner"); + let rate_limiter_window_duration = + value_of(arg_matches, "rate_limiter_window_duration"); + let rate_limiter_max_outflow = value_of(arg_matches, "rate_limiter_max_outflow"); + command_set_lending_market_owner_and_config( + &mut config, + lending_market_pubkey, + lending_market_owner_keypair, + new_lending_market_owner_keypair, + rate_limiter_window_duration, + rate_limiter_max_outflow, + ) + } ("update-reserve", Some(arg_matches)) => { let reserve_pubkey = pubkey_of(arg_matches, "reserve").unwrap(); let lending_market_owner_keypair = @@ -738,9 +1004,14 @@ fn main() { let borrow_limit = value_of(arg_matches, "borrow_limit"); let fee_receiver = pubkey_of(arg_matches, "fee_receiver"); let protocol_liquidation_fee = value_of(arg_matches, "protocol_liquidation_fee"); + let protocol_take_rate = value_of(arg_matches, "protocol_take_rate"); let pyth_product_pubkey = pubkey_of(arg_matches, "pyth_product"); let pyth_price_pubkey = pubkey_of(arg_matches, "pyth_price"); let switchboard_feed_pubkey = pubkey_of(arg_matches, "switchboard_feed"); + let rate_limiter_window_duration = + value_of(arg_matches, "rate_limiter_window_duration"); + let rate_limiter_max_outflow = value_of(arg_matches, "rate_limiter_max_outflow"); + let added_borrow_weight_bps = value_of(arg_matches, "added_borrow_weight_bps"); let borrow_fee_wad = borrow_fee.map(|fee| (fee * WAD as f64) as u64); let flash_loan_fee_wad = flash_loan_fee.map(|fee| (fee * WAD as f64) as u64); @@ -764,6 +1035,10 @@ fn main() { borrow_limit, fee_receiver, protocol_liquidation_fee, + protocol_take_rate, + rate_limiter_window_duration, + rate_limiter_max_outflow, + added_borrow_weight_bps, }, pyth_product_pubkey, pyth_price_pubkey, @@ -800,7 +1075,9 @@ fn command_create_lending_market( .rpc_client .get_minimum_balance_for_rent_exemption(LendingMarket::LEN)?; - let mut transaction = Transaction::new_with_payer( + let recent_blockhash = config.rpc_client.get_latest_blockhash()?; + + let message = Message::new_with_blockhash( &[ // Account for the lending market create_account( @@ -821,15 +1098,17 @@ fn command_create_lending_market( ), ], Some(&config.fee_payer.pubkey()), + &recent_blockhash, ); - let (recent_blockhash, fee_calculator) = config.rpc_client.get_recent_blockhash()?; check_fee_payer_balance( config, - lending_market_balance + fee_calculator.calculate_fee(transaction.message()), + lending_market_balance + config.rpc_client.get_fee_for_message(&message)?, )?; - transaction.sign( + + let transaction = Transaction::new( &vec![config.fee_payer.as_ref(), &lending_market_keypair], + message, recent_blockhash, ); send_transaction(config, transaction)?; @@ -845,6 +1124,215 @@ fn command_create_lending_market( Ok(()) } +#[allow(clippy::too_many_arguments)] +fn command_redeem_collateral( + config: &Config, + redeem_reserve_pubkey: &Pubkey, + collateral_amount: u64, +) -> CommandResult { + let redeem_reserve = { + let data = config + .rpc_client + .get_account(redeem_reserve_pubkey) + .unwrap(); + Reserve::unpack(&data.data).unwrap() + }; + + let source_ata = + get_or_create_associated_token_address(config, &redeem_reserve.collateral.mint_pubkey); + let dest_ata = + get_or_create_associated_token_address(config, &redeem_reserve.liquidity.mint_pubkey); + + let recent_blockhash = config.rpc_client.get_latest_blockhash()?; + let transaction = Transaction::new( + &vec![config.fee_payer.as_ref()], + Message::new_with_blockhash( + &[redeem_reserve_collateral( + config.lending_program_id, + collateral_amount, + source_ata, + dest_ata, + *redeem_reserve_pubkey, + redeem_reserve.collateral.mint_pubkey, + redeem_reserve.liquidity.supply_pubkey, + redeem_reserve.lending_market, + config.fee_payer.pubkey(), + )], + Some(&config.fee_payer.pubkey()), + &recent_blockhash, + ), + recent_blockhash, + ); + + send_transaction(config, transaction)?; + + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn command_withdraw_collateral( + config: &Config, + obligation_pubkey: Pubkey, + withdraw_reserve_pubkey: Pubkey, + collateral_amount: u64, +) -> CommandResult { + let solend_state = SolendState::new( + config.lending_program_id, + obligation_pubkey, + &config.rpc_client, + ); + + let withdraw_reserve = solend_state + .find_reserve_by_key(withdraw_reserve_pubkey) + .unwrap(); + + // make atas + get_or_create_associated_token_address(config, &withdraw_reserve.collateral.mint_pubkey); + + let instructions = solend_state.withdraw(&withdraw_reserve_pubkey, collateral_amount); + let recent_blockhash = config.rpc_client.get_latest_blockhash()?; + let transaction = Transaction::new( + &vec![config.fee_payer.as_ref()], + Message::new_with_blockhash( + &instructions, + Some(&config.fee_payer.pubkey()), + &recent_blockhash, + ), + recent_blockhash, + ); + + send_transaction(config, transaction)?; + + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn command_liquidate_obligation( + config: &Config, + obligation_pubkey: Pubkey, + repay_reserve_pubkey: Pubkey, + source_liquidity_pubkey: Pubkey, + withdraw_reserve_pubkey: Pubkey, + liquidity_amount: u64, +) -> CommandResult { + let obligation_state = { + let data = config.rpc_client.get_account(&obligation_pubkey)?; + Obligation::unpack(&data.data)? + }; + + // get reserve pubkeys + let reserve_pubkeys = { + let mut r = Vec::new(); + r.extend(obligation_state.deposits.iter().map(|d| d.deposit_reserve)); + r.extend(obligation_state.borrows.iter().map(|b| b.borrow_reserve)); + r + }; + + // get reserve accounts + let reserves: Vec<(Pubkey, Reserve)> = config + .rpc_client + .get_multiple_accounts(&reserve_pubkeys)? + .into_iter() + .zip(reserve_pubkeys.iter()) + .map(|(account, pubkey)| (*pubkey, Reserve::unpack(&account.unwrap().data).unwrap())) + .collect(); + + assert!(reserve_pubkeys.len() == reserves.len()); + + // find repay, withdraw reserve states + let withdraw_reserve_state = reserves + .iter() + .find_map(|(pubkey, reserve)| { + if withdraw_reserve_pubkey == *pubkey { + Some(reserve) + } else { + None + } + }) + .unwrap(); + let repay_reserve_state = reserves + .iter() + .find_map(|(pubkey, reserve)| { + if repay_reserve_pubkey == *pubkey { + Some(reserve) + } else { + None + } + }) + .unwrap(); + + // make sure atas exist. if they don't, create them. + let required_mints = [ + withdraw_reserve_state.collateral.mint_pubkey, + withdraw_reserve_state.liquidity.mint_pubkey, + ]; + + for mint in required_mints { + get_or_create_associated_token_address(config, &mint); + } + + let destination_collateral_pubkey = get_associated_token_address( + &config.fee_payer.pubkey(), + &withdraw_reserve_state.collateral.mint_pubkey, + ); + let destination_liquidity_pubkey = get_associated_token_address( + &config.fee_payer.pubkey(), + &withdraw_reserve_state.liquidity.mint_pubkey, + ); + + let mut instructions = vec![ComputeBudgetInstruction::set_compute_unit_price(30101)]; + + // refresh all reserves + instructions.extend(reserves.iter().map(|(pubkey, reserve)| { + refresh_reserve( + config.lending_program_id, + *pubkey, + reserve.liquidity.pyth_oracle_pubkey, + reserve.liquidity.switchboard_oracle_pubkey, + ) + })); + + // refresh obligation + instructions.push(refresh_obligation( + config.lending_program_id, + obligation_pubkey, + reserve_pubkeys, + )); + + instructions.push(liquidate_obligation_and_redeem_reserve_collateral( + config.lending_program_id, + liquidity_amount, + source_liquidity_pubkey, + destination_collateral_pubkey, + destination_liquidity_pubkey, + repay_reserve_pubkey, + repay_reserve_state.liquidity.supply_pubkey, + withdraw_reserve_pubkey, + withdraw_reserve_state.collateral.mint_pubkey, + withdraw_reserve_state.collateral.supply_pubkey, + withdraw_reserve_state.liquidity.supply_pubkey, + withdraw_reserve_state.config.fee_receiver, + obligation_pubkey, + obligation_state.lending_market, + config.fee_payer.pubkey(), + )); + + let recent_blockhash = config.rpc_client.get_latest_blockhash()?; + let transaction = Transaction::new( + &vec![config.fee_payer.as_ref()], + Message::new_with_blockhash( + &instructions, + Some(&config.fee_payer.pubkey()), + &recent_blockhash, + ), + recent_blockhash, + ); + + send_transaction(config, transaction)?; + + Ok(()) +} + #[allow(clippy::too_many_arguments)] fn command_add_reserve( config: &mut Config, @@ -915,8 +1403,9 @@ fn command_add_reserve( + user_collateral_balance + liquidity_supply_balance + liquidity_fee_receiver_balance; + let recent_blockhash = config.rpc_client.get_latest_blockhash()?; - let mut transaction_1 = Transaction::new_with_payer( + let message_1 = Message::new_with_blockhash( &[ create_account( &config.fee_payer.pubkey(), @@ -948,9 +1437,10 @@ fn command_add_reserve( ), ], Some(&config.fee_payer.pubkey()), + &recent_blockhash, ); - let mut transaction_2 = Transaction::new_with_payer( + let message_2 = Message::new_with_blockhash( &[ create_account( &config.fee_payer.pubkey(), @@ -968,9 +1458,10 @@ fn command_add_reserve( ), ], Some(&config.fee_payer.pubkey()), + &recent_blockhash, ); - let mut transaction_3 = Transaction::new_with_payer( + let message_3 = Message::new_with_blockhash( &[ approve( &spl_token::id(), @@ -1008,17 +1499,18 @@ fn command_add_reserve( .unwrap(), ], Some(&config.fee_payer.pubkey()), + &recent_blockhash, ); - let (recent_blockhash, fee_calculator) = config.rpc_client.get_recent_blockhash()?; check_fee_payer_balance( config, total_balance - + fee_calculator.calculate_fee(transaction_1.message()) - + fee_calculator.calculate_fee(transaction_2.message()) - + fee_calculator.calculate_fee(transaction_3.message()), + + config.rpc_client.get_fee_for_message(&message_1)? + + config.rpc_client.get_fee_for_message(&message_2)? + + config.rpc_client.get_fee_for_message(&message_3)?, )?; - transaction_1.sign( + + let transaction_1 = Transaction::new( &vec![ config.fee_payer.as_ref(), &reserve_keypair, @@ -1026,31 +1518,78 @@ fn command_add_reserve( &collateral_supply_keypair, &user_collateral_keypair, ], + message_1, recent_blockhash, ); - transaction_2.sign( + send_transaction(config, transaction_1)?; + let transaction_2 = Transaction::new( &vec![ config.fee_payer.as_ref(), &liquidity_supply_keypair, &liquidity_fee_receiver_keypair, ], + message_2, recent_blockhash, ); - transaction_3.sign( + send_transaction(config, transaction_2)?; + let transaction_3 = Transaction::new( &vec![ config.fee_payer.as_ref(), &source_liquidity_owner_keypair, &lending_market_owner_keypair, &user_transfer_authority_keypair, ], + message_3, recent_blockhash, ); - send_transaction(config, transaction_1)?; - send_transaction(config, transaction_2)?; send_transaction(config, transaction_3)?; Ok(()) } +fn command_set_lending_market_owner_and_config( + config: &mut Config, + lending_market_pubkey: Pubkey, + lending_market_owner_keypair: Keypair, + new_lending_market_owner_keypair: Option, + rate_limiter_window_duration: Option, + rate_limiter_max_outflow: Option, +) -> CommandResult { + let lending_market_info = config.rpc_client.get_account(&lending_market_pubkey)?; + let lending_market = LendingMarket::unpack_from_slice(lending_market_info.data.borrow())?; + println!("{:#?}", lending_market); + + let recent_blockhash = config.rpc_client.get_latest_blockhash()?; + let message = Message::new_with_blockhash( + &[set_lending_market_owner_and_config( + config.lending_program_id, + lending_market_pubkey, + lending_market_owner_keypair.pubkey(), + if let Some(owner) = new_lending_market_owner_keypair { + owner.pubkey() + } else { + lending_market.owner + }, + RateLimiterConfig { + window_duration: rate_limiter_window_duration + .unwrap_or(lending_market.rate_limiter.config.window_duration), + max_outflow: rate_limiter_max_outflow + .unwrap_or(lending_market.rate_limiter.config.max_outflow), + }, + )], + Some(&config.fee_payer.pubkey()), + &recent_blockhash, + ); + + let transaction = Transaction::new( + &vec![config.fee_payer.as_ref(), &lending_market_owner_keypair], + message, + recent_blockhash, + ); + + send_transaction(config, transaction)?; + Ok(()) +} + #[allow(clippy::too_many_arguments, clippy::unnecessary_unwrap)] fn command_update_reserve( config: &mut Config, @@ -1064,7 +1603,13 @@ fn command_update_reserve( ) -> CommandResult { let reserve_info = config.rpc_client.get_account(&reserve_pubkey)?; let mut reserve = Reserve::unpack_from_slice(reserve_info.data.borrow())?; - if reserve_config.optimal_utilization_rate.is_some() { + println!("Reserve: {:#?}", reserve); + let mut no_change = true; + if reserve_config.optimal_utilization_rate.is_some() + && reserve.config.optimal_utilization_rate + != reserve_config.optimal_utilization_rate.unwrap() + { + no_change = false; println!( "Updating optimal_utilization_rate from {} to {}", reserve.config.optimal_utilization_rate, @@ -1073,7 +1618,10 @@ fn command_update_reserve( reserve.config.optimal_utilization_rate = reserve_config.optimal_utilization_rate.unwrap(); } - if reserve_config.loan_to_value_ratio.is_some() { + if reserve_config.loan_to_value_ratio.is_some() + && reserve.config.loan_to_value_ratio != reserve_config.loan_to_value_ratio.unwrap() + { + no_change = false; println!( "Updating loan_to_value_ratio from {} to {}", reserve.config.loan_to_value_ratio, @@ -1082,7 +1630,10 @@ fn command_update_reserve( reserve.config.loan_to_value_ratio = reserve_config.loan_to_value_ratio.unwrap(); } - if reserve_config.liquidation_bonus.is_some() { + if reserve_config.liquidation_bonus.is_some() + && reserve.config.liquidation_bonus != reserve_config.liquidation_bonus.unwrap() + { + no_change = false; println!( "Updating liquidation_bonus from {} to {}", reserve.config.liquidation_bonus, @@ -1091,7 +1642,10 @@ fn command_update_reserve( reserve.config.liquidation_bonus = reserve_config.liquidation_bonus.unwrap(); } - if reserve_config.liquidation_threshold.is_some() { + if reserve_config.liquidation_threshold.is_some() + && reserve.config.liquidation_threshold != reserve_config.liquidation_threshold.unwrap() + { + no_change = false; println!( "Updating liquidation_threshold from {} to {}", reserve.config.liquidation_threshold, @@ -1100,7 +1654,10 @@ fn command_update_reserve( reserve.config.liquidation_threshold = reserve_config.liquidation_threshold.unwrap(); } - if reserve_config.min_borrow_rate.is_some() { + if reserve_config.min_borrow_rate.is_some() + && reserve.config.min_borrow_rate != reserve_config.min_borrow_rate.unwrap() + { + no_change = false; println!( "Updating min_borrow_rate from {} to {}", reserve.config.min_borrow_rate, @@ -1109,7 +1666,10 @@ fn command_update_reserve( reserve.config.min_borrow_rate = reserve_config.min_borrow_rate.unwrap(); } - if reserve_config.optimal_borrow_rate.is_some() { + if reserve_config.optimal_borrow_rate.is_some() + && reserve.config.optimal_borrow_rate != reserve_config.optimal_borrow_rate.unwrap() + { + no_change = false; println!( "Updating optimal_borrow_rate from {} to {}", reserve.config.optimal_borrow_rate, @@ -1118,7 +1678,10 @@ fn command_update_reserve( reserve.config.optimal_borrow_rate = reserve_config.optimal_borrow_rate.unwrap(); } - if reserve_config.max_borrow_rate.is_some() { + if reserve_config.max_borrow_rate.is_some() + && reserve.config.max_borrow_rate != reserve_config.max_borrow_rate.unwrap() + { + no_change = false; println!( "Updating max_borrow_rate from {} to {}", reserve.config.max_borrow_rate, @@ -1127,7 +1690,10 @@ fn command_update_reserve( reserve.config.max_borrow_rate = reserve_config.max_borrow_rate.unwrap(); } - if reserve_config.fees.borrow_fee_wad.is_some() { + if reserve_config.fees.borrow_fee_wad.is_some() + && reserve.config.fees.borrow_fee_wad != reserve_config.fees.borrow_fee_wad.unwrap() + { + no_change = false; println!( "Updating borrow_fee_wad from {} to {}", reserve.config.fees.borrow_fee_wad, @@ -1136,7 +1702,10 @@ fn command_update_reserve( reserve.config.fees.borrow_fee_wad = reserve_config.fees.borrow_fee_wad.unwrap(); } - if reserve_config.fees.flash_loan_fee_wad.is_some() { + if reserve_config.fees.flash_loan_fee_wad.is_some() + && reserve.config.fees.flash_loan_fee_wad != reserve_config.fees.flash_loan_fee_wad.unwrap() + { + no_change = false; println!( "Updating flash_loan_fee_wad from {} to {}", reserve.config.fees.flash_loan_fee_wad, @@ -1145,7 +1714,11 @@ fn command_update_reserve( reserve.config.fees.flash_loan_fee_wad = reserve_config.fees.flash_loan_fee_wad.unwrap(); } - if reserve_config.fees.host_fee_percentage.is_some() { + if reserve_config.fees.host_fee_percentage.is_some() + && reserve.config.fees.host_fee_percentage + != reserve_config.fees.host_fee_percentage.unwrap() + { + no_change = false; println!( "Updating host_fee_percentage from {} to {}", reserve.config.fees.host_fee_percentage, @@ -1154,7 +1727,10 @@ fn command_update_reserve( reserve.config.fees.host_fee_percentage = reserve_config.fees.host_fee_percentage.unwrap(); } - if reserve_config.deposit_limit.is_some() { + if reserve_config.deposit_limit.is_some() + && reserve.config.deposit_limit != reserve_config.deposit_limit.unwrap() + { + no_change = false; println!( "Updating deposit_limit from {} to {}", amount_to_ui_amount( @@ -1169,7 +1745,10 @@ fn command_update_reserve( ) } - if reserve_config.borrow_limit.is_some() { + if reserve_config.borrow_limit.is_some() + && reserve.config.borrow_limit != reserve_config.borrow_limit.unwrap() + { + no_change = false; println!( "Updating borrow_limit from {} to {}", amount_to_ui_amount(reserve.config.borrow_limit, reserve.liquidity.mint_decimals), @@ -1181,7 +1760,10 @@ fn command_update_reserve( ) } - if reserve_config.fee_receiver.is_some() { + if reserve_config.fee_receiver.is_some() + && reserve.config.fee_receiver != reserve_config.fee_receiver.unwrap() + { + no_change = false; println!( "Updating fee_receiver from {} to {}", reserve.config.fee_receiver, @@ -1190,7 +1772,11 @@ fn command_update_reserve( reserve.config.fee_receiver = reserve_config.fee_receiver.unwrap(); } - if reserve_config.protocol_liquidation_fee.is_some() { + if reserve_config.protocol_liquidation_fee.is_some() + && reserve.config.protocol_liquidation_fee + != reserve_config.protocol_liquidation_fee.unwrap() + { + no_change = false; println!( "Updating protocol_liquidation_fee from {} to {}", reserve.config.protocol_liquidation_fee, @@ -1199,8 +1785,21 @@ fn command_update_reserve( reserve.config.protocol_liquidation_fee = reserve_config.protocol_liquidation_fee.unwrap(); } - let mut new_pyth_product_pubkey = solend_program::NULL_PUBKEY; + if reserve_config.protocol_take_rate.is_some() + && reserve.config.protocol_take_rate != reserve_config.protocol_take_rate.unwrap() + { + no_change = false; + println!( + "Updating protocol_take_rate from {} to {}", + reserve.config.protocol_take_rate, + reserve_config.protocol_take_rate.unwrap(), + ); + reserve.config.protocol_take_rate = reserve_config.protocol_take_rate.unwrap(); + } + + let mut new_pyth_product_pubkey = solend_sdk::NULL_PUBKEY; if pyth_price_pubkey.is_some() { + no_change = false; println!( "Updating pyth oracle pubkey from {} to {}", reserve.liquidity.pyth_oracle_pubkey, @@ -1211,6 +1810,7 @@ fn command_update_reserve( } if switchboard_feed_pubkey.is_some() { + no_change = false; println!( "Updating switchboard_oracle_pubkey {} to {}", reserve.liquidity.switchboard_oracle_pubkey, @@ -1219,10 +1819,60 @@ fn command_update_reserve( reserve.liquidity.switchboard_oracle_pubkey = switchboard_feed_pubkey.unwrap(); } - let mut transaction = Transaction::new_with_payer( + if reserve_config.rate_limiter_window_duration.is_some() + && reserve.rate_limiter.config.window_duration + != reserve_config.rate_limiter_window_duration.unwrap() + { + no_change = false; + println!( + "Updating rate_limiter_window_duration from {} to {}", + reserve.rate_limiter.config.window_duration, + reserve_config.rate_limiter_window_duration.unwrap(), + ); + reserve.rate_limiter.config.window_duration = + reserve_config.rate_limiter_window_duration.unwrap(); + } + + if reserve_config.rate_limiter_max_outflow.is_some() + && reserve.rate_limiter.config.max_outflow + != reserve_config.rate_limiter_max_outflow.unwrap() + { + no_change = false; + println!( + "Updating rate_limiter_max_outflow from {} to {}", + reserve.rate_limiter.config.max_outflow, + reserve_config.rate_limiter_max_outflow.unwrap(), + ); + reserve.rate_limiter.config.max_outflow = reserve_config.rate_limiter_max_outflow.unwrap(); + } + + if reserve_config.added_borrow_weight_bps.is_some() + && reserve.config.added_borrow_weight_bps != reserve_config.added_borrow_weight_bps.unwrap() + { + no_change = false; + println!( + "Updating added_borrow_weight_bps from {} to {}", + reserve.config.added_borrow_weight_bps, + reserve_config.added_borrow_weight_bps.unwrap(), + ); + reserve.config.added_borrow_weight_bps = reserve_config.added_borrow_weight_bps.unwrap(); + } + + if no_change { + println!("No changes made for reserve {}", reserve_pubkey); + return Ok(()); + } + + let recent_blockhash = config.rpc_client.get_latest_blockhash()?; + + let message = Message::new_with_blockhash( &[update_reserve_config( config.lending_program_id, reserve.config, + RateLimiterConfig { + window_duration: reserve.rate_limiter.config.window_duration, + max_outflow: reserve.rate_limiter.config.max_outflow, + }, reserve_pubkey, lending_market_pubkey, lending_market_owner_keypair.pubkey(), @@ -1231,15 +1881,15 @@ fn command_update_reserve( reserve.liquidity.switchboard_oracle_pubkey, )], Some(&config.fee_payer.pubkey()), + &recent_blockhash, ); - let (recent_blockhash, fee_calculator) = config.rpc_client.get_recent_blockhash()?; - check_fee_payer_balance(config, fee_calculator.calculate_fee(transaction.message()))?; - - transaction.sign( + let transaction = Transaction::new( &vec![config.fee_payer.as_ref(), &lending_market_owner_keypair], + message, recent_blockhash, ); + send_transaction(config, transaction)?; Ok(()) } @@ -1271,7 +1921,17 @@ fn send_transaction( } else { let signature = config .rpc_client - .send_and_confirm_transaction_with_spinner(&transaction)?; + .send_and_confirm_transaction_with_spinner_and_config( + &transaction, + CommitmentConfig::confirmed(), + RpcSendTransactionConfig { + preflight_commitment: Some(CommitmentLevel::Processed), + skip_preflight: true, + encoding: None, + max_retries: None, + min_context_slot: None, + }, + )?; println!("Signature: {}", signature); } Ok(()) @@ -1292,3 +1952,31 @@ fn quote_currency_of(matches: &ArgMatches<'_>, name: &str) -> Option<[u8; 32]> { None } } + +fn get_or_create_associated_token_address(config: &Config, mint: &Pubkey) -> Pubkey { + let ata = get_associated_token_address(&config.fee_payer.pubkey(), mint); + + if config.rpc_client.get_account(&ata).is_err() { + println!("Creating ATA for mint {:?}", mint); + + let recent_blockhash = config.rpc_client.get_latest_blockhash().unwrap(); + let transaction = Transaction::new( + &vec![config.fee_payer.as_ref()], + Message::new_with_blockhash( + &[create_associated_token_account( + &config.fee_payer.pubkey(), + &config.fee_payer.pubkey(), + mint, + &spl_associated_token_account::id(), + )], + Some(&config.fee_payer.pubkey()), + &recent_blockhash, + ), + recent_blockhash, + ); + + send_transaction(config, transaction).unwrap(); + } + + ata +} diff --git a/token-lending/program/Cargo.toml b/token-lending/program/Cargo.toml index a9ffd4fb032..09b775274b6 100644 --- a/token-lending/program/Cargo.toml +++ b/token-lending/program/Cargo.toml @@ -12,26 +12,27 @@ no-entrypoint = [] test-bpf = [] [dependencies] -arrayref = "0.3.6" -bytemuck = "1.5.1" -num-derive = "0.3" -num-traits = "0.2" -solana-program = "1.14.2" -spl-token = { version = "3.2.0", features=["no-entrypoint"] } -switchboard-program = "0.2.1" +pyth-sdk-solana = "0.7.0" +solana-program = "=1.14.10" +spl-token = { version = "3.3.0", features=["no-entrypoint"] } +solend-sdk = { path = "../sdk" } +static_assertions = "1.1.0" +switchboard-program = "0.2.0" switchboard-v2 = "0.1.3" -thiserror = "1.0" -uint = "0.9.0" [dev-dependencies] assert_matches = "1.5.0" +bytemuck = "1.5.1" base64 = "0.13" log = "0.4.14" proptest = "1.0" -solana-program-test = "1.10.0" -solana-sdk = "1.10.0" -serde = "1.0" +solana-program-test = "=1.14.10" +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/src/lib.rs b/token-lending/program/src/lib.rs index d9d1f5265bc..fc727ffe647 100644 --- a/token-lending/program/src/lib.rs +++ b/token-lending/program/src/lib.rs @@ -3,12 +3,8 @@ //! A lending program for the Solana blockchain. pub mod entrypoint; -pub mod error; -pub mod instruction; -pub mod math; pub mod processor; -pub mod pyth; -pub mod state; +pub use solend_sdk::{error, instruction, math, oracles, state}; // Export current sdk types for downstream users building with a different sdk version pub use solana_program; diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index e07da678eb6..b61f4bac231 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -5,7 +5,7 @@ use crate::{ error::LendingError, instruction::LendingInstruction, math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub, WAD}, - pyth, + oracles::get_pyth_price, state::{ CalculateBorrowResult, CalculateLiquidationResult, CalculateRepayResult, InitLendingMarketParams, InitObligationParams, InitReserveParams, LendingMarket, @@ -13,37 +13,32 @@ use crate::{ ReserveCollateral, ReserveConfig, ReserveLiquidity, }, }; -use num_traits::FromPrimitive; +use pyth_sdk_solana::{self, state::ProductAccount}; use solana_program::{ account_info::{next_account_info, AccountInfo}, - decode_error::DecodeError, entrypoint::ProgramResult, - instruction::Instruction, + instruction::{get_stack_height, Instruction, TRANSACTION_LEVEL_STACK_HEIGHT}, msg, program::{invoke, invoke_signed}, - program_error::{PrintProgramError, ProgramError}, + program_error::ProgramError, program_pack::{IsInitialized, Pack}, pubkey::Pubkey, - sysvar::{clock::Clock, rent::Rent, Sysvar}, + sysvar::instructions::{load_current_index_checked, load_instruction_at_checked}, + sysvar::{ + clock::{self, Clock}, + rent::Rent, + Sysvar, + }, }; -use spl_token::solana_program::instruction::AccountMeta; -use spl_token::state::{Account, Mint}; -use std::{cmp::min, convert::TryInto, result::Result}; +use solend_sdk::state::{RateLimiter, RateLimiterConfig}; +use solend_sdk::{switchboard_v2_devnet, switchboard_v2_mainnet}; +use spl_token::state::Mint; +use std::{cmp::min, result::Result}; use switchboard_program::{ get_aggregator, get_aggregator_result, AggregatorState, RoundResult, SwitchboardAccountType, }; use switchboard_v2::AggregatorAccountData; -/// Mainnet program id for Switchboard v2. -pub mod switchboard_v2_mainnet { - solana_program::declare_id!("SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f"); -} - -/// Devnet program id for Switchboard v2. -pub mod switchboard_v2_devnet { - solana_program::declare_id!("2TfB33aLaneQb5TNVwyDz3jSZXS6jdW2ARw1Dgf84XCG"); -} - /// Processes an instruction pub fn process_instruction<'a>( program_id: &Pubkey, @@ -59,9 +54,17 @@ pub fn process_instruction<'a>( msg!("Instruction: Init Lending Market"); process_init_lending_market(program_id, owner, quote_currency, accounts) } - LendingInstruction::SetLendingMarketOwner { new_owner } => { + LendingInstruction::SetLendingMarketOwnerAndConfig { + new_owner, + rate_limiter_config, + } => { msg!("Instruction: Set Lending Market Owner"); - process_set_lending_market_owner(program_id, new_owner, accounts) + process_set_lending_market_owner_and_config( + program_id, + new_owner, + rate_limiter_config, + accounts, + ) } LendingInstruction::InitReserve { liquidity_amount, @@ -106,13 +109,15 @@ pub fn process_instruction<'a>( msg!("Instruction: Repay Obligation Liquidity"); process_repay_obligation_liquidity(program_id, liquidity_amount, accounts) } - LendingInstruction::LiquidateObligation { liquidity_amount } => { + LendingInstruction::LiquidateObligation { .. } => { msg!("Instruction: Liquidate Obligation"); - process_liquidate_obligation(program_id, liquidity_amount, accounts) + msg!("method deprecated, please migrate to Liquidate Obligation and Redeem Reserve Collateral"); + Err(LendingError::DeprecatedInstruction.into()) } - LendingInstruction::FlashLoan { amount } => { + LendingInstruction::FlashLoan { .. } => { msg!("Instruction: Flash Loan"); - process_flash_loan(program_id, amount, accounts) + msg!("This instruction has been deprecated. Use FlashBorrowReserveLiquidity instead"); + Err(LendingError::DeprecatedInstruction.into()) } LendingInstruction::DepositReserveLiquidityAndObligationCollateral { liquidity_amount } => { msg!("Instruction: Deposit Reserve Liquidity and Obligation Collateral"); @@ -132,9 +137,12 @@ pub fn process_instruction<'a>( accounts, ) } - LendingInstruction::UpdateReserveConfig { config } => { + LendingInstruction::UpdateReserveConfig { + config, + rate_limiter_config, + } => { msg!("Instruction: UpdateReserveConfig"); - process_update_reserve_config(program_id, config, accounts) + process_update_reserve_config(program_id, config, rate_limiter_config, accounts) } LendingInstruction::LiquidateObligationAndRedeemReserveCollateral { liquidity_amount } => { msg!("Instruction: Liquidate Obligation and Redeem Reserve Collateral"); @@ -148,6 +156,22 @@ pub fn process_instruction<'a>( msg!("Instruction: RedeemFees"); process_redeem_fees(program_id, accounts) } + LendingInstruction::FlashBorrowReserveLiquidity { liquidity_amount } => { + msg!("Instruction: Flash Borrow Reserve Liquidity"); + process_flash_borrow_reserve_liquidity(program_id, liquidity_amount, accounts) + } + LendingInstruction::FlashRepayReserveLiquidity { + liquidity_amount, + borrow_instruction_index, + } => { + msg!("Instruction: Flash Repay Reserve Liquidity"); + process_flash_repay_reserve_liquidity( + program_id, + liquidity_amount, + borrow_instruction_index, + accounts, + ) + } } } @@ -185,9 +209,10 @@ fn process_init_lending_market( } #[inline(never)] // avoid stack frame limit -fn process_set_lending_market_owner( +fn process_set_lending_market_owner_and_config( program_id: &Pubkey, new_owner: Pubkey, + rate_limiter_config: RateLimiterConfig, accounts: &[AccountInfo], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); @@ -209,6 +234,11 @@ fn process_set_lending_market_owner( } lending_market.owner = new_owner; + + if rate_limiter_config != lending_market.rate_limiter.config { + lending_market.rate_limiter = RateLimiter::new(rate_limiter_config, Clock::get()?.slot); + } + LendingMarket::pack(lending_market, &mut lending_market_info.data.borrow_mut())?; Ok(()) @@ -241,7 +271,12 @@ fn process_init_reserve<'a>( let lending_market_authority_info = next_account_info(account_info_iter)?; let lending_market_owner_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + + let clock = &Clock::get()?; + if account_info_iter.peek().map(|a| a.key) == Some(&clock::ID) { + next_account_info(account_info_iter)?; + } + let rent_info = next_account_info(account_info_iter)?; let rent = &Rent::from_account_info(rent_info)?; let token_program_id = next_account_info(account_info_iter)?; @@ -293,7 +328,8 @@ fn process_init_reserve<'a>( validate_pyth_keys(&lending_market, pyth_product_info, pyth_price_info)?; validate_switchboard_keys(&lending_market, switchboard_feed_info)?; - let market_price = get_price(switchboard_feed_info, pyth_price_info, clock)?; + let (market_price, smoothed_market_price) = + get_price(Some(switchboard_feed_info), pyth_price_info, clock)?; let authority_signer_seeds = &[ lending_market_info.key.as_ref(), @@ -324,12 +360,14 @@ fn process_init_reserve<'a>( pyth_oracle_pubkey: *pyth_price_info.key, switchboard_oracle_pubkey: *switchboard_feed_info.key, market_price, + smoothed_market_price: smoothed_market_price.unwrap_or(market_price), }), collateral: ReserveCollateral::new(NewReserveCollateralParams { mint_pubkey: *reserve_collateral_mint_info.key, supply_pubkey: *reserve_collateral_supply_info.key, }), config, + rate_limiter_config: RateLimiterConfig::default(), }); let collateral_amount = reserve.deposit_liquidity(liquidity_amount)?; @@ -403,8 +441,17 @@ fn process_refresh_reserve<'a>( let account_info_iter = &mut accounts.iter().peekable(); let reserve_info = next_account_info(account_info_iter)?; let pyth_price_info = next_account_info(account_info_iter)?; - let switchboard_feed_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + // set switchboard to a placeholder account info + let mut switchboard_feed_info = None; + // if the next account info exists and is not the clock set it to be switchboard + let switchboard_peek = account_info_iter.peek().map(|a| a.key); + if switchboard_peek.is_some() && switchboard_peek != Some(&clock::ID) { + switchboard_feed_info = Some(next_account_info(account_info_iter)?); + } + let clock = &Clock::get()?; + if account_info_iter.peek().map(|a| a.key) == Some(&clock::ID) { + next_account_info(account_info_iter)?; + } _refresh_reserve( program_id, reserve_info, @@ -416,9 +463,9 @@ fn process_refresh_reserve<'a>( fn _refresh_reserve<'a>( program_id: &Pubkey, - reserve_info: &'a AccountInfo<'a>, - pyth_price_info: &'a AccountInfo<'a>, - switchboard_feed_info: &'a AccountInfo<'a>, + reserve_info: &AccountInfo<'a>, + pyth_price_info: &AccountInfo<'a>, + switchboard_feed_info: Option<&AccountInfo<'a>>, clock: &Clock, ) -> ProgramResult { let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; @@ -430,13 +477,30 @@ fn _refresh_reserve<'a>( msg!("Reserve liquidity pyth oracle does not match the reserve liquidity pyth oracle provided"); return Err(LendingError::InvalidAccountInput.into()); } - - if &reserve.liquidity.switchboard_oracle_pubkey != switchboard_feed_info.key { + // the first check is to allow for the only passing in pyth case + // TODO maybe change this to is_some_and later + if switchboard_feed_info.is_some() + && &reserve.liquidity.switchboard_oracle_pubkey != switchboard_feed_info.unwrap().key + { msg!("Reserve liquidity switchboard oracle does not match the reserve liquidity switchboard oracle provided"); return Err(LendingError::InvalidOracleConfig.into()); } - reserve.liquidity.market_price = get_price(switchboard_feed_info, pyth_price_info, clock)?; + let (market_price, smoothed_market_price) = + get_price(switchboard_feed_info, pyth_price_info, clock)?; + + reserve.liquidity.market_price = market_price; + + if let Some(smoothed_market_price) = smoothed_market_price { + reserve.liquidity.smoothed_market_price = smoothed_market_price; + } + + // currently there's no way to support two prices without a pyth oracle. So if a reserve + // only supports switchboard, reserve.smoothed_market_price == reserve.market_price + if reserve.liquidity.pyth_oracle_pubkey == solend_program::NULL_PUBKEY { + reserve.liquidity.smoothed_market_price = market_price; + } + Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; _refresh_reserve_interest(program_id, reserve_info, clock) @@ -472,7 +536,7 @@ fn process_deposit_reserve_liquidity( return Err(LendingError::InvalidAmount.into()); } - let account_info_iter = &mut accounts.iter(); + let account_info_iter = &mut accounts.iter().peekable(); let source_liquidity_info = next_account_info(account_info_iter)?; let destination_collateral_info = next_account_info(account_info_iter)?; let reserve_info = next_account_info(account_info_iter)?; @@ -481,7 +545,10 @@ fn process_deposit_reserve_liquidity( let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let clock = &Clock::get()?; + if account_info_iter.peek().map(|a| a.key) == Some(&clock::ID) { + next_account_info(account_info_iter)?; + } let token_program_id = next_account_info(account_info_iter)?; _refresh_reserve_interest(program_id, reserve_info, clock)?; @@ -615,7 +682,7 @@ fn process_redeem_reserve_collateral( return Err(LendingError::InvalidAmount.into()); } - let account_info_iter = &mut accounts.iter(); + let account_info_iter = &mut accounts.iter().peekable(); let source_collateral_info = next_account_info(account_info_iter)?; let destination_liquidity_info = next_account_info(account_info_iter)?; let reserve_info = next_account_info(account_info_iter)?; @@ -624,10 +691,12 @@ fn process_redeem_reserve_collateral( let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let clock = &Clock::get()?; + if account_info_iter.peek().map(|a| a.key) == Some(&clock::ID) { + next_account_info(account_info_iter)?; + } let token_program_id = next_account_info(account_info_iter)?; - _refresh_reserve_interest(program_id, reserve_info, clock)?; _redeem_reserve_collateral( program_id, collateral_amount, @@ -641,6 +710,7 @@ fn process_redeem_reserve_collateral( user_transfer_authority_info, clock, token_program_id, + true, )?; let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; reserve.last_update.mark_stale(); @@ -663,8 +733,9 @@ fn _redeem_reserve_collateral<'a>( user_transfer_authority_info: &AccountInfo<'a>, clock: &Clock, token_program_id: &AccountInfo<'a>, + check_rate_limits: bool, ) -> Result { - let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; + let mut lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { msg!("Lending market provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); @@ -718,8 +789,31 @@ fn _redeem_reserve_collateral<'a>( } let liquidity_amount = reserve.redeem_collateral(collateral_amount)?; + + if check_rate_limits { + lending_market + .rate_limiter + .update( + clock.slot, + reserve.market_value_upper_bound(Decimal::from(liquidity_amount))?, + ) + .map_err(|err| { + msg!("Market outflow limit exceeded! Please try again later."); + err + })?; + + reserve + .rate_limiter + .update(clock.slot, Decimal::from(liquidity_amount)) + .map_err(|err| { + msg!("Reserve outflow limit exceeded! Please try again later."); + err + })?; + } + reserve.last_update.mark_stale(); Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; + LendingMarket::pack(lending_market, &mut lending_market_info.data.borrow_mut())?; spl_token_burn(TokenBurnParams { mint: reserve_collateral_mint_info.clone(), @@ -744,11 +838,14 @@ fn _redeem_reserve_collateral<'a>( #[inline(never)] // avoid stack frame limit fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); + let account_info_iter = &mut accounts.iter().peekable(); let obligation_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let obligation_owner_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let clock = &Clock::get()?; + if account_info_iter.peek().map(|a| a.key) == Some(&clock::ID) { + next_account_info(account_info_iter)?; + } let rent = &Rent::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; @@ -789,7 +886,10 @@ fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter().peekable(); let obligation_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let clock = &Clock::get()?; + if account_info_iter.peek().map(|a| a.key) == Some(&clock::ID) { + next_account_info(account_info_iter)?; + } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { @@ -799,6 +899,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> let mut deposited_value = Decimal::zero(); let mut borrowed_value = Decimal::zero(); + let mut borrowed_value_upper_bound = Decimal::zero(); let mut allowed_borrow_value = Decimal::zero(); let mut unhealthy_borrow_value = Decimal::zero(); @@ -828,25 +929,22 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> return Err(LendingError::ReserveStale.into()); } - // @TODO: add lookup table https://git.io/JOCYq - let decimals = 10u64 - .checked_pow(deposit_reserve.liquidity.mint_decimals as u32) - .ok_or(LendingError::MathOverflow)?; - - let market_value = deposit_reserve + let liquidity_amount = deposit_reserve .collateral_exchange_rate()? - .decimal_collateral_to_liquidity(collateral.deposited_amount.into())? - .try_mul(deposit_reserve.liquidity.market_price)? - .try_div(decimals)?; - collateral.market_value = market_value; + .decimal_collateral_to_liquidity(collateral.deposited_amount.into())?; + + let market_value = deposit_reserve.market_value(liquidity_amount)?; + let market_value_lower_bound = + deposit_reserve.market_value_lower_bound(liquidity_amount)?; let loan_to_value_rate = Rate::from_percent(deposit_reserve.config.loan_to_value_ratio); let liquidation_threshold_rate = Rate::from_percent(deposit_reserve.config.liquidation_threshold); + collateral.market_value = market_value; deposited_value = deposited_value.try_add(market_value)?; allowed_borrow_value = - allowed_borrow_value.try_add(market_value.try_mul(loan_to_value_rate)?)?; + allowed_borrow_value.try_add(market_value_lower_bound.try_mul(loan_to_value_rate)?)?; unhealthy_borrow_value = unhealthy_borrow_value.try_add(market_value.try_mul(liquidation_threshold_rate)?)?; } @@ -879,18 +977,15 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> liquidity.accrue_interest(borrow_reserve.liquidity.cumulative_borrow_rate_wads)?; - // @TODO: add lookup table https://git.io/JOCYq - let decimals = 10u64 - .checked_pow(borrow_reserve.liquidity.mint_decimals as u32) - .ok_or(LendingError::MathOverflow)?; - - let market_value = liquidity - .borrowed_amount_wads - .try_mul(borrow_reserve.liquidity.market_price)? - .try_div(decimals)?; + let market_value = borrow_reserve.market_value(liquidity.borrowed_amount_wads)?; + let market_value_upper_bound = + borrow_reserve.market_value_upper_bound(liquidity.borrowed_amount_wads)?; liquidity.market_value = market_value; - borrowed_value = borrowed_value.try_add(market_value)?; + borrowed_value = + borrowed_value.try_add(market_value.try_mul(borrow_reserve.borrow_weight())?)?; + borrowed_value_upper_bound = borrowed_value_upper_bound + .try_add(market_value_upper_bound.try_mul(borrow_reserve.borrow_weight())?)?; } if account_info_iter.peek().is_some() { @@ -900,9 +995,10 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> obligation.deposited_value = deposited_value; obligation.borrowed_value = borrowed_value; + obligation.borrowed_value_upper_bound = borrowed_value_upper_bound; - let global_unhealthy_borrow_value = Decimal::from(50000000u64); - let global_allowed_borrow_value = Decimal::from(45000000u64); + let global_unhealthy_borrow_value = Decimal::from(70000000u64); + let global_allowed_borrow_value = Decimal::from(65000000u64); obligation.allowed_borrow_value = min(allowed_borrow_value, global_allowed_borrow_value); obligation.unhealthy_borrow_value = min(unhealthy_borrow_value, global_unhealthy_borrow_value); @@ -924,7 +1020,7 @@ fn process_deposit_obligation_collateral( return Err(LendingError::InvalidAmount.into()); } - let account_info_iter = &mut accounts.iter(); + let account_info_iter = &mut accounts.iter().peekable(); let source_collateral_info = next_account_info(account_info_iter)?; let destination_collateral_info = next_account_info(account_info_iter)?; let deposit_reserve_info = next_account_info(account_info_iter)?; @@ -932,7 +1028,10 @@ fn process_deposit_obligation_collateral( let lending_market_info = next_account_info(account_info_iter)?; let obligation_owner_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let clock = &Clock::get()?; + if account_info_iter.peek().map(|a| a.key) == Some(&clock::ID) { + next_account_info(account_info_iter)?; + } let token_program_id = next_account_info(account_info_iter)?; _refresh_reserve_interest(program_id, deposit_reserve_info, clock)?; _deposit_obligation_collateral( @@ -1001,10 +1100,6 @@ fn _deposit_obligation_collateral<'a>( msg!("Deposit reserve is stale and must be refreshed in the current slot"); return Err(LendingError::ReserveStale.into()); } - if deposit_reserve.config.loan_to_value_ratio == 0 { - msg!("Deposit reserve has collateral disabled for borrowing"); - return Err(LendingError::ReserveCollateralDisabled.into()); - } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { @@ -1051,7 +1146,7 @@ fn process_deposit_reserve_liquidity_and_obligation_collateral( return Err(LendingError::InvalidAmount.into()); } - let account_info_iter = &mut accounts.iter(); + let account_info_iter = &mut accounts.iter().peekable(); let source_liquidity_info = next_account_info(account_info_iter)?; let user_collateral_info = next_account_info(account_info_iter)?; let reserve_info = next_account_info(account_info_iter)?; @@ -1065,7 +1160,10 @@ fn process_deposit_reserve_liquidity_and_obligation_collateral( let _pyth_price_info = next_account_info(account_info_iter)?; let _switchboard_feed_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let clock = &Clock::get()?; + if account_info_iter.peek().map(|a| a.key) == Some(&clock::ID) { + next_account_info(account_info_iter)?; + } let token_program_id = next_account_info(account_info_iter)?; _refresh_reserve_interest(program_id, reserve_info, clock)?; @@ -1116,7 +1214,7 @@ fn process_withdraw_obligation_collateral( return Err(LendingError::InvalidAmount.into()); } - let account_info_iter = &mut accounts.iter(); + let account_info_iter = &mut accounts.iter().peekable(); let source_collateral_info = next_account_info(account_info_iter)?; let destination_collateral_info = next_account_info(account_info_iter)?; let withdraw_reserve_info = next_account_info(account_info_iter)?; @@ -1124,7 +1222,10 @@ fn process_withdraw_obligation_collateral( let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; let obligation_owner_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let clock = &Clock::get()?; + if account_info_iter.peek().map(|a| a.key) == Some(&clock::ID) { + next_account_info(account_info_iter)?; + } let token_program_id = next_account_info(account_info_iter)?; _withdraw_obligation_collateral( program_id, @@ -1230,49 +1331,13 @@ fn _withdraw_obligation_collateral<'a>( return Err(LendingError::InvalidMarketAuthority.into()); } - let withdraw_amount = if obligation.borrows.is_empty() { - if collateral_amount == u64::MAX { - collateral.deposited_amount - } else { - collateral.deposited_amount.min(collateral_amount) - } - } else if obligation.deposited_value == Decimal::zero() { - msg!("Obligation deposited value is zero"); - return Err(LendingError::ObligationDepositsZero.into()); - } else { - let max_withdraw_value = obligation.max_withdraw_value(Rate::from_percent( - withdraw_reserve.config.loan_to_value_ratio, - ))?; - - if max_withdraw_value == Decimal::zero() { - msg!("Maximum withdraw value is zero"); - return Err(LendingError::WithdrawTooLarge.into()); - } + let max_withdraw_amount = obligation.max_withdraw_amount(collateral, &withdraw_reserve)?; + let withdraw_amount = std::cmp::min(collateral_amount, max_withdraw_amount); - let withdraw_amount = if collateral_amount == u64::MAX { - let withdraw_value = max_withdraw_value.min(collateral.market_value); - let withdraw_pct = withdraw_value.try_div(collateral.market_value)?; - withdraw_pct - .try_mul(collateral.deposited_amount)? - .try_floor_u64()? - .min(collateral.deposited_amount) - } else { - let withdraw_amount = collateral_amount.min(collateral.deposited_amount); - let withdraw_pct = - Decimal::from(withdraw_amount).try_div(collateral.deposited_amount)?; - let withdraw_value = collateral.market_value.try_mul(withdraw_pct)?; - if withdraw_value > max_withdraw_value { - msg!("Withdraw value cannot exceed maximum withdraw value"); - return Err(LendingError::WithdrawTooLarge.into()); - } - withdraw_amount - }; - if withdraw_amount == 0 { - msg!("Withdraw amount is too small to transfer collateral"); - return Err(LendingError::WithdrawTooSmall.into()); - } - withdraw_amount - }; + if withdraw_amount == 0 { + msg!("Maximum withdraw value is zero"); + return Err(LendingError::WithdrawTooLarge.into()); + } obligation.withdraw(withdraw_amount, collateral_index)?; obligation.last_update.mark_stale(); @@ -1301,7 +1366,7 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::InvalidAmount.into()); } - let account_info_iter = &mut accounts.iter(); + let account_info_iter = &mut accounts.iter().peekable(); let source_liquidity_info = next_account_info(account_info_iter)?; let destination_liquidity_info = next_account_info(account_info_iter)?; let borrow_reserve_info = next_account_info(account_info_iter)?; @@ -1310,10 +1375,13 @@ fn process_borrow_obligation_liquidity( let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; let obligation_owner_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let clock = &Clock::get()?; + if account_info_iter.peek().map(|a| a.key) == Some(&clock::ID) { + next_account_info(account_info_iter)?; + } let token_program_id = next_account_info(account_info_iter)?; - let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; + let mut lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { msg!("Lending market provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); @@ -1403,7 +1471,9 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::InvalidMarketAuthority.into()); } - let remaining_borrow_value = obligation.remaining_borrow_value()?; + let remaining_borrow_value = obligation + .remaining_borrow_value() + .unwrap_or_else(|_| Decimal::zero()); if remaining_borrow_value == Decimal::zero() { msg!("Remaining borrow value is zero"); return Err(LendingError::BorrowTooLarge.into()); @@ -1431,6 +1501,30 @@ fn process_borrow_obligation_liquidity( let cumulative_borrow_rate_wads = borrow_reserve.liquidity.cumulative_borrow_rate_wads; + // check outflow rate limits + { + lending_market + .rate_limiter + .update( + clock.slot, + borrow_reserve.market_value_upper_bound(borrow_amount)?, + ) + .map_err(|err| { + msg!("Market outflow limit exceeded! Please try again later."); + err + })?; + + borrow_reserve + .rate_limiter + .update(clock.slot, borrow_amount) + .map_err(|err| { + msg!("Reserve outflow limit exceeded! Please try again later"); + err + })?; + } + + LendingMarket::pack(lending_market, &mut lending_market_info.data.borrow_mut())?; + borrow_reserve.liquidity.borrow(borrow_amount)?; borrow_reserve.last_update.mark_stale(); Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?; @@ -1492,14 +1586,17 @@ fn process_repay_obligation_liquidity( msg!("Liquidity amount provided cannot be zero"); return Err(LendingError::InvalidAmount.into()); } - let account_info_iter = &mut accounts.iter(); + let account_info_iter = &mut accounts.iter().peekable(); let source_liquidity_info = next_account_info(account_info_iter)?; let destination_liquidity_info = next_account_info(account_info_iter)?; let repay_reserve_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let clock = &Clock::get()?; + if account_info_iter.peek().map(|a| a.key) == Some(&clock::ID) { + next_account_info(account_info_iter)?; + } let token_program_id = next_account_info(account_info_iter)?; let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; @@ -1585,50 +1682,6 @@ fn process_repay_obligation_liquidity( Ok(()) } -#[inline(never)] // avoid stack frame limit -fn process_liquidate_obligation( - program_id: &Pubkey, - liquidity_amount: u64, - accounts: &[AccountInfo], -) -> ProgramResult { - if liquidity_amount == 0 { - msg!("Liquidity amount provided cannot be zero"); - return Err(LendingError::InvalidAmount.into()); - } - - let account_info_iter = &mut accounts.iter(); - let source_liquidity_info = next_account_info(account_info_iter)?; - let destination_collateral_info = next_account_info(account_info_iter)?; - let repay_reserve_info = next_account_info(account_info_iter)?; - let repay_reserve_liquidity_supply_info = next_account_info(account_info_iter)?; - let withdraw_reserve_info = next_account_info(account_info_iter)?; - let withdraw_reserve_collateral_supply_info = next_account_info(account_info_iter)?; - let obligation_info = next_account_info(account_info_iter)?; - let lending_market_info = next_account_info(account_info_iter)?; - let lending_market_authority_info = next_account_info(account_info_iter)?; - let user_transfer_authority_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; - let token_program_id = next_account_info(account_info_iter)?; - - _liquidate_obligation( - program_id, - liquidity_amount, - source_liquidity_info, - destination_collateral_info, - repay_reserve_info, - repay_reserve_liquidity_supply_info, - withdraw_reserve_info, - withdraw_reserve_collateral_supply_info, - obligation_info, - lending_market_info, - lending_market_authority_info, - user_transfer_authority_info, - clock, - token_program_id, - )?; - Ok(()) -} - #[allow(clippy::too_many_arguments)] fn _liquidate_obligation<'a>( program_id: &Pubkey, @@ -1880,6 +1933,7 @@ fn process_liquidate_obligation_and_redeem_reserve_collateral( user_transfer_authority_info, clock, token_program_id, + false, )?; let withdraw_reserve = Reserve::unpack(&withdraw_reserve_info.data.borrow())?; if &withdraw_reserve.config.fee_receiver != withdraw_reserve_liquidity_fee_receiver_info.key @@ -1904,39 +1958,108 @@ fn process_liquidate_obligation_and_redeem_reserve_collateral( } #[inline(never)] // avoid stack frame limit -fn process_flash_loan( +fn process_withdraw_obligation_collateral_and_redeem_reserve_liquidity( program_id: &Pubkey, - liquidity_amount: u64, + collateral_amount: u64, accounts: &[AccountInfo], ) -> ProgramResult { - if liquidity_amount == 0 { - msg!("Liquidity amount provided cannot be zero"); - return Err(LendingError::InvalidAmount.into()); + let account_info_iter = &mut accounts.iter().peekable(); + let reserve_collateral_info = next_account_info(account_info_iter)?; + let user_collateral_info = next_account_info(account_info_iter)?; + let reserve_info = next_account_info(account_info_iter)?; + let obligation_info = next_account_info(account_info_iter)?; + let lending_market_info = next_account_info(account_info_iter)?; + let lending_market_authority_info = next_account_info(account_info_iter)?; + let user_liquidity_info = next_account_info(account_info_iter)?; + let reserve_collateral_mint_info = next_account_info(account_info_iter)?; + let reserve_liquidity_supply_info = next_account_info(account_info_iter)?; + let obligation_owner_info = next_account_info(account_info_iter)?; + let user_transfer_authority_info = next_account_info(account_info_iter)?; + let clock = &Clock::get()?; + if account_info_iter.peek().map(|a| a.key) == Some(&clock::ID) { + next_account_info(account_info_iter)?; } + let token_program_id = next_account_info(account_info_iter)?; + + let liquidity_amount = _withdraw_obligation_collateral( + program_id, + collateral_amount, + reserve_collateral_info, + user_collateral_info, + reserve_info, + obligation_info, + lending_market_info, + lending_market_authority_info, + obligation_owner_info, + clock, + token_program_id, + )?; + + _redeem_reserve_collateral( + program_id, + liquidity_amount, + user_collateral_info, + user_liquidity_info, + reserve_info, + reserve_collateral_mint_info, + reserve_liquidity_supply_info, + lending_market_info, + lending_market_authority_info, + user_transfer_authority_info, + clock, + token_program_id, + true, + )?; + Ok(()) +} +#[inline(never)] // avoid stack frame limit +fn process_update_reserve_config( + program_id: &Pubkey, + config: ReserveConfig, + rate_limiter_config: RateLimiterConfig, + accounts: &[AccountInfo], +) -> ProgramResult { + validate_reserve_config(config)?; let account_info_iter = &mut accounts.iter(); - let source_liquidity_info = next_account_info(account_info_iter)?; - let destination_liquidity_info = next_account_info(account_info_iter)?; let reserve_info = next_account_info(account_info_iter)?; - let reserve_liquidity_fee_receiver_info = next_account_info(account_info_iter)?; - let host_fee_receiver_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let lending_market_authority_info = next_account_info(account_info_iter)?; - let token_program_id = next_account_info(account_info_iter)?; - let flash_loan_receiver_program_id = next_account_info(account_info_iter)?; + let lending_market_owner_info = next_account_info(account_info_iter)?; + let pyth_product_info = next_account_info(account_info_iter)?; + let pyth_price_info = next_account_info(account_info_iter)?; + let switchboard_feed_info = next_account_info(account_info_iter)?; - if program_id == flash_loan_receiver_program_id.key { - msg!("Lending program cannot be used as the flash loan receiver program provided"); - return Err(LendingError::InvalidFlashLoanReceiverProgram.into()); + let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; + if reserve_info.owner != program_id { + msg!( + "Reserve provided is not owned by the lending program {} != {}", + &reserve_info.owner.to_string(), + &program_id.to_string(), + ); + return Err(LendingError::InvalidAccountOwner.into()); + } + if &reserve.lending_market != lending_market_info.key { + msg!("Reserve lending market does not match the lending market provided"); + return Err(LendingError::InvalidAccountInput.into()); } let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { + msg!( + "Lending market provided is not owned by the lending program {} != {}", + &lending_market_info.owner.to_string(), + &program_id.to_string(), + ); return Err(LendingError::InvalidAccountOwner.into()); } - if &lending_market.token_program_id != token_program_id.key { - msg!("Lending market token program does not match the token program provided"); - return Err(LendingError::InvalidTokenProgram.into()); + if &lending_market.owner != lending_market_owner_info.key { + msg!("Lending market owner does not match the lending market owner provided"); + return Err(LendingError::InvalidMarketOwner.into()); + } + if !lending_market_owner_info.is_signer { + msg!("Lending market owner provided must be a signer"); + return Err(LendingError::InvalidSigner.into()); } let authority_signer_seeds = &[ @@ -1952,268 +2075,475 @@ fn process_flash_loan( return Err(LendingError::InvalidMarketAuthority.into()); } - let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; - if reserve_info.owner != program_id { - msg!("Reserve provided is not owned by the lending program"); - return Err(LendingError::InvalidAccountOwner.into()); + // if window duration or max outflow are different, then create a new rate limiter instance. + if rate_limiter_config != reserve.rate_limiter.config { + reserve.rate_limiter = RateLimiter::new(rate_limiter_config, Clock::get()?.slot); } - if &reserve.lending_market != lending_market_info.key { - msg!("Invalid reserve lending market account"); - return Err(LendingError::InvalidAccountInput.into()); + + if *pyth_price_info.key != reserve.liquidity.pyth_oracle_pubkey { + validate_pyth_keys(&lending_market, pyth_product_info, pyth_price_info)?; + reserve.liquidity.pyth_oracle_pubkey = *pyth_price_info.key; } - if &reserve.liquidity.supply_pubkey != source_liquidity_info.key { - msg!("Reserve liquidity supply must be used as the source liquidity provided"); - return Err(LendingError::InvalidAccountInput.into()); + + if *switchboard_feed_info.key != reserve.liquidity.switchboard_oracle_pubkey { + validate_switchboard_keys(&lending_market, switchboard_feed_info)?; + reserve.liquidity.switchboard_oracle_pubkey = *switchboard_feed_info.key; } - if &reserve.config.fee_receiver != reserve_liquidity_fee_receiver_info.key { - msg!("Reserve liquidity fee receiver does not match the reserve liquidity fee receiver provided"); - return Err(LendingError::InvalidAccountInput.into()); + if reserve.liquidity.switchboard_oracle_pubkey == solend_program::NULL_PUBKEY + && (*pyth_price_info.key == solend_program::NULL_PUBKEY + || *pyth_product_info.key == solend_program::NULL_PUBKEY) + { + msg!("At least one price oracle must have a non-null pubkey"); + return Err(LendingError::InvalidOracleConfig.into()); } - // @FIXME: if u64::MAX is flash loaned, fees should be inclusive as with ordinary borrows - let flash_loan_amount = if liquidity_amount == u64::MAX { - reserve.liquidity.available_amount - } else { - liquidity_amount - }; + reserve.config = config; + Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; + Ok(()) +} - let flash_loan_amount_decimal = Decimal::from(flash_loan_amount); - let (origination_fee, host_fee) = reserve - .config - .fees - .calculate_flash_loan_fees(flash_loan_amount_decimal)?; +#[inline(never)] // avoid stack frame limit +fn process_redeem_fees(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let account_info_iter = &mut accounts.iter().peekable(); + let reserve_info = next_account_info(account_info_iter)?; + let reserve_liquidity_fee_receiver_info = next_account_info(account_info_iter)?; + let reserve_supply_liquidity_info = next_account_info(account_info_iter)?; + let lending_market_info = next_account_info(account_info_iter)?; + let lending_market_authority_info = next_account_info(account_info_iter)?; + let token_program_id = next_account_info(account_info_iter)?; + let clock = &Clock::get()?; - let balance_before_flash_loan = Account::unpack(&source_liquidity_info.data.borrow())?.amount; - let expected_balance_after_flash_loan = balance_before_flash_loan - .checked_add(origination_fee) - .ok_or(LendingError::MathOverflow)?; - let returned_amount_required = flash_loan_amount - .checked_add(origination_fee) - .ok_or(LendingError::MathOverflow)?; + let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; + if reserve_info.owner != program_id { + msg!( + "Reserve provided is not owned by the lending program {} != {}", + &reserve_info.owner.to_string(), + &program_id.to_string(), + ); + return Err(LendingError::InvalidAccountOwner.into()); + } - let mut flash_loan_instruction_accounts = vec![ - AccountMeta::new(*destination_liquidity_info.key, false), - AccountMeta::new(*source_liquidity_info.key, false), - AccountMeta::new_readonly(*token_program_id.key, false), - ]; - let mut flash_loan_instruction_account_infos = vec![ - destination_liquidity_info.clone(), - flash_loan_receiver_program_id.clone(), - source_liquidity_info.clone(), - token_program_id.clone(), + if &reserve.config.fee_receiver != reserve_liquidity_fee_receiver_info.key { + msg!("Reserve liquidity fee receiver does not match the reserve liquidity fee receiver provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &reserve.liquidity.supply_pubkey != reserve_supply_liquidity_info.key { + msg!("Reserve liquidity supply must be used as the reserve supply liquidity provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &reserve.lending_market != lending_market_info.key { + msg!("Reserve lending market does not match the lending market provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if reserve.last_update.is_stale(clock.slot)? { + msg!("reserve is stale and must be refreshed in the current slot"); + return Err(LendingError::ReserveStale.into()); + } + + let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; + if lending_market_info.owner != program_id { + msg!("Lending market provided is not owned by the lending program"); + return Err(LendingError::InvalidAccountOwner.into()); + } + if &lending_market.token_program_id != token_program_id.key { + msg!("Lending market token program does not match the token program provided"); + return Err(LendingError::InvalidTokenProgram.into()); + } + let authority_signer_seeds = &[ + lending_market_info.key.as_ref(), + &[lending_market.bump_seed], ]; - for account_info in account_info_iter { - flash_loan_instruction_accounts.push(AccountMeta { - pubkey: *account_info.key, - is_signer: account_info.is_signer, - is_writable: account_info.is_writable, - }); - flash_loan_instruction_account_infos.push(account_info.clone()); + let lending_market_authority_pubkey = + Pubkey::create_program_address(authority_signer_seeds, program_id)?; + if &lending_market_authority_pubkey != lending_market_authority_info.key { + msg!( + "Derived lending market authority does not match the lending market authority provided" + ); + return Err(LendingError::InvalidMarketAuthority.into()); } - reserve.liquidity.borrow(flash_loan_amount_decimal)?; + let withdraw_amount = reserve.calculate_redeem_fees()?; + if withdraw_amount == 0 { + return Err(LendingError::InsufficientProtocolFeesToRedeem.into()); + } + + reserve.liquidity.redeem_fees(withdraw_amount)?; + reserve.last_update.mark_stale(); Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; spl_token_transfer(TokenTransferParams { - source: source_liquidity_info.clone(), - destination: destination_liquidity_info.clone(), - amount: flash_loan_amount, + source: reserve_supply_liquidity_info.clone(), + destination: reserve_liquidity_fee_receiver_info.clone(), + amount: withdraw_amount, authority: lending_market_authority_info.clone(), authority_signer_seeds, token_program: token_program_id.clone(), })?; - const RECEIVE_FLASH_LOAN_INSTRUCTION_DATA_SIZE: usize = 9; - // @FIXME: don't use 0 to indicate a flash loan receiver instruction https://git.io/JGzz9 - const RECEIVE_FLASH_LOAN_INSTRUCTION_TAG: u8 = 0u8; - - let mut data = Vec::with_capacity(RECEIVE_FLASH_LOAN_INSTRUCTION_DATA_SIZE); - data.push(RECEIVE_FLASH_LOAN_INSTRUCTION_TAG); - data.extend_from_slice(&returned_amount_required.to_le_bytes()); - - invoke( - &Instruction { - program_id: *flash_loan_receiver_program_id.key, - accounts: flash_loan_instruction_accounts, - data, - }, - &flash_loan_instruction_account_infos[..], + Ok(()) +} + +fn process_flash_borrow_reserve_liquidity( + program_id: &Pubkey, + liquidity_amount: u64, + accounts: &[AccountInfo], +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let source_liquidity_info = next_account_info(account_info_iter)?; + let destination_liquidity_info = next_account_info(account_info_iter)?; + let reserve_info = next_account_info(account_info_iter)?; + let lending_market_info = next_account_info(account_info_iter)?; + let lending_market_authority_info = next_account_info(account_info_iter)?; + let sysvar_info = next_account_info(account_info_iter)?; + let token_program_id = next_account_info(account_info_iter)?; + let clock = Clock::get()?; + + _refresh_reserve_interest(program_id, reserve_info, &clock)?; + _flash_borrow_reserve_liquidity( + program_id, + liquidity_amount, + source_liquidity_info, + destination_liquidity_info, + reserve_info, + lending_market_info, + lending_market_authority_info, + sysvar_info, + token_program_id, )?; + Ok(()) +} - reserve = Reserve::unpack(&reserve_info.data.borrow())?; - reserve - .liquidity - .repay(flash_loan_amount, flash_loan_amount_decimal)?; - Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; +#[allow(clippy::too_many_arguments)] +fn _flash_borrow_reserve_liquidity<'a>( + program_id: &Pubkey, + liquidity_amount: u64, + source_liquidity_info: &AccountInfo<'a>, + destination_liquidity_info: &AccountInfo<'a>, + reserve_info: &AccountInfo<'a>, + lending_market_info: &AccountInfo<'a>, + lending_market_authority_info: &AccountInfo<'a>, + sysvar_info: &AccountInfo<'a>, + token_program_id: &AccountInfo<'a>, +) -> ProgramResult { + let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; + if lending_market_info.owner != program_id { + msg!("Lending market provided is not owned by the lending program"); + return Err(LendingError::InvalidAccountOwner.into()); + } + if &lending_market.token_program_id != token_program_id.key { + msg!("Lending market token program does not match the token program provided"); + return Err(LendingError::InvalidTokenProgram.into()); + } + let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; + if reserve_info.owner != program_id { + msg!("Reserve provided is not owned by the lending program"); + return Err(LendingError::InvalidAccountOwner.into()); + } + if &reserve.lending_market != lending_market_info.key { + msg!("Reserve lending market does not match the lending market provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &reserve.liquidity.supply_pubkey != source_liquidity_info.key { + msg!("Borrow reserve liquidity supply must be used as the source liquidity provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &reserve.liquidity.supply_pubkey == destination_liquidity_info.key { + msg!( + "Borrow reserve liquidity supply cannot be used as the destination liquidity provided" + ); + return Err(LendingError::InvalidAccountInput.into()); + } + let authority_signer_seeds = &[ + lending_market_info.key.as_ref(), + &[lending_market.bump_seed], + ]; + let lending_market_authority_pubkey = + Pubkey::create_program_address(authority_signer_seeds, program_id)?; + if &lending_market_authority_pubkey != lending_market_authority_info.key { + msg!( + "Derived lending market authority {} does not match the lending market authority provided {}", + &lending_market_authority_pubkey.to_string(), + &lending_market_authority_info.key.to_string(), + ); + return Err(LendingError::InvalidMarketAuthority.into()); + } - let actual_balance_after_flash_loan = - Account::unpack(&source_liquidity_info.data.borrow())?.amount; - if actual_balance_after_flash_loan < expected_balance_after_flash_loan { - msg!("Insufficient reserve liquidity after flash loan"); - return Err(LendingError::NotEnoughLiquidityAfterFlashLoan.into()); + if reserve.config.fees.flash_loan_fee_wad == u64::MAX { + msg!("Flash loans are disabled for this reserve"); + return Err(LendingError::FlashLoansDisabled.into()); } - let mut owner_fee = origination_fee; - if host_fee > 0 { - owner_fee = owner_fee - .checked_sub(host_fee) - .ok_or(LendingError::MathOverflow)?; - spl_token_transfer(TokenTransferParams { - source: source_liquidity_info.clone(), - destination: host_fee_receiver_info.clone(), - amount: host_fee, - authority: lending_market_authority_info.clone(), - authority_signer_seeds, - token_program: token_program_id.clone(), - })?; + if Decimal::from(liquidity_amount) + .try_add(reserve.liquidity.borrowed_amount_wads)? + .try_floor_u64()? + > reserve.config.borrow_limit + { + msg!("Cannot borrow above the borrow limit"); + return Err(LendingError::InvalidAmount.into()); } - if owner_fee > 0 { - spl_token_transfer(TokenTransferParams { - source: source_liquidity_info.clone(), - destination: reserve_liquidity_fee_receiver_info.clone(), - amount: owner_fee, - authority: lending_market_authority_info.clone(), - authority_signer_seeds, - token_program: token_program_id.clone(), - })?; + // Make sure this isnt a cpi call + let current_index = load_current_index_checked(sysvar_info)? as usize; + if is_cpi_call(program_id, current_index, sysvar_info)? { + msg!("Flash Borrow was called via CPI!"); + return Err(LendingError::FlashBorrowCpi.into()); + } + + // Find and validate the flash repay instruction. + // + // 1. Ensure the instruction is for this program + // 2. Ensure the instruction can be unpacked into a LendingInstruction + // 3. Ensure that the reserve for the repay matches the borrow + // 4. Ensure that there are no other flash instructions in the rest of the transaction + // 5. Ensure that the repay amount matches the borrow amount + // + // If all of these conditions are not met, the flash borrow fails. + let mut i = current_index; + let mut found_repay_ix = false; + + loop { + i += 1; + + let ixn = match load_instruction_at_checked(i, sysvar_info) { + Ok(ix) => ix, + Err(ProgramError::InvalidArgument) => break, // out of bounds + Err(e) => { + return Err(e); + } + }; + + if ixn.program_id != *program_id { + continue; + } + + let unpacked = LendingInstruction::unpack(ixn.data.as_slice())?; + match unpacked { + LendingInstruction::FlashRepayReserveLiquidity { + liquidity_amount: repay_liquidity_amount, + borrow_instruction_index, + } => { + if found_repay_ix { + msg!("Multiple flash repays not allowed"); + return Err(LendingError::MultipleFlashBorrows.into()); + } + if ixn.accounts[4].pubkey != *reserve_info.key { + msg!("Invalid reserve account on flash repay"); + return Err(LendingError::InvalidFlashRepay.into()); + } + if repay_liquidity_amount != liquidity_amount { + msg!("Liquidity amount for flash repay doesn't match borrow"); + return Err(LendingError::InvalidFlashRepay.into()); + } + if (borrow_instruction_index as usize) != current_index { + msg!("Borrow instruction index {} for flash repay doesn't match current index {}", borrow_instruction_index, current_index); + return Err(LendingError::InvalidFlashRepay.into()); + } + + found_repay_ix = true; + } + LendingInstruction::FlashBorrowReserveLiquidity { .. } => { + msg!("Multiple flash borrows not allowed"); + return Err(LendingError::MultipleFlashBorrows.into()); + } + _ => (), + }; } + if !found_repay_ix { + msg!("No flash repay found"); + return Err(LendingError::NoFlashRepayFound.into()); + } + + reserve.liquidity.borrow(Decimal::from(liquidity_amount))?; + reserve.last_update.mark_stale(); + Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; + + spl_token_transfer(TokenTransferParams { + source: source_liquidity_info.clone(), + destination: destination_liquidity_info.clone(), + amount: liquidity_amount, + authority: lending_market_authority_info.clone(), + authority_signer_seeds, + token_program: token_program_id.clone(), + })?; + Ok(()) } -#[inline(never)] // avoid stack frame limit -fn process_withdraw_obligation_collateral_and_redeem_reserve_liquidity( +fn process_flash_repay_reserve_liquidity( program_id: &Pubkey, - collateral_amount: u64, + liquidity_amount: u64, + borrow_instruction_index: u8, accounts: &[AccountInfo], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); - let reserve_collateral_info = next_account_info(account_info_iter)?; - let user_collateral_info = next_account_info(account_info_iter)?; + let source_liquidity_info = next_account_info(account_info_iter)?; + let destination_liquidity_info = next_account_info(account_info_iter)?; + let reserve_liquidity_fee_receiver_info = next_account_info(account_info_iter)?; + let host_fee_receiver_info = next_account_info(account_info_iter)?; let reserve_info = next_account_info(account_info_iter)?; - let obligation_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; - let lending_market_authority_info = next_account_info(account_info_iter)?; - let user_liquidity_info = next_account_info(account_info_iter)?; - let reserve_collateral_mint_info = next_account_info(account_info_iter)?; - let reserve_liquidity_supply_info = next_account_info(account_info_iter)?; - let obligation_owner_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; + let sysvar_info = next_account_info(account_info_iter)?; let token_program_id = next_account_info(account_info_iter)?; - let liquidity_amount = _withdraw_obligation_collateral( - program_id, - collateral_amount, - reserve_collateral_info, - user_collateral_info, - reserve_info, - obligation_info, - lending_market_info, - lending_market_authority_info, - obligation_owner_info, - clock, - token_program_id, - )?; - - _redeem_reserve_collateral( + _flash_repay_reserve_liquidity( program_id, liquidity_amount, - user_collateral_info, - user_liquidity_info, + borrow_instruction_index, + source_liquidity_info, + destination_liquidity_info, + reserve_liquidity_fee_receiver_info, + host_fee_receiver_info, reserve_info, - reserve_collateral_mint_info, - reserve_liquidity_supply_info, lending_market_info, - lending_market_authority_info, user_transfer_authority_info, - clock, + sysvar_info, token_program_id, )?; Ok(()) } -#[inline(never)] // avoid stack frame limit -fn process_update_reserve_config( +#[allow(clippy::too_many_arguments)] +fn _flash_repay_reserve_liquidity<'a>( program_id: &Pubkey, - config: ReserveConfig, - accounts: &[AccountInfo], + liquidity_amount: u64, + borrow_instruction_index: u8, + source_liquidity_info: &AccountInfo<'a>, + destination_liquidity_info: &AccountInfo<'a>, + reserve_liquidity_fee_receiver_info: &AccountInfo<'a>, + host_fee_receiver_info: &AccountInfo<'a>, + reserve_info: &AccountInfo<'a>, + lending_market_info: &AccountInfo<'a>, + user_transfer_authority_info: &AccountInfo<'a>, + sysvar_info: &AccountInfo<'a>, + token_program_id: &AccountInfo<'a>, ) -> ProgramResult { - validate_reserve_config(config)?; - let account_info_iter = &mut accounts.iter(); - let reserve_info = next_account_info(account_info_iter)?; - let lending_market_info = next_account_info(account_info_iter)?; - let lending_market_authority_info = next_account_info(account_info_iter)?; - let lending_market_owner_info = next_account_info(account_info_iter)?; - let pyth_product_info = next_account_info(account_info_iter)?; - let pyth_price_info = next_account_info(account_info_iter)?; - let switchboard_feed_info = next_account_info(account_info_iter)?; - + let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; + if lending_market_info.owner != program_id { + msg!("Lending market provided is not owned by the lending program"); + return Err(LendingError::InvalidAccountOwner.into()); + } + if &lending_market.token_program_id != token_program_id.key { + msg!("Lending market token program does not match the token program provided"); + return Err(LendingError::InvalidTokenProgram.into()); + } let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; if reserve_info.owner != program_id { - msg!( - "Reserve provided is not owned by the lending program {} != {}", - &reserve_info.owner.to_string(), - &program_id.to_string(), - ); + msg!("Reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &reserve.lending_market != lending_market_info.key { msg!("Reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } - - let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; - if lending_market_info.owner != program_id { - msg!( - "Lending market provided is not owned by the lending program {} != {}", - &lending_market_info.owner.to_string(), - &program_id.to_string(), - ); - return Err(LendingError::InvalidAccountOwner.into()); + if &reserve.liquidity.supply_pubkey != destination_liquidity_info.key { + msg!("Reserve liquidity supply does not match the reserve liquidity supply provided"); + return Err(LendingError::InvalidAccountInput.into()); } - if &lending_market.owner != lending_market_owner_info.key { - msg!("Lending market owner does not match the lending market owner provided"); - return Err(LendingError::InvalidMarketOwner.into()); + if &reserve.liquidity.supply_pubkey == source_liquidity_info.key { + msg!("Reserve liquidity supply cannot be used as the source liquidity provided"); + return Err(LendingError::InvalidAccountInput.into()); } - if !lending_market_owner_info.is_signer { - msg!("Lending market owner provided must be a signer"); - return Err(LendingError::InvalidSigner.into()); + if &reserve.config.fee_receiver != reserve_liquidity_fee_receiver_info.key { + msg!("Reserve liquidity fee receiver does not match the reserve liquidity fee receiver provided"); + return Err(LendingError::InvalidAccountInput.into()); } - let authority_signer_seeds = &[ - lending_market_info.key.as_ref(), - &[lending_market.bump_seed], - ]; - let lending_market_authority_pubkey = - Pubkey::create_program_address(authority_signer_seeds, program_id)?; - if &lending_market_authority_pubkey != lending_market_authority_info.key { + let flash_loan_amount = liquidity_amount; + + let flash_loan_amount_decimal = Decimal::from(flash_loan_amount); + let (origination_fee, host_fee) = reserve + .config + .fees + .calculate_flash_loan_fees(flash_loan_amount_decimal)?; + + // Make sure this isnt a cpi call + let current_index = load_current_index_checked(sysvar_info)? as usize; + if is_cpi_call(program_id, current_index, sysvar_info)? { + msg!("Flash Repay was called via CPI!"); + return Err(LendingError::FlashRepayCpi.into()); + } + + // validate flash borrow + if (borrow_instruction_index as usize) > current_index { msg!( - "Derived lending market authority does not match the lending market authority provided" + "Flash repay: borrow instruction index {} has to be less than current index {}", + borrow_instruction_index, + current_index ); - return Err(LendingError::InvalidMarketAuthority.into()); + return Err(LendingError::InvalidFlashRepay.into()); } - if *pyth_price_info.key != reserve.liquidity.pyth_oracle_pubkey { - validate_pyth_keys(&lending_market, pyth_product_info, pyth_price_info)?; - reserve.liquidity.pyth_oracle_pubkey = *pyth_price_info.key; + let ixn = load_instruction_at_checked(borrow_instruction_index as usize, sysvar_info)?; + if ixn.program_id != *program_id { + msg!( + "Flash repay: supplied instruction index {} doesn't belong to program id {}", + borrow_instruction_index, + *program_id + ); + return Err(LendingError::InvalidFlashRepay.into()); } - if *switchboard_feed_info.key != reserve.liquidity.switchboard_oracle_pubkey { - validate_switchboard_keys(&lending_market, switchboard_feed_info)?; - reserve.liquidity.switchboard_oracle_pubkey = *switchboard_feed_info.key; + let unpacked = LendingInstruction::unpack(ixn.data.as_slice())?; + match unpacked { + LendingInstruction::FlashBorrowReserveLiquidity { + liquidity_amount: borrow_liquidity_amount, + } => { + // re-check everything here out of paranoia + if ixn.accounts[2].pubkey != *reserve_info.key { + msg!("Invalid reserve account on flash repay"); + return Err(LendingError::InvalidFlashRepay.into()); + } + + if liquidity_amount != borrow_liquidity_amount { + msg!("Liquidity amount for flash repay doesn't match borrow"); + return Err(LendingError::InvalidFlashRepay.into()); + } + } + _ => { + msg!("Flash repay: Supplied borrow instruction index is not a flash borrow"); + return Err(LendingError::InvalidFlashRepay.into()); + } + }; + + reserve + .liquidity + .repay(flash_loan_amount, flash_loan_amount_decimal)?; + reserve.last_update.mark_stale(); + Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; + + spl_token_transfer(TokenTransferParams { + source: source_liquidity_info.clone(), + destination: destination_liquidity_info.clone(), + amount: flash_loan_amount, + authority: user_transfer_authority_info.clone(), + authority_signer_seeds: &[], + token_program: token_program_id.clone(), + })?; + + if host_fee > 0 { + spl_token_transfer(TokenTransferParams { + source: source_liquidity_info.clone(), + destination: host_fee_receiver_info.clone(), + amount: host_fee, + authority: user_transfer_authority_info.clone(), + authority_signer_seeds: &[], + token_program: token_program_id.clone(), + })?; } - if reserve.liquidity.switchboard_oracle_pubkey == solend_program::NULL_PUBKEY - && (*pyth_price_info.key == solend_program::NULL_PUBKEY - || *pyth_product_info.key == solend_program::NULL_PUBKEY) - { - msg!("At least one price oracle must have a non-null pubkey"); - return Err(LendingError::InvalidOracleConfig.into()); + + if origination_fee > 0 { + spl_token_transfer(TokenTransferParams { + source: source_liquidity_info.clone(), + destination: reserve_liquidity_fee_receiver_info.clone(), + amount: origination_fee, + authority: user_transfer_authority_info.clone(), + authority_signer_seeds: &[], + token_program: token_program_id.clone(), + })?; } - reserve.config = config; - Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; Ok(()) } @@ -2327,136 +2657,49 @@ fn unpack_mint(data: &[u8]) -> Result { Mint::unpack(data).map_err(|_| LendingError::InvalidTokenMint) } -fn get_pyth_product_quote_currency(pyth_product: &pyth::Product) -> Result<[u8; 32], ProgramError> { - const LEN: usize = 14; - const KEY: &[u8; LEN] = b"quote_currency"; - - let mut start = 0; - while start < pyth::PROD_ATTR_SIZE { - let mut length = pyth_product.attr[start] as usize; - start += 1; - - if length == LEN { - let mut end = start + length; - if end > pyth::PROD_ATTR_SIZE { - msg!("Pyth product attribute key length too long"); - return Err(LendingError::InvalidOracleConfig.into()); - } - - let key = &pyth_product.attr[start..end]; - if key == KEY { - start += length; - length = pyth_product.attr[start] as usize; - start += 1; - - end = start + length; - if length > 32 || end > pyth::PROD_ATTR_SIZE { - msg!("Pyth product quote currency value too long"); - return Err(LendingError::InvalidOracleConfig.into()); - } - +fn get_pyth_product_quote_currency( + pyth_product: &ProductAccount, +) -> Result<[u8; 32], ProgramError> { + pyth_product + .iter() + .find_map(|(key, val)| { + if key == "quote_currency" { let mut value = [0u8; 32]; - value[0..length].copy_from_slice(&pyth_product.attr[start..end]); - return Ok(value); + value[0..val.len()].copy_from_slice(val.as_bytes()); + Some(value) + } else { + None } - } - - start += length; - start += 1 + pyth_product.attr[start] as usize; - } - - msg!("Pyth product quote currency not found"); - Err(LendingError::InvalidOracleConfig.into()) + }) + .ok_or_else(|| { + msg!("Pyth product quote currency not found"); + LendingError::InvalidOracleConfig.into() + }) } -fn get_price<'a>( - switchboard_feed_info: &'a AccountInfo<'a>, - pyth_price_account_info: &'a AccountInfo<'a>, +/// get_price tries to load the oracle price from pyth, and if it fails, uses switchboard. +/// The first element in the returned tuple is the market price, and the second is the optional +/// smoothed price (eg ema, twap). +fn get_price( + switchboard_feed_info: Option<&AccountInfo>, + pyth_price_account_info: &AccountInfo, clock: &Clock, -) -> Result { - let pyth_price = get_pyth_price(pyth_price_account_info, clock).unwrap_or_default(); - if pyth_price != Decimal::zero() { - return Ok(pyth_price); - } - get_switchboard_price(switchboard_feed_info, clock) -} - -fn get_pyth_price(pyth_price_info: &AccountInfo, clock: &Clock) -> Result { - const STALE_AFTER_SLOTS_ELAPSED: u64 = 240; - - if *pyth_price_info.key == solend_program::NULL_PUBKEY { - return Err(LendingError::NullOracleConfig.into()); - } - - let pyth_price_data = pyth_price_info.try_borrow_data()?; - let pyth_price = pyth::load::(&pyth_price_data) - .map_err(|_| ProgramError::InvalidAccountData)?; - - if pyth_price.ptype != pyth::PriceType::Price { - msg!("Oracle price type is invalid {}", pyth_price.ptype as u8); - return Err(LendingError::InvalidOracleConfig.into()); - } - - if pyth_price.agg.status != pyth::PriceStatus::Trading { - msg!( - "Oracle price status is invalid: {}", - pyth_price.agg.status as u8 - ); - return Err(LendingError::InvalidOracleConfig.into()); - } - - let slots_elapsed = clock - .slot - .checked_sub(pyth_price.valid_slot) - .ok_or(LendingError::MathOverflow)?; - if slots_elapsed >= STALE_AFTER_SLOTS_ELAPSED { - msg!("Pyth oracle price is stale"); - return Err(LendingError::InvalidOracleConfig.into()); - } - - let price: u64 = pyth_price.agg.price.try_into().map_err(|_| { - msg!("Oracle price cannot be negative"); - LendingError::InvalidOracleConfig - })?; - - let conf = pyth_price.agg.conf; - - let confidence_ratio: u64 = 10; - // Perhaps confidence_ratio should exist as a per reserve config - // 100/confidence_ratio = maximum size of confidence range as a percent of price - // confidence_ratio of 10 filters out pyth prices with conf > 10% of price - if conf.checked_mul(confidence_ratio).unwrap() > price { - msg!( - "Oracle price confidence is too wide. price: {}, conf: {}", - price, - conf, - ); - return Err(LendingError::InvalidOracleConfig.into()); +) -> Result<(Decimal, Option), ProgramError> { + if let Ok(prices) = get_pyth_price(pyth_price_account_info, clock) { + return Ok((prices.0, Some(prices.1))); + } + + // if switchboard was not passed in don't try to grab the price + if let Some(switchboard_feed_info_unwrapped) = switchboard_feed_info { + // TODO: add support for switchboard smoothed prices. Probably need to add a new + // switchboard account per reserve. + return match get_switchboard_price(switchboard_feed_info_unwrapped, clock) { + Ok(price) => Ok((price, None)), + Err(e) => Err(e), + }; } - let market_price = if pyth_price.expo >= 0 { - let exponent = pyth_price - .expo - .try_into() - .map_err(|_| LendingError::MathOverflow)?; - let zeros = 10u64 - .checked_pow(exponent) - .ok_or(LendingError::MathOverflow)?; - Decimal::from(price).try_mul(zeros)? - } else { - let exponent = pyth_price - .expo - .checked_abs() - .ok_or(LendingError::MathOverflow)? - .try_into() - .map_err(|_| LendingError::MathOverflow)?; - let decimals = 10u64 - .checked_pow(exponent) - .ok_or(LendingError::MathOverflow)?; - Decimal::from(price).try_div(decimals)? - }; - - Ok(market_price) + Err(LendingError::InvalidOracleConfig.into()) } fn get_switchboard_price<'a>( @@ -2512,8 +2755,9 @@ fn get_switchboard_price_v2<'a>( clock: &Clock, ) -> Result { const STALE_AFTER_SLOTS_ELAPSED: u64 = 240; + let data = &switchboard_feed_info.try_borrow_data()?; + let feed = AggregatorAccountData::new_from_bytes(data)?; - let feed = AggregatorAccountData::new(switchboard_feed_info)?; let slots_elapsed = clock .slot .checked_sub(feed.latest_confirmed_round.round_open_slot) @@ -2611,6 +2855,7 @@ fn spl_token_transfer(params: TokenTransferParams<'_, '_>) -> ProgramResult { &[source, destination, authority, token_program], authority_signer_seeds, ); + result.map_err(|_| LendingError::TokenTransferFailed.into()) } @@ -2733,27 +2978,9 @@ fn validate_pyth_keys( } let pyth_product_data = pyth_product_info.try_borrow_data()?; - let pyth_product = pyth::load::(&pyth_product_data) - .map_err(|_| ProgramError::InvalidAccountData)?; - if pyth_product.magic != pyth::MAGIC { - msg!("Pyth product account provided is not a valid Pyth account"); - return Err(LendingError::InvalidOracleConfig.into()); - } - if pyth_product.ver != pyth::VERSION_2 { - msg!("Pyth product account provided has a different version than expected"); - return Err(LendingError::InvalidOracleConfig.into()); - } - if pyth_product.atype != pyth::AccountType::Product as u32 { - msg!("Pyth product account provided is not a valid Pyth product account"); - return Err(LendingError::InvalidOracleConfig.into()); - } + let pyth_product = pyth_sdk_solana::state::load_product_account(&pyth_product_data)?; - let pyth_price_pubkey_bytes: &[u8; 32] = pyth_price_info - .key - .as_ref() - .try_into() - .map_err(|_| LendingError::InvalidAccountInput)?; - if &pyth_product.px_acc.val != pyth_price_pubkey_bytes { + if &pyth_product.px_acc != pyth_price_info.key { msg!("Pyth product price account does not match the Pyth price provided"); return Err(LendingError::InvalidOracleConfig.into()); } @@ -2784,6 +3011,35 @@ fn validate_switchboard_keys( Ok(()) } +fn is_cpi_call( + program_id: &Pubkey, + current_index: usize, + sysvar_info: &AccountInfo, +) -> Result { + // say the tx looks like: + // ix 0 + // - ix a + // - ix b + // - ix c + // ix 1 + // and we call "load_current_index_checked" from b, we will get 0. And when we + // load_instruction_at_checked(0), we will get ix 0. + // tldr; instructions sysvar only stores top-level instructions, never CPI instructions. + let current_ixn = load_instruction_at_checked(current_index, sysvar_info)?; + + // the current ixn must match the flash_* ix. otherwise, it's a CPI. Comparing program_ids is a + // cheaper way of verifying this property, bc token-lending doesn't allow re-entrancy anywhere. + if *program_id != current_ixn.program_id { + return Ok(true); + } + + if get_stack_height() > TRANSACTION_LEVEL_STACK_HEIGHT { + return Ok(true); + } + + Ok(false) +} + struct TokenInitializeMintParams<'a: 'b, 'b> { mint: AccountInfo<'a>, rent: AccountInfo<'a>, @@ -2826,12 +3082,3 @@ struct TokenBurnParams<'a: 'b, 'b> { authority_signer_seeds: &'b [&'b [u8]], token_program: AccountInfo<'a>, } - -impl PrintProgramError for LendingError { - fn print(&self) - where - E: 'static + std::error::Error + DecodeError + PrintProgramError + FromPrimitive, - { - msg!(&self.to_string()); - } -} diff --git a/token-lending/program/src/pyth.rs b/token-lending/program/src/pyth.rs deleted file mode 100644 index 10ec0408a62..00000000000 --- a/token-lending/program/src/pyth.rs +++ /dev/null @@ -1,135 +0,0 @@ -#![allow(missing_docs)] -/// Derived from https://github.com/project-serum/anchor/blob/9224e0fa99093943a6190e396bccbc3387e5b230/examples/pyth/programs/pyth/src/pc.rs -use bytemuck::{ - cast_slice, cast_slice_mut, from_bytes, from_bytes_mut, try_cast_slice, try_cast_slice_mut, - Pod, PodCastError, Zeroable, -}; -use std::mem::size_of; - -pub const MAGIC: u32 = 0xa1b2c3d4; -pub const VERSION_2: u32 = 2; -pub const VERSION: u32 = VERSION_2; -pub const MAP_TABLE_SIZE: usize = 640; -pub const PROD_ACCT_SIZE: usize = 512; -pub const PROD_HDR_SIZE: usize = 48; -pub const PROD_ATTR_SIZE: usize = PROD_ACCT_SIZE - PROD_HDR_SIZE; - -#[derive(Copy, Clone)] -#[repr(C)] -pub struct AccKey { - pub val: [u8; 32], -} - -#[derive(PartialEq, Copy, Clone)] -#[repr(C)] -pub enum AccountType { - Unknown, - Mapping, - Product, - Price, -} - -#[derive(PartialEq, Copy, Clone)] -#[repr(C)] -pub enum PriceStatus { - Unknown, - Trading, - Halted, - Auction, -} - -#[derive(PartialEq, Copy, Clone)] -#[repr(C)] -pub enum CorpAction { - NoCorpAct, -} - -#[derive(Copy, Clone)] -#[repr(C)] -pub struct PriceInfo { - pub price: i64, - pub conf: u64, - pub status: PriceStatus, - pub corp_act: CorpAction, - pub pub_slot: u64, -} - -#[derive(Copy, Clone)] -#[repr(C)] -pub struct PriceComp { - publisher: AccKey, - agg: PriceInfo, - latest: PriceInfo, -} - -#[derive(PartialEq, Copy, Clone)] -#[repr(C)] -pub enum PriceType { - Unknown, - Price, -} - -#[derive(Copy, Clone)] -#[repr(C)] -pub struct Price { - pub magic: u32, // pyth magic number - pub ver: u32, // program version - pub atype: u32, // account type - pub size: u32, // price account size - pub ptype: PriceType, // price or calculation type - pub expo: i32, // price exponent - pub num: u32, // number of component prices - pub unused: u32, - pub curr_slot: u64, // currently accumulating price slot - pub valid_slot: u64, // valid slot-time of agg. price - pub twap: i64, // time-weighted average price - pub avol: u64, // annualized price volatility - pub drv0: i64, // space for future derived values - pub drv1: i64, // space for future derived values - pub drv2: i64, // space for future derived values - pub drv3: i64, // space for future derived values - pub drv4: i64, // space for future derived values - pub drv5: i64, // space for future derived values - pub prod: AccKey, // product account key - pub next: AccKey, // next Price account in linked list - pub agg_pub: AccKey, // quoter who computed last aggregate price - pub agg: PriceInfo, // aggregate price info - pub comp: [PriceComp; 32], // price components one per quoter -} - -#[cfg(target_endian = "little")] -unsafe impl Zeroable for Price {} - -#[cfg(target_endian = "little")] -unsafe impl Pod for Price {} - -#[derive(Copy, Clone)] -#[repr(C)] -pub struct Product { - pub magic: u32, // pyth magic number - pub ver: u32, // program version - pub atype: u32, // account type - pub size: u32, // price account size - pub px_acc: AccKey, // first price account in list - pub attr: [u8; PROD_ATTR_SIZE], // key/value pairs of reference attr. -} - -#[cfg(target_endian = "little")] -unsafe impl Zeroable for Product {} - -#[cfg(target_endian = "little")] -unsafe impl Pod for Product {} - -pub fn load(data: &[u8]) -> Result<&T, PodCastError> { - let size = size_of::(); - Ok(from_bytes(cast_slice::(try_cast_slice( - &data[0..size], - )?))) -} - -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])?, - ))) -} diff --git a/token-lending/program/tests/borrow_obligation_liquidity.rs b/token-lending/program/tests/borrow_obligation_liquidity.rs index ffa67bbdf9d..0016bd37215 100644 --- a/token-lending/program/tests/borrow_obligation_liquidity.rs +++ b/token-lending/program/tests/borrow_obligation_liquidity.rs @@ -1,589 +1,473 @@ #![cfg(feature = "test-bpf")] +use solend_program::math::TryDiv; mod helpers; -use helpers::*; +use solend_program::state::{RateLimiterConfig, 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, - pubkey::Pubkey, - 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_bpf_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( - &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( +async fn setup( + wsol_reserve_config: &ReserveConfig, +) -> ( + SolendProgramTest, + Info, + Info, + Info, + User, + Info, + User, + User, +) { + let (mut test, lending_market, usdc_reserve, wsol_reserve, lending_market_owner, 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, - &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, + lending_market_owner, + ) } #[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_bpf_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); - - 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 { - 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 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() +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 test_obligation = add_obligation( + let balance_checker = BalanceChecker::start( &mut test, - &lending_market, - &user_accounts_owner, - AddObligationArgs { - deposits: &[(&usdc_test_reserve, USDC_DEPOSIT_AMOUNT_FRACTIONAL)], - ..AddObligationArgs::default() - }, - ); - - 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) + &[&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(); - assert_eq!(total_fee, FEE_AMOUNT); - assert_eq!(host_fee, HOST_FEE_AMOUNT); - - 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); + // check token balances + let (balance_changes, mint_supply_changes) = + balance_checker.find_balance_changes(&mut test).await; - let liquidity = &obligation.borrows[0]; + 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), + }, + TokenBalanceChange { + token_account: user.get_account(&wsol_mint::id()).unwrap(), + mint: wsol_mint::id(), + diff: (4 * LAMPORTS_PER_SOL) as i128, + }, + 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!( - liquidity.borrowed_amount_wads, - Decimal::from(SOL_BORROW_AMOUNT_LAMPORTS) + balance_changes, expected_balance_changes, + "{:#?} \n {:#?}", + balance_changes, expected_balance_changes ); + assert_eq!(mint_supply_changes, HashSet::new()); - let liquidity_supply = - get_token_balance(&mut banks_client, sol_test_reserve.liquidity_supply_pubkey).await; + // check program state + let lending_market_post = test + .load_account::(lending_market.pubkey) + .await; assert_eq!( - liquidity_supply, - initial_liquidity_supply - SOL_BORROW_AMOUNT_LAMPORTS - ); - - 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); -} - -#[tokio::test] -async fn test_borrow_too_large() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), + lending_market_post.account, + LendingMarket { + rate_limiter: { + let mut rate_limiter = lending_market.account.rate_limiter; + rate_limiter + .update( + 1000, + Decimal::from(10 * (4 * LAMPORTS_PER_SOL + 400)) + .try_div(Decimal::from(1_000_000_000_u64)) + .unwrap(), + ) + .unwrap(); + rate_limiter + }, + ..lending_market.account + } ); - 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); - - 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 wsol_reserve_post = test.load_account::(wsol_reserve.pubkey).await; + let expected_wsol_reserve_post = Reserve { + last_update: LastUpdate { + slot: 1000, + stale: true, }, - ); - - 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() + 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 }, - ); + rate_limiter: { + let mut rate_limiter = wsol_reserve.account.rate_limiter; + rate_limiter + .update(1000, Decimal::from(4 * LAMPORTS_PER_SOL + 400)) + .unwrap(); - let test_obligation = add_obligation( - &mut test, - &lending_market, - &user_accounts_owner, - AddObligationArgs { - deposits: &[(&sol_test_reserve, SOL_DEPOSIT_AMOUNT_LAMPORTS)], - ..AddObligationArgs::default() + rate_limiter }, - ); + ..wsol_reserve.account + }; - 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()), + assert_eq!( + wsol_reserve_post.account, expected_wsol_reserve_post, + "{:#?} {:#?}", + wsol_reserve_post, expected_wsol_reserve_post ); - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); - - // check that transaction fails + let obligation_post = test.load_account::(obligation.pubkey).await; assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError( - 1, - InstructionError::Custom(LendingError::BorrowTooLarge as u32) - ) + 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 ); } +// FIXME this should really be a unit test #[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; +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 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, &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; - reserve_config.borrow_limit = 15; + // 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: 1_000_000_000, - 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, + }, + TokenBalanceChange { + token_account: host_fee_receiver.get_account(&wsol_mint::id()).unwrap(), + mint: wsol_mint::id(), + diff: 100, }, + ]); + + 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; +#[tokio::test] +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_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( - 1, + 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_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()); +#[tokio::test] +async fn test_fail_reserve_borrow_rate_limit_exceeded() { + let ( + mut test, + lending_market, + _, + wsol_reserve, + user, + obligation, + host_fee_receiver, + lending_market_owner, + ) = setup(&ReserveConfig { + ..test_reserve_config() + }) + .await; + + // ie, within 10 slots, the maximum outflow is 1 SOL + lending_market + .update_reserve_config( + &mut test, + &lending_market_owner, + &wsol_reserve, + wsol_reserve.account.config, + RateLimiterConfig { + window_duration: 10, + max_outflow: LAMPORTS_PER_SOL, + }, + None, + ) + .await + .unwrap(); - let reserve = usdc_test_reserve.get_state(&mut banks_client).await; - assert_eq!( - reserve.liquidity.borrowed_amount_wads, - Decimal::from(reserve_config.borrow_limit) - ); + // borrow maximum amount + lending_market + .borrow_obligation_liquidity( + &mut test, + &wsol_reserve, + &obligation, + &user, + &host_fee_receiver.get_account(&wsol_mint::id()).unwrap(), + LAMPORTS_PER_SOL, + ) + .await + .unwrap(); - // 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); + // for the next 10 slots, we shouldn't be able to borrow anything. + let cur_slot = test.get_clock().await.slot; + for _ in cur_slot..(cur_slot + 10) { + let res = lending_market + .borrow_obligation_liquidity( + &mut test, + &wsol_reserve, + &obligation, + &user, + &host_fee_receiver.get_account(&wsol_mint::id()).unwrap(), + 1, + ) + .await + .err() + .unwrap() + .unwrap(); + + assert_eq!( + res, + TransactionError::InstructionError( + 3, + InstructionError::Custom(LendingError::OutflowRateLimitExceeded as u32) + ) + ); + + test.advance_clock_by_slots(1).await; + } + + // after 10 slots, we should be able to at borrow most 0.1 SOL + 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 / 10 + 1, + ) + .await + .err() + .unwrap() + .unwrap(); assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), + res, TransactionError::InstructionError( - 2, - InstructionError::Custom(LendingError::BorrowTooSmall as u32) + 3, + InstructionError::Custom(LendingError::OutflowRateLimitExceeded as u32) ) ); + + lending_market + .borrow_obligation_liquidity( + &mut test, + &wsol_reserve, + &obligation, + &user, + &host_fee_receiver.get_account(&wsol_mint::id()).unwrap(), + LAMPORTS_PER_SOL / 10, + ) + .await + .unwrap(); } diff --git a/token-lending/program/tests/borrow_weight.rs b/token-lending/program/tests/borrow_weight.rs new file mode 100644 index 00000000000..b873b6d07fd --- /dev/null +++ b/token-lending/program/tests/borrow_weight.rs @@ -0,0 +1,382 @@ +#![cfg(feature = "test-bpf")] +/// the borrow weight feature affects a bunch of instructions. All of those instructions are tested +/// here for correctness. +use crate::solend_program_test::setup_world; +use crate::solend_program_test::BalanceChecker; +use crate::solend_program_test::TokenBalanceChange; +use solana_program::native_token::LAMPORTS_PER_SOL; +use solana_sdk::instruction::InstructionError; +use solana_sdk::transaction::TransactionError; +use solend_program::error::LendingError; +use solend_program::state::ReserveConfig; +use solend_sdk::state::ReserveFees; +mod helpers; + +use crate::solend_program_test::scenario_1; +use crate::solend_program_test::User; +use helpers::*; +use solana_program_test::*; +use solana_sdk::signature::Keypair; +use solend_program::math::Decimal; +use solend_program::state::Obligation; +use std::collections::HashSet; + +#[tokio::test] +async fn test_refresh_obligation() { + let (mut test, lending_market, _, _, _, obligation) = scenario_1( + &test_reserve_config(), + &ReserveConfig { + added_borrow_weight_bps: 10_000, + ..test_reserve_config() + }, + ) + .await; + + lending_market + .refresh_obligation(&mut test, &obligation) + .await + .unwrap(); + + let obligation_post = test.load_account::(obligation.pubkey).await; + + // obligation has borrowed 10 sol and sol = $10 but since borrow weight == 2, the + // borrowed_value is 200 instead of 100. + assert_eq!( + obligation_post.account, + Obligation { + borrowed_value: Decimal::from(200u64), + ..obligation.account + } + ); +} + +#[tokio::test] +async fn test_borrow() { + let (mut test, lending_market, usdc_reserve, wsol_reserve, _, _) = setup_world( + &test_reserve_config(), + &ReserveConfig { + added_borrow_weight_bps: 10_000, + fees: ReserveFees { + borrow_fee_wad: 10_000_000_000_000_000, // 1% + host_fee_percentage: 20, + flash_loan_fee_wad: 0, + }, + ..test_reserve_config() + }, + ) + .await; + + // create obligation with 100 USDC deposited. + let (user, obligation) = { + let user = User::new_with_balances( + &mut test, + &[ + (&usdc_mint::id(), 200 * FRACTIONAL_TO_USDC), + (&usdc_reserve.account.collateral.mint_pubkey, 0), + (&wsol_mint::id(), 0), + ], + ) + .await; + + let obligation = lending_market + .init_obligation(&mut test, Keypair::new(), &user) + .await + .expect("This should succeed"); + + lending_market + .deposit_reserve_liquidity_and_obligation_collateral( + &mut test, + &usdc_reserve, + &obligation, + &user, + 100 * FRACTIONAL_TO_USDC, + ) + .await + .unwrap(); + (user, obligation) + }; + + // deposit 100 WSOL into reserve + let host_fee_receiver = { + let wsol_depositor = User::new_with_balances( + &mut test, + &[ + (&wsol_mint::id(), 5 * LAMPORTS_PER_SOL), + (&wsol_reserve.account.collateral.mint_pubkey, 0), + ], + ) + .await; + + lending_market + .deposit( + &mut test, + &wsol_reserve, + &wsol_depositor, + 5 * LAMPORTS_PER_SOL, + ) + .await + .unwrap(); + + wsol_depositor.get_account(&wsol_mint::id()).unwrap() + }; + + // borrow max amount of SOL + { + lending_market + .borrow_obligation_liquidity( + &mut test, + &wsol_reserve, + &obligation, + &user, + &host_fee_receiver, + u64::MAX, + ) + .await + .unwrap(); + + let obligation_post = test.load_account::(obligation.pubkey).await; + // - usdc ltv is 0.5, + // - sol borrow weight is 2 + // max you can borrow is 100 * 0.5 / 2 = 2.5 SOL + assert_eq!( + obligation_post.account.borrows[0].borrowed_amount_wads, + Decimal::from(LAMPORTS_PER_SOL * 25 / 10) + ); + } + + // check that we shouldn't be able to withdraw anything + { + let res = lending_market + .withdraw_obligation_collateral(&mut test, &usdc_reserve, &obligation, &user, u64::MAX) + .await + .err() + .unwrap() + .unwrap(); + + assert_eq!( + res, + TransactionError::InstructionError( + 3, + InstructionError::Custom(LendingError::WithdrawTooLarge as u32) + ) + ); + } + + // deposit another 50 USDC + lending_market + .deposit_reserve_liquidity_and_obligation_collateral( + &mut test, + &usdc_reserve, + &obligation, + &user, + 50 * FRACTIONAL_TO_USDC, + ) + .await + .unwrap(); + + test.advance_clock_by_slots(1).await; + + // max withdraw + { + let balance_checker = BalanceChecker::start(&mut test, &[&user]).await; + + lending_market + .withdraw_obligation_collateral(&mut test, &usdc_reserve, &obligation, &user, u64::MAX) + .await + .unwrap(); + + let (balance_changes, _) = balance_checker.find_balance_changes(&mut test).await; + // should only be able to withdraw 50 USDC because the rest is needed to collateralize the + // SOL borrow + assert_eq!( + 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: (50 * FRACTIONAL_TO_USDC - 1) as i128, + }]) + ); + } +} + +#[tokio::test] +async fn test_liquidation() { + let (mut test, lending_market, usdc_reserve, wsol_reserve, lending_market_owner, _) = + setup_world( + &test_reserve_config(), + &ReserveConfig { + added_borrow_weight_bps: 0, + fees: ReserveFees { + borrow_fee_wad: 10_000_000_000_000_000, // 1% + host_fee_percentage: 20, + flash_loan_fee_wad: 0, + }, + ..test_reserve_config() + }, + ) + .await; + + // create obligation with 100 USDC deposited. + let (user, obligation) = { + let user = User::new_with_balances( + &mut test, + &[ + (&usdc_mint::id(), 200 * FRACTIONAL_TO_USDC), + (&usdc_reserve.account.collateral.mint_pubkey, 0), + (&wsol_mint::id(), 0), + ], + ) + .await; + + let obligation = lending_market + .init_obligation(&mut test, Keypair::new(), &user) + .await + .expect("This should succeed"); + + lending_market + .deposit_reserve_liquidity_and_obligation_collateral( + &mut test, + &usdc_reserve, + &obligation, + &user, + 100 * FRACTIONAL_TO_USDC, + ) + .await + .unwrap(); + (user, obligation) + }; + + // deposit 100 WSOL into reserve + let host_fee_receiver = { + let wsol_depositor = User::new_with_balances( + &mut test, + &[ + (&wsol_mint::id(), 5 * LAMPORTS_PER_SOL), + (&wsol_reserve.account.collateral.mint_pubkey, 0), + ], + ) + .await; + + lending_market + .deposit( + &mut test, + &wsol_reserve, + &wsol_depositor, + 5 * LAMPORTS_PER_SOL, + ) + .await + .unwrap(); + + wsol_depositor.get_account(&wsol_mint::id()).unwrap() + }; + + // borrow max amount of SOL + { + lending_market + .borrow_obligation_liquidity( + &mut test, + &wsol_reserve, + &obligation, + &user, + &host_fee_receiver, + u64::MAX, + ) + .await + .unwrap(); + + let obligation_post = test.load_account::(obligation.pubkey).await; + // - usdc ltv is 0.5, + // - sol borrow weight is 1 + // max you can borrow is 100 * 0.5 = 5 SOL + assert_eq!( + obligation_post.account.borrows[0].borrowed_amount_wads, + Decimal::from(LAMPORTS_PER_SOL * 5) + ); + } + + let liquidator = User::new_with_balances( + &mut test, + &[ + (&wsol_mint::id(), 100 * LAMPORTS_TO_SOL), + (&usdc_reserve.account.collateral.mint_pubkey, 0), + (&usdc_mint::id(), 0), + ], + ) + .await; + + // liquidating now would clearly fail because the obligation is healthy + { + let res = lending_market + .liquidate_obligation_and_redeem_reserve_collateral( + &mut test, + &wsol_reserve, + &usdc_reserve, + &obligation, + &liquidator, + u64::MAX, + ) + .await + .err() + .unwrap() + .unwrap(); + assert_eq!( + res, + TransactionError::InstructionError( + 3, + InstructionError::Custom(LendingError::ObligationHealthy as u32) + ) + ); + } + + // what is the minimum borrow weight we need for the obligation to be eligible for liquidation? + // 100 * 0.55 = 5 * 10 * borrow_weight + // => borrow_weight = 1.1 + + // set borrow weight to 1.1 + lending_market + .update_reserve_config( + &mut test, + &lending_market_owner, + &wsol_reserve, + ReserveConfig { + added_borrow_weight_bps: 1_000, + ..wsol_reserve.account.config + }, + wsol_reserve.account.rate_limiter.config, + None, + ) + .await + .unwrap(); + + test.advance_clock_by_slots(1).await; + + // liquidating now should work + { + let balance_checker = BalanceChecker::start(&mut test, &[&liquidator]).await; + lending_market + .liquidate_obligation_and_redeem_reserve_collateral( + &mut test, + &wsol_reserve, + &usdc_reserve, + &obligation, + &liquidator, + u64::MAX, + ) + .await + .unwrap(); + + // how much should be liquidated? + // => borrow value * close factor + // (5 sol * $10 * 1.1) * 0.2 = 11 usd worth of sol => repay ~1.1 sol (approximate because + // there is 1 slot worth of interest that is unaccounted for) + // note that if there were no borrow weight, we would only liquidate 10 usdc. + let (balance_changes, _) = balance_checker.find_balance_changes(&mut test).await; + assert!(balance_changes.contains(&TokenBalanceChange { + token_account: liquidator.get_account(&wsol_mint::id()).unwrap(), + mint: wsol_mint::id(), + diff: -1100000002 // ~1.1 SOL + })); + } +} diff --git a/token-lending/program/tests/deposit_obligation_collateral.rs b/token-lending/program/tests/deposit_obligation_collateral.rs index 4335b60c7ab..3d9307c5197 100644 --- a/token-lending/program/tests/deposit_obligation_collateral.rs +++ b/token-lending/program/tests/deposit_obligation_collateral.rs @@ -2,131 +2,118 @@ mod helpers; -use helpers::*; -use solana_program_test::*; -use solana_sdk::{ - pubkey::Pubkey, - 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_bpf_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_eq!(sol_reserve.last_update.stale, true); + 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 99313f11649..7e7da771b88 100644 --- a/token-lending/program/tests/deposit_reserve_liquidity.rs +++ b/token-lending/program/tests/deposit_reserve_liquidity.rs @@ -2,78 +2,164 @@ 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::{pubkey::Pubkey, 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_bpf_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 usdc_reserve_post = test.load_account::(usdc_reserve.pubkey).await; + let expected_usdc_reserve_post = 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_eq!( + usdc_reserve_post.account, expected_usdc_reserve_post, + "{:#?} {:#?}", + usdc_reserve_post.account, expected_usdc_reserve_post + ); +} - let ProgramTestContext { - mut banks_client, - payer, - .. - } = test_context; +#[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_eq!(usdc_reserve.last_update.stale, true); +#[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 e7c0e9c1655..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::{pubkey::Pubkey, signature::Keypair}; -use solend_program::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 solend_program::math::Decimal; +use solend_program::state::{ + LastUpdate, LendingMarket, Obligation, ObligationCollateral, Reserve, ReserveCollateral, + ReserveLiquidity, +}; - // limit to track compute unit increase - test.set_bpf_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_eq!(usdc_reserve.last_update.stale, true); + 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/fixtures/3Mnn2fX6rQyUsyELYms1sBJyChWofzSNRoqYzvgMVz5E.bin b/token-lending/program/tests/fixtures/3Mnn2fX6rQyUsyELYms1sBJyChWofzSNRoqYzvgMVz5E.bin index 32a3793d8467d5fcef054f47a156700d5b9087ae..45cb4695d42476614778914d3f5ed25f579dcaee 100644 GIT binary patch delta 119 zcmew$`9YHR%Hd55nHU%tKzQCnUM&|s=c3Ajl6<{je;@tOU>B~$;^Nej_>#(kR5pkV zUtwu}Noss@X;D#XUUDTfP!V@ZYH@N=WB~$;^Nej_>#(kR5pkV zUtwu}Noss@X;D#XUUDTfP!V@ZYH@N=WKr(Vl1G0bT&kZ!fd} delta 96 zcmew$`9YHR%Hd55nHU%tKzPwaUM)uUiT0A#?8*71c_l@aOrgPig{Ao=sqx9BMMbH3 y$(78Z!7i*Nsd@QDEWri&CEO{g#mPmP1tppJd91;yMWwkD>%}&1wC9<4fFA(%q#-B( diff --git a/token-lending/program/tests/fixtures/992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs.bin b/token-lending/program/tests/fixtures/992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs.bin index 65bf11a0f27ab3e68af01934ad9a5b54cab66049..09deb80330b9cda758621bb81bb4a7d41ef4575a 100644 GIT binary patch literal 3312 zcmca|c+)~A1_lOZApXF^z`zKkfBgUdUjaz70I{uF9~Y2fuz}KB^%j7XF#J>2dI_TA z=hlRR=gr?=9|rs@FRZ#@yW~Nwx?(~!6GQpBDc?jBx7}UY z@iA!>r2{O#R8M$hJgUU&bC>!+As+9D?BAQzE1Epmg2;`Rr)^bspATqefBYW}U-&qwgV z?0ylWiJ_Y}ybm;V{JZVe>fbjj^oTVFm%1zAPt^iX@#nm(G?sk1hO-#K9vhInA``aX9r$Tiif0k5X~i{$PS_j$x92G3d^qMVJfOTVP=0yVO4*wP0}pB zWlr_}eX{PS&wm7)K>?8x#L4zn>+nZTuhX(0S4I5)qW?3qvQ=W&_swZtt8Rt)QeZS# zYhQBm+-F*@(!1ZR&y+ZF=6=BIH&wgh3mVE=&doXganG`dL!ZIwT4W(M!P0+PdLB63 z2+0Q@c`K}8HD^)PeXj$bXIdM6F`Bk0q3E#llbshf&U?6IH>K{MaU(#yAKc$Td;$v}nEANmVdnE5medV> zeBg7^@2~uar{7`tmwAyZxJt=FP%kY?X7|#RaHhpzBZIa5)LnVaV&lHxnHN$rQq#Uw%|CMMY{ZUVpB3)M+nY=~8!fK_04S!-)ajktJsB?&6ZPYzK7PT(!ByshIw`(EYlA@ z()N2MQ~1W8$GENFNc@@Et(}ybU-pyHf5lz@2e;np-TUG4weF$X&c4I(9&els9>*Qr zeMym0^Jl8Cb6HI>-g)WO{vYu@%fv*4oj$*snt42I=aJ3sMoDIrntxe!c0~E7hMbfu zAFf?~RhOQ4dXtmlKXE~ynGh(q4h(ub-xn@{N%E9-icv2ip!TTJ~@?)9%ze!k2`LEEe$o z3YpToxvKI&lM$uvziHo_>B|&r*X4Mvq=NTgi0QqCKk3J-xf&nrPw2Vxr;bwddBjEi znYpx7G6N6Y4c2Ox)kOcs zRqD&QJbcD6@32^&?kftM4%QkyyI;nj*M}!@!o0@iHdQ;{ddT@yE@9f6+dk(`Hjn-5 zfV1;qNd~MQ#<7654++W7dRzZ9{AW4Aw1QtH!jxFH!10i{VS6{byua&PLQ`GoO(A5}0hj<{vnwi`9Hw@-XukO%b^<;kod%2z9P^ zS(hu1r)wnk)}9S;+O8Jev7#`^8*Jz(F&YA+Aut*OqaiRF0;3@?8UmvsFd71*Api~m E0KtcF(*OVf diff --git a/token-lending/program/tests/flash_borrow_repay.rs b/token-lending/program/tests/flash_borrow_repay.rs new file mode 100644 index 00000000000..e2f302e9d30 --- /dev/null +++ b/token-lending/program/tests/flash_borrow_repay.rs @@ -0,0 +1,1108 @@ +#![cfg(feature = "test-bpf")] + +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::*; +use solana_sdk::{ + instruction::InstructionError, + pubkey::Pubkey, + signature::{Keypair, Signer}, + 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}, + 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, 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; + 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(); + + // 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()); + + // check program state changes + let lending_market_post = test + .load_account::(lending_market.pubkey) + .await; + assert_eq!(lending_market, lending_market_post); + + 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, 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; + 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!( + res, + TransactionError::InstructionError( + 0, + InstructionError::Custom(LendingError::FlashLoansDisabled as u32) + ) + ); +} + +#[tokio::test] +async fn test_fail_borrow_over_borrow_limit() { + 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; + 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!( + res, + TransactionError::InstructionError( + 0, + InstructionError::Custom(LendingError::InvalidAmount as u32) + ) + ); +} + +#[tokio::test] +async fn test_fail_double_borrow() { + 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; + 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!( + res, + TransactionError::InstructionError( + 0, + InstructionError::Custom(LendingError::MultipleFlashBorrows as u32) + ) + ); +} + +#[tokio::test] +async fn test_fail_double_repay() { + 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; + 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]), + ) + .await + .unwrap_err() + .unwrap(); + + assert_eq!( + res, + TransactionError::InstructionError( + 0, + InstructionError::Custom(LendingError::MultipleFlashBorrows as u32) + ) + ); +} + +#[tokio::test] +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; + 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_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, + 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(&[&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!( + res, + TransactionError::InstructionError( + 0, + InstructionError::Custom(LendingError::InvalidFlashRepay as u32) + ) + ); + } + + // case 2: invalid liquidity amount + { + 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!( + res, + TransactionError::InstructionError( + 0, + InstructionError::Custom(LendingError::InvalidFlashRepay as u32) + ) + ); + } + + // case 3: no 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, + )], + None, + ) + .await + .unwrap_err() + .unwrap(); + + assert_eq!( + res, + TransactionError::InstructionError( + 0, + InstructionError::Custom(LendingError::NoFlashRepayFound as u32) + ) + ); + } + + // case 4: cpi 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, + ), + 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!( + res, + TransactionError::InstructionError( + 0, + InstructionError::Custom(LendingError::NoFlashRepayFound as u32) + ) + ); + } + + // case 5: insufficient funds to pay fees on repay. + { + 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(); + + // weird glitch. depending on cargo version the error type is different. idek. + assert!( + res == TransactionError::InstructionError( + 1, + InstructionError::Custom(TokenError::InsufficientFunds as u32) + ) || res + == TransactionError::InstructionError( + 1, + InstructionError::Custom(LendingError::TokenTransferFailed as u32) + ) + ); + } + + // case 6: Sole repay instruction + { + 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!( + res, + TransactionError::InstructionError( + 0, + InstructionError::Custom(LendingError::InvalidFlashRepay as u32) + ) + ); + } + + // case 7: Incorrect borrow instruction index -- points to itself + { + 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!( + res, + TransactionError::InstructionError( + 0, + InstructionError::Custom(LendingError::InvalidFlashRepay as u32) + ) + ); + } + + // case 8: Incorrect borrow instruction index -- points to some other program + { + let user_transfer_authority = Keypair::new(); + 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!( + res, + TransactionError::InstructionError( + 1, + InstructionError::Custom(LendingError::InvalidFlashRepay as u32) + ) + ); + } + // case 9: Incorrect borrow instruction index -- points to a later borrow + { + 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!( + res, + TransactionError::InstructionError( + 0, + InstructionError::Custom(LendingError::InvalidFlashRepay as u32) + ) + ); + } +} + +#[tokio::test] +async fn test_fail_insufficient_liquidity_for_borrow() { + 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!( + res, + TransactionError::InstructionError( + 0, + InstructionError::Custom(LendingError::InsufficientLiquidity as u32) + ) + ); +} + +#[tokio::test] +async fn test_fail_cpi_borrow() { + 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; + + 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!( + res, + TransactionError::InstructionError( + 0, + InstructionError::Custom(LendingError::FlashBorrowCpi as u32) + ) + ); +} + +#[tokio::test] +async fn test_fail_cpi_repay() { + 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; + 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!( + res, + TransactionError::InstructionError( + 0, + InstructionError::Custom(LendingError::FlashRepayCpi as u32) + ) + ); +} + +#[tokio::test] +async fn test_fail_repay_from_diff_reserve() { + 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 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 res { + BanksClientError::RpcError(..) => (), + BanksClientError::TransactionError(TransactionError::InstructionError( + 1, + InstructionError::PrivilegeEscalation, + )) => (), + _ => panic!("Unexpected error: {:?}", res), + }; +} + +// don't explicitly check user_transfer_authority signer +#[allow(clippy::too_many_arguments)] +pub fn malicious_flash_repay_reserve_liquidity( + program_id: Pubkey, + liquidity_amount: u64, + borrow_instruction_index: u8, + source_liquidity_pubkey: Pubkey, + destination_liquidity_pubkey: Pubkey, + reserve_liquidity_fee_receiver_pubkey: Pubkey, + host_fee_receiver_pubkey: Pubkey, + reserve_pubkey: Pubkey, + lending_market_pubkey: Pubkey, + user_transfer_authority_pubkey: Pubkey, +) -> Instruction { + Instruction { + program_id, + accounts: vec![ + AccountMeta::new(source_liquidity_pubkey, false), + AccountMeta::new(destination_liquidity_pubkey, false), + AccountMeta::new(reserve_liquidity_fee_receiver_pubkey, false), + AccountMeta::new(host_fee_receiver_pubkey, false), + AccountMeta::new(reserve_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(user_transfer_authority_pubkey, false), + AccountMeta::new_readonly(sysvar::instructions::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: LendingInstruction::FlashRepayReserveLiquidity { + liquidity_amount, + borrow_instruction_index, + } + .pack(), + } +} diff --git a/token-lending/program/tests/flash_loan.rs b/token-lending/program/tests/flash_loan.rs deleted file mode 100644 index 4fa9be5f588..00000000000 --- a/token-lending/program/tests/flash_loan.rs +++ /dev/null @@ -1,227 +0,0 @@ -#![cfg(feature = "test-bpf")] - -mod helpers; - -use helpers::*; -use solana_program::instruction::AccountMeta; -use solana_program_test::*; -use solana_sdk::{ - pubkey::Pubkey, - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, -}; -use solend_program::{ - error::LendingError, instruction::flash_loan, processor::process_instruction, -}; -use spl_token::solana_program::instruction::InstructionError; - -#[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_bpf_compute_max_units(50_000); - - 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 receiver_program_account = Keypair::new(); - let receiver_program_id = receiver_program_account.pubkey(); - test.prefer_bpf(false); - test.add_program( - "flash_loan_receiver", - receiver_program_id.clone(), - processor!(helpers::flash_loan_receiver::process_instruction), - ); - - 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 = 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 { - liquidity_amount: FLASH_LOAN_AMOUNT, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - ..AddReserveArgs::default() - }, - ); - - let (receiver_authority_pubkey, _) = - Pubkey::find_program_address(&[b"flashloan"], &receiver_program_id); - let program_owned_token_account = add_account_for_program( - &mut test, - &receiver_authority_pubkey, - FEE_AMOUNT, - &usdc_mint.pubkey, - ); - - 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; - assert_eq!(initial_liquidity_supply, FLASH_LOAN_AMOUNT); - - let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; - let initial_available_amount = usdc_reserve.liquidity.available_amount; - assert_eq!(initial_available_amount, FLASH_LOAN_AMOUNT); - - let initial_token_balance = - get_token_balance(&mut banks_client, program_owned_token_account).await; - assert_eq!(initial_token_balance, FEE_AMOUNT); - - let mut transaction = Transaction::new_with_payer( - &[flash_loan( - solend_program::id(), - FLASH_LOAN_AMOUNT, - usdc_test_reserve.liquidity_supply_pubkey, - program_owned_token_account, - usdc_test_reserve.pubkey, - usdc_test_reserve.config.fee_receiver, - usdc_test_reserve.liquidity_host_pubkey, - lending_market.pubkey, - receiver_program_id.clone(), - vec![AccountMeta::new_readonly( - receiver_authority_pubkey.clone(), - false, - )], - )], - Some(&payer.pubkey()), - ); - - transaction.sign(&[&payer], 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, - initial_available_amount - ); - - let (total_fee, host_fee) = usdc_reserve - .config - .fees - .calculate_flash_loan_fees(FLASH_LOAN_AMOUNT.into()) - .unwrap(); - assert_eq!(total_fee, FEE_AMOUNT); - assert_eq!(host_fee, HOST_FEE_AMOUNT); - - let liquidity_supply = - get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; - assert_eq!(liquidity_supply, initial_liquidity_supply); - - let token_balance = get_token_balance(&mut banks_client, program_owned_token_account).await; - assert_eq!(token_balance, initial_token_balance - FEE_AMOUNT); - - 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); -} - -#[tokio::test] -async fn test_failure() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - const FLASH_LOAN_AMOUNT: u64 = 1_000 * FRACTIONAL_TO_USDC; - const FEE_AMOUNT: u64 = 3_000_000; - - let flash_loan_receiver_program_keypair = Keypair::new(); - let flash_loan_receiver_program_id = flash_loan_receiver_program_keypair.pubkey(); - test.prefer_bpf(false); - test.add_program( - "flash_loan_receiver", - flash_loan_receiver_program_id.clone(), - processor!(helpers::flash_loan_receiver::process_instruction), - ); - - 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 = 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 { - liquidity_amount: FLASH_LOAN_AMOUNT, - liquidity_mint_pubkey: usdc_mint.pubkey, - liquidity_mint_decimals: usdc_mint.decimals, - config: reserve_config, - ..AddReserveArgs::default() - }, - ); - - let (receiver_authority_pubkey, _) = - Pubkey::find_program_address(&[b"flashloan"], &flash_loan_receiver_program_id); - let program_owned_token_account = add_account_for_program( - &mut test, - &receiver_authority_pubkey, - FEE_AMOUNT - 1, - &usdc_mint.pubkey, - ); - - let (mut banks_client, payer, recent_blockhash) = test.start().await; - - let initial_token_balance = - get_token_balance(&mut banks_client, program_owned_token_account).await; - assert_eq!(initial_token_balance, FEE_AMOUNT - 1); - - let mut transaction = Transaction::new_with_payer( - &[flash_loan( - solend_program::id(), - FLASH_LOAN_AMOUNT, - usdc_test_reserve.liquidity_supply_pubkey, - program_owned_token_account, - usdc_test_reserve.pubkey, - usdc_test_reserve.config.fee_receiver, - usdc_test_reserve.liquidity_host_pubkey, - lending_market.pubkey, - flash_loan_receiver_program_id.clone(), - vec![AccountMeta::new_readonly( - receiver_authority_pubkey.clone(), - false, - )], - )], - Some(&payer.pubkey()), - ); - - transaction.sign(&[&payer], recent_blockhash); - assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError( - 0, - InstructionError::Custom(LendingError::NotEnoughLiquidityAfterFlashLoan as u32) - ) - ); -} diff --git a/token-lending/program/tests/helpers/flash_loan_proxy.rs b/token-lending/program/tests/helpers/flash_loan_proxy.rs new file mode 100644 index 00000000000..7058aafaea6 --- /dev/null +++ b/token-lending/program/tests/helpers/flash_loan_proxy.rs @@ -0,0 +1,285 @@ +use solana_program::{ + account_info::AccountInfo, + entrypoint::ProgramResult, + instruction::{AccountMeta, Instruction}, + msg, + program::invoke, + pubkey::Pubkey, + sysvar, +}; + +use crate::helpers::flash_loan_proxy::FlashLoanProxyError::InvalidInstruction; +use spl_token::solana_program::{account_info::next_account_info, program_error::ProgramError}; +use std::convert::TryInto; +use std::mem::size_of; +use thiserror::Error; + +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, + }, + ProxyRepay { + liquidity_amount: u64, + borrow_instruction_index: u8, + }, +} + +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 = FlashLoanProxyInstruction::unpack(instruction_data)?; + + match instruction { + FlashLoanProxyInstruction::ProxyBorrow { liquidity_amount } => { + msg!("Instruction: Proxy Borrow"); + Self::process_proxy_borrow(accounts, liquidity_amount, program_id) + } + FlashLoanProxyInstruction::ProxyRepay { + liquidity_amount, + borrow_instruction_index, + } => { + msg!("Instruction: Proxy Repay"); + Self::process_proxy_repay( + accounts, + liquidity_amount, + borrow_instruction_index, + program_id, + ) + } + } + } + + fn process_proxy_repay( + accounts: &[AccountInfo], + liquidity_amount: u64, + borrow_instruction_index: u8, + _program_id: &Pubkey, + ) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let source_liquidity_info = next_account_info(account_info_iter)?; + let destination_liquidity_info = next_account_info(account_info_iter)?; + let reserve_liquidity_fee_receiver_info = next_account_info(account_info_iter)?; + let host_fee_receiver_info = next_account_info(account_info_iter)?; + let reserve_info = next_account_info(account_info_iter)?; + let token_lending_info = next_account_info(account_info_iter)?; + let lending_market_info = next_account_info(account_info_iter)?; + let user_transfer_authority_info = next_account_info(account_info_iter)?; + + invoke( + &flash_repay_reserve_liquidity( + *token_lending_info.key, + liquidity_amount, + borrow_instruction_index, + *source_liquidity_info.key, + *destination_liquidity_info.key, + *reserve_liquidity_fee_receiver_info.key, + *host_fee_receiver_info.key, + *reserve_info.key, + *lending_market_info.key, + *user_transfer_authority_info.key, + ), + accounts, + )?; + + Ok(()) + } + + fn process_proxy_borrow( + accounts: &[AccountInfo], + liquidity_amount: u64, + _program_id: &Pubkey, + ) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let source_liquidity_info = next_account_info(account_info_iter)?; + let destination_liquidity_info = next_account_info(account_info_iter)?; + let reserve_info = next_account_info(account_info_iter)?; + let token_lending_info = next_account_info(account_info_iter)?; + let lending_market_info = next_account_info(account_info_iter)?; + + invoke( + &flash_borrow_reserve_liquidity( + *token_lending_info.key, + liquidity_amount, + *source_liquidity_info.key, + *destination_liquidity_info.key, + *reserve_info.key, + *lending_market_info.key, + ), + accounts, + )?; + + Ok(()) + } +} + +impl FlashLoanProxyInstruction { + pub fn unpack(input: &[u8]) -> Result { + let (tag, rest) = input.split_first().ok_or(InvalidInstruction)?; + + Ok(match tag { + 0 => Self::ProxyBorrow { + liquidity_amount: Self::unpack_u64(rest)?.0, + }, + 1 => { + let (liquidity_amount, rest) = Self::unpack_u64(rest)?; + let (borrow_instruction_index, _rest) = Self::unpack_u8(rest)?; + Self::ProxyRepay { + liquidity_amount, + borrow_instruction_index, + } + } + _ => return Err(InvalidInstruction.into()), + }) + } + + fn unpack_u64(input: &[u8]) -> Result<(u64, &[u8]), ProgramError> { + if input.len() < 8 { + msg!("u64 cannot be unpacked"); + return Err(FlashLoanProxyError::InvalidInstruction.into()); + } + let (bytes, rest) = input.split_at(8); + let value = bytes + .get(..8) + .and_then(|slice| slice.try_into().ok()) + .map(u64::from_le_bytes) + .ok_or(FlashLoanProxyError::InvalidInstruction)?; + Ok((value, rest)) + } + + fn unpack_u8(input: &[u8]) -> Result<(u8, &[u8]), ProgramError> { + if input.is_empty() { + msg!("u8 cannot be unpacked"); + return Err(FlashLoanProxyError::InvalidInstruction.into()); + } + let (bytes, rest) = input.split_at(1); + let value = bytes + .get(..1) + .and_then(|slice| slice.try_into().ok()) + .map(u8::from_le_bytes) + .ok_or(FlashLoanProxyError::InvalidInstruction)?; + Ok((value, rest)) + } +} + +#[derive(Error, Debug, Copy, Clone)] +pub enum FlashLoanProxyError { + /// Invalid instruction + #[error("Invalid Instruction")] + InvalidInstruction, + #[error("The account is not currently owned by the program")] + IncorrectProgramId, +} + +impl From for ProgramError { + fn from(e: FlashLoanProxyError) -> Self { + ProgramError::Custom(e as u32) + } +} + +/// Creates a 'RepayProxy' instruction. +#[allow(clippy::too_many_arguments)] +pub fn repay_proxy( + program_id: Pubkey, + liquidity_amount: u64, + borrow_instruction_index: u8, + source_liquidity_pubkey: Pubkey, + destination_liquidity_pubkey: Pubkey, + reserve_liquidity_fee_receiver_pubkey: Pubkey, + host_fee_receiver_pubkey: Pubkey, + reserve_pubkey: Pubkey, + token_lending_pubkey: Pubkey, + lending_market_pubkey: Pubkey, + user_transfer_authority_pubkey: Pubkey, +) -> Instruction { + Instruction { + program_id, + accounts: vec![ + AccountMeta::new(source_liquidity_pubkey, false), + AccountMeta::new(destination_liquidity_pubkey, false), + AccountMeta::new(reserve_liquidity_fee_receiver_pubkey, false), + AccountMeta::new(host_fee_receiver_pubkey, false), + AccountMeta::new(reserve_pubkey, false), + AccountMeta::new_readonly(token_lending_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(user_transfer_authority_pubkey, true), + AccountMeta::new_readonly(sysvar::instructions::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: FlashLoanProxyInstruction::ProxyRepay { + liquidity_amount, + borrow_instruction_index, + } + .pack(), + } +} + +/// Creates a 'BorrowProxy' instruction. +#[allow(clippy::too_many_arguments)] +pub fn borrow_proxy( + program_id: Pubkey, + liquidity_amount: u64, + source_liquidity_pubkey: Pubkey, + destination_liquidity_pubkey: Pubkey, + reserve_pubkey: Pubkey, + token_lending_pubkey: Pubkey, + lending_market_pubkey: Pubkey, + lending_market_authority_pubkey: Pubkey, +) -> Instruction { + Instruction { + program_id, + accounts: vec![ + AccountMeta::new(source_liquidity_pubkey, false), + AccountMeta::new(destination_liquidity_pubkey, false), + AccountMeta::new(reserve_pubkey, false), + AccountMeta::new_readonly(token_lending_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(lending_market_authority_pubkey, false), + AccountMeta::new_readonly(sysvar::instructions::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + ], + data: FlashLoanProxyInstruction::ProxyBorrow { liquidity_amount }.pack(), + } +} + +impl FlashLoanProxyInstruction { + pub fn pack(&self) -> Vec { + let mut buf = Vec::with_capacity(size_of::()); + match *self { + Self::ProxyBorrow { liquidity_amount } => { + buf.push(0); + buf.extend_from_slice(&liquidity_amount.to_le_bytes()); + } + Self::ProxyRepay { + liquidity_amount, + borrow_instruction_index, + } => { + buf.push(1); + buf.extend_from_slice(&liquidity_amount.to_le_bytes()); + buf.extend_from_slice(&borrow_instruction_index.to_le_bytes()); + } + } + buf + } +} diff --git a/token-lending/program/tests/helpers/genesis.rs b/token-lending/program/tests/helpers/genesis.rs index 6eba2f0e8d8..2cdc2ceca42 100644 --- a/token-lending/program/tests/helpers/genesis.rs +++ b/token-lending/program/tests/helpers/genesis.rs @@ -42,7 +42,7 @@ impl GenesisAccounts { let programdata_address = Pubkey::find_program_address(&[program_id.as_ref()], &bpf_loader_upgradeable::id()).0; - let programdata_data_offset = UpgradeableLoaderState::programdata_data_offset().unwrap(); + let programdata_data_offset = UpgradeableLoaderState::size_of_programdata_metadata(); let programdata_space = 2 * program_data.len() + programdata_data_offset; let mut programdata_account = Account::new_data_with_space( u32::MAX as 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..0deac090b4a --- /dev/null +++ b/token-lending/program/tests/helpers/mock_pyth.rs @@ -0,0 +1,271 @@ +use pyth_sdk_solana::state::{ + AccountType, PriceAccount, PriceStatus, ProductAccount, Rational, 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, + ema_price: i64, + ema_conf: u64, + }, + + /// 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, + ema_price, + ema_conf, + } => { + 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.ema_price = Rational { + val: ema_price, + // these fields don't matter + numer: 1, + denom: 1, + }; + + price_account.ema_conf = Rational { + val: ema_conf as i64, + numer: 1, + denom: 1, + }; + + 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, + ema_price: i64, + ema_conf: u64, +) -> Instruction { + let data = MockPythInstruction::SetPrice { + price, + conf, + expo, + ema_price, + ema_conf, + } + .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 50805befdab..821e32dc32c 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -1,39 +1,31 @@ #![allow(dead_code)] +pub mod flash_loan_proxy; pub mod flash_loan_receiver; pub mod genesis; +pub mod mock_pyth; +pub mod solend_program_test; + +use bytemuck::{cast_slice_mut, from_bytes_mut, try_cast_slice_mut, Pod, PodCastError}; -use assert_matches::*; 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}, - processor::switchboard_v2_mainnet, - pyth, - state::{ - InitLendingMarketParams, InitObligationParams, InitReserveParams, LendingMarket, - NewReserveCollateralParams, NewReserveLiquidityParams, Obligation, ObligationCollateral, - ObligationLiquidity, Reserve, ReserveCollateral, ReserveConfig, ReserveFees, - ReserveLiquidity, INITIAL_COLLATERAL_RATIO, PROGRAM_VERSION, - }, + state::{Obligation, ReserveConfig, ReserveFees}, }; -use spl_token::{ - instruction::approve, - state::{Account as Token, AccountState, Mint}, -}; -use std::{convert::TryInto, str::FromStr}; + +use spl_token::state::Mint; + +use std::mem::size_of; use switchboard_v2::AggregatorAccountData; pub const QUOTE_CURRENCY: [u8; 32] = @@ -52,30 +44,31 @@ 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_liquidation_fee: 0, + protocol_take_rate: 0, + added_borrow_weight_bps: 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 mod usdc_mint { + solana_program::declare_id!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); +} -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 usdt_mint { + solana_program::declare_id!("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"); +} -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( @@ -101,1364 +94,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(1) - } 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_recent_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_recent_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_recent_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_recent_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_recent_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_recent_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_recent_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_recent_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: name, - pubkey: reserve_pubkey, - lending_market_pubkey: lending_market.pubkey, - config: config, - liquidity_mint_pubkey: 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_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_recent_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), - ) -} - -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), - ) -} - -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), - ) -} - -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), - ) -} - -pub fn add_oracle( - test: &mut ProgramTest, - pyth_product_pubkey: Pubkey, - pyth_price_pubkey: Pubkey, - switchboard_feed_pubkey: Pubkey, - price: Decimal, -) -> 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.to_string()), - ); - } - if pyth_price_pubkey.to_string() != NULL_PUBKEY { - // Add Pyth price account after setting the price - let filename = &format!("{}.bin", pyth_price_pubkey.to_string()); - let mut pyth_price_data = read_file(find_file(filename).unwrap_or_else(|| { - panic!("Unable to locate {}", filename); - })); - - let mut pyth_price = pyth::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 = 0; - pyth_price.agg.price = price - .try_round_u64() - .unwrap() - .checked_mul(decimals) - .unwrap() - .try_into() - .unwrap(); - - 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.to_string()); - // 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_recent_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_recent_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..4b92ff95fc8 --- /dev/null +++ b/token-lending/program/tests/helpers/solend_program_test.rs @@ -0,0 +1,1639 @@ +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_program::state::RateLimiterConfig; +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_and_config, + 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, usdt_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), + (usdt_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, + price.ema_price, + price.ema_conf, + )], + None, + ) + .await + .unwrap(); + } + + pub async fn init_switchboard_feed(&mut self, mint: &Pubkey) -> 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); + switchboard_feed_pubkey + } else { + panic!("oracle not initialized"); + } + } + + pub async fn set_switchboard_price(&mut self, mint: &Pubkey, price: SwitchboardPriceArgs) { + 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(t) => t.clone(), + } + } + + 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, + pub ema_price: i64, + pub ema_conf: u64, +} + +pub struct SwitchboardPriceArgs { + pub price: i64, + 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, + rate_limiter_config: RateLimiterConfig, + 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); + + let instructions = [update_reserve_config( + solend_program::id(), + config, + rate_limiter_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 = [ + refresh_reserve( + solend_program::id(), + reserve.pubkey, + reserve.account.liquidity.pyth_oracle_pubkey, + reserve.account.liquidity.switchboard_oracle_pubkey, + ), + 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 obligation = test.load_account::(obligation.pubkey).await; + 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 obligation = test.load_account::(obligation.pubkey).await; + + 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 obligation = test.load_account::(obligation.pubkey).await; + + 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_and_config( + &self, + test: &mut SolendProgramTest, + lending_market_owner: &User, + new_owner: &Pubkey, + config: RateLimiterConfig, + ) -> Result<(), BanksClientError> { + let instructions = [set_lending_market_owner_and_config( + solend_program::id(), + self.pubkey, + lending_market_owner.keypair.pubkey(), + *new_owner, + config, + )]; + + 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, + ema_price: 1, + ema_conf: 0, + }, + ) + .await; + + test.init_pyth_feed(&wsol_mint::id()).await; + test.set_price( + &wsol_mint::id(), + &PriceArgs { + price: 10, + conf: 0, + expo: 0, + ema_price: 10, + ema_conf: 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 +/// sol = $10 +/// usdc = $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, + ) +} + +pub struct ReserveArgs { + pub mint: Pubkey, + pub config: ReserveConfig, + pub liquidity_amount: u64, + pub price: PriceArgs, +} + +pub struct ObligationArgs { + pub deposits: Vec<(Pubkey, u64)>, + pub borrows: Vec<(Pubkey, u64)>, +} + +pub async fn custom_scenario( + reserve_args: &[ReserveArgs], + obligation_args: &ObligationArgs, +) -> ( + SolendProgramTest, + Info, + Vec>, + Info, + User, +) { + let mut test = SolendProgramTest::start_new().await; + let mints_and_liquidity_amounts = reserve_args + .iter() + .map(|reserve_arg| (&reserve_arg.mint, reserve_arg.liquidity_amount)) + .collect::>(); + + let lending_market_owner = + User::new_with_balances(&mut test, &mints_and_liquidity_amounts).await; + + let lending_market = test + .init_lending_market(&lending_market_owner, &Keypair::new()) + .await + .unwrap(); + + let deposits_and_balances = obligation_args + .deposits + .iter() + .map(|(mint, amount)| (mint, *amount)) + .collect::>(); + + let mut obligation_owner = User::new_with_balances(&mut test, &deposits_and_balances).await; + + let obligation = lending_market + .init_obligation(&mut test, Keypair::new(), &obligation_owner) + .await + .unwrap(); + + test.advance_clock_by_slots(999).await; + + let mut reserves = Vec::new(); + for reserve_arg in reserve_args { + test.init_pyth_feed(&reserve_arg.mint).await; + + test.set_price(&reserve_arg.mint, &reserve_arg.price).await; + + let reserve = test + .init_reserve( + &lending_market, + &lending_market_owner, + &reserve_arg.mint, + &reserve_arg.config, + &Keypair::new(), + reserve_arg.liquidity_amount, + None, + ) + .await + .unwrap(); + + let user = User::new_with_balances( + &mut test, + &[ + (&reserve_arg.mint, reserve_arg.liquidity_amount), + (&reserve.account.collateral.mint_pubkey, 0), + ], + ) + .await; + + lending_market + .deposit(&mut test, &reserve, &user, reserve_arg.liquidity_amount) + .await + .unwrap(); + + obligation_owner + .create_token_account(&reserve_arg.mint, &mut test) + .await; + + reserves.push(reserve); + } + + for (mint, amount) in obligation_args.deposits.iter() { + let reserve = reserves + .iter() + .find(|reserve| reserve.account.liquidity.mint_pubkey == *mint) + .unwrap(); + + obligation_owner + .create_token_account(&reserve.account.collateral.mint_pubkey, &mut test) + .await; + + lending_market + .deposit_reserve_liquidity_and_obligation_collateral( + &mut test, + reserve, + &obligation, + &obligation_owner, + *amount, + ) + .await + .unwrap(); + } + + for (mint, amount) in obligation_args.borrows.iter() { + let reserve = reserves + .iter() + .find(|reserve| reserve.account.liquidity.mint_pubkey == *mint) + .unwrap(); + + obligation_owner.create_token_account(mint, &mut test).await; + let fee_receiver = User::new_with_balances(&mut test, &[(mint, 0)]).await; + + lending_market + .borrow_obligation_liquidity( + &mut test, + reserve, + &obligation, + &obligation_owner, + &fee_receiver.get_account(mint).unwrap(), + *amount, + ) + .await + .unwrap(); + } + + (test, lending_market, reserves, obligation, obligation_owner) +} + +pub fn find_reserve(reserves: &[Info], mint: &Pubkey) -> Option> { + reserves + .iter() + .find(|reserve| reserve.account.liquidity.mint_pubkey == *mint) + .cloned() +} diff --git a/token-lending/program/tests/init_lending_market.rs b/token-lending/program/tests/init_lending_market.rs index bd131099b8b..3cf7937c179 100644 --- a/token-lending/program/tests/init_lending_market.rs +++ b/token-lending/program/tests/init_lending_market.rs @@ -2,65 +2,76 @@ 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, - pubkey::Pubkey, - 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, RateLimiter, 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_bpf_compute_max_units(17_000); - - let (mut banks_client, payer, _recent_blockhash) = test.start().await; + let mut test = SolendProgramTest::start_new().await; + test.advance_clock_by_slots(1000).await; - let test_lending_market = TestLendingMarket::init(&mut banks_client, &payer).await; + let lending_market_owner = User::new_with_balances(&mut test, &[]).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(), + rate_limiter: RateLimiter::default(), + } + ); } #[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; + test.advance_clock_by_slots(1000).await; - let existing_market = add_lending_market(&mut test); - let (mut banks_client, payer, recent_blockhash) = test.start().await; + let lending_market_owner = User::new_with_balances(&mut test, &[]).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 593c6aa0d55..d7964010e80 100644 --- a/token-lending/program/tests/init_obligation.rs +++ b/token-lending/program/tests/init_obligation.rs @@ -2,84 +2,86 @@ 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, - pubkey::Pubkey, - 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_bpf_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(), + borrowed_value_upper_bound: 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_bpf_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 66a8c75c32e..d3bd170f97c 100644 --- a/token-lending/program/tests/init_reserve.rs +++ b/token-lending/program/tests/init_reserve.rs @@ -1,278 +1,253 @@ #![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, - pubkey::Pubkey, signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, + transaction::TransactionError, }; +use solend_program::state::LastUpdate; +use solend_program::state::RateLimiter; +use solend_program::state::RateLimiterConfig; +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_bpf_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); +async fn setup() -> (SolendProgramTest, Info, User) { + let (test, lending_market, _, _, lending_market_owner, _) = + setup_world(&test_reserve_config(), &test_reserve_config()).await; - let (mut banks_client, payer, _recent_blockhash) = test.start().await; + (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), + smoothed_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, + rate_limiter: RateLimiter::new(RateLimiter::default().config, 1001) + } ); } #[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_bpf_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_bpf_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 banks_client, payer, _recent_blockhash) = test.start().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; - - 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) ) ); @@ -280,94 +255,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; - - 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; - - // 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) ) ); @@ -376,179 +302,94 @@ 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 user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); + let (mut test, lending_market, lending_market_owner) = setup().await; - 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() - }, - ); + let wsol_reserve = test + .init_reserve( + &lending_market, + &lending_market_owner, + &wsol_mint::id(), + &test_reserve_config(), + &Keypair::new(), + 1000, + None, + ) + .await + .unwrap(); - // 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, + let new_reserve_config = test_reserve_config(); + let new_rate_limiter_config = RateLimiterConfig { + window_duration: 50, + max_outflow: 100, }; - 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()), - ); - transaction.sign(&[&payer, &lending_market.owner], recent_blockhash); - assert!(banks_client.process_transaction(transaction).await.is_ok()); + lending_market + .update_reserve_config( + &mut test, + &lending_market_owner, + &wsol_reserve, + new_reserve_config, + new_rate_limiter_config, + None, + ) + .await + .unwrap(); - let updated_reserve = test_reserve.get_state(&mut banks_client).await; - assert_eq!(updated_reserve.config, new_config); + let wsol_reserve_post = test.load_account::(wsol_reserve.pubkey).await; + assert_eq!( + wsol_reserve_post.account, + Reserve { + config: new_reserve_config, + rate_limiter: RateLimiter::new(new_rate_limiter_config, 1000), + ..wsol_reserve.account + } + ); } #[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: 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(); + let new_rate_limiter_config = RateLimiterConfig { + window_duration: 50, + max_outflow: 100, + }; // 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, + new_rate_limiter_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 21f1aa5883a..4261628cfc3 100644 --- a/token-lending/program/tests/liquidate_obligation.rs +++ b/token-lending/program/tests/liquidate_obligation.rs @@ -2,184 +2,39 @@ mod helpers; -use helpers::*; -use solana_program_test::*; -use solana_sdk::{ - pubkey::Pubkey, - signature::{Keypair, Signer}, - transaction::Transaction, -}; -use solend_program::{ - instruction::{liquidate_obligation, refresh_obligation}, - processor::process_instruction, - state::INITIAL_COLLATERAL_RATIO, -}; -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_bpf_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 / 5; - // 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 / 5; - - 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; +use crate::solend_program_test::scenario_1; - 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 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, - ); - 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 - ); +use helpers::*; +use solana_program::instruction::InstructionError; - 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 - ); +use solana_program_test::*; - 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 - ); +use solana_sdk::transaction::TransactionError; +use solend_program::error::LendingError; - let collateral_supply_balance = - get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; - assert_eq!( - collateral_supply_balance, - initial_collateral_supply_balance - SOL_LIQUIDATION_AMOUNT_LAMPORTS - ); +#[tokio::test] +async fn test_fail_deprecated() { + 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(); - 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 + res, + TransactionError::InstructionError( + 3, + InstructionError::Custom(LendingError::DeprecatedInstruction as u32) + ) ); - assert_eq!( - obligation.borrows[0].borrowed_amount_wads, - (USDC_BORROW_AMOUNT_FRACTIONAL - USDC_LIQUIDATION_AMOUNT_FRACTIONAL).into() - ) } 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 c97545979d4..337bae32c3d 100644 --- a/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs +++ b/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs @@ -1,201 +1,391 @@ #![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::{ - pubkey::Pubkey, - signature::{Keypair, Signer}, - transaction::Transaction, -}; -use solend_program::{ - instruction::{liquidate_obligation_and_redeem_reserve_collateral, refresh_obligation}, - processor::process_instruction, - state::INITIAL_COLLATERAL_RATIO, -}; -use std::cmp::max; +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; + +use std::collections::HashSet; #[tokio::test] -async fn test_success() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); +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; - // limit to track compute unit increase - test.set_bpf_compute_max_units(95_000); + let liquidator = User::new_with_balances( + &mut test, + &[ + (&wsol_mint::id(), 100 * LAMPORTS_TO_SOL), + (&usdc_reserve.account.collateral.mint_pubkey, 0), + (&usdc_mint::id(), 0), + ], + ) + .await; - // 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 / 5; - // 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 / 5; + let balance_checker = BalanceChecker::start( + &mut test, + &[ + &usdc_reserve, + &user, + &wsol_reserve, + &usdc_reserve, + &liquidator, + ], + ) + .await; - const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS; - const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 2 * USDC_BORROW_AMOUNT_FRACTIONAL; + // 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, + ema_price: 5500, + ema_conf: 0, + }, + ) + .await; - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); + lending_market + .liquidate_obligation_and_redeem_reserve_collateral( + &mut test, + &wsol_reserve, + &usdc_reserve, + &obligation, + &liquidator, + u64::MAX, + ) + .await + .unwrap(); - 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 (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_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() - }, - ); + 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 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( - &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 expected_borrow_repaid = 10 * (LIQUIDATION_CLOSE_FACTOR as u64) / 100; + let expected_usdc_withdrawn = expected_borrow_repaid * 5500 * (100 + bonus) / 100; - 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 expected_total_bonus = expected_usdc_withdrawn - expected_borrow_repaid * 5500; + let expected_protocol_liquidation_fee = + expected_total_bonus * protocol_liquidation_fee_pct / 100; - 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()), + 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!( + mint_supply_changes, + HashSet::from([MintSupplyChange { + mint: usdc_reserve.account.collateral.mint_pubkey, + diff: -((expected_usdc_withdrawn * FRACTIONAL_TO_USDC) as i128) + }]) ); - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); - assert!(banks_client.process_transaction(transaction).await.is_ok()); + // 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_liquidity_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; + let usdc_reserve_post = test.load_account::(usdc_reserve.pubkey).await; assert_eq!( - user_liquidity_balance, - initial_user_liquidity_balance - USDC_LIQUIDATION_AMOUNT_FRACTIONAL + 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 liquidity_supply_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; + let wsol_reserve_post = test.load_account::(wsol_reserve.pubkey).await; assert_eq!( - liquidity_supply_balance, - initial_liquidity_supply_balance + USDC_LIQUIDATION_AMOUNT_FRACTIONAL + 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), + smoothed_market_price: Decimal::from(5500u64), + ..wsol_reserve.account.liquidity + }, + ..wsol_reserve.account + } ); - 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); - - 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; + let obligation_post = test.load_account::(obligation.pubkey).await; assert_eq!( - user_withdraw_liquidity_balance + fee_reciever_withdraw_liquidity_balance, - initial_user_withdraw_liquidity_balance - + initial_fee_reciever_withdraw_liquidity_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), + borrowed_value_upper_bound: Decimal::from(55_000u64), + allowed_borrow_value: Decimal::from(50_000u64), + unhealthy_borrow_value: Decimal::from(55_000u64), + ..obligation.account + } ); +} - assert_eq!( - // 30% of the bonus - // SOL_LIQUIDATION_AMOUNT_LAMPORTS * 3 / 10 / 11, - // 0 % min 1 for now - max(SOL_LIQUIDATION_AMOUNT_LAMPORTS * 0 / 10 / 11, 1), - (fee_reciever_withdraw_liquidity_balance - initial_fee_reciever_withdraw_liquidity_balance) - ); +#[tokio::test] +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(); - let collateral_supply_balance = - get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; + 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, + &[ + (&wsol_mint::id(), 100 * LAMPORTS_TO_SOL), + (&usdc_reserve.account.collateral.mint_pubkey, 0), + (&usdc_mint::id(), 0), + ], + ) + .await; + + let balance_checker = BalanceChecker::start( + &mut test, + &[&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, + ema_price: 5500, + ema_conf: 0, + }, + ) + .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, + }, + 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 634bf94e56f..00700cc4057 100644 --- a/token-lending/program/tests/obligation_end_to_end.rs +++ b/token-lending/program/tests/obligation_end_to_end.rs @@ -1,542 +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, - pubkey::Pubkey, - 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_bpf_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 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 - ); + ) + .await; - 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_bpf_compute_max_units(148_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/outflow_rate_limits.rs b/token-lending/program/tests/outflow_rate_limits.rs new file mode 100644 index 00000000000..6a685004764 --- /dev/null +++ b/token-lending/program/tests/outflow_rate_limits.rs @@ -0,0 +1,213 @@ +#![cfg(feature = "test-bpf")] + +use solana_program::instruction::InstructionError; +use solana_sdk::native_token::LAMPORTS_PER_SOL; +use solana_sdk::signature::Signer; +use solana_sdk::signer::keypair::Keypair; +use solana_sdk::transaction::TransactionError; + +mod helpers; + +use helpers::solend_program_test::{setup_world, Info, SolendProgramTest, User}; +use solend_sdk::error::LendingError; + +use solend_sdk::state::{LendingMarket, RateLimiterConfig, Reserve, ReserveConfig}; + +use helpers::*; + +use solana_program_test::*; + +use solend_sdk::state::Obligation; + +async fn setup( + wsol_reserve_config: &ReserveConfig, +) -> ( + SolendProgramTest, + Info, + Info, + Info, + User, + Info, + User, + User, + User, +) { + let (mut test, lending_market, usdc_reserve, wsol_reserve, lending_market_owner, 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, + &[ + (&wsol_mint::id(), 5 * LAMPORTS_PER_SOL), + (&wsol_reserve.account.collateral.mint_pubkey, 0), + ], + ) + .await; + + lending_market + .deposit( + &mut test, + &wsol_reserve, + &wsol_depositor, + 5 * LAMPORTS_PER_SOL, + ) + .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; + + 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, + lending_market_owner, + wsol_depositor, + ) +} + +#[tokio::test] +async fn test_outflow_reserve() { + let ( + mut test, + lending_market, + usdc_reserve, + wsol_reserve, + user, + obligation, + host_fee_receiver, + lending_market_owner, + wsol_depositor, + ) = setup(&ReserveConfig { + ..test_reserve_config() + }) + .await; + + // ie, within 10 slots, the maximum outflow is $10 + lending_market + .set_lending_market_owner_and_config( + &mut test, + &lending_market_owner, + &lending_market_owner.keypair.pubkey(), + RateLimiterConfig { + window_duration: 10, + max_outflow: 10, + }, + ) + .await + .unwrap(); + + // borrow max amount + lending_market + .borrow_obligation_liquidity( + &mut test, + &wsol_reserve, + &obligation, + &user, + &host_fee_receiver.get_account(&wsol_mint::id()).unwrap(), + LAMPORTS_PER_SOL, + ) + .await + .unwrap(); + + // for the next 10 slots, we shouldn't be able to withdraw, borrow, or redeem anything. + let cur_slot = test.get_clock().await.slot; + for _ in cur_slot..(cur_slot + 10) { + let res = lending_market + .borrow_obligation_liquidity( + &mut test, + &wsol_reserve, + &obligation, + &user, + &host_fee_receiver.get_account(&wsol_mint::id()).unwrap(), + 1, + ) + .await + .err() + .unwrap() + .unwrap(); + + assert_eq!( + res, + TransactionError::InstructionError( + 3, + InstructionError::Custom(LendingError::OutflowRateLimitExceeded as u32) + ) + ); + + let res = lending_market + .withdraw_obligation_collateral_and_redeem_reserve_collateral( + &mut test, + &usdc_reserve, + &obligation, + &user, + 1, + ) + .await + .err() + .unwrap() + .unwrap(); + + assert_eq!( + res, + TransactionError::InstructionError( + 3, + InstructionError::Custom(LendingError::OutflowRateLimitExceeded as u32) + ) + ); + + let res = lending_market + .redeem(&mut test, &wsol_reserve, &wsol_depositor, 1) + .await + .err() + .unwrap() + .unwrap(); + + assert_eq!( + res, + TransactionError::InstructionError( + 1, + InstructionError::Custom(LendingError::OutflowRateLimitExceeded as u32) + ) + ); + + test.advance_clock_by_slots(1).await; + } +} diff --git a/token-lending/program/tests/redeem_fees.rs b/token-lending/program/tests/redeem_fees.rs new file mode 100644 index 00000000000..ff3b441b5ec --- /dev/null +++ b/token-lending/program/tests/redeem_fees.rs @@ -0,0 +1,110 @@ +#![cfg(feature = "test-bpf")] + +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 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 solend_program::{ + math::{Decimal, TrySub}, + state::SLOTS_PER_YEAR, +}; + +#[tokio::test] +async fn test_success() { + let (mut test, lending_market, _, wsol_reserve, user, _) = scenario_1( + &test_reserve_config(), + &ReserveConfig { + protocol_take_rate: 10, + ..test_reserve_config() + }, + ) + .await; + + test.advance_clock_by_slots(SLOTS_PER_YEAR).await; + + test.set_price( + &wsol_mint::id(), + &PriceArgs { + price: 10, + expo: 0, + conf: 0, + ema_price: 10, + ema_conf: 0, + }, + ) + .await; + + lending_market + .refresh_reserve(&mut test, &wsol_reserve) + .await + .unwrap(); + + // 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 wsol_reserve = test.load_account::(wsol_reserve.pubkey).await; + + // redeem fees + let balance_checker = BalanceChecker::start(&mut test, &[&wsol_reserve]).await; + + lending_market + .redeem_fees(&mut test, &wsol_reserve) + .await + .unwrap(); + + let expected_fees = wsol_reserve.account.calculate_redeem_fees().unwrap(); + + // 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 5cbfdab2485..ac5605e3721 100644 --- a/token-lending/program/tests/redeem_reserve_collateral.rs +++ b/token-lending/program/tests/redeem_reserve_collateral.rs @@ -2,104 +2,149 @@ mod helpers; +use crate::solend_program_test::MintSupplyChange; +use solend_sdk::math::Decimal; +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::{ - pubkey::Pubkey, - 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; + + 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"); - // limit to track compute unit increase - test.set_bpf_compute_max_units(47_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() - }, + // 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 ); - 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 program state changes + let lending_market_post = test + .load_account::(lending_market.pubkey) + .await; + assert_eq!( + lending_market_post.account, + LendingMarket { + rate_limiter: { + let mut rate_limiter = lending_market.account.rate_limiter; + rate_limiter.update(1000, Decimal::from(1u64)).unwrap(); + rate_limiter + }, + ..lending_market.account + } ); - transaction.sign( - &[&payer, &user_accounts_owner, &user_transfer_authority], - recent_blockhash, + 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 + }, + rate_limiter: { + let mut rate_limiter = usdc_reserve.account.rate_limiter; + rate_limiter + .update(1000, Decimal::from(1_000_000u64)) + .unwrap(); + + rate_limiter + }, + ..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_eq!(usdc_reserve.last_update.stale, true); + 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(1, InstructionError::Custom(1)) => (), + // LendingError::TokenBurnFailed + TransactionError::InstructionError(1, 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 48f976b840d..3eda662d984 100644 --- a/token-lending/program/tests/refresh_obligation.rs +++ b/token-lending/program/tests/refresh_obligation.rs @@ -2,167 +2,279 @@ mod helpers; +use crate::solend_program_test::PriceArgs; +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::{ - pubkey::Pubkey, - signature::{Keypair, Signer}, - transaction::Transaction, -}; -use solend_program::math::{Rate, TryAdd, TryMul}; +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; - // limit to track compute unit increase - test.set_bpf_compute_max_units(43_000); + // init obligation + let obligation = lending_market + .init_obligation(&mut test, Keypair::new(), &user) + .await + .expect("This should succeed"); - 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; + // deposit 100k USDC + lending_market + .deposit(&mut test, &usdc_reserve, &user, 100_000_000_000) + .await + .expect("This should succeed"); - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); + let usdc_reserve = test.load_account(usdc_reserve.pubkey).await; - let mut reserve_config = test_reserve_config(); - reserve_config.loan_to_value_ratio = 50; + // deposit 100k cUSDC + lending_market + .deposit_obligation_collateral( + &mut test, + &usdc_reserve, + &obligation, + &user, + 100_000_000_000, + ) + .await + .expect("This should succeed"); - // 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 sol_oracle = add_sol_oracle(&mut test); - let sol_test_reserve = add_reserve( + let wsol_depositor = User::new_with_balances( &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: 1, // elapsed from 1; clock.slot = 2 - ..AddReserveArgs::default() - }, - ); + &[ + (&wsol_mint::id(), 5 * LAMPORTS_PER_SOL), + (&wsol_reserve.account.collateral.mint_pubkey, 0), + ], + ) + .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: 1, // elapsed from 1; clock.slot = 2 - ..AddReserveArgs::default() + // deposit 5SOL. wSOL reserve now has 6 SOL. + lending_market + .deposit( + &mut test, + &wsol_reserve, + &wsol_depositor, + 5 * LAMPORTS_PER_SOL, + ) + .await + .unwrap(); + + // 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(); + + // 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, + ) +} + +#[tokio::test] +async fn test_success() { + let (mut test, lending_market, usdc_reserve, wsol_reserve, user, obligation) = setup().await; + + test.set_price( + &usdc_mint::id(), + &PriceArgs { + price: 10, + conf: 1, + expo: -1, + ema_price: 9, + ema_conf: 1, }, - ); + ) + .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: 1, // elapsed from 1; clock.slot = 2 - ..AddObligationArgs::default() + test.set_price( + &wsol_mint::id(), + &PriceArgs { + price: 10, + conf: 1, + expo: 0, + ema_price: 11, + ema_conf: 1, }, - ); + ) + .await; - let mut test_context = test.start_with_context().await; - test_context.warp_to_slot(3).unwrap(); // clock.slot = 3 + test.advance_clock_by_slots(1).await; - let ProgramTestContext { - mut banks_client, - payer, - last_blockhash: recent_blockhash, - .. - } = test_context; + let balance_checker = + BalanceChecker::start(&mut test, &[&usdc_reserve, &user, &wsol_reserve]).await; - 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()), - ); + lending_market + .refresh_obligation(&mut test, &obligation) + .await + .unwrap(); - transaction.sign(&[&payer], recent_blockhash); - assert!(banks_client.process_transaction(transaction).await.is_ok()); + // 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 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; + // check program state + let lending_market_post = test + .load_account::(lending_market.pubkey) + .await; + assert_eq!(lending_market_post, lending_market); - let collateral = &obligation.deposits[0]; - let liquidity = &obligation.borrows[0]; + 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 { + smoothed_market_price: Decimal::from_percent(90), + ..usdc_reserve.account.liquidity + }, + ..usdc_reserve.account + } + ); - let collateral_price = collateral.market_value.try_div(SOL_DEPOSIT_AMOUNT).unwrap(); + 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 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 liquidity_price = liquidity.market_value.try_div(compound_borrow).unwrap(); - assert_eq!( - usdc_reserve.liquidity.cumulative_borrow_rate_wads, - liquidity.cumulative_borrow_rate_wads + 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, + smoothed_market_price: Decimal::from(11u64), + ..wsol_reserve.account.liquidity + }, + ..wsol_reserve.account + } ); - assert_eq!(liquidity.cumulative_borrow_rate_wads, compound_rate.into()); + + 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!( - usdc_reserve.liquidity.borrowed_amount_wads, - liquidity.borrowed_amount_wads + 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_borrowed_amount_wads + .try_mul(Decimal::from(10u64)) + .unwrap() + .try_div(Decimal::from(LAMPORTS_PER_SOL)) + .unwrap(), + + // uses max(10, 11) = 11 for sol price + borrowed_value_upper_bound: new_borrowed_amount_wads + .try_mul(Decimal::from(11u64)) + .unwrap() + .try_div(Decimal::from(LAMPORTS_PER_SOL)) + .unwrap(), + + // uses min(1, 0.9) for usdc price + allowed_borrow_value: Decimal::from(100_000u64) + .try_mul(Decimal::from_percent( + usdc_reserve.account.config.loan_to_value_ratio + )) + .unwrap() + .try_mul(Decimal::from_percent(90)) + .unwrap(), + + ..obligation.account + } ); - assert_eq!(liquidity.borrowed_amount_wads, compound_borrow_wads); - 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 349a46947e1..0824d1ce48b 100644 --- a/token-lending/program/tests/refresh_reserve.rs +++ b/token-lending/program/tests/refresh_reserve.rs @@ -2,141 +2,354 @@ 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::Oracle; +use crate::solend_program_test::PriceArgs; +use crate::solend_program_test::SolendProgramTest; +use crate::solend_program_test::SwitchboardPriceArgs; +use crate::solend_program_test::User; use helpers::*; +use solana_program::instruction::InstructionError; +use solana_program::native_token::LAMPORTS_PER_SOL; use solana_program_test::*; -use solana_sdk::{ - pubkey::Pubkey, - signature::{Keypair, Signer}, - transaction::Transaction, -}; +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::NULL_PUBKEY; use solend_program::{ - instruction::refresh_reserve, - math::{Decimal, Rate, TryAdd, TryDiv, TryMul}, - processor::process_instruction, + error::LendingError, + math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub}, state::SLOTS_PER_YEAR, }; +use std::collections::HashSet; -#[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_bpf_compute_max_units(28_000); +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; - 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; + // init obligation + let obligation = lending_market + .init_obligation(&mut test, Keypair::new(), &user) + .await + .expect("This should succeed"); - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); + // deposit 100k USDC + lending_market + .deposit(&mut test, &usdc_reserve, &user, 100_000_000_000) + .await + .expect("This should succeed"); - let mut reserve_config = test_reserve_config(); - reserve_config.loan_to_value_ratio = 80; + let usdc_reserve = test.load_account(usdc_reserve.pubkey).await; - // 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; + // deposit 100k cUSDC + lending_market + .deposit_obligation_collateral( + &mut test, + &usdc_reserve, + &obligation, + &user, + 100_000_000_000, + ) + .await + .expect("This should succeed"); - let usdc_mint = add_usdc_mint(&mut test); - let usdc_oracle = add_usdc_oracle(&mut test); - let usdc_test_reserve = add_reserve( + 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: 1, // elapsed from 1; clock.slot = 2 - ..AddReserveArgs::default() - }, - ); + &[ + (&wsol_mint::id(), 5 * LAMPORTS_PER_SOL), + (&wsol_reserve.account.collateral.mint_pubkey, 0), + ], + ) + .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: 1, // elapsed from 1; clock.slot = 2 - ..AddReserveArgs::default() + // deposit 5SOL. wSOL reserve now has 6 SOL. + lending_market + .deposit( + &mut test, + &wsol_reserve, + &wsol_depositor, + 5 * LAMPORTS_PER_SOL, + ) + .await + .unwrap(); + + // 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(); + + // 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, + lending_market_owner, + obligation, + ) +} + +#[tokio::test] +async fn test_success() { + let (mut test, lending_market, _, wsol_reserve, _, _) = setup().await; + + // should be maxed out at 30% + let borrow_rate = wsol_reserve.account.current_borrow_rate().unwrap(); + + test.set_price( + &wsol_mint::id(), + &PriceArgs { + price: 20, + conf: 1, + expo: 1, + ema_price: 15, + ema_conf: 1, }, + ) + .await; + + test.advance_clock_by_slots(1).await; + let balance_checker = BalanceChecker::start(&mut test, &[&wsol_reserve]).await; + + lending_market + .refresh_reserve(&mut test, &wsol_reserve) + .await + .unwrap(); + + // check balances + assert_eq!( + balance_checker.find_balance_changes(&mut test).await, + (HashSet::new(), HashSet::new()) ); - let mut test_context = test.start_with_context().await; - test_context.warp_to_slot(3).unwrap(); // clock.slot = 3 + // check program state + let wsol_reserve_post = test.load_account::(wsol_reserve.pubkey).await; - let ProgramTestContext { - mut banks_client, - payer, - last_blockhash: recent_blockhash, - .. - } = test_context; + 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(6 * LAMPORTS_PER_SOL) + .try_mul(compound_rate) + .unwrap(); + let net_new_debt = compound_borrow + .try_sub(Decimal::from(6 * LAMPORTS_PER_SOL)) + .unwrap(); + 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(); - 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()), + assert_eq!( + 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, + market_price: Decimal::from(200u64), + smoothed_market_price: Decimal::from(150u64), + ..wsol_reserve.account.liquidity + }, + ..wsol_reserve.account + } ); +} - transaction.sign(&[&payer], recent_blockhash); - assert!(banks_client.process_transaction(transaction).await.is_ok()); +#[tokio::test] +async fn test_fail_pyth_price_stale() { + let (mut test, lending_market, _usdc_reserve, wsol_reserve, _user, _obligation) = setup().await; - let sol_reserve = sol_test_reserve.get_state(&mut banks_client).await; - let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; + test.advance_clock_by_slots(241).await; - let slot_rate = Rate::from_percent(BORROW_RATE) - .try_div(SLOTS_PER_YEAR) + let res = lending_market + .refresh_reserve(&mut test, &wsol_reserve) + .await + .unwrap_err() .unwrap(); - let compound_rate = Rate::one().try_add(slot_rate).unwrap(); - let compound_borrow = Decimal::from(BORROW_AMOUNT).try_mul(compound_rate).unwrap(); + println!("{:?}", res); assert_eq!( - sol_reserve.liquidity.cumulative_borrow_rate_wads, - compound_rate.into() + res, + TransactionError::InstructionError( + 0, + 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.set_price( + &wsol_mint::id(), + &PriceArgs { + price: 9, + conf: 0, + expo: 0, + ema_price: 11, + ema_conf: 0, + }, + ) + .await; + test.advance_clock_by_slots(1).await; + + lending_market + .refresh_reserve(&mut test, &wsol_reserve) + .await + .unwrap(); + + test.advance_clock_by_slots(241).await; + + test.init_switchboard_feed(&wsol_mint::id()).await; + test.set_switchboard_price(&wsol_mint::id(), SwitchboardPriceArgs { price: 8, expo: 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, + wsol_reserve.account.rate_limiter.config, + None, + ) + .await + .unwrap(); + + let wsol_reserve = test.load_account::(wsol_reserve.pubkey).await; + lending_market + .refresh_reserve(&mut test, &wsol_reserve) + .await + .unwrap(); + + let wsol_reserve_post = test.load_account::(wsol_reserve.pubkey).await; + + // overwrite liquidity market price with the switchboard price but keep the pyth ema price assert_eq!( - sol_reserve.liquidity.cumulative_borrow_rate_wads, - usdc_reserve.liquidity.cumulative_borrow_rate_wads + wsol_reserve_post.account.liquidity.market_price, + Decimal::from(8u64) ); - assert_eq!(sol_reserve.liquidity.borrowed_amount_wads, compound_borrow); assert_eq!( - sol_reserve.liquidity.borrowed_amount_wads, - usdc_reserve.liquidity.borrowed_amount_wads + wsol_reserve_post.account.liquidity.smoothed_market_price, + Decimal::from(11u64) ); +} + +#[tokio::test] +async fn test_success_only_switchboard_reserve() { + let (mut test, lending_market, _, wsol_reserve, lending_market_owner, _) = setup().await; + + test.set_price( + &wsol_mint::id(), + &PriceArgs { + price: 10, + conf: 0, + expo: 0, + ema_price: 11, + ema_conf: 0, + }, + ) + .await; + + test.advance_clock_by_slots(1).await; + + let feed = test.init_switchboard_feed(&wsol_mint::id()).await; + test.set_switchboard_price(&wsol_mint::id(), SwitchboardPriceArgs { price: 8, expo: 0 }) + .await; + + test.advance_clock_by_slots(1).await; + + lending_market + .update_reserve_config( + &mut test, + &lending_market_owner, + &wsol_reserve, + wsol_reserve.account.config, + wsol_reserve.account.rate_limiter.config, + Some(&Oracle { + pyth_price_pubkey: NULL_PUBKEY, + pyth_product_pubkey: NULL_PUBKEY, + switchboard_feed_pubkey: Some(feed), + }), + ) + .await + .unwrap(); + + test.advance_clock_by_slots(1).await; + + let wsol_reserve = test.load_account::(wsol_reserve.pubkey).await; + lending_market + .refresh_reserve(&mut test, &wsol_reserve) + .await + .unwrap(); + + let wsol_reserve_post = test.load_account::(wsol_reserve.pubkey).await; + + // when pyth is null and only switchboard exists, both price fields get overwritten assert_eq!( - sol_reserve.liquidity.market_price, - sol_test_reserve.market_price + wsol_reserve_post.account.liquidity.market_price, + Decimal::from(8u64) ); assert_eq!( - usdc_reserve.liquidity.market_price, - usdc_test_reserve.market_price + wsol_reserve_post.account.liquidity.smoothed_market_price, + Decimal::from(8u64) ); } diff --git a/token-lending/program/tests/repay_obligation_liquidity.rs b/token-lending/program/tests/repay_obligation_liquidity.rs index 0cfa3e9e6cd..a94f24cd21f 100644 --- a/token-lending/program/tests/repay_obligation_liquidity.rs +++ b/token-lending/program/tests/repay_obligation_liquidity.rs @@ -2,158 +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::{ - pubkey::Pubkey, - 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_bpf_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 7d06114df1c..f848a73bdd0 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,73 @@ use solana_sdk::{ instruction::InstructionError, pubkey::Pubkey, signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, + transaction::TransactionError, }; -use solend_program::{ - error::LendingError, - instruction::{set_lending_market_owner, LendingInstruction}, - processor::process_instruction, -}; - -#[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_bpf_compute_max_units(4_000); +use solend_program::state::LendingMarket; +use solend_program::state::RateLimiterConfig; +use solend_sdk::state::RateLimiter; - let lending_market = add_lending_market(&mut test); - let (mut banks_client, payer, recent_blockhash) = test.start().await; +use solend_program::{error::LendingError, instruction::LendingInstruction}; - 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()), - ); +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; - 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(); + let new_config = RateLimiterConfig { + max_outflow: 100, + window_duration: 5, + }; + + lending_market + .set_lending_market_owner_and_config( + &mut test, + &lending_market_owner, + &new_owner.pubkey(), + new_config, + ) .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_post = test + .load_account::(lending_market.pubkey) + .await; + + assert_eq!( + lending_market_post.account, + LendingMarket { + owner: new_owner.pubkey(), + rate_limiter: RateLimiter::new(new_config, 1000), + ..lending_market_post.account + } + ); } #[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 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 (mut test, lending_market, _lending_market_owner) = setup().await; + let invalid_owner = User::new_with_keypair(Keypair::new()); + let new_owner = Keypair::new(); + + let res = lending_market + .set_lending_market_owner_and_config( + &mut test, + &invalid_owner, + &new_owner.pubkey(), + RateLimiterConfig::default(), + ) + .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 +89,30 @@ 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::SetLendingMarketOwnerAndConfig { + new_owner, + rate_limiter_config: RateLimiterConfig::default(), + } + .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/two_prices.rs b/token-lending/program/tests/two_prices.rs new file mode 100644 index 00000000000..ca48a15a436 --- /dev/null +++ b/token-lending/program/tests/two_prices.rs @@ -0,0 +1,487 @@ +#![cfg(feature = "test-bpf")] + +use crate::solend_program_test::custom_scenario; +use crate::solend_program_test::find_reserve; +use crate::solend_program_test::User; + +use crate::solend_program_test::BalanceChecker; +use crate::solend_program_test::ObligationArgs; +use crate::solend_program_test::PriceArgs; +use crate::solend_program_test::ReserveArgs; +use crate::solend_program_test::TokenBalanceChange; +use solana_program::native_token::LAMPORTS_PER_SOL; +use solana_sdk::instruction::InstructionError; +use solana_sdk::transaction::TransactionError; +use solend_program::error::LendingError; + +use solend_program::state::ReserveConfig; +use solend_program::NULL_PUBKEY; +use solend_sdk::state::ReserveFees; +mod helpers; + +use helpers::*; +use solana_program_test::*; + +use std::collections::HashSet; + +/// the two prices feature affects a bunch of instructions. All of those instructions are tested +/// here for correctness. + +#[tokio::test] +async fn test_borrow() { + let (mut test, lending_market, reserves, obligation, user) = custom_scenario( + &[ + ReserveArgs { + mint: usdc_mint::id(), + config: test_reserve_config(), + liquidity_amount: 100_000 * FRACTIONAL_TO_USDC, + price: PriceArgs { + price: 10, + conf: 0, + expo: -1, + ema_price: 10, + ema_conf: 1, + }, + }, + ReserveArgs { + mint: wsol_mint::id(), + config: ReserveConfig { + loan_to_value_ratio: 50, + liquidation_threshold: 55, + fees: ReserveFees::default(), + optimal_borrow_rate: 0, + max_borrow_rate: 0, + ..test_reserve_config() + }, + liquidity_amount: 100 * LAMPORTS_PER_SOL, + price: PriceArgs { + price: 10, + conf: 0, + expo: 0, + ema_price: 10, + ema_conf: 0, + }, + }, + ], + &ObligationArgs { + deposits: vec![(usdc_mint::id(), 100 * FRACTIONAL_TO_USDC)], + borrows: vec![(wsol_mint::id(), LAMPORTS_PER_SOL)], + }, + ) + .await; + + // update prices + test.set_price( + &usdc_mint::id(), + &PriceArgs { + price: 9, + conf: 0, + expo: -1, + ema_price: 10, + ema_conf: 0, + }, + ) + .await; + + test.set_price( + &wsol_mint::id(), + &PriceArgs { + price: 10, + conf: 0, + expo: 0, + ema_price: 20, + ema_conf: 0, + }, + ) + .await; + + test.advance_clock_by_slots(1).await; + + let balance_checker = BalanceChecker::start(&mut test, &[&user]).await; + + // obligation currently has 100 USDC deposited and 1 sol borrowed + // if we try to borrow the max amount, how much SOL should we receive? + // allowed borrow value = 100 * min(1, 0.9) * 0.5 = $45 + // borrow value upper bound: 1 * max(10, 20) = $20 + // max SOL that can be borrowed is: ($45 - $20) / $20 = 1.25 SOL + lending_market + .borrow_obligation_liquidity( + &mut test, + &find_reserve(&reserves, &wsol_mint::id()).unwrap(), + &obligation, + &user, + &NULL_PUBKEY, + u64::MAX, + ) + .await + .unwrap(); + + let (balance_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: (LAMPORTS_PER_SOL * 125 / 100) as i128, + }]); + + assert_eq!(balance_changes, expected_balance_changes); + + test.advance_clock_by_slots(1).await; + + // shouldn't be able to borrow any more + let err = lending_market + .borrow_obligation_liquidity( + &mut test, + &find_reserve(&reserves, &wsol_mint::id()).unwrap(), + &obligation, + &user, + &NULL_PUBKEY, + u64::MAX, + ) + .await + .unwrap_err() + .unwrap(); + + assert_eq!( + err, + TransactionError::InstructionError( + 3, + InstructionError::Custom(LendingError::BorrowTooLarge as u32) + ) + ); +} + +#[tokio::test] +async fn test_withdraw() { + let (mut test, lending_market, reserves, obligation, user) = custom_scenario( + &[ + ReserveArgs { + mint: usdc_mint::id(), + config: test_reserve_config(), + liquidity_amount: 100_000 * FRACTIONAL_TO_USDC, + price: PriceArgs { + price: 10, + conf: 0, + expo: -1, + ema_price: 10, + ema_conf: 1, + }, + }, + ReserveArgs { + mint: usdt_mint::id(), + config: test_reserve_config(), + liquidity_amount: 100_000 * FRACTIONAL_TO_USDC, + price: PriceArgs { + price: 10, + conf: 0, + expo: -1, + ema_price: 10, + ema_conf: 1, + }, + }, + ReserveArgs { + mint: wsol_mint::id(), + config: ReserveConfig { + loan_to_value_ratio: 50, + liquidation_threshold: 55, + optimal_borrow_rate: 0, + max_borrow_rate: 0, + fees: ReserveFees::default(), + ..test_reserve_config() + }, + liquidity_amount: 100 * LAMPORTS_PER_SOL, + price: PriceArgs { + price: 10, + conf: 0, + expo: 0, + ema_price: 10, + ema_conf: 0, + }, + }, + ], + &ObligationArgs { + deposits: vec![ + (usdc_mint::id(), 100 * FRACTIONAL_TO_USDC), + (usdt_mint::id(), 20 * FRACTIONAL_TO_USDC), + ], + borrows: vec![(wsol_mint::id(), LAMPORTS_PER_SOL)], + }, + ) + .await; + + // update prices + test.set_price( + &usdc_mint::id(), + &PriceArgs { + price: 100, // massive price increase + conf: 0, + expo: 0, + ema_price: 1, + ema_conf: 0, + }, + ) + .await; + + test.set_price( + &wsol_mint::id(), + &PriceArgs { + price: 10, // big price decrease + conf: 0, + expo: 0, + ema_price: 20, + ema_conf: 0, + }, + ) + .await; + + test.advance_clock_by_slots(1).await; + + let balance_checker = BalanceChecker::start(&mut test, &[&user]).await; + + lending_market + .withdraw_obligation_collateral_and_redeem_reserve_collateral( + &mut test, + &find_reserve(&reserves, &usdc_mint::id()).unwrap(), + &obligation, + &user, + u64::MAX, + ) + .await + .unwrap(); + + // how much usdc should we able to withdraw? + // current allowed borrow value: 100 * min(100, 1) * 0.5 + 20 * min(1, 1) * 0.5 = $60 + // borrow value upper bound = 1 SOL * max($20, $10) = $20 + // max withdraw value = ($60 - $20) / 0.5 = $80 + // max withdraw liquidity amount = $80 / min(100, 1) = *80 USDC* + // note that if we didn't have this two prices feature, you could withdraw all of the USDC + // cUSDC/USDC exchange rate = 1 => max withdraw is 80 cUSDC + // + // reconciliation: + // after withdraw, we are left with 20 USDC, 20 USDT + // allowed borrow value is now 20 * min(100, 1) * 0.5 + 20 * min(1, 1) * 0.5 = $20 + // borrow value upper bound = $20 + // we have successfully borrowed the max amount + let (balance_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: (80 * FRACTIONAL_TO_USDC) as i128, + }]); + + assert_eq!(balance_changes, expected_balance_changes); + + test.advance_clock_by_slots(1).await; + + // we shouldn't be able to withdraw anything else + for mint in [usdc_mint::id(), usdt_mint::id()] { + let err = lending_market + .withdraw_obligation_collateral_and_redeem_reserve_collateral( + &mut test, + &find_reserve(&reserves, &mint).unwrap(), + &obligation, + &user, + u64::MAX, + ) + .await + .unwrap_err() + .unwrap(); + + assert_eq!( + err, + TransactionError::InstructionError( + 4, + InstructionError::Custom(LendingError::WithdrawTooLarge as u32) + ) + ); + } +} + +#[tokio::test] +async fn test_liquidation_doesnt_use_smoothed_price() { + let (mut test, lending_market, reserves, obligation, user) = custom_scenario( + &[ + ReserveArgs { + mint: usdc_mint::id(), + config: test_reserve_config(), + liquidity_amount: 100_000 * FRACTIONAL_TO_USDC, + price: PriceArgs { + price: 1, + conf: 0, + expo: 0, + ema_price: 1, + ema_conf: 0, + }, + }, + ReserveArgs { + mint: wsol_mint::id(), + config: ReserveConfig { + loan_to_value_ratio: 50, + liquidation_threshold: 55, + fees: ReserveFees::default(), + optimal_borrow_rate: 0, + max_borrow_rate: 0, + protocol_liquidation_fee: 0, + ..test_reserve_config() + }, + liquidity_amount: 100 * LAMPORTS_PER_SOL, + price: PriceArgs { + price: 10, + conf: 0, + expo: 0, + ema_price: 10, + ema_conf: 0, + }, + }, + ], + &ObligationArgs { + deposits: vec![(usdc_mint::id(), 100 * FRACTIONAL_TO_USDC)], + borrows: vec![(wsol_mint::id(), LAMPORTS_PER_SOL)], + }, + ) + .await; + + // set ema price to 100 + test.set_price( + &wsol_mint::id(), + &PriceArgs { + price: 10, + conf: 0, + expo: 0, + ema_price: 100, + ema_conf: 0, + }, + ) + .await; + + test.advance_clock_by_slots(1).await; + + // this should fail bc the obligation is still healthy wrt the current non-ema market prices + let err = lending_market + .liquidate_obligation_and_redeem_reserve_collateral( + &mut test, + &find_reserve(&reserves, &wsol_mint::id()).unwrap(), + &find_reserve(&reserves, &usdc_mint::id()).unwrap(), + &obligation, + &user, + u64::MAX, + ) + .await + .unwrap_err() + .unwrap(); + + assert_eq!( + err, + TransactionError::InstructionError( + 3, + InstructionError::Custom(LendingError::ObligationHealthy as u32) + ) + ); + + test.set_price( + &usdc_mint::id(), + &PriceArgs { + price: 1, + conf: 0, + expo: 0, + ema_price: 0, + ema_conf: 0, + }, + ) + .await; + + test.advance_clock_by_slots(1).await; + + // this should fail bc the obligation is still healthy wrt the current non-ema market prices + let err = lending_market + .liquidate_obligation_and_redeem_reserve_collateral( + &mut test, + &find_reserve(&reserves, &wsol_mint::id()).unwrap(), + &find_reserve(&reserves, &usdc_mint::id()).unwrap(), + &obligation, + &user, + u64::MAX, + ) + .await + .unwrap_err() + .unwrap(); + + assert_eq!( + err, + TransactionError::InstructionError( + 3, + InstructionError::Custom(LendingError::ObligationHealthy as u32) + ) + ); + + // now set the spot prices. this time, the liquidation should actually work + test.set_price( + &usdc_mint::id(), + &PriceArgs { + price: 1, + conf: 0, + expo: 0, + ema_price: 10, + ema_conf: 0, + }, + ) + .await; + + test.set_price( + &wsol_mint::id(), + &PriceArgs { + price: 100, + conf: 0, + expo: 0, + ema_price: 10, + ema_conf: 0, + }, + ) + .await; + + test.advance_clock_by_slots(1).await; + + let usdc_reserve = find_reserve(&reserves, &usdc_mint::id()).unwrap(); + let wsol_reserve = find_reserve(&reserves, &wsol_mint::id()).unwrap(); + + let liquidator = User::new_with_balances( + &mut test, + &[ + (&usdc_mint::id(), 100 * FRACTIONAL_TO_USDC), + (&usdc_reserve.account.collateral.mint_pubkey, 0), + (&wsol_mint::id(), 100 * LAMPORTS_PER_SOL), + (&wsol_reserve.account.collateral.mint_pubkey, 0), + ], + ) + .await; + + let balance_checker = BalanceChecker::start(&mut test, &[&liquidator]).await; + + lending_market + .liquidate_obligation_and_redeem_reserve_collateral( + &mut test, + &find_reserve(&reserves, &wsol_mint::id()).unwrap(), + &find_reserve(&reserves, &usdc_mint::id()).unwrap(), + &obligation, + &liquidator, + u64::MAX, + ) + .await + .unwrap(); + + let (balance_changes, _) = balance_checker.find_balance_changes(&mut test).await; + // make sure the liquidation amounts are also wrt spot prices + let expected_balances_changes = HashSet::from([ + TokenBalanceChange { + token_account: liquidator.get_account(&usdc_mint::id()).unwrap(), + mint: usdc_mint::id(), + diff: (20 * FRACTIONAL_TO_USDC * 105 / 100) as i128 - 1, + }, + TokenBalanceChange { + token_account: liquidator.get_account(&wsol_mint::id()).unwrap(), + mint: wsol_mint::id(), + diff: -((LAMPORTS_PER_SOL / 5) as i128), + }, + ]); + + assert_eq!(balance_changes, expected_balances_changes); +} diff --git a/token-lending/program/tests/withdraw_obligation_collateral.rs b/token-lending/program/tests/withdraw_obligation_collateral.rs index bc390ca307a..49fd7dbc327 100644 --- a/token-lending/program/tests/withdraw_obligation_collateral.rs +++ b/token-lending/program/tests/withdraw_obligation_collateral.rs @@ -2,357 +2,129 @@ 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, - pubkey::Pubkey, - 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 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_bpf_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() - }, - ); - - 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 test_collateral = &test_obligation.deposits[0]; - let test_liquidity = &test_obligation.borrows[0]; - - 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; - test_liquidity.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 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; - 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 - ); +use solana_program_test::*; - 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 - ); -} +use solend_program::state::{LastUpdate, Obligation, ObligationCollateral, Reserve}; +use std::collections::HashSet; +use std::u64; #[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_bpf_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_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 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: -1_000_000, }, - ); - - let test_collateral = &test_obligation.deposits[0]; - - 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; + ]); + assert_eq!(balance_changes, expected_balance_changes); + assert_eq!(mint_supply_changes, HashSet::new()); - 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 usdc_reserve_post = test.load_account::(usdc_reserve.pubkey).await; + assert_eq!(usdc_reserve_post.account, usdc_reserve.account); - 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()), - ); - - 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; + let obligation_post = test.load_account::(obligation.pubkey).await; assert_eq!( - collateral_supply_balance, - initial_collateral_supply_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: 100_000_000_000 - 1_000_000, + ..obligation.account.deposits[0] + }] + .to_vec(), + ..obligation.account + } ); - let user_collateral_balance = - get_token_balance(&mut banks_client, usdc_test_reserve.user_collateral_pubkey).await; - assert_eq!( - user_collateral_balance, - initial_user_collateral_balance + USDC_DEPOSIT_AMOUNT_FRACTIONAL - ); - - 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() +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: &[(&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: -((100_000_000_000 - expected_remaining_collateral) as i128), }, - ); - - 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()), - ); + ]); + assert_eq!(balance_changes, expected_balance_changes); + assert_eq!(mint_supply_changes, HashSet::new()); - transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); + let usdc_reserve_post = test.load_account::(usdc_reserve.pubkey).await; + assert_eq!(usdc_reserve_post.account, usdc_reserve.account); - // check that transaction fails + let obligation_post = test.load_account::(obligation.pubkey).await; assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError( - 1, - InstructionError::Custom(LendingError::WithdrawTooLarge as u32) - ) + 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 + } ); } 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 17c2c12c552..221a0880e11 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 @@ -1,98 +1,147 @@ #![cfg(feature = "test-bpf")] +use solend_program::math::TryDiv; mod helpers; -use helpers::*; -use solana_program_test::*; -use solana_sdk::{pubkey::Pubkey, signature::Keypair}; -use solend_program::processor::process_instruction; +use crate::solend_program_test::MintSupplyChange; +use solend_sdk::math::Decimal; +use solend_sdk::state::LendingMarket; +use solend_sdk::state::ObligationCollateral; +use solend_sdk::state::ReserveCollateral; +use std::collections::HashSet; -#[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_bpf_compute_max_units(70_000); +use crate::solend_program_test::scenario_1; +use crate::solend_program_test::BalanceChecker; +use crate::solend_program_test::TokenBalanceChange; +use helpers::*; - let user_accounts_owner = Keypair::new(); - let lending_market = add_lending_market(&mut test); +use solana_program_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() - }, - ); +use solend_sdk::state::LastUpdate; +use solend_sdk::state::Obligation; - let test_obligation = add_obligation( - &mut test, - &lending_market, - &user_accounts_owner, - AddObligationArgs::default(), - ); +use solend_sdk::state::Reserve; +use solend_sdk::state::ReserveLiquidity; - let (mut banks_client, payer, _recent_blockhash) = test.start().await; +#[tokio::test] +async fn test_success() { + let (mut test, lending_market, usdc_reserve, wsol_reserve, user, obligation) = + scenario_1(&test_reserve_config(), &test_reserve_config()).await; - test_obligation.validate_state(&mut banks_client).await; + let balance_checker = + BalanceChecker::start(&mut test, &[&usdc_reserve, &user, &wsol_reserve]).await; lending_market - .deposit_obligation_and_collateral( - &mut banks_client, - &user_accounts_owner, - &payer, - &usdc_test_reserve, - &test_obligation, - 100 * FRACTIONAL_TO_USDC, + .withdraw_obligation_collateral_and_redeem_reserve_collateral( + &mut test, + &usdc_reserve, + &obligation, + &user, + u64::MAX, ) - .await; - - let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; - assert_eq!(usdc_reserve.last_update.stale, true); - - 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; + .await + .unwrap(); + + // check token balances + let (balance_changes, mint_supply_changes) = + balance_checker.find_balance_changes(&mut test).await; + // still borrowing 100usd worth of sol so we need to leave 200usd in the obligation. + 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 + }]) + ); - lending_market - .withdraw_and_redeem_collateral( - &mut banks_client, - &user_accounts_owner, - &payer, - &usdc_test_reserve, - &test_obligation, - 50 * FRACTIONAL_TO_USDC, - ) + // check program state + let lending_market_post = test + .load_account::(lending_market.pubkey) .await; + assert_eq!( + lending_market_post.account, + LendingMarket { + rate_limiter: { + let mut rate_limiter = lending_market.account.rate_limiter; + rate_limiter + .update( + 1000, + Decimal::from(withdraw_amount as u64) + .try_div(Decimal::from(1_000_000u64)) + .unwrap(), + ) + .unwrap(); + rate_limiter + }, + ..lending_market.account + } + ); - let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; - assert_eq!(usdc_reserve.last_update.stale, true); + 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 + }, + rate_limiter: { + let mut rate_limiter = usdc_reserve.account.rate_limiter; + rate_limiter + .update(1000, Decimal::from(withdraw_amount as u64)) + .unwrap(); + + rate_limiter + }, + ..usdc_reserve.account + } + ); - 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 + } + ); } diff --git a/token-lending/sdk/Cargo.toml b/token-lending/sdk/Cargo.toml new file mode 100644 index 00000000000..154056ad863 --- /dev/null +++ b/token-lending/sdk/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "solend-sdk" +version = "0.1.0" +description = "Solend Sdk" +authors = ["Solend Maintainers "] +repository = "https://github.com/solendprotocol/solana-program-library" +license = "Apache-2.0" +edition = "2018" + +[dependencies] +arrayref = "0.3.6" +bytemuck = "1.5.1" +num-derive = "0.3" +num-traits = "0.2" +pyth-sdk-solana = "0.7.0" +solana-program = ">=1.9, < 1.15" +spl-token = { version = "3.2.0", features=["no-entrypoint"] } +static_assertions = "1.1.0" +thiserror = "1.0" +uint = "=0.9.1" + +[dev-dependencies] +assert_matches = "1.5.0" +base64 = "0.13" +log = "0.4.14" +proptest = "1.0" +solana-sdk = ">=1.9, < 1.15" +serde = "=1.0.140" +serde_yaml = "0.8" + +[lib] +crate-type = ["cdylib", "lib"] + +[profile.release] +lto = "fat" +codegen-units = 1 + +[profile.release.build-override] +opt-level = 3 +incremental = false +codegen-units = 1 diff --git a/token-lending/program/src/error.rs b/token-lending/sdk/src/error.rs similarity index 83% rename from token-lending/program/src/error.rs rename to token-lending/sdk/src/error.rs index 12447c8fc1d..cbf8f845812 100644 --- a/token-lending/program/src/error.rs +++ b/token-lending/sdk/src/error.rs @@ -1,7 +1,9 @@ //! Error types use num_derive::FromPrimitive; +use num_traits::FromPrimitive; use solana_program::{decode_error::DecodeError, program_error::ProgramError}; +use solana_program::{msg, program_error::PrintProgramError}; use thiserror::Error; /// Errors that may be returned by the TokenLending program. @@ -167,6 +169,32 @@ pub enum LendingError { /// Insufficent protocol fees to redeem or no liquidity availible to process redeem #[error("Insufficent protocol fees to claim or no liquidity availible")] InsufficientProtocolFeesToRedeem, + /// No cpi flash borrows allowed + #[error("No cpi flash borrows allowed")] + FlashBorrowCpi, + /// No corresponding repay found for flash borrow + #[error("No corresponding repay found for flash borrow")] + NoFlashRepayFound, + /// Invalid flash repay found for borrow + #[error("Invalid repay found")] + InvalidFlashRepay, + + // 50 + /// No cpi flash repays allowed + #[error("No cpi flash repays allowed")] + FlashRepayCpi, + /// Multiple flash borrows not allowed in the same transaction + #[error("Multiple flash borrows not allowed in the same transaction")] + MultipleFlashBorrows, + /// Flash loans are disabled for this reserve + #[error("Flash loans are disabled for this reserve")] + FlashLoansDisabled, + /// Deprecated instruction + #[error("Instruction is deprecated")] + DeprecatedInstruction, + /// Outflow Rate Limit Exceeded + #[error("Outflow Rate Limit Exceeded")] + OutflowRateLimitExceeded, } impl From for ProgramError { @@ -180,3 +208,12 @@ impl DecodeError for LendingError { "Lending Error" } } + +impl PrintProgramError for LendingError { + fn print(&self) + where + E: 'static + std::error::Error + DecodeError + PrintProgramError + FromPrimitive, + { + msg!(&self.to_string()); + } +} diff --git a/token-lending/program/src/instruction.rs b/token-lending/sdk/src/instruction.rs similarity index 87% rename from token-lending/program/src/instruction.rs rename to token-lending/sdk/src/instruction.rs index 33edad27b13..f1551cba4c2 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/sdk/src/instruction.rs @@ -2,7 +2,7 @@ use crate::{ error::LendingError, - state::{ReserveConfig, ReserveFees}, + state::{RateLimiterConfig, ReserveConfig, ReserveFees}, }; use solana_program::{ instruction::{AccountMeta, Instruction}, @@ -14,7 +14,7 @@ use solana_program::{ use std::{convert::TryInto, mem::size_of}; /// Instructions supported by the lending program. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum LendingInstruction { // 0 /// Initializes a new lending market. @@ -41,9 +41,11 @@ pub enum LendingInstruction { /// /// 0. `[writable]` Lending market account. /// 1. `[signer]` Current owner. - SetLendingMarketOwner { + SetLendingMarketOwnerAndConfig { /// The new owner new_owner: Pubkey, + /// The new config + rate_limiter_config: RateLimiterConfig, }, // 2 @@ -68,7 +70,7 @@ pub enum LendingInstruction { /// 12 `[]` Derived lending market authority. /// 13 `[signer]` Lending market owner. /// 14 `[signer]` User transfer authority ($authority). - /// 15 `[]` Clock sysvar. + /// 15 `[]` Clock sysvar (optional, will be removed soon). /// 16 `[]` Rent sysvar. /// 17 `[]` Token program id. InitReserve { @@ -88,7 +90,7 @@ pub enum LendingInstruction { /// Must be the Pyth price account specified at InitReserve. /// 2. `[]` Switchboard Reserve liquidity oracle account. /// Must be the Switchboard price feed account specified at InitReserve. - /// 3. `[]` Clock sysvar. + /// 3. `[]` Clock sysvar (optional, will be removed soon). RefreshReserve, // 4 @@ -106,7 +108,7 @@ pub enum LendingInstruction { /// 5. `[]` Lending market account. /// 6. `[]` Derived lending market authority. /// 7. `[signer]` User transfer authority ($authority). - /// 8. `[]` Clock sysvar. + /// 8. `[]` Clock sysvar (optional, will be removed soon). /// 9. `[]` Token program id. DepositReserveLiquidity { /// Amount of liquidity to deposit in exchange for collateral tokens @@ -127,7 +129,7 @@ pub enum LendingInstruction { /// 5. `[]` Lending market account. /// 6. `[]` Derived lending market authority. /// 7. `[signer]` User transfer authority ($authority). - /// 8. `[]` Clock sysvar. + /// 8. `[]` Clock sysvar (optional, will be removed soon). /// 9. `[]` Token program id. RedeemReserveCollateral { /// Amount of collateral tokens to redeem in exchange for liquidity @@ -142,7 +144,7 @@ pub enum LendingInstruction { /// 0. `[writable]` Obligation account - uninitialized. /// 1. `[]` Lending market account. /// 2. `[signer]` Obligation owner. - /// 3. `[]` Clock sysvar. + /// 3. `[]` Clock sysvar (optional, will be removed soon). /// 4. `[]` Rent sysvar. /// 5. `[]` Token program id. InitObligation, @@ -155,7 +157,7 @@ pub enum LendingInstruction { /// Accounts expected by this instruction: /// /// 0. `[writable]` Obligation account. - /// 1. `[]` Clock sysvar. + /// 1. `[]` Clock sysvar (optional, will be removed soon). /// .. `[]` Collateral deposit reserve accounts - refreshed, all, in order. /// .. `[]` Liquidity borrow reserve accounts - refreshed, all, in order. RefreshObligation, @@ -174,7 +176,7 @@ pub enum LendingInstruction { /// 4. `[]` Lending market account. /// 5. `[signer]` Obligation owner. /// 6. `[signer]` User transfer authority ($authority). - /// 7. `[]` Clock sysvar. + /// 7. `[]` Clock sysvar (optional, will be removed soon). /// 8. `[]` Token program id. DepositObligationCollateral { /// Amount of collateral tokens to deposit @@ -194,7 +196,7 @@ pub enum LendingInstruction { /// 4. `[]` Lending market account. /// 5. `[]` Derived lending market authority. /// 6. `[signer]` Obligation owner. - /// 7. `[]` Clock sysvar. + /// 7. `[]` Clock sysvar (optional, will be removed soon). /// 8. `[]` Token program id. WithdrawObligationCollateral { /// Amount of collateral tokens to withdraw - u64::MAX for up to 100% of deposited amount @@ -217,7 +219,7 @@ pub enum LendingInstruction { /// 5. `[]` Lending market account. /// 6. `[]` Derived lending market authority. /// 7. `[signer]` Obligation owner. - /// 8. `[]` Clock sysvar. + /// 8. `[]` Clock sysvar (optional, will be removed soon). /// 9. `[]` Token program id. /// 10 `[optional, writable]` Host fee receiver account. BorrowObligationLiquidity { @@ -239,7 +241,7 @@ pub enum LendingInstruction { /// 3. `[writable]` Obligation account - refreshed. /// 4. `[]` Lending market account. /// 5. `[signer]` User transfer authority ($authority). - /// 6. `[]` Clock sysvar. + /// 6. `[]` Clock sysvar (optional, will be removed soon). /// 7. `[]` Token program id. RepayObligationLiquidity { /// Amount of liquidity to repay - u64::MAX for 100% of borrowed amount @@ -265,7 +267,7 @@ pub enum LendingInstruction { /// 7. `[]` Lending market account. /// 8. `[]` Derived lending market authority. /// 9. `[signer]` User transfer authority ($authority). - /// 10 `[]` Clock sysvar. + /// 10 `[]` Clock sysvar (optional, will be removed soon). /// 11 `[]` Token program id. LiquidateObligation { /// Amount of liquidity to repay - u64::MAX for up to 100% of borrowed amount @@ -273,6 +275,7 @@ pub enum LendingInstruction { }, // 13 + /// This instruction is now deprecated. Use FlashBorrowReserveLiquidity instead. /// Make a flash loan. /// /// Accounts expected by this instruction: @@ -332,7 +335,7 @@ pub enum LendingInstruction { /// 10 `[]` Pyth price oracle account. /// 11 `[]` Switchboard price feed oracle account. /// 12 `[signer]` User transfer authority ($authority). - /// 13 `[]` Clock sysvar. + /// 13 `[]` Clock sysvar (optional, will be removed soon). /// 14 `[]` Token program id. DepositReserveLiquidityAndObligationCollateral { /// Amount of liquidity to deposit in exchange @@ -356,7 +359,7 @@ pub enum LendingInstruction { /// 8. `[writable]` Reserve liquidity supply SPL Token account. /// 9. `[signer]` Obligation owner /// 10 `[signer]` User transfer authority ($authority). - /// 11. `[]` Clock sysvar. + /// 11. `[]` Clock sysvar (optional, will be removed soon). /// 12. `[]` Token program id. WithdrawObligationCollateralAndRedeemReserveCollateral { /// liquidity_amount is the amount of collateral tokens to withdraw @@ -378,6 +381,8 @@ pub enum LendingInstruction { UpdateReserveConfig { /// Reserve config to update to config: ReserveConfig, + /// Rate limiter config + rate_limiter_config: RateLimiterConfig, }, // 17 @@ -418,6 +423,47 @@ pub enum LendingInstruction { /// 4. `[]` Derived lending market authority. /// 5. `[]` Token program id. RedeemFees, + + // 19 + /// Flash borrow reserve liquidity + // + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Source liquidity token account. + /// 1. `[writable]` Destination liquidity token account. + /// 2. `[writable]` Reserve account. + /// 3. `[]` Lending market account. + /// 4. `[]` Derived lending market authority. + /// 5. `[]` Instructions sysvar. + /// 6. `[]` Token program id. + /// 7. `[]` Clock sysvar (optional, will be removed soon). + FlashBorrowReserveLiquidity { + /// Amount of liquidity to flash borrow + liquidity_amount: u64, + }, + + // 18 + /// Flash repay reserve liquidity + // + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Source liquidity token account. + /// $authority can transfer $liquidity_amount. + /// 1. `[writable]` Destination liquidity token account. + /// 2. `[writable]` Flash loan fee receiver account. + /// Must match the reserve liquidity fee receiver. + /// 3. `[writable]` Host fee receiver. + /// 4. `[writable]` Reserve account. + /// 5. `[]` Lending market account. + /// 6. `[signer]` User transfer authority ($authority). + /// 7. `[]` Instructions sysvar. + /// 8. `[]` Token program id. + FlashRepayReserveLiquidity { + /// Amount of liquidity to flash repay + liquidity_amount: u64, + /// Index of FlashBorrowReserveLiquidity instruction + borrow_instruction_index: u8, + }, } impl LendingInstruction { @@ -436,8 +482,16 @@ impl LendingInstruction { } } 1 => { - let (new_owner, _rest) = Self::unpack_pubkey(rest)?; - Self::SetLendingMarketOwner { new_owner } + let (new_owner, rest) = Self::unpack_pubkey(rest)?; + let (window_duration, rest) = Self::unpack_u64(rest)?; + let (max_outflow, _rest) = Self::unpack_u64(rest)?; + Self::SetLendingMarketOwnerAndConfig { + new_owner, + rate_limiter_config: RateLimiterConfig { + window_duration, + max_outflow, + }, + } } 2 => { let (liquidity_amount, rest) = Self::unpack_u64(rest)?; @@ -455,7 +509,8 @@ impl LendingInstruction { let (borrow_limit, rest) = Self::unpack_u64(rest)?; let (fee_receiver, rest) = Self::unpack_pubkey(rest)?; let (protocol_liquidation_fee, rest) = Self::unpack_u8(rest)?; - let (protocol_take_rate, _rest) = Self::unpack_u8(rest)?; + let (protocol_take_rate, rest) = Self::unpack_u8(rest)?; + let (added_borrow_weight_bps, _rest) = Self::unpack_u64(rest)?; Self::InitReserve { liquidity_amount, config: ReserveConfig { @@ -476,6 +531,7 @@ impl LendingInstruction { fee_receiver, protocol_liquidation_fee, protocol_take_rate, + added_borrow_weight_bps, }, } } @@ -537,7 +593,11 @@ impl LendingInstruction { let (borrow_limit, rest) = Self::unpack_u64(rest)?; let (fee_receiver, rest) = Self::unpack_pubkey(rest)?; let (protocol_liquidation_fee, rest) = Self::unpack_u8(rest)?; - let (protocol_take_rate, _rest) = Self::unpack_u8(rest)?; + let (protocol_take_rate, rest) = Self::unpack_u8(rest)?; + let (added_borrow_weight_bps, rest) = Self::unpack_u64(rest)?; + let (window_duration, rest) = Self::unpack_u64(rest)?; + let (max_outflow, _rest) = Self::unpack_u64(rest)?; + Self::UpdateReserveConfig { config: ReserveConfig { optimal_utilization_rate, @@ -557,6 +617,11 @@ impl LendingInstruction { fee_receiver, protocol_liquidation_fee, protocol_take_rate, + added_borrow_weight_bps, + }, + rate_limiter_config: RateLimiterConfig { + window_duration, + max_outflow, }, } } @@ -565,6 +630,18 @@ impl LendingInstruction { Self::LiquidateObligationAndRedeemReserveCollateral { liquidity_amount } } 18 => Self::RedeemFees, + 19 => { + let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; + Self::FlashBorrowReserveLiquidity { liquidity_amount } + } + 20 => { + let (liquidity_amount, rest) = Self::unpack_u64(rest)?; + let (borrow_instruction_index, _rest) = Self::unpack_u8(rest)?; + Self::FlashRepayReserveLiquidity { + liquidity_amount, + borrow_instruction_index, + } + } _ => { msg!("Instruction cannot be unpacked"); return Err(LendingError::InstructionUnpackError.into()); @@ -636,9 +713,14 @@ impl LendingInstruction { buf.extend_from_slice(owner.as_ref()); buf.extend_from_slice(quote_currency.as_ref()); } - Self::SetLendingMarketOwner { new_owner } => { + Self::SetLendingMarketOwnerAndConfig { + new_owner, + rate_limiter_config: config, + } => { buf.push(1); buf.extend_from_slice(new_owner.as_ref()); + buf.extend_from_slice(&config.window_duration.to_le_bytes()); + buf.extend_from_slice(&config.max_outflow.to_le_bytes()); } Self::InitReserve { liquidity_amount, @@ -662,6 +744,7 @@ impl LendingInstruction { fee_receiver, protocol_liquidation_fee, protocol_take_rate, + added_borrow_weight_bps: borrow_weight_bps, }, } => { buf.push(2); @@ -681,6 +764,7 @@ impl LendingInstruction { buf.extend_from_slice(&fee_receiver.to_bytes()); buf.extend_from_slice(&protocol_liquidation_fee.to_le_bytes()); buf.extend_from_slice(&protocol_take_rate.to_le_bytes()); + buf.extend_from_slice(&borrow_weight_bps.to_le_bytes()); } Self::RefreshReserve => { buf.push(3); @@ -731,7 +815,10 @@ impl LendingInstruction { buf.push(15); buf.extend_from_slice(&collateral_amount.to_le_bytes()); } - Self::UpdateReserveConfig { config } => { + Self::UpdateReserveConfig { + config, + rate_limiter_config, + } => { buf.push(16); buf.extend_from_slice(&config.optimal_utilization_rate.to_le_bytes()); buf.extend_from_slice(&config.loan_to_value_ratio.to_le_bytes()); @@ -748,6 +835,9 @@ impl LendingInstruction { buf.extend_from_slice(&config.fee_receiver.to_bytes()); buf.extend_from_slice(&config.protocol_liquidation_fee.to_le_bytes()); buf.extend_from_slice(&config.protocol_take_rate.to_le_bytes()); + buf.extend_from_slice(&config.added_borrow_weight_bps.to_le_bytes()); + buf.extend_from_slice(&rate_limiter_config.window_duration.to_le_bytes()); + buf.extend_from_slice(&rate_limiter_config.max_outflow.to_le_bytes()); } Self::LiquidateObligationAndRedeemReserveCollateral { liquidity_amount } => { buf.push(17); @@ -756,6 +846,18 @@ impl LendingInstruction { Self::RedeemFees {} => { buf.push(18); } + Self::FlashBorrowReserveLiquidity { liquidity_amount } => { + buf.push(19); + buf.extend_from_slice(&liquidity_amount.to_le_bytes()); + } + Self::FlashRepayReserveLiquidity { + liquidity_amount, + borrow_instruction_index, + } => { + buf.push(20); + buf.extend_from_slice(&liquidity_amount.to_le_bytes()); + buf.extend_from_slice(&borrow_instruction_index.to_le_bytes()); + } } buf } @@ -788,11 +890,12 @@ pub fn init_lending_market( } /// Creates a 'SetLendingMarketOwner' instruction. -pub fn set_lending_market_owner( +pub fn set_lending_market_owner_and_config( program_id: Pubkey, lending_market_pubkey: Pubkey, lending_market_owner: Pubkey, new_owner: Pubkey, + rate_limiter_config: RateLimiterConfig, ) -> Instruction { Instruction { program_id, @@ -800,7 +903,11 @@ pub fn set_lending_market_owner( AccountMeta::new(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_owner, true), ], - data: LendingInstruction::SetLendingMarketOwner { new_owner }.pack(), + data: LendingInstruction::SetLendingMarketOwnerAndConfig { + new_owner, + rate_limiter_config, + } + .pack(), } } @@ -844,7 +951,6 @@ pub fn init_reserve( AccountMeta::new_readonly(lending_market_authority_pubkey, false), AccountMeta::new_readonly(lending_market_owner_pubkey, true), AccountMeta::new_readonly(user_transfer_authority_pubkey, true), - AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(sysvar::rent::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ]; @@ -870,7 +976,6 @@ pub fn refresh_reserve( AccountMeta::new(reserve_pubkey, false), AccountMeta::new_readonly(reserve_liquidity_pyth_oracle_pubkey, false), AccountMeta::new_readonly(reserve_liquidity_switchboard_oracle_pubkey, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), ]; Instruction { program_id, @@ -907,7 +1012,6 @@ pub fn deposit_reserve_liquidity( AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), AccountMeta::new_readonly(user_transfer_authority_pubkey, true), - AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], data: LendingInstruction::DepositReserveLiquidity { liquidity_amount }.pack(), @@ -939,10 +1043,9 @@ pub fn redeem_reserve_collateral( AccountMeta::new(reserve_pubkey, false), AccountMeta::new(reserve_collateral_mint_pubkey, false), AccountMeta::new(reserve_liquidity_supply_pubkey, false), - AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), AccountMeta::new_readonly(user_transfer_authority_pubkey, true), - AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], data: LendingInstruction::RedeemReserveCollateral { collateral_amount }.pack(), @@ -963,7 +1066,6 @@ pub fn init_obligation( AccountMeta::new(obligation_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(obligation_owner_pubkey, true), - AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(sysvar::rent::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], @@ -978,10 +1080,7 @@ pub fn refresh_obligation( obligation_pubkey: Pubkey, reserve_pubkeys: Vec, ) -> Instruction { - let mut accounts = vec![ - AccountMeta::new(obligation_pubkey, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - ]; + let mut accounts = vec![AccountMeta::new(obligation_pubkey, false)]; accounts.extend( reserve_pubkeys .into_iter() @@ -1017,7 +1116,6 @@ pub fn deposit_obligation_collateral( AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(obligation_owner_pubkey, true), AccountMeta::new_readonly(user_transfer_authority_pubkey, true), - AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], data: LendingInstruction::DepositObligationCollateral { collateral_amount }.pack(), @@ -1062,7 +1160,6 @@ pub fn deposit_reserve_liquidity_and_obligation_collateral( AccountMeta::new_readonly(reserve_liquidity_pyth_oracle_pubkey, false), AccountMeta::new_readonly(reserve_liquidity_switchboard_oracle_pubkey, false), AccountMeta::new_readonly(user_transfer_authority_pubkey, true), - AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], data: LendingInstruction::DepositReserveLiquidityAndObligationCollateral { @@ -1099,14 +1196,13 @@ pub fn withdraw_obligation_collateral_and_redeem_reserve_collateral( AccountMeta::new(destination_collateral_pubkey, false), AccountMeta::new(withdraw_reserve_pubkey, false), AccountMeta::new(obligation_pubkey, false), - AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), AccountMeta::new(destination_liquidity_pubkey, false), AccountMeta::new(reserve_collateral_mint_pubkey, false), AccountMeta::new(reserve_liquidity_supply_pubkey, false), - AccountMeta::new(obligation_owner_pubkey, true), + AccountMeta::new_readonly(obligation_owner_pubkey, true), AccountMeta::new_readonly(user_transfer_authority_pubkey, true), - AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], data: LendingInstruction::WithdrawObligationCollateralAndRedeemReserveCollateral { @@ -1142,7 +1238,6 @@ pub fn withdraw_obligation_collateral( AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), AccountMeta::new_readonly(obligation_owner_pubkey, true), - AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], data: LendingInstruction::WithdrawObligationCollateral { collateral_amount }.pack(), @@ -1173,10 +1268,9 @@ pub fn borrow_obligation_liquidity( AccountMeta::new(borrow_reserve_pubkey, false), AccountMeta::new(borrow_reserve_liquidity_fee_receiver_pubkey, false), AccountMeta::new(obligation_pubkey, false), - AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), AccountMeta::new_readonly(obligation_owner_pubkey, true), - AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ]; if let Some(host_fee_receiver_pubkey) = host_fee_receiver_pubkey { @@ -1210,7 +1304,6 @@ pub fn repay_obligation_liquidity( AccountMeta::new(obligation_pubkey, false), AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(user_transfer_authority_pubkey, true), - AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], data: LendingInstruction::RepayObligationLiquidity { liquidity_amount }.pack(), @@ -1249,55 +1342,18 @@ pub fn liquidate_obligation( AccountMeta::new_readonly(lending_market_pubkey, false), AccountMeta::new_readonly(lending_market_authority_pubkey, false), AccountMeta::new_readonly(user_transfer_authority_pubkey, true), - AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(spl_token::id(), false), ], data: LendingInstruction::LiquidateObligation { liquidity_amount }.pack(), } } -/// Creates a `FlashLoan` instruction. -#[allow(clippy::too_many_arguments)] -pub fn flash_loan( - program_id: Pubkey, - amount: u64, - source_liquidity_pubkey: Pubkey, - destination_liquidity_pubkey: Pubkey, - reserve_pubkey: Pubkey, - reserve_liquidity_fee_receiver_pubkey: Pubkey, - host_fee_receiver_pubkey: Pubkey, - lending_market_pubkey: Pubkey, - flash_loan_receiver_program_id: Pubkey, - flash_loan_receiver_program_accounts: Vec, -) -> Instruction { - let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( - &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], - &program_id, - ); - let mut accounts = vec![ - AccountMeta::new(source_liquidity_pubkey, false), - AccountMeta::new(destination_liquidity_pubkey, false), - AccountMeta::new(reserve_pubkey, false), - AccountMeta::new(reserve_liquidity_fee_receiver_pubkey, false), - AccountMeta::new(host_fee_receiver_pubkey, false), - AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(lending_market_authority_pubkey, false), - AccountMeta::new_readonly(spl_token::id(), false), - AccountMeta::new_readonly(flash_loan_receiver_program_id, false), - ]; - accounts.extend(flash_loan_receiver_program_accounts); - Instruction { - program_id, - accounts, - data: LendingInstruction::FlashLoan { amount }.pack(), - } -} - /// Creates an 'UpdateReserveConfig' instruction. #[allow(clippy::too_many_arguments)] pub fn update_reserve_config( program_id: Pubkey, config: ReserveConfig, + rate_limiter_config: RateLimiterConfig, reserve_pubkey: Pubkey, lending_market_pubkey: Pubkey, lending_market_owner_pubkey: Pubkey, @@ -1321,7 +1377,11 @@ pub fn update_reserve_config( Instruction { program_id, accounts, - data: LendingInstruction::UpdateReserveConfig { config }.pack(), + data: LendingInstruction::UpdateReserveConfig { + config, + rate_limiter_config, + } + .pack(), } } @@ -1400,3 +1460,68 @@ pub fn redeem_fees( data: LendingInstruction::RedeemFees.pack(), } } + +/// Creates a 'FlashBorrowReserveLiquidity' instruction. +#[allow(clippy::too_many_arguments)] +pub fn flash_borrow_reserve_liquidity( + program_id: Pubkey, + liquidity_amount: u64, + source_liquidity_pubkey: Pubkey, + destination_liquidity_pubkey: Pubkey, + reserve_pubkey: Pubkey, + lending_market_pubkey: Pubkey, +) -> Instruction { + let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( + &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], + &program_id, + ); + + Instruction { + program_id, + accounts: vec![ + AccountMeta::new(source_liquidity_pubkey, false), + AccountMeta::new(destination_liquidity_pubkey, false), + AccountMeta::new(reserve_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(lending_market_authority_pubkey, false), + AccountMeta::new_readonly(sysvar::instructions::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: LendingInstruction::FlashBorrowReserveLiquidity { liquidity_amount }.pack(), + } +} + +/// Creates a 'FlashRepayReserveLiquidity' instruction. +#[allow(clippy::too_many_arguments)] +pub fn flash_repay_reserve_liquidity( + program_id: Pubkey, + liquidity_amount: u64, + borrow_instruction_index: u8, + source_liquidity_pubkey: Pubkey, + destination_liquidity_pubkey: Pubkey, + reserve_liquidity_fee_receiver_pubkey: Pubkey, + host_fee_receiver_pubkey: Pubkey, + reserve_pubkey: Pubkey, + lending_market_pubkey: Pubkey, + user_transfer_authority_pubkey: Pubkey, +) -> Instruction { + Instruction { + program_id, + accounts: vec![ + AccountMeta::new(source_liquidity_pubkey, false), + AccountMeta::new(destination_liquidity_pubkey, false), + AccountMeta::new(reserve_liquidity_fee_receiver_pubkey, false), + AccountMeta::new(host_fee_receiver_pubkey, false), + AccountMeta::new(reserve_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(user_transfer_authority_pubkey, true), + AccountMeta::new_readonly(sysvar::instructions::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: LendingInstruction::FlashRepayReserveLiquidity { + liquidity_amount, + borrow_instruction_index, + } + .pack(), + } +} diff --git a/token-lending/sdk/src/lib.rs b/token-lending/sdk/src/lib.rs new file mode 100644 index 00000000000..8fc5cd4c978 --- /dev/null +++ b/token-lending/sdk/src/lib.rs @@ -0,0 +1,39 @@ +#![deny(missing_docs)] + +//! A lending program for the Solana blockchain. + +pub mod error; +pub mod instruction; +pub mod math; +pub mod oracles; +pub mod state; + +// Export current sdk types for downstream users building with a different sdk version +pub use solana_program; + +/// mainnet program id +pub mod solend_mainnet { + solana_program::declare_id!("So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo"); +} + +/// devnet program id +pub mod solend_devnet { + solana_program::declare_id!("So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo"); +} + +/// Canonical null pubkey. Prints out as "nu11111111111111111111111111111111111111111" +pub const NULL_PUBKEY: solana_program::pubkey::Pubkey = + solana_program::pubkey::Pubkey::new_from_array([ + 11, 193, 238, 216, 208, 116, 241, 195, 55, 212, 76, 22, 75, 202, 40, 216, 76, 206, 27, 169, + 138, 64, 177, 28, 19, 90, 156, 0, 0, 0, 0, 0, + ]); + +/// Mainnet program id for Switchboard v2. +pub mod switchboard_v2_mainnet { + solana_program::declare_id!("SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f"); +} + +/// Devnet program id for Switchboard v2. +pub mod switchboard_v2_devnet { + solana_program::declare_id!("2TfB33aLaneQb5TNVwyDz3jSZXS6jdW2ARw1Dgf84XCG"); +} diff --git a/token-lending/program/src/math/common.rs b/token-lending/sdk/src/math/common.rs similarity index 92% rename from token-lending/program/src/math/common.rs rename to token-lending/sdk/src/math/common.rs index 878e224fe74..081ee56b0f0 100644 --- a/token-lending/program/src/math/common.rs +++ b/token-lending/sdk/src/math/common.rs @@ -10,6 +10,8 @@ pub const WAD: u64 = 1_000_000_000_000_000_000; pub const HALF_WAD: u64 = 500_000_000_000_000_000; /// Scale for percentages pub const PERCENT_SCALER: u64 = 10_000_000_000_000_000; +/// Scale for basis points +pub const BPS_SCALER: u64 = 100_000_000_000_000; /// Try to subtract, return an error on underflow pub trait TrySub: Sized { diff --git a/token-lending/program/src/math/decimal.rs b/token-lending/sdk/src/math/decimal.rs similarity index 71% rename from token-lending/program/src/math/decimal.rs rename to token-lending/sdk/src/math/decimal.rs index 8cc3fc8baa9..e3362f72ac6 100644 --- a/token-lending/program/src/math/decimal.rs +++ b/token-lending/sdk/src/math/decimal.rs @@ -26,7 +26,7 @@ construct_uint! { } /// Large decimal values, precise to 18 digits -#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Eq, Ord)] +#[derive(Clone, Copy, Default, PartialEq, PartialOrd, Eq, Ord)] pub struct Decimal(pub U192); impl Decimal { @@ -55,6 +55,11 @@ impl Decimal { Self(U192::from(percent as u64 * PERCENT_SCALER)) } + /// Create scaled decimal from bps value + pub fn from_bps(bps: u64) -> Self { + Self::from(bps).try_div(10_000).unwrap() + } + /// Return raw scaled value if it fits within u128 #[allow(clippy::wrong_self_convention)] pub fn to_scaled_val(&self) -> Result { @@ -111,6 +116,12 @@ impl fmt::Display for Decimal { } } +impl fmt::Debug for Decimal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self) + } +} + impl From for Decimal { fn from(val: u64) -> Self { Self(Self::wad() * U192::from(val)) @@ -213,4 +224,76 @@ mod test { fn test_scaler() { assert_eq!(U192::exp10(SCALE), Decimal::wad()); } + + #[test] + fn test_u192() { + let one = U192::from(1); + assert_eq!(one.0, [1u64, 0, 0]); + + let wad = Decimal::wad(); + assert_eq!(wad.0, [WAD, 0, 0]); + + let hundred = Decimal::from(100u64); + // 2^64 * 5 + 7766279631452241920 = 1e20 + assert_eq!(hundred.0 .0, [7766279631452241920, 5, 0]); + } + + #[test] + fn test_from_percent() { + let left = Decimal::from_percent(20); + let right = Decimal::from(20u64).try_div(Decimal::from(100u64)).unwrap(); + + assert_eq!(left, right); + } + + #[test] + fn test_from_bps() { + let left = Decimal::from_bps(190000); + assert_eq!(left, Decimal::from(19u64)); + } + + #[test] + fn test_to_scaled_val() { + assert_eq!( + Decimal(U192::from(u128::MAX)).to_scaled_val().unwrap(), + u128::MAX + ); + + assert_eq!( + Decimal(U192::from(u128::MAX)) + .try_add(Decimal(U192::from(1))) + .unwrap() + .to_scaled_val(), + Err(ProgramError::from(LendingError::MathOverflow)) + ); + } + + #[test] + fn test_round_floor_ceil_u64() { + let mut val = Decimal::one(); + assert_eq!(val.try_round_u64().unwrap(), 1); + assert_eq!(val.try_floor_u64().unwrap(), 1); + assert_eq!(val.try_ceil_u64().unwrap(), 1); + + val = val + .try_add(Decimal::from_scaled_val(HALF_WAD as u128 - 1)) + .unwrap(); + assert_eq!(val.try_round_u64().unwrap(), 1); + assert_eq!(val.try_floor_u64().unwrap(), 1); + assert_eq!(val.try_ceil_u64().unwrap(), 2); + + val = val.try_add(Decimal::from_scaled_val(1)).unwrap(); + assert_eq!(val.try_round_u64().unwrap(), 2); + assert_eq!(val.try_floor_u64().unwrap(), 1); + assert_eq!(val.try_ceil_u64().unwrap(), 2); + } + + #[test] + fn test_display() { + assert_eq!(Decimal::from(1u64).to_string(), "1.000000000000000000"); + assert_eq!( + Decimal::from_scaled_val(1u128).to_string(), + "0.000000000000000001" + ); + } } diff --git a/token-lending/program/src/math/mod.rs b/token-lending/sdk/src/math/mod.rs similarity index 100% rename from token-lending/program/src/math/mod.rs rename to token-lending/sdk/src/math/mod.rs diff --git a/token-lending/program/src/math/rate.rs b/token-lending/sdk/src/math/rate.rs similarity index 79% rename from token-lending/program/src/math/rate.rs rename to token-lending/sdk/src/math/rate.rs index c404c8ff2b8..0190b5edad2 100644 --- a/token-lending/program/src/math/rate.rs +++ b/token-lending/sdk/src/math/rate.rs @@ -176,9 +176,55 @@ impl TryMul for Rate { #[cfg(test)] mod test { use super::*; + use std::convert::TryInto; + + #[test] + fn test_scaled_val() { + assert_eq!(Rate::from_percent(50).to_scaled_val(), HALF_WAD as u128); + } #[test] fn checked_pow() { assert_eq!(Rate::one(), Rate::one().try_pow(u64::MAX).unwrap()); + assert_eq!( + Rate::from_percent(200).try_pow(7).unwrap(), + Decimal::from(128u64).try_into().unwrap() + ); + } + + #[test] + fn test_display() { + assert_eq!( + Rate::one().try_div(3u64).unwrap().to_string(), + "0.333333333333333333" + ); + } + + #[test] + fn test_basic_arithmetic() { + assert_eq!( + Rate::one().try_add(Rate::one()).unwrap(), + Rate::from_scaled_val(2 * WAD) + ); + + assert_eq!(Rate::one().try_sub(Rate::one()).unwrap(), Rate::zero()); + + assert_eq!( + Rate::from_percent(240) + .try_mul(Rate::from_percent(50)) + .unwrap(), + Rate::from_percent(120) + ); + assert_eq!( + Rate::from_percent(240).try_mul(10).unwrap(), + Decimal::from(24u64).try_into().unwrap() + ); + + assert_eq!( + Rate::from_percent(240) + .try_div(Rate::from_percent(60)) + .unwrap(), + Rate::from_scaled_val(4 * WAD) + ); } } diff --git a/token-lending/sdk/src/oracles.rs b/token-lending/sdk/src/oracles.rs new file mode 100644 index 00000000000..63f91f799b5 --- /dev/null +++ b/token-lending/sdk/src/oracles.rs @@ -0,0 +1,415 @@ +#![allow(missing_docs)] +use crate::{ + self as solend_program, + error::LendingError, + math::{Decimal, TryDiv, TryMul}, +}; +use pyth_sdk_solana::Price; +// use pyth_sdk_solana; +use solana_program::{ + account_info::AccountInfo, msg, program_error::ProgramError, sysvar::clock::Clock, +}; +use std::{convert::TryInto, result::Result}; + +pub fn get_pyth_price( + pyth_price_info: &AccountInfo, + clock: &Clock, +) -> Result<(Decimal, Decimal), ProgramError> { + const PYTH_CONFIDENCE_RATIO: u64 = 10; + const STALE_AFTER_SLOTS_ELAPSED: u64 = 240; // roughly 2 min + + if *pyth_price_info.key == solend_program::NULL_PUBKEY { + return Err(LendingError::NullOracleConfig.into()); + } + + let data = &pyth_price_info.try_borrow_data()?; + let price_account = pyth_sdk_solana::state::load_price_account(data).map_err(|e| { + msg!("Couldn't load price feed from account info: {:?}", e); + LendingError::InvalidOracleConfig + })?; + let pyth_price = price_account + .get_price_no_older_than(clock, STALE_AFTER_SLOTS_ELAPSED) + .ok_or_else(|| { + msg!("Pyth oracle price is too stale!"); + LendingError::InvalidOracleConfig + })?; + + let price: u64 = pyth_price.price.try_into().map_err(|_| { + msg!("Oracle price cannot be negative"); + LendingError::InvalidOracleConfig + })?; + + // Perhaps confidence_ratio should exist as a per reserve config + // 100/confidence_ratio = maximum size of confidence range as a percent of price + // confidence_ratio of 10 filters out pyth prices with conf > 10% of price + if pyth_price.conf.saturating_mul(PYTH_CONFIDENCE_RATIO) > price { + msg!( + "Oracle price confidence is too wide. price: {}, conf: {}", + price, + pyth_price.conf, + ); + return Err(LendingError::InvalidOracleConfig.into()); + } + + let market_price = pyth_price_to_decimal(&pyth_price); + let ema_price = { + let price_feed = price_account.to_price_feed(pyth_price_info.key); + // this can be unchecked bc the ema price is only used to _limit_ borrows and withdraws. + // ie staleness doesn't _really_ matter for this field. + // + // the pyth EMA is also updated every time the regular spot price is updated anyways so in + // reality the staleness should never be an issue. + let ema_price = price_feed.get_ema_price_unchecked(); + pyth_price_to_decimal(&ema_price)? + }; + + Ok((market_price?, ema_price)) +} + +fn pyth_price_to_decimal(pyth_price: &Price) -> Result { + let price: u64 = pyth_price.price.try_into().map_err(|_| { + msg!("Oracle price cannot be negative"); + LendingError::InvalidOracleConfig + })?; + + if pyth_price.expo >= 0 { + let exponent = pyth_price + .expo + .try_into() + .map_err(|_| LendingError::MathOverflow)?; + let zeros = 10u64 + .checked_pow(exponent) + .ok_or(LendingError::MathOverflow)?; + Decimal::from(price).try_mul(zeros) + } else { + let exponent = pyth_price + .expo + .checked_abs() + .ok_or(LendingError::MathOverflow)? + .try_into() + .map_err(|_| LendingError::MathOverflow)?; + let decimals = 10u64 + .checked_pow(exponent) + .ok_or(LendingError::MathOverflow)?; + Decimal::from(price).try_div(decimals) + } +} + +#[cfg(test)] +mod test { + use super::*; + use bytemuck::bytes_of_mut; + use proptest::prelude::*; + use pyth_sdk_solana::state::Rational; + use pyth_sdk_solana::state::{ + AccountType, CorpAction, PriceAccount, PriceInfo, PriceStatus, PriceType, MAGIC, VERSION_2, + }; + use solana_program::pubkey::Pubkey; + + #[derive(Clone, Debug)] + struct PythPriceTestCase { + price_account: PriceAccount, + clock: Clock, + expected_result: Result<(Decimal, Decimal), ProgramError>, + } + + fn pyth_price_cases() -> impl Strategy { + prop_oneof![ + // case 2: failure. bad magic value + Just(PythPriceTestCase { + price_account: PriceAccount { + magic: MAGIC + 1, + ver: VERSION_2, + atype: AccountType::Price as u32, + ptype: PriceType::Price, + expo: 10, + ema_price: Rational { + val: 11, + numer: 110, + denom: 10, + }, + agg: PriceInfo { + price: 10, + conf: 1, + status: PriceStatus::Trading, + corp_act: CorpAction::NoCorpAct, + pub_slot: 0 + }, + ..PriceAccount::default() + }, + clock: Clock { + slot: 4, + ..Clock::default() + }, + // PythError::InvalidAccountData. + expected_result: Err(LendingError::InvalidOracleConfig.into()), + }), + // case 3: failure. bad version number + Just(PythPriceTestCase { + price_account: PriceAccount { + magic: MAGIC, + ver: VERSION_2 - 1, + atype: AccountType::Price as u32, + ptype: PriceType::Price, + expo: 10, + ema_price: Rational { + val: 11, + numer: 110, + denom: 10, + }, + agg: PriceInfo { + price: 10, + conf: 1, + status: PriceStatus::Trading, + corp_act: CorpAction::NoCorpAct, + pub_slot: 0 + }, + ..PriceAccount::default() + }, + clock: Clock { + slot: 4, + ..Clock::default() + }, + expected_result: Err(LendingError::InvalidOracleConfig.into()), + }), + // case 4: failure. bad account type + Just(PythPriceTestCase { + price_account: PriceAccount { + magic: MAGIC, + ver: VERSION_2, + atype: AccountType::Product as u32, + ptype: PriceType::Price, + expo: 10, + ema_price: Rational { + val: 11, + numer: 110, + denom: 10, + }, + agg: PriceInfo { + price: 10, + conf: 1, + status: PriceStatus::Trading, + corp_act: CorpAction::NoCorpAct, + pub_slot: 0 + }, + ..PriceAccount::default() + }, + clock: Clock { + slot: 4, + ..Clock::default() + }, + expected_result: Err(LendingError::InvalidOracleConfig.into()), + }), + // case 5: ignore. bad price type is fine. not testing this + // case 6: success. most recent price has status == trading, not stale + Just(PythPriceTestCase { + price_account: PriceAccount { + magic: MAGIC, + ver: VERSION_2, + atype: AccountType::Price as u32, + ptype: PriceType::Price, + expo: 1, + timestamp: 0, + ema_price: Rational { + val: 11, + numer: 110, + denom: 10, + }, + agg: PriceInfo { + price: 200, + conf: 1, + status: PriceStatus::Trading, + corp_act: CorpAction::NoCorpAct, + pub_slot: 0 + }, + ..PriceAccount::default() + }, + clock: Clock { + slot: 240, + ..Clock::default() + }, + expected_result: Ok((Decimal::from(2000_u64), Decimal::from(110_u64))) + }), + // case 7: success. most recent price has status == unknown, previous price not stale + Just(PythPriceTestCase { + price_account: PriceAccount { + magic: MAGIC, + ver: VERSION_2, + atype: AccountType::Price as u32, + ptype: PriceType::Price, + expo: 1, + timestamp: 20, + ema_price: Rational { + val: 11, + numer: 110, + denom: 10, + }, + agg: PriceInfo { + price: 200, + conf: 1, + status: PriceStatus::Unknown, + corp_act: CorpAction::NoCorpAct, + pub_slot: 1 + }, + prev_price: 190, + prev_conf: 10, + prev_slot: 0, + ..PriceAccount::default() + }, + clock: Clock { + slot: 240, + ..Clock::default() + }, + expected_result: Ok((Decimal::from(1900_u64), Decimal::from(110_u64))) + }), + // case 8: failure. most recent price is stale + Just(PythPriceTestCase { + price_account: PriceAccount { + magic: MAGIC, + ver: VERSION_2, + atype: AccountType::Price as u32, + ptype: PriceType::Price, + expo: 1, + timestamp: 0, + ema_price: Rational { + val: 11, + numer: 110, + denom: 10, + }, + agg: PriceInfo { + price: 200, + conf: 1, + status: PriceStatus::Trading, + corp_act: CorpAction::NoCorpAct, + pub_slot: 1 + }, + prev_slot: 0, // there is no case where prev_slot > agg.pub_slot + ..PriceAccount::default() + }, + clock: Clock { + slot: 242, + ..Clock::default() + }, + expected_result: Err(LendingError::InvalidOracleConfig.into()) + }), + // case 9: failure. most recent price has status == unknown and previous price is stale + Just(PythPriceTestCase { + price_account: PriceAccount { + magic: MAGIC, + ver: VERSION_2, + atype: AccountType::Price as u32, + ptype: PriceType::Price, + expo: 1, + timestamp: 1, + ema_price: Rational { + val: 11, + numer: 110, + denom: 10, + }, + agg: PriceInfo { + price: 200, + conf: 1, + status: PriceStatus::Unknown, + corp_act: CorpAction::NoCorpAct, + pub_slot: 1 + }, + prev_price: 190, + prev_conf: 10, + prev_slot: 0, + ..PriceAccount::default() + }, + clock: Clock { + slot: 241, + ..Clock::default() + }, + expected_result: Err(LendingError::InvalidOracleConfig.into()) + }), + // case 10: failure. price is negative + Just(PythPriceTestCase { + price_account: PriceAccount { + magic: MAGIC, + ver: VERSION_2, + atype: AccountType::Price as u32, + ptype: PriceType::Price, + expo: 1, + timestamp: 1, + ema_price: Rational { + val: 11, + numer: 110, + denom: 10, + }, + agg: PriceInfo { + price: -200, + conf: 1, + status: PriceStatus::Trading, + corp_act: CorpAction::NoCorpAct, + pub_slot: 0 + }, + ..PriceAccount::default() + }, + clock: Clock { + slot: 240, + ..Clock::default() + }, + expected_result: Err(LendingError::InvalidOracleConfig.into()) + }), + // case 11: failure. confidence interval is too wide + Just(PythPriceTestCase { + price_account: PriceAccount { + magic: MAGIC, + ver: VERSION_2, + atype: AccountType::Price as u32, + ptype: PriceType::Price, + expo: 1, + timestamp: 1, + ema_price: Rational { + val: 11, + numer: 110, + denom: 10, + }, + agg: PriceInfo { + price: 200, + conf: 40, + status: PriceStatus::Trading, + corp_act: CorpAction::NoCorpAct, + pub_slot: 0 + }, + ..PriceAccount::default() + }, + clock: Clock { + slot: 240, + ..Clock::default() + }, + expected_result: Err(LendingError::InvalidOracleConfig.into()) + }), + ] + } + + proptest! { + #[test] + fn test_pyth_price(mut test_case in pyth_price_cases()) { + // wrap price account into an account info + let mut lamports = 20; + let pubkey = Pubkey::new_unique(); + let account_info = AccountInfo::new( + &pubkey, + false, + false, + &mut lamports, + bytes_of_mut(&mut test_case.price_account), + &pubkey, + false, + 0, + ); + + let result = get_pyth_price(&account_info, &test_case.clock); + assert_eq!( + result, + test_case.expected_result, + "actual: {:#?} expected: {:#?}", + result, + test_case.expected_result + ); + } + } +} diff --git a/token-lending/program/src/state/last_update.rs b/token-lending/sdk/src/state/last_update.rs similarity index 100% rename from token-lending/program/src/state/last_update.rs rename to token-lending/sdk/src/state/last_update.rs diff --git a/token-lending/program/src/state/lending_market.rs b/token-lending/sdk/src/state/lending_market.rs similarity index 91% rename from token-lending/program/src/state/lending_market.rs rename to token-lending/sdk/src/state/lending_market.rs index b15fd78ff50..cb199b74bb1 100644 --- a/token-lending/program/src/state/lending_market.rs +++ b/token-lending/sdk/src/state/lending_market.rs @@ -8,7 +8,7 @@ use solana_program::{ }; /// Lending market state -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct LendingMarket { /// Version of lending market pub version: u8, @@ -25,6 +25,8 @@ pub struct LendingMarket { pub oracle_program_id: Pubkey, /// Oracle (Switchboard) program id pub switchboard_oracle_program_id: Pubkey, + /// Outflow rate limiter denominated in dollars + pub rate_limiter: RateLimiter, } impl LendingMarket { @@ -44,6 +46,7 @@ impl LendingMarket { self.token_program_id = params.token_program_id; self.oracle_program_id = params.oracle_program_id; self.switchboard_oracle_program_id = params.switchboard_oracle_program_id; + self.rate_limiter = RateLimiter::default(); } } @@ -86,6 +89,7 @@ impl Pack for LendingMarket { token_program_id, oracle_program_id, switchboard_oracle_program_id, + rate_limiter, _padding, ) = mut_array_refs![ output, @@ -96,7 +100,8 @@ impl Pack for LendingMarket { PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, - 128 + RATE_LIMITER_LEN, + 128 - RATE_LIMITER_LEN ]; *version = self.version.to_le_bytes(); @@ -106,6 +111,7 @@ impl Pack for LendingMarket { token_program_id.copy_from_slice(self.token_program_id.as_ref()); oracle_program_id.copy_from_slice(self.oracle_program_id.as_ref()); switchboard_oracle_program_id.copy_from_slice(self.switchboard_oracle_program_id.as_ref()); + self.rate_limiter.pack_into_slice(rate_limiter); } /// Unpacks a byte buffer into a [LendingMarketInfo](struct.LendingMarketInfo.html) @@ -120,6 +126,7 @@ impl Pack for LendingMarket { token_program_id, oracle_program_id, switchboard_oracle_program_id, + rate_limiter, _padding, ) = array_refs![ input, @@ -130,7 +137,8 @@ impl Pack for LendingMarket { PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, - 128 + RATE_LIMITER_LEN, + 128 - RATE_LIMITER_LEN ]; let version = u8::from_le_bytes(*version); @@ -147,6 +155,7 @@ impl Pack for LendingMarket { token_program_id: Pubkey::new_from_array(*token_program_id), oracle_program_id: Pubkey::new_from_array(*oracle_program_id), switchboard_oracle_program_id: Pubkey::new_from_array(*switchboard_oracle_program_id), + rate_limiter: RateLimiter::unpack_from_slice(rate_limiter)?, }) } } diff --git a/token-lending/program/src/state/mod.rs b/token-lending/sdk/src/state/mod.rs similarity index 97% rename from token-lending/program/src/state/mod.rs rename to token-lending/sdk/src/state/mod.rs index a14b2566fdf..278804c65bb 100644 --- a/token-lending/program/src/state/mod.rs +++ b/token-lending/sdk/src/state/mod.rs @@ -3,11 +3,13 @@ mod last_update; mod lending_market; mod obligation; +mod rate_limiter; mod reserve; pub use last_update::*; pub use lending_market::*; pub use obligation::*; +pub use rate_limiter::*; pub use reserve::*; use crate::math::{Decimal, WAD}; diff --git a/token-lending/program/src/state/obligation.rs b/token-lending/sdk/src/state/obligation.rs similarity index 61% rename from token-lending/program/src/state/obligation.rs rename to token-lending/sdk/src/state/obligation.rs index d9afa1436a1..fa58645f29f 100644 --- a/token-lending/program/src/state/obligation.rs +++ b/token-lending/sdk/src/state/obligation.rs @@ -13,7 +13,7 @@ use solana_program::{ pubkey::{Pubkey, PUBKEY_BYTES}, }; use std::{ - cmp::Ordering, + cmp::{min, Ordering}, convert::{TryFrom, TryInto}, }; @@ -37,11 +37,20 @@ pub struct Obligation { pub borrows: Vec, /// Market value of deposits pub deposited_value: Decimal, - /// Market value of borrows + /// Risk-adjusted market value of borrows. + /// ie sum(b.borrowed_amount * b.current_spot_price * b.borrow_weight for b in borrows) pub borrowed_value: Decimal, - /// The maximum borrow value at the weighted average loan to value ratio + /// Risk-adjusted upper bound market value of borrows. + /// ie sum(b.borrowed_amount * max(b.current_spot_price, b.smoothed_price) * b.borrow_weight for b in borrows) + pub borrowed_value_upper_bound: Decimal, + /// The maximum open borrow value. + /// ie sum(d.deposited_amount * d.ltv * min(d.current_spot_price, d.smoothed_price) for d in deposits) + /// if borrowed_value_upper_bound >= allowed_borrow_value, then the obligation is unhealthy and + /// borrows and withdraws are disabled. pub allowed_borrow_value: Decimal, - /// The dangerous borrow value at the weighted average liquidation threshold + /// The dangerous borrow value at the weighted average liquidation threshold. + /// ie sum(d.deposited_amount * d.liquidation_threshold * d.current_spot_price for d in deposits) + /// if borrowed_value >= unhealthy_borrow_value, the obligation can be liquidated pub unhealthy_borrow_value: Decimal, } @@ -90,25 +99,71 @@ impl Obligation { Ok(()) } - /// Calculate the maximum collateral value that can be withdrawn - pub fn max_withdraw_value( + /// calculate the maximum amount of collateral that can be borrowed + pub fn max_withdraw_amount( &self, - withdraw_collateral_ltv: Rate, - ) -> Result { - if self.allowed_borrow_value <= self.borrowed_value { - return Ok(Decimal::zero()); + collateral: &ObligationCollateral, + withdraw_reserve: &Reserve, + ) -> Result { + if self.borrows.is_empty() { + return Ok(collateral.deposited_amount); + } + + if self.allowed_borrow_value <= self.borrowed_value_upper_bound { + return Ok(0); } - if withdraw_collateral_ltv == Rate::zero() { - return Ok(self.deposited_value); + + let loan_to_value_ratio = withdraw_reserve.loan_to_value_ratio(); + if loan_to_value_ratio == Rate::zero() { + return Ok(collateral.deposited_amount); } - self.allowed_borrow_value - .try_sub(self.borrowed_value)? - .try_div(withdraw_collateral_ltv) + + // max usd value that can be withdrawn + let max_withdraw_value = self + .allowed_borrow_value + .try_sub(self.borrowed_value_upper_bound)? + .try_div(loan_to_value_ratio)?; + + // convert max_withdraw_value to max withdraw liquidity amount + + // why is min used and not max? seems scary + // + // the tldr is that allowed borrow value is calculated with the minimum + // of the spot price and the smoothed price, so we have to use the min here to be + // consistent. + // + // note that safety-wise, it doesn't actually matter. if we used the max (which appears safer), + // the initial max withdraw would be lower, but the user can immediately make another max withdraw call + // because allowed_borrow_value is still greater than borrowed_value_upper_bound + // after a large amount of consecutive max withdraw calls, the end state of using max would be the same + // as using min. + // + // therefore, we use min for the better UX. + let price = min( + withdraw_reserve.liquidity.market_price, + withdraw_reserve.liquidity.smoothed_market_price, + ); + + let decimals = 10u64 + .checked_pow(withdraw_reserve.liquidity.mint_decimals as u32) + .ok_or(LendingError::MathOverflow)?; + + let max_withdraw_liquidity_amount = max_withdraw_value.try_mul(decimals)?.try_div(price)?; + + // convert max withdraw liquidity amount to max withdraw collateral amount + Ok(min( + withdraw_reserve + .collateral_exchange_rate()? + .decimal_liquidity_to_collateral(max_withdraw_liquidity_amount)? + .try_floor_u64()?, + collateral.deposited_amount, + )) } /// Calculate the maximum liquidity value that can be borrowed pub fn remaining_borrow_value(&self) -> Result { - self.allowed_borrow_value.try_sub(self.borrowed_value) + self.allowed_borrow_value + .try_sub(self.borrowed_value_upper_bound) } /// Calculate the maximum liquidation amount for a given liquidity @@ -119,7 +174,9 @@ impl Obligation { let max_liquidation_value = self .borrowed_value .try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))? - .min(liquidity.market_value); + .min(liquidity.market_value) + .min(Decimal::from(MAX_LIQUIDATABLE_VALUE_AT_ONCE)); + let max_liquidation_pct = max_liquidation_value.try_div(liquidity.market_value)?; liquidity.borrowed_amount_wads.try_mul(max_liquidation_pct) } @@ -245,7 +302,7 @@ impl IsInitialized for Obligation { } /// Obligation collateral state -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct ObligationCollateral { /// Reserve collateral is deposited to pub deposit_reserve: Pubkey, @@ -285,7 +342,7 @@ impl ObligationCollateral { } /// Obligation liquidity state -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct ObligationLiquidity { /// Reserve liquidity is borrowed from pub borrow_reserve: Pubkey, @@ -364,6 +421,7 @@ impl Pack for Obligation { borrowed_value, allowed_borrow_value, unhealthy_borrow_value, + borrowed_value_upper_bound, _padding, deposits_len, borrows_len, @@ -379,7 +437,8 @@ impl Pack for Obligation { 16, 16, 16, - 64, + 16, + 48, 1, 1, OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1)) @@ -393,6 +452,7 @@ impl Pack for Obligation { owner.copy_from_slice(self.owner.as_ref()); pack_decimal(self.deposited_value, deposited_value); pack_decimal(self.borrowed_value, borrowed_value); + pack_decimal(self.borrowed_value_upper_bound, borrowed_value_upper_bound); pack_decimal(self.allowed_borrow_value, allowed_borrow_value); pack_decimal(self.unhealthy_borrow_value, unhealthy_borrow_value); *deposits_len = u8::try_from(self.deposits.len()).unwrap().to_le_bytes(); @@ -448,6 +508,7 @@ impl Pack for Obligation { borrowed_value, allowed_borrow_value, unhealthy_borrow_value, + borrowed_value_upper_bound, _padding, deposits_len, borrows_len, @@ -463,7 +524,8 @@ impl Pack for Obligation { 16, 16, 16, - 64, + 16, + 48, 1, 1, OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1)) @@ -524,6 +586,7 @@ impl Pack for Obligation { borrows, deposited_value: unpack_decimal(deposited_value), borrowed_value: unpack_decimal(borrowed_value), + borrowed_value_upper_bound: unpack_decimal(borrowed_value_upper_bound), allowed_borrow_value: unpack_decimal(allowed_borrow_value), unhealthy_borrow_value: unpack_decimal(unhealthy_borrow_value), }) @@ -535,6 +598,7 @@ mod test { use super::*; use crate::math::TryAdd; use proptest::prelude::*; + use solana_program::native_token::LAMPORTS_PER_SOL; const MAX_COMPOUNDED_INTEREST: u64 = 100; // 10,000% @@ -661,4 +725,276 @@ mod test { } } } + + #[test] + fn max_liquidation_amount_normal() { + let obligation_liquidity = ObligationLiquidity { + borrowed_amount_wads: Decimal::from(50u64), + market_value: Decimal::from(100u64), + ..ObligationLiquidity::default() + }; + + let obligation = Obligation { + deposited_value: Decimal::from(100u64), + borrowed_value: Decimal::from(100u64), + borrows: vec![obligation_liquidity.clone()], + ..Obligation::default() + }; + + let expected_collateral = Decimal::from(50u64) + .try_mul(Decimal::from(LIQUIDATION_CLOSE_FACTOR as u64)) + .unwrap() + .try_div(100) + .unwrap(); + + assert_eq!( + obligation + .max_liquidation_amount(&obligation_liquidity) + .unwrap(), + expected_collateral + ); + } + + #[test] + fn max_liquidation_amount_low_liquidity() { + let obligation_liquidity = ObligationLiquidity { + borrowed_amount_wads: Decimal::from(100u64), + market_value: Decimal::from(1u64), + ..ObligationLiquidity::default() + }; + + let obligation = Obligation { + deposited_value: Decimal::from(100u64), + borrowed_value: Decimal::from(100u64), + borrows: vec![obligation_liquidity.clone()], + ..Obligation::default() + }; + + assert_eq!( + obligation + .max_liquidation_amount(&obligation_liquidity) + .unwrap(), + Decimal::from(100u64) + ); + } + + #[test] + fn max_liquidation_amount_big_whale() { + let obligation_liquidity = ObligationLiquidity { + borrowed_amount_wads: Decimal::from(1_000_000_000u64), + market_value: Decimal::from(1_000_000_000u64), + ..ObligationLiquidity::default() + }; + + let obligation = Obligation { + deposited_value: Decimal::from(1_000_000_000u64), + borrowed_value: Decimal::from(1_000_000_000u64), + borrows: vec![obligation_liquidity.clone()], + ..Obligation::default() + }; + + assert_eq!( + obligation + .max_liquidation_amount(&obligation_liquidity) + .unwrap(), + Decimal::from(MAX_LIQUIDATABLE_VALUE_AT_ONCE) + ); + } + + #[derive(Debug, Clone)] + struct MaxWithdrawAmountTestCase { + obligation: Obligation, + reserve: Reserve, + + expected_max_withdraw_amount: u64, + } + + fn max_withdraw_amount_test_cases() -> impl Strategy { + prop_oneof![ + // borrowed as much as we can already, so can't borrow anything more + Just(MaxWithdrawAmountTestCase { + obligation: Obligation { + deposits: vec![ObligationCollateral { + deposited_amount: 20 * LAMPORTS_PER_SOL, + ..ObligationCollateral::default() + }], + borrows: vec![ObligationLiquidity { + borrowed_amount_wads: Decimal::from(10u64), + ..ObligationLiquidity::default() + }], + deposited_value: Decimal::from(100u64), + borrowed_value_upper_bound: Decimal::from(50u64), + allowed_borrow_value: Decimal::from(50u64), + ..Obligation::default() + }, + reserve: Reserve { + config: ReserveConfig { + loan_to_value_ratio: 50, + ..ReserveConfig::default() + }, + ..Reserve::default() + }, + expected_max_withdraw_amount: 0, + }), + // regular case + Just(MaxWithdrawAmountTestCase { + obligation: Obligation { + deposits: vec![ObligationCollateral { + deposited_amount: 20 * LAMPORTS_PER_SOL, + ..ObligationCollateral::default() + }], + borrows: vec![ObligationLiquidity { + borrowed_amount_wads: Decimal::from(10u64), + ..ObligationLiquidity::default() + }], + + allowed_borrow_value: Decimal::from(100u64), + borrowed_value_upper_bound: Decimal::from(50u64), + ..Obligation::default() + }, + + reserve: Reserve { + config: ReserveConfig { + loan_to_value_ratio: 50, + ..ReserveConfig::default() + }, + liquidity: ReserveLiquidity { + available_amount: 100 * LAMPORTS_PER_SOL, + borrowed_amount_wads: Decimal::zero(), + market_price: Decimal::from(10u64), + smoothed_market_price: Decimal::from(5u64), + mint_decimals: 9, + ..ReserveLiquidity::default() + }, + collateral: ReserveCollateral { + mint_total_supply: 50 * LAMPORTS_PER_SOL, + ..ReserveCollateral::default() + }, + ..Reserve::default() + }, + + // deposited 20 cSOL + // => allowed borrow value: 20 cSOL * 2(SOL/cSOL) * 0.5(ltv) * $5 = $100 + // => borrowed value upper bound: $50 + // => max withdraw value: ($100 - $50) / 0.5 = $100 + // => max withdraw liquidity amount: $100 / $5 = 20 SOL + // => max withdraw collateral amount: 20 SOL / 2(SOL/cSOL) = 10 cSOL + // after withdrawing, the new allowed borrow value is: + // 10 cSOL * 2(SOL/cSOL) * 0.5(ltv) * $5 = $50, which is exactly what we want. + expected_max_withdraw_amount: 10 * LAMPORTS_PER_SOL, // 10 cSOL + }), + // same case as above but this time we didn't deposit that much collateral + Just(MaxWithdrawAmountTestCase { + obligation: Obligation { + deposits: vec![ObligationCollateral { + deposited_amount: 2 * LAMPORTS_PER_SOL, + ..ObligationCollateral::default() + }], + borrows: vec![ObligationLiquidity { + borrowed_amount_wads: Decimal::from(10u64), + ..ObligationLiquidity::default() + }], + + allowed_borrow_value: Decimal::from(100u64), + borrowed_value_upper_bound: Decimal::from(50u64), + ..Obligation::default() + }, + + reserve: Reserve { + config: ReserveConfig { + loan_to_value_ratio: 50, + ..ReserveConfig::default() + }, + liquidity: ReserveLiquidity { + available_amount: 100 * LAMPORTS_PER_SOL, + borrowed_amount_wads: Decimal::zero(), + market_price: Decimal::from(10u64), + smoothed_market_price: Decimal::from(5u64), + mint_decimals: 9, + ..ReserveLiquidity::default() + }, + collateral: ReserveCollateral { + mint_total_supply: 50 * LAMPORTS_PER_SOL, + ..ReserveCollateral::default() + }, + ..Reserve::default() + }, + + expected_max_withdraw_amount: 2 * LAMPORTS_PER_SOL, + }), + // no borrows so we can withdraw everything + Just(MaxWithdrawAmountTestCase { + obligation: Obligation { + deposits: vec![ObligationCollateral { + deposited_amount: 100 * LAMPORTS_PER_SOL, + ..ObligationCollateral::default() + }], + + allowed_borrow_value: Decimal::from(100u64), + ..Obligation::default() + }, + + reserve: Reserve { + config: ReserveConfig { + loan_to_value_ratio: 50, + ..ReserveConfig::default() + }, + ..Reserve::default() + }, + expected_max_withdraw_amount: 100 * LAMPORTS_PER_SOL, + }), + // ltv is 0 and the obligation is healthy so we can withdraw everything + Just(MaxWithdrawAmountTestCase { + obligation: Obligation { + deposits: vec![ObligationCollateral { + deposited_amount: 100 * LAMPORTS_PER_SOL, + ..ObligationCollateral::default() + }], + borrows: vec![ObligationLiquidity { + borrowed_amount_wads: Decimal::from(10u64), + ..ObligationLiquidity::default() + }], + + allowed_borrow_value: Decimal::from(100u64), + borrowed_value_upper_bound: Decimal::from(50u64), + ..Obligation::default() + }, + + reserve: Reserve::default(), + expected_max_withdraw_amount: 100 * LAMPORTS_PER_SOL, + }), + // ltv is 0 but the obligation is unhealthy so we can't withdraw anything + Just(MaxWithdrawAmountTestCase { + obligation: Obligation { + deposits: vec![ObligationCollateral { + deposited_amount: 100 * LAMPORTS_PER_SOL, + ..ObligationCollateral::default() + }], + borrows: vec![ObligationLiquidity { + borrowed_amount_wads: Decimal::from(10u64), + ..ObligationLiquidity::default() + }], + + allowed_borrow_value: Decimal::from(100u64), + borrowed_value_upper_bound: Decimal::from(100u64), + ..Obligation::default() + }, + + reserve: Reserve::default(), + expected_max_withdraw_amount: 0, + }), + ] + } + + proptest! { + #[test] + fn max_withdraw_amount(test_case in max_withdraw_amount_test_cases()) { + let max_withdraw_amount = test_case.obligation.max_withdraw_amount( + &test_case.obligation.deposits[0], + &test_case.reserve, + ).unwrap(); + + assert_eq!(max_withdraw_amount, test_case.expected_max_withdraw_amount); + } + } } diff --git a/token-lending/sdk/src/state/rate_limiter.rs b/token-lending/sdk/src/state/rate_limiter.rs new file mode 100644 index 00000000000..d075e514b79 --- /dev/null +++ b/token-lending/sdk/src/state/rate_limiter.rs @@ -0,0 +1,236 @@ +use crate::state::{pack_decimal, unpack_decimal}; +use solana_program::msg; +use solana_program::program_pack::IsInitialized; +use solana_program::{program_error::ProgramError, slot_history::Slot}; + +use crate::{ + error::LendingError, + math::{Decimal, TryAdd, TryDiv, TryMul, TrySub}, +}; +use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; +use solana_program::program_pack::{Pack, Sealed}; + +/// Sliding Window Rate limiter +/// guarantee: at any point, the outflow between [cur_slot - slot.window_duration, cur_slot] +/// is less than 2x max_outflow. + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RateLimiter { + /// configuration parameters + pub config: RateLimiterConfig, + + // state + /// prev qty is the sum of all outflows from [window_start - config.window_duration, window_start) + prev_qty: Decimal, + /// window_start is the start of the current window + window_start: Slot, + /// cur qty is the sum of all outflows from [window_start, window_start + config.window_duration) + cur_qty: Decimal, +} + +/// Lending market configuration parameters +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct RateLimiterConfig { + /// Rate limiter window size in slots + pub window_duration: u64, + /// Rate limiter param. Max outflow of tokens in a window + pub max_outflow: u64, +} + +impl Default for RateLimiterConfig { + fn default() -> Self { + Self { + window_duration: 1, + max_outflow: u64::MAX, + } + } +} + +impl RateLimiter { + /// initialize rate limiter + pub fn new(config: RateLimiterConfig, cur_slot: u64) -> Self { + let slot_start = if config.window_duration != 0 { + cur_slot / config.window_duration * config.window_duration + } else { + cur_slot + }; + + Self { + config, + prev_qty: Decimal::zero(), + window_start: slot_start, + cur_qty: Decimal::zero(), + } + } + + /// update rate limiter with new quantity. errors if rate limit has been reached + pub fn update(&mut self, cur_slot: u64, qty: Decimal) -> Result<(), ProgramError> { + if cur_slot < self.window_start { + msg!("Current slot is less than window start, which is impossible"); + return Err(LendingError::InvalidAccountInput.into()); + } + + // rate limiter is disabled if window duration == 0. this is here because we don't want to + // brick borrows/withdraws in permissionless pools on program upgrade. + if self.config.window_duration == 0 { + return Ok(()); + } + + // floor wrt window duration + let cur_slot_start = cur_slot / self.config.window_duration * self.config.window_duration; + + // update prev window, current window + match cur_slot_start.cmp(&(self.window_start + self.config.window_duration)) { + // |<-prev window->|<-cur window (cur_slot is in here)->| + std::cmp::Ordering::Less => (), + + // |<-prev window->|<-cur window->| (cur_slot is in here) | + std::cmp::Ordering::Equal => { + self.prev_qty = self.cur_qty; + self.window_start = cur_slot_start; + self.cur_qty = Decimal::zero(); + } + + // |<-prev window->|<-cur window->|<-cur window + 1->| ... | (cur_slot is in here) | + std::cmp::Ordering::Greater => { + self.prev_qty = Decimal::zero(); + self.window_start = cur_slot_start; + self.cur_qty = Decimal::zero(); + } + }; + + // assume the prev_window's outflow is even distributed across the window + // this isn't true, but it's a good enough approximation + let prev_weight = Decimal::from(self.config.window_duration) + .try_sub(Decimal::from(cur_slot - self.window_start + 1))? + .try_div(self.config.window_duration)?; + let cur_outflow = prev_weight.try_mul(self.prev_qty)?.try_add(self.cur_qty)?; + + if cur_outflow.try_add(qty)? > Decimal::from(self.config.max_outflow) { + Err(LendingError::OutflowRateLimitExceeded.into()) + } else { + self.cur_qty = self.cur_qty.try_add(qty)?; + Ok(()) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_rate_limiter() { + let mut rate_limiter = RateLimiter::new( + RateLimiterConfig { + window_duration: 10, + max_outflow: 100, + }, + 10, + ); + + assert_eq!( + rate_limiter.update(9, Decimal::from(1u64)), + Err(LendingError::InvalidAccountInput.into()) + ); + + // case 1: no prev window, all quantity is taken up in first slot + assert_eq!( + rate_limiter.update(10, Decimal::from(101u64)), + Err(LendingError::OutflowRateLimitExceeded.into()) + ); + assert_eq!(rate_limiter.update(10, Decimal::from(100u64)), Ok(())); + for i in 11..20 { + assert_eq!( + rate_limiter.update(i, Decimal::from(1u64)), + Err(LendingError::OutflowRateLimitExceeded.into()) + ); + } + + // case 2: prev window qty affects cur window's allowed qty. exactly 10 qty frees up every + // slot. + for i in 20..30 { + assert_eq!( + rate_limiter.update(i, Decimal::from(11u64)), + Err(LendingError::OutflowRateLimitExceeded.into()) + ); + + assert_eq!(rate_limiter.update(i, Decimal::from(10u64)), Ok(())); + + assert_eq!( + rate_limiter.update(i, Decimal::from(1u64)), + Err(LendingError::OutflowRateLimitExceeded.into()) + ); + } + + // case 3: new slot is so far ahead, prev window is dropped + assert_eq!(rate_limiter.update(100, Decimal::from(10u64)), Ok(())); + for i in 101..109 { + assert_eq!(rate_limiter.update(i, Decimal::from(10u64)), Ok(())); + } + println!("{:#?}", rate_limiter); + } +} + +impl Default for RateLimiter { + fn default() -> Self { + Self::new( + RateLimiterConfig { + window_duration: 1, + max_outflow: u64::MAX, + }, + 1, + ) + } +} + +impl Sealed for RateLimiter {} + +impl IsInitialized for RateLimiter { + fn is_initialized(&self) -> bool { + true + } +} + +/// Size of RateLimiter when packed into account +pub const RATE_LIMITER_LEN: usize = 56; +impl Pack for RateLimiter { + const LEN: usize = RATE_LIMITER_LEN; + + fn pack_into_slice(&self, dst: &mut [u8]) { + let dst = array_mut_ref![dst, 0, RATE_LIMITER_LEN]; + let ( + config_max_outflow_dst, + config_window_duration_dst, + prev_qty_dst, + window_start_dst, + cur_qty_dst, + ) = mut_array_refs![dst, 8, 8, 16, 8, 16]; + *config_max_outflow_dst = self.config.max_outflow.to_le_bytes(); + *config_window_duration_dst = self.config.window_duration.to_le_bytes(); + pack_decimal(self.prev_qty, prev_qty_dst); + *window_start_dst = self.window_start.to_le_bytes(); + pack_decimal(self.cur_qty, cur_qty_dst); + } + + fn unpack_from_slice(src: &[u8]) -> Result { + let src = array_ref![src, 0, RATE_LIMITER_LEN]; + let ( + config_max_outflow_src, + config_window_duration_src, + prev_qty_src, + window_start_src, + cur_qty_src, + ) = array_refs![src, 8, 8, 16, 8, 16]; + + Ok(Self { + config: RateLimiterConfig { + max_outflow: u64::from_le_bytes(*config_max_outflow_src), + window_duration: u64::from_le_bytes(*config_window_duration_src), + }, + prev_qty: unpack_decimal(prev_qty_src), + window_start: u64::from_le_bytes(*window_start_src), + cur_qty: unpack_decimal(cur_qty_src), + }) + } +} diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/sdk/src/state/reserve.rs similarity index 73% rename from token-lending/program/src/state/reserve.rs rename to token-lending/sdk/src/state/reserve.rs index 3f9dd97bdda..9c7b6402580 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/sdk/src/state/reserve.rs @@ -13,7 +13,7 @@ use solana_program::{ pubkey::{Pubkey, PUBKEY_BYTES}, }; use std::{ - cmp::{min, Ordering}, + cmp::{max, min, Ordering}, convert::{TryFrom, TryInto}, }; @@ -41,6 +41,8 @@ pub struct Reserve { pub collateral: ReserveCollateral, /// Reserve configuration values pub config: ReserveConfig, + /// Outflow Rate Limiter (denominated in tokens) + pub rate_limiter: RateLimiter, } impl Reserve { @@ -59,6 +61,71 @@ impl Reserve { self.liquidity = params.liquidity; self.collateral = params.collateral; self.config = params.config; + self.rate_limiter = RateLimiter::new(params.rate_limiter_config, params.current_slot); + } + + /// get borrow weight. Guaranteed to be greater than 1 + pub fn borrow_weight(&self) -> Decimal { + Decimal::one() + .try_add(Decimal::from_bps(self.config.added_borrow_weight_bps)) + .unwrap() + } + + /// get loan to value ratio as a Rate + pub fn loan_to_value_ratio(&self) -> Rate { + Rate::from_percent(self.config.loan_to_value_ratio) + } + + /// find current market value of tokens + pub fn market_value(&self, liquidity_amount: Decimal) -> Result { + self.liquidity + .market_price + .try_mul(liquidity_amount)? + .try_div(Decimal::from( + (10u128) + .checked_pow(self.liquidity.mint_decimals as u32) + .ok_or(LendingError::MathOverflow)?, + )) + } + + /// find the current upper bound market value of tokens. + /// ie max(market_price, smoothed_market_price) * liquidity_amount + pub fn market_value_upper_bound( + &self, + liquidity_amount: Decimal, + ) -> Result { + let price_upper_bound = std::cmp::max( + self.liquidity.market_price, + self.liquidity.smoothed_market_price, + ); + + price_upper_bound + .try_mul(liquidity_amount)? + .try_div(Decimal::from( + (10u128) + .checked_pow(self.liquidity.mint_decimals as u32) + .ok_or(LendingError::MathOverflow)?, + )) + } + + /// find the current lower bound market value of tokens. + /// ie min(market_price, smoothed_market_price) * liquidity_amount + pub fn market_value_lower_bound( + &self, + liquidity_amount: Decimal, + ) -> Result { + let price_lower_bound = std::cmp::min( + self.liquidity.market_price, + self.liquidity.smoothed_market_price, + ); + + price_lower_bound + .try_mul(liquidity_amount)? + .try_div(Decimal::from( + (10u128) + .checked_pow(self.liquidity.mint_decimals as u32) + .ok_or(LendingError::MathOverflow)?, + )) } /// Record deposited liquidity and return amount of collateral tokens to mint @@ -168,7 +235,11 @@ impl Reserve { if amount_to_borrow == u64::MAX { let borrow_amount = max_borrow_value .try_mul(decimals)? - .try_div(self.liquidity.market_price)? + .try_div(max( + self.liquidity.market_price, + self.liquidity.smoothed_market_price, + ))? + .try_div(self.borrow_weight())? .min(remaining_reserve_borrow) .min(self.liquidity.available_amount.into()); let (borrow_fee, host_fee) = self @@ -195,9 +266,9 @@ impl Reserve { .calculate_borrow_fees(borrow_amount, FeeCalculation::Exclusive)?; let borrow_amount = borrow_amount.try_add(borrow_fee.into())?; - let borrow_value = borrow_amount - .try_mul(self.liquidity.market_price)? - .try_div(decimals)?; + let borrow_value = self + .market_value_upper_bound(borrow_amount)? + .try_mul(self.borrow_weight())?; if borrow_value > max_borrow_value { msg!("Borrow value cannot exceed maximum borrow value"); return Err(LendingError::BorrowTooLarge.into()); @@ -327,8 +398,12 @@ impl Reserve { let bonus = amount_liquidated_wads.try_sub(amount_liquidated_wads.try_div(bonus_rate)?)?; // After deploying must update all reserves to set liquidation fee then redeploy with this line instead of hardcode - // let protocol_fee = max(bonus.try_mul(Rate::from_percent(self.config.protocol_liquidation_fee))?.try_ceil_u64()?, 1); - let protocol_fee = std::cmp::max(bonus.try_mul(Rate::from_percent(0))?.try_ceil_u64()?, 1); + let protocol_fee = std::cmp::max( + bonus + .try_mul(Rate::from_percent(self.config.protocol_liquidation_fee))? + .try_ceil_u64()?, + 1, + ); Ok(protocol_fee) } @@ -355,10 +430,12 @@ pub struct InitReserveParams { pub collateral: ReserveCollateral, /// Reserve configuration values pub config: ReserveConfig, + /// rate limiter config + pub rate_limiter_config: RateLimiterConfig, } /// Calculate borrow result -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct CalculateBorrowResult { /// Total amount of borrow including fees pub borrow_amount: Decimal, @@ -380,7 +457,7 @@ pub struct CalculateRepayResult { } /// Calculate liquidation result -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct CalculateLiquidationResult { /// Amount of liquidity that is settled from the obligation. It includes /// the amount of loan that was defaulted if collateral is depleted. @@ -392,7 +469,7 @@ pub struct CalculateLiquidationResult { } /// Reserve liquidity -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct ReserveLiquidity { /// Reserve liquidity mint address pub mint_pubkey: Pubkey, @@ -414,6 +491,8 @@ pub struct ReserveLiquidity { pub accumulated_protocol_fees_wads: Decimal, /// Reserve liquidity market price in quote currency pub market_price: Decimal, + /// Smoothed reserve liquidity market price for the liquidity (eg TWAP, VWAP, EMA) + pub smoothed_market_price: Decimal, } impl ReserveLiquidity { @@ -430,6 +509,7 @@ impl ReserveLiquidity { cumulative_borrow_rate_wads: Decimal::one(), accumulated_protocol_fees_wads: Decimal::zero(), market_price: params.market_price, + smoothed_market_price: params.smoothed_market_price, } } @@ -507,10 +587,13 @@ impl ReserveLiquidity { /// Calculate the liquidity utilization rate of the reserve pub fn utilization_rate(&self) -> Result { let total_supply = self.total_supply()?; - if total_supply == Decimal::zero() { + if total_supply == Decimal::zero() || self.borrowed_amount_wads == Decimal::zero() { return Ok(Rate::zero()); } - self.borrowed_amount_wads.try_div(total_supply)?.try_into() + let denominator = self + .borrowed_amount_wads + .try_add(Decimal::from(self.available_amount))?; + self.borrowed_amount_wads.try_div(denominator)?.try_into() } /// Compound current borrow rate over elapsed slots @@ -556,10 +639,12 @@ pub struct NewReserveLiquidityParams { pub switchboard_oracle_pubkey: Pubkey, /// Reserve liquidity market price in quote currency pub market_price: Decimal, + /// Smoothed reserve liquidity market price in quote currency + pub smoothed_market_price: Decimal, } /// Reserve collateral -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct ReserveCollateral { /// Reserve collateral mint address pub mint_pubkey: Pubkey, @@ -662,7 +747,7 @@ impl From for Rate { } /// Reserve configuration values -#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct ReserveConfig { /// Optimal utilization rate, as a percentage pub optimal_utilization_rate: u8, @@ -691,6 +776,9 @@ pub struct ReserveConfig { pub protocol_liquidation_fee: u8, /// Protocol take rate is the amount borrowed interest protocol recieves, as a percentage pub protocol_take_rate: u8, + /// Added borrow weight in basis points. THIS FIELD SHOULD NEVER BE USED DIRECTLY. Always use + /// borrow_weight() + pub added_borrow_weight_bps: u64, } /// Additional fee information on a reserve @@ -698,7 +786,7 @@ pub struct ReserveConfig { /// These exist separately from interest accrual fees, and are specifically for the program owner /// and frontend host. The fees are paid out as a percentage of liquidity token amounts during /// repayments and liquidations. -#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct ReserveFees { /// Fee assessed on `BorrowObligationLiquidity`, expressed as a Wad. /// Must be between 0 and 10^18, such that 10^18 = 1. A few examples for @@ -729,11 +817,16 @@ impl ReserveFees { &self, flash_loan_amount: Decimal, ) -> Result<(u64, u64), ProgramError> { - self.calculate_fees( + let (total_fees, host_fee) = self.calculate_fees( flash_loan_amount, self.flash_loan_fee_wad, FeeCalculation::Exclusive, - ) + )?; + + let origination_fee = total_fees + .checked_sub(host_fee) + .ok_or(LendingError::MathOverflow)?; + Ok((origination_fee, host_fee)) } fn calculate_fees( @@ -842,6 +935,9 @@ impl Pack for Reserve { config_protocol_liquidation_fee, config_protocol_take_rate, liquidity_accumulated_protocol_fees_wads, + rate_limiter, + config_added_borrow_weight_bps, + liquidity_smoothed_market_price, _padding, ) = mut_array_refs![ output, @@ -877,7 +973,10 @@ impl Pack for Reserve { 1, 1, 16, - 230 + RATE_LIMITER_LEN, + 8, + 16, + 150 ]; // reserve @@ -907,6 +1006,10 @@ impl Pack for Reserve { liquidity_accumulated_protocol_fees_wads, ); pack_decimal(self.liquidity.market_price, liquidity_market_price); + pack_decimal( + self.liquidity.smoothed_market_price, + liquidity_smoothed_market_price, + ); // collateral collateral_mint_pubkey.copy_from_slice(self.collateral.mint_pubkey.as_ref()); @@ -929,6 +1032,10 @@ impl Pack for Reserve { config_fee_receiver.copy_from_slice(self.config.fee_receiver.as_ref()); *config_protocol_liquidation_fee = self.config.protocol_liquidation_fee.to_le_bytes(); *config_protocol_take_rate = self.config.protocol_take_rate.to_le_bytes(); + + self.rate_limiter.pack_into_slice(rate_limiter); + + *config_added_borrow_weight_bps = self.config.added_borrow_weight_bps.to_le_bytes(); } /// Unpacks a byte buffer into a [ReserveInfo](struct.ReserveInfo.html). @@ -968,6 +1075,9 @@ impl Pack for Reserve { config_protocol_liquidation_fee, config_protocol_take_rate, liquidity_accumulated_protocol_fees_wads, + rate_limiter, + config_added_borrow_weight_bps, + liquidity_smoothed_market_price, _padding, ) = array_refs![ input, @@ -1003,7 +1113,10 @@ impl Pack for Reserve { 1, 1, 16, - 230 + RATE_LIMITER_LEN, + 8, + 16, + 150 ]; let version = u8::from_le_bytes(*version); @@ -1034,6 +1147,7 @@ impl Pack for Reserve { liquidity_accumulated_protocol_fees_wads, ), market_price: unpack_decimal(liquidity_market_price), + smoothed_market_price: unpack_decimal(liquidity_smoothed_market_price), }, collateral: ReserveCollateral { mint_pubkey: Pubkey::new_from_array(*collateral_mint_pubkey), @@ -1058,7 +1172,9 @@ impl Pack for Reserve { fee_receiver: Pubkey::new_from_array(*config_fee_receiver), protocol_liquidation_fee: u8::from_le_bytes(*config_protocol_liquidation_fee), protocol_take_rate: u8::from_le_bytes(*config_protocol_take_rate), + added_borrow_weight_bps: u64::from_le_bytes(*config_added_borrow_weight_bps), }, + rate_limiter: RateLimiter::unpack_from_slice(rate_limiter)?, }) } } @@ -1068,7 +1184,9 @@ mod test { use super::*; use crate::math::{PERCENT_SCALER, WAD}; use proptest::prelude::*; + use solana_program::native_token::LAMPORTS_PER_SOL; use std::cmp::Ordering; + use std::default::Default; const MAX_LIQUIDITY: u64 = u64::MAX / 5; @@ -1325,25 +1443,22 @@ mod test { flash_loan_fee_wad, host_fee_percentage, }; - let (total_fee, host_fee) = fees.calculate_flash_loan_fees(Decimal::from(borrow_amount))?; + let (origination_fee, host_fee) = fees.calculate_flash_loan_fees(Decimal::from(borrow_amount))?; // The total fee can't be greater than the amount borrowed, as long // as amount borrowed is greater than 2. // At a borrow amount of 2, we can get a total fee of 2 if a host // fee is also specified. - assert!(total_fee <= borrow_amount); - - // the host fee can't be greater than the total fee - assert!(host_fee <= total_fee); + assert!(origination_fee + host_fee <= borrow_amount); // for all fee rates greater than 0, we must have some fee if borrow_fee_wad > 0 { - assert!(total_fee > 0); + assert!(origination_fee + host_fee > 0); } if host_fee_percentage == 100 { // if the host fee percentage is maxed at 100%, it should get all the fee - assert_eq!(host_fee, total_fee); + assert_eq!(origination_fee, 0); } // if there's a host fee and some borrow fee, host fee must be greater than 0 @@ -1443,4 +1558,349 @@ mod test { assert_eq!(total_fee, 10); // 1% of 1000 assert_eq!(host_fee, 0); // 0 host fee } + + #[derive(Debug, Clone)] + struct LiquidationTestCase { + deposit_amount: u64, + deposit_market_value: u64, + borrow_amount: u64, + borrow_market_value: u64, + liquidation_result: CalculateLiquidationResult, + } + + fn calculate_liquidation_test_cases() -> impl Strategy { + let close_factor: Decimal = Rate::from_percent(LIQUIDATION_CLOSE_FACTOR) + .try_into() + .unwrap(); + let liquidation_bonus: Decimal = Rate::from_percent(5) + .try_add(Rate::one()) + .unwrap() + .try_into() + .unwrap(); + + prop_oneof![ + // collateral market value > liquidation value + Just(LiquidationTestCase { + deposit_amount: 1000, + deposit_market_value: 100, + borrow_amount: 800, + borrow_market_value: 80, + liquidation_result: CalculateLiquidationResult { + settle_amount: close_factor.try_mul(Decimal::from(800u64)).unwrap(), + repay_amount: close_factor + .try_mul(Decimal::from(800u64)) + .unwrap() + .try_ceil_u64() + .unwrap(), + withdraw_amount: close_factor + .try_mul(liquidation_bonus) + .unwrap() + .try_mul(Decimal::from(800u64)) + .unwrap() + .try_floor_u64() + .unwrap(), + }, + }), + // collateral market value == liquidation_value + Just(LiquidationTestCase { + borrow_amount: 8000, + borrow_market_value: 8000, + deposit_amount: (8000 * LIQUIDATION_CLOSE_FACTOR as u64) * 105 / 10000, + deposit_market_value: (8000 * LIQUIDATION_CLOSE_FACTOR as u64) * 105 / 10000, + + liquidation_result: CalculateLiquidationResult { + settle_amount: Decimal::from((8000 * LIQUIDATION_CLOSE_FACTOR as u64) / 100), + repay_amount: (8000 * LIQUIDATION_CLOSE_FACTOR as u64) / 100, + withdraw_amount: (8000 * LIQUIDATION_CLOSE_FACTOR as u64) * 105 / 10000, + }, + }), + // collateral market value < liquidation_value + Just(LiquidationTestCase { + borrow_amount: 8000, + borrow_market_value: 8000, + + // half of liquidation value + deposit_amount: (8000 * LIQUIDATION_CLOSE_FACTOR as u64) * 105 / 10000 / 2, + deposit_market_value: (8000 * LIQUIDATION_CLOSE_FACTOR as u64) * 105 / 10000 / 2, + + liquidation_result: CalculateLiquidationResult { + settle_amount: Decimal::from( + (8000 * LIQUIDATION_CLOSE_FACTOR as u64) / 100 / 2 + ), + repay_amount: (8000 * LIQUIDATION_CLOSE_FACTOR as u64) / 100 / 2, + withdraw_amount: (8000 * LIQUIDATION_CLOSE_FACTOR as u64) * 105 / 10000 / 2, + }, + }), + // dust ObligationLiquidity where collateral market value > liquidation value + Just(LiquidationTestCase { + borrow_amount: 1, + borrow_market_value: 1000, + deposit_amount: 1000, + deposit_market_value: 2100, + + liquidation_result: CalculateLiquidationResult { + settle_amount: Decimal::from(1u64), + repay_amount: 1, + withdraw_amount: 500, + }, + }), + // dust ObligationLiquidity where collateral market value == liquidation value + Just(LiquidationTestCase { + borrow_amount: 1, + borrow_market_value: 1000, + deposit_amount: 1000, + deposit_market_value: 1050, + + liquidation_result: CalculateLiquidationResult { + settle_amount: Decimal::from(1u64), + repay_amount: 1, + withdraw_amount: 1000, + }, + }), + // dust ObligationLiquidity where collateral market value < liquidation value + Just(LiquidationTestCase { + borrow_amount: 1, + borrow_market_value: 1000, + deposit_amount: 1000, + deposit_market_value: 1000, + + liquidation_result: CalculateLiquidationResult { + settle_amount: Decimal::from(1u64), + repay_amount: 1, + withdraw_amount: 1000, + }, + }), + ] + } + + proptest! { + #[test] + fn calculate_liquidation(test_case in calculate_liquidation_test_cases()) { + let reserve = Reserve { + config: ReserveConfig { + liquidation_bonus: 5, + ..ReserveConfig::default() + }, + ..Reserve::default() + }; + + let obligation = Obligation { + deposits: vec![ObligationCollateral { + deposit_reserve: Pubkey::new_unique(), + deposited_amount: test_case.deposit_amount, + market_value: Decimal::from(test_case.deposit_market_value), + }], + borrows: vec![ObligationLiquidity { + borrow_reserve: Pubkey::new_unique(), + cumulative_borrow_rate_wads: Decimal::one(), + borrowed_amount_wads: Decimal::from(test_case.borrow_amount), + market_value: Decimal::from(test_case.borrow_market_value), + }], + borrowed_value: Decimal::from(test_case.borrow_market_value), + ..Obligation::default() + }; + + assert_eq!( + reserve.calculate_liquidation( + u64::MAX, &obligation, &obligation.borrows[0], &obligation.deposits[0]).unwrap(), + test_case.liquidation_result); + } + } + + #[derive(Debug, Clone)] + struct CalculateBorrowTestCase { + // args + borrow_amount: u64, + remaining_borrow_value: Decimal, + remaining_reserve_capacity: Decimal, + + // reserve state + market_price: Decimal, + smoothed_market_price: Decimal, + decimal: u8, + added_borrow_weight_bps: u64, + + borrow_fee_wad: u64, + host_fee: u8, + + result: Result, + } + + fn calculate_borrow_test_cases() -> impl Strategy { + // borrow fee is 1%, host fee is 20% on all test cases + prop_oneof![ + Just(CalculateBorrowTestCase { + borrow_amount: LAMPORTS_PER_SOL, + remaining_borrow_value: Decimal::from(10u64), + remaining_reserve_capacity: Decimal::from(LAMPORTS_PER_SOL * 10), + + market_price: Decimal::from(1u64), + smoothed_market_price: Decimal::from(1u64), + decimal: 9, + added_borrow_weight_bps: 0, + + borrow_fee_wad: 10_000_000_000_000_000, // 1% + host_fee: 20, + + result: Ok(CalculateBorrowResult { + borrow_amount: Decimal::from(LAMPORTS_PER_SOL * 101 / 100), + receive_amount: LAMPORTS_PER_SOL, + borrow_fee: LAMPORTS_PER_SOL / 100, + host_fee: LAMPORTS_PER_SOL / 100 / 100 * 20 + }), + }), + // borrow max + Just(CalculateBorrowTestCase { + borrow_amount: u64::MAX, + remaining_borrow_value: Decimal::from(10u64), + remaining_reserve_capacity: Decimal::from(LAMPORTS_PER_SOL * 101 / 100), + + market_price: Decimal::from(1u64), + smoothed_market_price: Decimal::from(1u64), + decimal: 9, + added_borrow_weight_bps: 0, + + borrow_fee_wad: 10_000_000_000_000_000, // 1% + host_fee: 20, + + result: Ok(CalculateBorrowResult { + borrow_amount: Decimal::from(LAMPORTS_PER_SOL * 101 / 100), + receive_amount: LAMPORTS_PER_SOL, + borrow_fee: LAMPORTS_PER_SOL / 100, + host_fee: LAMPORTS_PER_SOL / 100 / 100 * 20 + }), + }), + // borrow weight is 2, can only borrow 0.5 sol + Just(CalculateBorrowTestCase { + borrow_amount: LAMPORTS_PER_SOL / 2, + remaining_borrow_value: Decimal::from(1u64), + remaining_reserve_capacity: Decimal::from(LAMPORTS_PER_SOL), + + market_price: Decimal::from(1u64), + smoothed_market_price: Decimal::from(1u64), + decimal: 9, + added_borrow_weight_bps: 10_000, + + borrow_fee_wad: 0, + host_fee: 0, + + result: Ok(CalculateBorrowResult { + borrow_amount: Decimal::from(LAMPORTS_PER_SOL / 2), + receive_amount: LAMPORTS_PER_SOL / 2, + borrow_fee: 0, + host_fee: 0, + }), + }), + // borrow weight is 2, can only max borrow 0.5 sol + Just(CalculateBorrowTestCase { + borrow_amount: u64::MAX, + remaining_borrow_value: Decimal::from(1u64), + remaining_reserve_capacity: Decimal::from(LAMPORTS_PER_SOL), + + market_price: Decimal::from(1u64), + smoothed_market_price: Decimal::from(1u64), + decimal: 9, + added_borrow_weight_bps: 10_000, + + borrow_fee_wad: 0, + host_fee: 0, + + result: Ok(CalculateBorrowResult { + borrow_amount: Decimal::from(LAMPORTS_PER_SOL / 2), + receive_amount: LAMPORTS_PER_SOL / 2, + borrow_fee: 0, + host_fee: 0, + }), + }), + // borrow max where ema price is 2x the market price + Just(CalculateBorrowTestCase { + borrow_amount: u64::MAX, + remaining_borrow_value: Decimal::from(100u64), + remaining_reserve_capacity: Decimal::from(100 * LAMPORTS_PER_SOL), + + market_price: Decimal::from(10u64), + smoothed_market_price: Decimal::from(20u64), + decimal: 9, + added_borrow_weight_bps: 0, + + borrow_fee_wad: 0, + host_fee: 0, + + result: Ok(CalculateBorrowResult { + borrow_amount: Decimal::from(5 * LAMPORTS_PER_SOL), + receive_amount: 5 * LAMPORTS_PER_SOL, + borrow_fee: 0, + host_fee: 0 + }), + }), + // borrow max where market price is 2x ema price + Just(CalculateBorrowTestCase { + borrow_amount: u64::MAX, + remaining_borrow_value: Decimal::from(100u64), + remaining_reserve_capacity: Decimal::from(100 * LAMPORTS_PER_SOL), + + market_price: Decimal::from(20u64), + smoothed_market_price: Decimal::from(10u64), + decimal: 9, + added_borrow_weight_bps: 0, + + borrow_fee_wad: 0, + host_fee: 0, + + result: Ok(CalculateBorrowResult { + borrow_amount: Decimal::from(5 * LAMPORTS_PER_SOL), + receive_amount: 5 * LAMPORTS_PER_SOL, + borrow_fee: 0, + host_fee: 0 + }), + }), + // borrow enough where it would be fine if we were just using the market price but + // not fine when using both market and ema price + Just(CalculateBorrowTestCase { + borrow_amount: 7 * LAMPORTS_PER_SOL, + remaining_borrow_value: Decimal::from(100u64), + remaining_reserve_capacity: Decimal::from(100 * LAMPORTS_PER_SOL), + + market_price: Decimal::from(10u64), + smoothed_market_price: Decimal::from(20u64), + decimal: 9, + added_borrow_weight_bps: 0, + + borrow_fee_wad: 0, + host_fee: 0, + + result: Err(LendingError::BorrowTooLarge.into()), + }), + ] + } + + proptest! { + #[test] + fn calculate_borrow(test_case in calculate_borrow_test_cases()) { + let reserve = Reserve { + config: ReserveConfig { + added_borrow_weight_bps: test_case.added_borrow_weight_bps, + fees: ReserveFees { + borrow_fee_wad: test_case.borrow_fee_wad, + host_fee_percentage: test_case.host_fee, + flash_loan_fee_wad: 0, + }, + ..ReserveConfig::default() + }, + liquidity: ReserveLiquidity { + mint_decimals: test_case.decimal, + market_price: test_case.market_price, + smoothed_market_price: test_case.smoothed_market_price, + available_amount: test_case.remaining_reserve_capacity.to_scaled_val().unwrap() as u64, + ..ReserveLiquidity::default() + }, + ..Reserve::default() + }; + assert_eq!(reserve.calculate_borrow( + test_case.borrow_amount, + test_case.remaining_borrow_value, + test_case.remaining_reserve_capacity, + ), test_case.result); + } + } }