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 32a3793d846..45cb4695d42 100644 Binary files a/token-lending/program/tests/fixtures/3Mnn2fX6rQyUsyELYms1sBJyChWofzSNRoqYzvgMVz5E.bin and b/token-lending/program/tests/fixtures/3Mnn2fX6rQyUsyELYms1sBJyChWofzSNRoqYzvgMVz5E.bin differ diff --git a/token-lending/program/tests/fixtures/6MEwdxe4g1NeAF9u6KDG14anJpFsVEa2cvr5H6iriFZ8.bin b/token-lending/program/tests/fixtures/6MEwdxe4g1NeAF9u6KDG14anJpFsVEa2cvr5H6iriFZ8.bin index 08c6d816cf1..9a8cbb7d823 100644 Binary files a/token-lending/program/tests/fixtures/6MEwdxe4g1NeAF9u6KDG14anJpFsVEa2cvr5H6iriFZ8.bin and b/token-lending/program/tests/fixtures/6MEwdxe4g1NeAF9u6KDG14anJpFsVEa2cvr5H6iriFZ8.bin differ diff --git a/token-lending/program/tests/fixtures/992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs.bin b/token-lending/program/tests/fixtures/992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs.bin index 65bf11a0f27..09deb80330b 100644 Binary files a/token-lending/program/tests/fixtures/992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs.bin and b/token-lending/program/tests/fixtures/992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs.bin differ 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); + } + } }