├── token-lending ├── program │ ├── program-id.md │ ├── Xargo.toml │ ├── proptest-regressions │ │ └── state │ │ │ └── reserve.txt │ ├── tests │ │ ├── fixtures │ │ │ ├── 3Mnn2fX6rQyUsyELYms1sBJyChWofzSNRoqYzvgMVz5E.bin │ │ │ ├── 6MEwdxe4g1NeAF9u6KDG14anJpFsVEa2cvr5H6iriFZ8.bin │ │ │ ├── 992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs.bin │ │ │ ├── AdtRGGhmqvom3Jemp5YNrxd9q9unX36BZk1pujkkXijL.bin │ │ │ ├── BAoygKcKN7wk8yKzLD6sxzUQUqLvhBV1rjMA4UJqfZuH.bin │ │ │ ├── CUgoqwiQ4wCt6Tthkrgx5saAEpLBjPCdHshVa4Pbfcx2.bin │ │ │ ├── GcNZRMqGSEyEULZnLDD3ParcHTgFBrNfUdUCDtThP55e.bin │ │ │ ├── GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR.bin │ │ │ ├── J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix.bin │ │ │ ├── lending_market.json │ │ │ ├── oracle_program_id.json │ │ │ ├── usdc_mint.json │ │ │ ├── lending_market_owner.json │ │ │ ├── srm_mint.json │ │ │ └── README.md │ │ ├── liquidate_obligation.rs │ │ ├── donate_to_reserve.rs │ │ ├── init_lending_market.rs │ │ ├── init_obligation.rs │ │ ├── helpers │ │ │ ├── genesis.rs │ │ │ ├── mock_switchboard_pull.rs │ │ │ ├── mock_switchboard.rs │ │ │ ├── mod.rs │ │ │ ├── flash_loan_receiver.rs │ │ │ ├── mock_pyth_pull.rs │ │ │ └── mock_pyth.rs │ │ ├── redeem_fees.rs │ │ ├── update_metadata.rs │ │ ├── repay_obligation_liquidity.rs │ │ ├── deposit_obligation_collateral.rs │ │ ├── obligation_end_to_end.rs │ │ ├── deposit_reserve_liquidity_and_obligation_collateral.rs │ │ ├── redeem_reserve_collateral.rs │ │ ├── withdraw_obligation_collateral.rs │ │ ├── deposit_reserve_liquidity.rs │ │ ├── set_lending_market_owner.rs │ │ └── outflow_rate_limits.rs │ ├── cbindgen.toml │ ├── src │ │ ├── entrypoint.rs │ │ └── lib.rs │ └── Cargo.toml ├── brick │ ├── Xargo.toml │ ├── src │ │ ├── lib.rs │ │ └── entrypoint.rs │ └── Cargo.toml ├── audit │ └── solend-audit-v1.0.pdf ├── sdk │ ├── src │ │ ├── math │ │ │ ├── mod.rs │ │ │ ├── common.rs │ │ │ └── rate.rs │ │ ├── lib.rs │ │ ├── state │ │ │ ├── last_update.rs │ │ │ ├── lending_market_metadata.rs │ │ │ ├── mod.rs │ │ │ └── lending_market.rs │ │ └── error.rs │ ├── fixtures │ │ └── 7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE.bin │ └── Cargo.toml ├── oracles │ ├── fixtures │ │ ├── 5CKzb9j4ChgLUt8Gfm5CNGLN6khXKiqMbnGAW4cgXgxK.bin │ │ └── 7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE.bin │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── switchboard.rs ├── cli │ ├── Cargo.toml │ ├── scripts │ │ ├── withdraw.sh │ │ └── liquidate.sh │ ├── README.md │ └── src │ │ └── lending_state.rs ├── flash_loan_design.md └── README.md ├── .gitignore ├── Cargo.toml ├── Anchor.toml ├── cbindgen.sh ├── ci ├── install-program-deps.sh ├── install-build-deps.sh ├── cargo-build-test.sh ├── cargo-test-bpf.sh ├── solana-version.sh ├── fuzz.sh ├── rust-version.sh └── env.sh ├── LICENSE ├── .travis.yml ├── coverage.sh ├── .github ├── dependabot.yml └── workflows │ ├── pull-request-token-lending.yml │ └── pull-request.yml ├── update-solana-dependencies.sh ├── .mergify.yml ├── README.md └── deploy_token_lending.sh /token-lending/program/program-id.md: -------------------------------------------------------------------------------- 1 | LendZqTs7gn5CTSJU1jWKhKuVpjJGom45nnwPb2AMTi 2 | -------------------------------------------------------------------------------- /token-lending/brick/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] -------------------------------------------------------------------------------- /token-lending/program/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] -------------------------------------------------------------------------------- /token-lending/program/proptest-regressions/state/reserve.txt: -------------------------------------------------------------------------------- 1 | cc 39c6d681e691839313426bb0801cf3f46d0befb69b568a28cf6d8e752d74526b 2 | -------------------------------------------------------------------------------- /token-lending/audit/solend-audit-v1.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solendprotocol/solana-program-library/HEAD/token-lending/audit/solend-audit-v1.0.pdf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .env 3 | .vscode 4 | bin 5 | config.json 6 | node_modules 7 | ./package-lock.json 8 | hfuzz_target 9 | hfuzz_workspace 10 | **/*.so 11 | **/.DS_Store 12 | test-ledger 13 | -------------------------------------------------------------------------------- /token-lending/sdk/src/math/mod.rs: -------------------------------------------------------------------------------- 1 | //! Math for preserving precision 2 | 3 | mod common; 4 | mod decimal; 5 | mod rate; 6 | 7 | pub use common::*; 8 | pub use decimal::*; 9 | pub use rate::*; 10 | -------------------------------------------------------------------------------- /token-lending/sdk/fixtures/7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solendprotocol/solana-program-library/HEAD/token-lending/sdk/fixtures/7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE.bin -------------------------------------------------------------------------------- /token-lending/oracles/fixtures/5CKzb9j4ChgLUt8Gfm5CNGLN6khXKiqMbnGAW4cgXgxK.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solendprotocol/solana-program-library/HEAD/token-lending/oracles/fixtures/5CKzb9j4ChgLUt8Gfm5CNGLN6khXKiqMbnGAW4cgXgxK.bin -------------------------------------------------------------------------------- /token-lending/oracles/fixtures/7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solendprotocol/solana-program-library/HEAD/token-lending/oracles/fixtures/7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE.bin -------------------------------------------------------------------------------- /token-lending/brick/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | 3 | //! A brick. 4 | 5 | pub use solana_program; 6 | 7 | solana_program::declare_id!("So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo"); 8 | 9 | pub mod entrypoint; 10 | -------------------------------------------------------------------------------- /token-lending/program/tests/fixtures/3Mnn2fX6rQyUsyELYms1sBJyChWofzSNRoqYzvgMVz5E.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solendprotocol/solana-program-library/HEAD/token-lending/program/tests/fixtures/3Mnn2fX6rQyUsyELYms1sBJyChWofzSNRoqYzvgMVz5E.bin -------------------------------------------------------------------------------- /token-lending/program/tests/fixtures/6MEwdxe4g1NeAF9u6KDG14anJpFsVEa2cvr5H6iriFZ8.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solendprotocol/solana-program-library/HEAD/token-lending/program/tests/fixtures/6MEwdxe4g1NeAF9u6KDG14anJpFsVEa2cvr5H6iriFZ8.bin -------------------------------------------------------------------------------- /token-lending/program/tests/fixtures/992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solendprotocol/solana-program-library/HEAD/token-lending/program/tests/fixtures/992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs.bin -------------------------------------------------------------------------------- /token-lending/program/tests/fixtures/AdtRGGhmqvom3Jemp5YNrxd9q9unX36BZk1pujkkXijL.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solendprotocol/solana-program-library/HEAD/token-lending/program/tests/fixtures/AdtRGGhmqvom3Jemp5YNrxd9q9unX36BZk1pujkkXijL.bin -------------------------------------------------------------------------------- /token-lending/program/tests/fixtures/BAoygKcKN7wk8yKzLD6sxzUQUqLvhBV1rjMA4UJqfZuH.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solendprotocol/solana-program-library/HEAD/token-lending/program/tests/fixtures/BAoygKcKN7wk8yKzLD6sxzUQUqLvhBV1rjMA4UJqfZuH.bin -------------------------------------------------------------------------------- /token-lending/program/tests/fixtures/CUgoqwiQ4wCt6Tthkrgx5saAEpLBjPCdHshVa4Pbfcx2.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solendprotocol/solana-program-library/HEAD/token-lending/program/tests/fixtures/CUgoqwiQ4wCt6Tthkrgx5saAEpLBjPCdHshVa4Pbfcx2.bin -------------------------------------------------------------------------------- /token-lending/program/tests/fixtures/GcNZRMqGSEyEULZnLDD3ParcHTgFBrNfUdUCDtThP55e.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solendprotocol/solana-program-library/HEAD/token-lending/program/tests/fixtures/GcNZRMqGSEyEULZnLDD3ParcHTgFBrNfUdUCDtThP55e.bin -------------------------------------------------------------------------------- /token-lending/program/tests/fixtures/GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solendprotocol/solana-program-library/HEAD/token-lending/program/tests/fixtures/GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR.bin -------------------------------------------------------------------------------- /token-lending/program/tests/fixtures/J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solendprotocol/solana-program-library/HEAD/token-lending/program/tests/fixtures/J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix.bin -------------------------------------------------------------------------------- /token-lending/program/tests/fixtures/lending_market.json: -------------------------------------------------------------------------------- 1 | [203,123,29,107,44,194,199,159,73,87,44,135,219,143,105,205,50,46,221,112,57,204,38,172,131,203,182,189,62,76,57,3,81,199,160,36,169,53,203,113,246,98,0,64,242,171,158,252,244,178,125,153,52,75,148,210,143,30,142,191,135,52,242,25] -------------------------------------------------------------------------------- /token-lending/program/tests/fixtures/oracle_program_id.json: -------------------------------------------------------------------------------- 1 | [34,165,56,243,236,153,203,167,72,136,131,212,217,45,33,184,51,188,81,218,245,67,252,172,250,244,94,239,138,13,166,47,132,99,44,63,242,187,236,116,168,172,11,28,66,228,151,55,166,71,44,51,64,111,49,62,187,222,61,97,138,87,87,216] -------------------------------------------------------------------------------- /token-lending/program/tests/fixtures/usdc_mint.json: -------------------------------------------------------------------------------- 1 | [54,145,24,15,126,194,110,22,253,254,251,8,199,129,146,68,215,154,174,126,138,116,37,4,117,86,129,129,89,175,244,126,148,200,124,73,201,204,74,241,165,229,204,237,90,194,169,78,162,215,149,186,235,67,211,112,2,102,233,153,22,216,200,163] -------------------------------------------------------------------------------- /token-lending/program/tests/fixtures/lending_market_owner.json: -------------------------------------------------------------------------------- 1 | [237,94,30,208,149,9,211,113,89,130,186,215,210,240,152,201,225,9,164,38,6,66,245,251,44,121,38,3,210,200,217,24,114,133,232,227,86,67,182,15,253,36,214,87,201,19,105,189,111,157,211,250,12,167,115,73,3,116,254,73,245,75,104,105] -------------------------------------------------------------------------------- /token-lending/program/tests/fixtures/srm_mint.json: -------------------------------------------------------------------------------- 1 | [58,208,121,211,147,191,252,194,27,112,242,181,234,8,178,221,146,159,34,49,159,191,43,185,225,149,79,50,132,251,191,160,190,232,3,127,76,66,126,248,122,226,243,163,63,22,80,127,18,193,145,126,162,197,113,216,136,135,137,237,47,207,231,220] -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "token-lending/cli", 4 | "token-lending/program", 5 | "token-lending/sdk", 6 | "token-lending/brick" 7 | , "token-lending/oracles"] 8 | 9 | [profile.dev] 10 | split-debuginfo = "unpacked" 11 | 12 | [profile.release] 13 | overflow-checks = true 14 | -------------------------------------------------------------------------------- /Anchor.toml: -------------------------------------------------------------------------------- 1 | anchor_version = "0.13.2" 2 | 3 | [workspace] 4 | members = [ 5 | "token-lending/program", 6 | "token-lending/brick", 7 | ] 8 | 9 | [provider] 10 | cluster = "mainnet" 11 | wallet = "~/.config/solana/id.json" 12 | 13 | [programs.mainnet] 14 | spl_token_lending = "So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo" 15 | -------------------------------------------------------------------------------- /cbindgen.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd "$(dirname "$0")" 4 | set -x 5 | 6 | # Cargo.lock can cause older spl-token bindings to be generated? Move it out of 7 | # the way... 8 | mv -f Cargo.lock Cargo.lock.org 9 | 10 | cargo run --manifest-path=utils/cgen/Cargo.toml 11 | exitcode=$? 12 | 13 | mv -f Cargo.lock.org Cargo.lock 14 | 15 | exit $exitcode 16 | -------------------------------------------------------------------------------- /token-lending/program/cbindgen.toml: -------------------------------------------------------------------------------- 1 | language = "C" 2 | header = "/* Autogenerated SPL Token-Lending program C Bindings */" 3 | pragma_once = true 4 | cpp_compat = true 5 | line_length = 80 6 | tab_width = 4 7 | style = "both" 8 | 9 | [export] 10 | prefix = "TokenLending_" 11 | include = ["LendingInstruction", "State"] 12 | 13 | [parse] 14 | parse_deps = true 15 | include = ["solana-sdk"] 16 | -------------------------------------------------------------------------------- /ci/install-program-deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | source ci/rust-version.sh stable 6 | source ci/solana-version.sh install 7 | 8 | set -x 9 | 10 | cargo --version 11 | cargo install rustfilt || true 12 | cargo install honggfuzz --version=0.5.52 --force || true 13 | cargo +"$rust_stable" install grcov --version=0.8.18 --force --locked 14 | 15 | cargo +"$rust_stable" build-bpf --version 16 | rustup component add llvm-tools-preview 17 | -------------------------------------------------------------------------------- /ci/install-build-deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - 6 | sudo apt-add-repository "deb http://apt.llvm.org/bionic/ llvm-toolchain-bionic-10 main" 7 | sudo apt-get update 8 | sudo apt-get install -y openssl --allow-unauthenticated 9 | sudo apt-get install -y libssl-dev --allow-unauthenticated 10 | sudo apt-get install -y libudev-dev 11 | sudo apt-get install -y binutils-dev 12 | sudo apt-get install -y libunwind-dev 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Solana Foundation. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | sudo: required 3 | 4 | branches: 5 | only: 6 | - master 7 | 8 | notifications: 9 | email: false 10 | 11 | jobs: 12 | include: 13 | # docs pull request or commit to master 14 | - name: "Build Docs" 15 | if: type IN (push, pull_request) AND branch = master 16 | language: node_js 17 | node_js: 18 | - "node" 19 | 20 | before_install: 21 | - .travis/affects.sh docs/ .travis || travis_terminate 0 22 | - cd docs/ 23 | - source .travis/before_install.sh 24 | script: 25 | - source .travis/script.sh 26 | -------------------------------------------------------------------------------- /token-lending/brick/src/entrypoint.rs: -------------------------------------------------------------------------------- 1 | //! Program entrypoint definitions 2 | 3 | #![cfg(all(target_arch = "bpf", not(feature = "no-entrypoint")))] 4 | 5 | use solana_program::{ 6 | account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg, 7 | program_error::ProgramError, pubkey::Pubkey, 8 | }; 9 | 10 | entrypoint!(process_instruction); 11 | fn process_instruction( 12 | _program_id: &Pubkey, 13 | _accounts: &[AccountInfo], 14 | _instruction_data: &[u8], 15 | ) -> ProgramResult { 16 | msg!("got me feeling bricked up"); 17 | 18 | Err(ProgramError::InvalidArgument) 19 | } 20 | -------------------------------------------------------------------------------- /token-lending/oracles/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oracles" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | pyth-sdk-solana = "0.8.0" 10 | pyth-solana-receiver-sdk = "0.3.0" 11 | solana-program = ">=1.9" 12 | anchor-lang = "0.28.0" 13 | solend-sdk = { path = "../sdk" } 14 | switchboard-on-demand = "0.1.12" 15 | switchboard-program = "0.2.0" 16 | switchboard-v2 = "0.1.3" 17 | 18 | [dev-dependencies] 19 | bytemuck = "1.5.1" 20 | assert_matches = "1.5.0" 21 | base64 = "0.13" 22 | log = "0.4.14" 23 | proptest = "1.0" 24 | solana-sdk = ">=1.9" 25 | serde = "1.0.140" 26 | serde_yaml = "0.8" 27 | rand = "0.8.5" 28 | -------------------------------------------------------------------------------- /token-lending/program/src/entrypoint.rs: -------------------------------------------------------------------------------- 1 | //! Program entrypoint definitions 2 | 3 | #![cfg(all(target_arch = "bpf", not(feature = "no-entrypoint")))] 4 | 5 | use crate::{error::LendingError, processor}; 6 | use solana_program::{ 7 | account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, 8 | program_error::PrintProgramError, pubkey::Pubkey, 9 | }; 10 | 11 | entrypoint!(process_instruction); 12 | fn process_instruction( 13 | program_id: &Pubkey, 14 | accounts: &[AccountInfo], 15 | instruction_data: &[u8], 16 | ) -> ProgramResult { 17 | if let Err(error) = processor::process_instruction(program_id, accounts, instruction_data) { 18 | // catch the error so we can print it 19 | error.print::(); 20 | return Err(error); 21 | } 22 | Ok(()) 23 | } 24 | -------------------------------------------------------------------------------- /token-lending/program/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | 3 | //! A lending program for the Solana blockchain. 4 | 5 | pub mod entrypoint; 6 | pub mod processor; 7 | pub use solend_sdk::{error, instruction, math, state}; 8 | 9 | // Export current sdk types for downstream users building with a different sdk version 10 | pub use solana_program; 11 | 12 | solana_program::declare_id!("So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo"); 13 | 14 | /// Canonical null pubkey. Prints out as "nu11111111111111111111111111111111111111111" 15 | pub const NULL_PUBKEY: solana_program::pubkey::Pubkey = 16 | solana_program::pubkey::Pubkey::new_from_array([ 17 | 11, 193, 238, 216, 208, 116, 241, 195, 55, 212, 76, 22, 75, 202, 40, 216, 76, 206, 27, 169, 18 | 138, 64, 177, 28, 19, 90, 156, 0, 0, 0, 0, 0, 19 | ]); 20 | -------------------------------------------------------------------------------- /token-lending/brick/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "brick" 3 | version = "1.0.1" 4 | description = "Solend Brick" 5 | authors = ["Solend Maintainers "] 6 | repository = "https://github.com/solendprotocol/solana-program-library" 7 | license = "Apache-2.0" 8 | edition = "2018" 9 | 10 | [features] 11 | no-entrypoint = [] 12 | test-bpf = [] 13 | 14 | [dependencies] 15 | solana-program = "=1.16.20" 16 | 17 | [dev-dependencies] 18 | assert_matches = "1.5.0" 19 | bytemuck = "1.5.1" 20 | base64 = "0.13" 21 | log = "0.4.14" 22 | proptest = "1.0" 23 | solana-program-test = "=1.16.20" 24 | solana-sdk = "=1.16.20" 25 | serde = ">=1.0.140" 26 | serde_yaml = "0.8" 27 | thiserror = "1.0" 28 | bincode = "1.3.3" 29 | borsh = "0.10.3" 30 | 31 | [lib] 32 | crate-type = ["cdylib", "lib"] 33 | 34 | [profile.release] 35 | lto = "fat" 36 | codegen-units = 1 37 | 38 | [profile.release.build-override] 39 | opt-level = 3 40 | incremental = false 41 | codegen-units = 1 42 | -------------------------------------------------------------------------------- /ci/cargo-build-test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | cd "$(dirname "$0")/.." 5 | 6 | source ./ci/rust-version.sh stable 7 | source ./ci/solana-version.sh 8 | 9 | export RUSTFLAGS="-D warnings" 10 | export RUSTBACKTRACE=1 11 | 12 | set -x 13 | 14 | 15 | # Build/test all host crates 16 | cargo +"$rust_stable" build 17 | cargo +"$rust_stable" test -- --nocapture 18 | 19 | # client_ristretto isn't in the workspace, test it explictly 20 | # client_ristretto disabled because it requires RpcBanksService, which is no longer supported. 21 | #cargo +"$rust_stable" test --manifest-path=themis/client_ristretto/Cargo.toml -- --nocapture 22 | 23 | # # Check generated C headers 24 | # cargo run --manifest-path=utils/cgen/Cargo.toml 25 | # 26 | # git diff --exit-code token/program/inc/token.h 27 | # cc token/program/inc/token.h -o target/token.gch 28 | # git diff --exit-code token-swap/program/inc/token-swap.h 29 | # cc token-swap/program/inc/token-swap.h -o target/token-swap.gch 30 | 31 | exit 0 32 | -------------------------------------------------------------------------------- /coverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Runs all program tests and builds a code coverage report 4 | # 5 | set -ex 6 | 7 | cd "$(dirname "$0")" 8 | 9 | if ! which grcov; then 10 | echo "Error: grcov not found. Try |cargo install grcov|" 11 | exit 1 12 | fi 13 | 14 | rm *.profraw || true 15 | rm **/**/*.profraw || true 16 | rm -r target/coverage || true 17 | 18 | # run tests with instrumented binary 19 | RUST_LOG="error" CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='cargo-test-%p-%m.profraw' cargo test --features test-bpf 20 | 21 | # generate report 22 | mkdir -p target/coverage/html 23 | 24 | grcov . --binary-path ./target/debug/deps/ -s . -t html --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/html 25 | 26 | grcov . --binary-path ./target/debug/deps/ -s . -t lcov --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/tests.lcov 27 | 28 | # cleanup 29 | rm *.profraw || true 30 | rm **/**/*.profraw || true 31 | -------------------------------------------------------------------------------- /token-lending/sdk/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | 3 | //! A lending program for the Solana blockchain. 4 | 5 | pub mod error; 6 | pub mod instruction; 7 | pub mod math; 8 | pub mod state; 9 | 10 | // Export current sdk types for downstream users building with a different sdk version 11 | pub use solana_program; 12 | 13 | /// mainnet program id 14 | pub mod solend_mainnet { 15 | solana_program::declare_id!("So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo"); 16 | } 17 | 18 | /// devnet program id 19 | pub mod solend_devnet { 20 | solana_program::declare_id!("So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo"); 21 | } 22 | 23 | /// Canonical null pubkey. Prints out as "nu11111111111111111111111111111111111111111" 24 | pub const NULL_PUBKEY: solana_program::pubkey::Pubkey = 25 | solana_program::pubkey::Pubkey::new_from_array([ 26 | 11, 193, 238, 216, 208, 116, 241, 195, 55, 212, 76, 22, 75, 202, 40, 216, 76, 206, 27, 169, 27 | 138, 64, 177, 28, 19, 90, 156, 0, 0, 0, 0, 0, 28 | ]); 29 | -------------------------------------------------------------------------------- /token-lending/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Solend Maintainers "] 3 | description = "Solend Program CLI" 4 | edition = "2018" 5 | homepage = "https://solend.fi" 6 | license = "Apache-2.0" 7 | name = "solend-program-cli" 8 | repository = "https://github.com/solendprotocol/solana-program-library" 9 | version = "2.0.2" 10 | 11 | [dependencies] 12 | clap = "=2.34.0" 13 | solana-clap-utils = "1.14.10" 14 | solana-cli-config = "1.14.10" 15 | solana-client = "1.14.10" 16 | solana-logger = "1.14.10" 17 | solana-sdk = "1.14.10" 18 | solana-program = "1.14.10" 19 | solend-sdk = { path="../sdk" } 20 | solend-program = { path="../program", features = [ "no-entrypoint" ] } 21 | spl-token = { version = "3.3.0", features=["no-entrypoint"] } 22 | spl-associated-token-account = "1.0" 23 | solana-account-decoder = "1.14.10" 24 | reqwest = { version = "0.12.2", features = ["blocking", "json"] } 25 | bincode = "1.3.3" 26 | serde_json = "1.0.120" 27 | 28 | [[bin]] 29 | name = "solend-cli" 30 | path = "src/main.rs" 31 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/token/js" 5 | schedule: 6 | interval: daily 7 | time: "01:00" 8 | timezone: America/Los_Angeles 9 | open-pull-requests-limit: 3 10 | labels: 11 | - "automerge" 12 | - package-ecosystem: npm 13 | directory: "/token-lending/js" 14 | schedule: 15 | interval: daily 16 | time: "02:00" 17 | timezone: America/Los_Angeles 18 | open-pull-requests-limit: 3 19 | labels: 20 | - "automerge" 21 | - package-ecosystem: npm 22 | directory: "/token-swap/js" 23 | schedule: 24 | interval: daily 25 | time: "03:00" 26 | timezone: America/Los_Angeles 27 | open-pull-requests-limit: 3 28 | labels: 29 | - "automerge" 30 | - package-ecosystem: cargo 31 | directory: "/" 32 | schedule: 33 | interval: daily 34 | time: "04:00" 35 | timezone: America/Los_Angeles 36 | labels: 37 | - "automerge" 38 | open-pull-requests-limit: 3 39 | ignore: 40 | - dependency-name: "cbindgen" 41 | -------------------------------------------------------------------------------- /token-lending/sdk/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solend-sdk" 3 | version = "2.0.2" 4 | description = "Solend Sdk" 5 | authors = ["Solend Maintainers "] 6 | repository = "https://github.com/solendprotocol/solana-program-library" 7 | license = "Apache-2.0" 8 | edition = "2018" 9 | 10 | [dependencies] 11 | arrayref = "0.3.6" 12 | borsh = "0.10.3" 13 | bytemuck = "1.5.1" 14 | num-derive = "0.3" 15 | num-traits = "0.2" 16 | solana-program = ">=1.9" 17 | spl-token = { version = "3.2.0", features=["no-entrypoint"] } 18 | static_assertions = "1.1.0" 19 | thiserror = "1.0" 20 | uint = "=0.9.1" 21 | 22 | [dev-dependencies] 23 | assert_matches = "1.5.0" 24 | base64 = "0.13" 25 | log = "0.4.14" 26 | proptest = "1.0" 27 | solana-sdk = ">=1.9" 28 | serde = ">=1.0.140" 29 | serde_yaml = "0.8" 30 | rand = "0.8.5" 31 | 32 | [lib] 33 | crate-type = ["cdylib", "lib"] 34 | 35 | [profile.release] 36 | lto = "fat" 37 | codegen-units = 1 38 | 39 | [profile.release.build-override] 40 | opt-level = 3 41 | incremental = false 42 | codegen-units = 1 43 | -------------------------------------------------------------------------------- /ci/cargo-test-bpf.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | cd "$(dirname "$0")/.." 5 | 6 | source ./ci/rust-version.sh stable 7 | source ./ci/solana-version.sh 8 | 9 | export RUSTFLAGS="-D warnings" 10 | export RUSTBACKTRACE=1 11 | 12 | 13 | usage() { 14 | exitcode=0 15 | if [[ -n "$1" ]]; then 16 | exitcode=1 17 | echo "Error: $*" 18 | fi 19 | echo "Usage: $0 [program-directory]" 20 | exit $exitcode 21 | } 22 | 23 | program_directory=$1 24 | if [[ -z $program_directory ]]; then 25 | usage "No program directory provided" 26 | fi 27 | 28 | set -x 29 | 30 | cd $program_directory 31 | run_dir=$(pwd) 32 | 33 | if [[ -d $run_dir/program ]]; then 34 | # Build/test just one BPF program 35 | cd $run_dir/program 36 | RUST_LOG="error" cargo +"$rust_stable" test-bpf --features test-bpf -j 1 -- --nocapture 37 | else 38 | # Build/test all BPF programs 39 | for directory in $(ls -d $run_dir/*/); do 40 | cd $directory 41 | RUST_LOG="error" cargo +"$rust_stable" test-bpf --features test-bpf -j 1 -- --nocapture 42 | done 43 | fi 44 | -------------------------------------------------------------------------------- /ci/solana-version.sh: -------------------------------------------------------------------------------- 1 | # 2 | # This file maintains the solana versions for use by CI. 3 | # 4 | # Obtain the environment variables without any automatic updating: 5 | # $ source ci/solana-version.sh 6 | # 7 | # Obtain the environment variables and install update: 8 | # $ source ci/solana-version.sh install 9 | 10 | # Then to access the solana version: 11 | # $ echo "$solana_version" 12 | # 13 | 14 | if [[ -n $SOLANA_VERSION ]]; then 15 | solana_version="$SOLANA_VERSION" 16 | else 17 | # we use 1.16.20 for sdk but this version has been deleted from the solana servers so we use 18 | # this version's CLI instead 19 | solana_version=v1.17.34 20 | fi 21 | 22 | export solana_version="$solana_version" 23 | export PATH="$HOME"/.local/share/solana/install/active_release/bin:"$PATH" 24 | 25 | if [[ -n $1 ]]; then 26 | case $1 in 27 | install) 28 | sh -c "$(curl -sSfL https://release.anza.xyz/$solana_version/install)" 29 | solana --version 30 | ;; 31 | *) 32 | echo "$0: Note: ignoring unknown argument: $1" >&2 33 | ;; 34 | esac 35 | fi 36 | -------------------------------------------------------------------------------- /update-solana-dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Updates the solana version in all the SPL crates 4 | # 5 | 6 | solana_ver=$1 7 | if [[ -z $solana_ver ]]; then 8 | echo "Usage: $0 " 9 | exit 1 10 | fi 11 | 12 | cd "$(dirname "$0")" 13 | 14 | sed -i'' -e "s#solana_version=v.*#solana_version=v${solana_ver}#" ./ci/solana-version.sh 15 | 16 | declare tomls=() 17 | while IFS='' read -r line; do tomls+=("$line"); done < <(find . -name Cargo.toml) 18 | 19 | crates=( 20 | solana-account-decoder 21 | solana-banks-client 22 | solana-banks-server 23 | solana-bpf-loader-program 24 | solana-clap-utils 25 | solana-cli-config 26 | solana-cli-output 27 | solana-client 28 | solana-core 29 | solana-logger 30 | solana-notifier 31 | solana-program 32 | solana-program-test 33 | solana-remote-wallet 34 | solana-runtime 35 | solana-sdk 36 | solana-stake-program 37 | solana-transaction-status 38 | solana-vote-program 39 | ) 40 | 41 | set -x 42 | for crate in "${crates[@]}"; do 43 | sed -i'' -e "s#\(${crate} = \"\)\(=\?\).*\(\"\)#\1\2$solana_ver\3#g" "${tomls[@]}" 44 | done 45 | -------------------------------------------------------------------------------- /token-lending/program/tests/liquidate_obligation.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "test-bpf")] 2 | 3 | mod helpers; 4 | 5 | use crate::solend_program_test::scenario_1; 6 | 7 | use helpers::*; 8 | use solana_program::instruction::InstructionError; 9 | 10 | use solana_program_test::*; 11 | 12 | use solana_sdk::transaction::TransactionError; 13 | use solend_program::error::LendingError; 14 | 15 | #[tokio::test] 16 | async fn test_fail_deprecated() { 17 | let (mut test, lending_market, usdc_reserve, wsol_reserve, user, obligation, _) = 18 | scenario_1(&test_reserve_config(), &test_reserve_config()).await; 19 | 20 | let res = lending_market 21 | .liquidate_obligation( 22 | &mut test, 23 | &wsol_reserve, 24 | &usdc_reserve, 25 | &obligation, 26 | &user, 27 | 1, 28 | ) 29 | .await 30 | .unwrap_err() 31 | .unwrap(); 32 | 33 | assert_eq!( 34 | res, 35 | TransactionError::InstructionError( 36 | 3, 37 | InstructionError::Custom(LendingError::DeprecatedInstruction as u32) 38 | ) 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /token-lending/cli/scripts/withdraw.sh: -------------------------------------------------------------------------------- 1 | set -ex 2 | 3 | ETH_RESERVE=CPDiKagfozERtJ33p7HHhEfJERjvfk1VAjMXAFLrvrKP 4 | SOL_RESERVE=8PbodeaosQP19SjYFx855UMqWxH2HynZLdBXmsrbac36 5 | STSOL_RESERVE=5sjkv6HD8wycocJ4tC4U36HHbvgcXYqcyiPRUkncnwWs 6 | MSOL_RESERVE=CCpirWrgNuBVLdkP2haxLTbD6XqEgaYuVXixbbpxUB6 7 | USDC_RESERVE=BgxfHJDzm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw 8 | USDT_RESERVE=8K9WC8xoh2rtQNY7iEGXtPvfbDCi563SdWhCAhuMP2xE 9 | BTC_RESERVE=GYzjMCXTDue12eUGKKWAqtF5jcBYNmewr6Db6LaguEaX 10 | RAY_RESERVE=9n2exoMQwMTzfw6NFoFFujxYPndWVLtKREJePssrKb36 11 | SLND_RESERVE=CviGNzD2C9ZCMmjDt5DKCce5cLV4Emrcm3NFvwudBFKA 12 | 13 | USDC_ATA=Bqn9qMjFEHRNS4wBRVAs3Uc52Dr3vm2AXJ5GaMkepBiQ 14 | USDT_ATA=2bEeupwb9eC5R9LjCCrfetPm5yLwdGVLYng6XhNtue9H 15 | BTC_ATA=A6Fu8DtnUqeYpzUMbZnnDpUFE5URnNUd8toZzcNBMkJ4 16 | 17 | OBLIGATION_PUBKEY=9XQ18M6VvB9X9QVXxj1bCvKifAW2RWDePEfBwi6fsLhq 18 | WITHDRAW_RESERVE=$USDC_RESERVE 19 | COLLATERAL_AMOUNT=1000 20 | 21 | 22 | cargo run -- \ 23 | withdraw-collateral \ 24 | --obligation $OBLIGATION_PUBKEY \ 25 | --withdraw-reserve $WITHDRAW_RESERVE \ 26 | --withdraw-amount $COLLATERAL_AMOUNT \ 27 | 28 | # cargo run -- \ 29 | # redeem-collateral \ 30 | # --redeem-reserve $WITHDRAW_RESERVE \ 31 | # --redeem-amount $COLLATERAL_AMOUNT \ 32 | -------------------------------------------------------------------------------- /token-lending/cli/scripts/liquidate.sh: -------------------------------------------------------------------------------- 1 | set -ex 2 | 3 | ETH_RESERVE=CPDiKagfozERtJ33p7HHhEfJERjvfk1VAjMXAFLrvrKP 4 | SOL_RESERVE=8PbodeaosQP19SjYFx855UMqWxH2HynZLdBXmsrbac36 5 | STSOL_RESERVE=5sjkv6HD8wycocJ4tC4U36HHbvgcXYqcyiPRUkncnwWs 6 | MSOL_RESERVE=CCpirWrgNuBVLdkP2haxLTbD6XqEgaYuVXixbbpxUB6 7 | USDC_RESERVE=BgxfHJDzm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw 8 | USDT_RESERVE=8K9WC8xoh2rtQNY7iEGXtPvfbDCi563SdWhCAhuMP2xE 9 | BTC_RESERVE=GYzjMCXTDue12eUGKKWAqtF5jcBYNmewr6Db6LaguEaX 10 | RAY_RESERVE=9n2exoMQwMTzfw6NFoFFujxYPndWVLtKREJePssrKb36 11 | SLND_RESERVE=CviGNzD2C9ZCMmjDt5DKCce5cLV4Emrcm3NFvwudBFKA 12 | 13 | USDC_ATA=Bqn9qMjFEHRNS4wBRVAs3Uc52Dr3vm2AXJ5GaMkepBiQ 14 | USDT_ATA=2bEeupwb9eC5R9LjCCrfetPm5yLwdGVLYng6XhNtue9H 15 | BTC_ATA=A6Fu8DtnUqeYpzUMbZnnDpUFE5URnNUd8toZzcNBMkJ4 16 | 17 | OBLIGATION_PUBKEY=HLRd6Dn4RUs4XbVzYhdp6UswQMCJTqWc9PgJ6VxvsyXu 18 | # OBLIGATION_PUBKEY=3ErCznFWTRmhZE8C1mAQCkcneqcZQedB5ACqAwbbWUAP 19 | REPAY_RESERVE=$USDC_RESERVE 20 | WITHDRAW_RESERVE=$SOL_RESERVE 21 | LIQUIDITY_AMOUNT=1000000000 22 | SOURCE_LIQUIDITY=$USDC_ATA 23 | 24 | 25 | cargo run liquidate-obligation \ 26 | --obligation $OBLIGATION_PUBKEY \ 27 | --repay-reserve $REPAY_RESERVE \ 28 | --withdraw-reserve $WITHDRAW_RESERVE \ 29 | --liquidity-amount $LIQUIDITY_AMOUNT \ 30 | --source-liquidity $SOURCE_LIQUIDITY 31 | -------------------------------------------------------------------------------- /token-lending/sdk/src/math/common.rs: -------------------------------------------------------------------------------- 1 | //! Common module for Decimal and Rate 2 | 3 | use solana_program::program_error::ProgramError; 4 | 5 | /// Scale of precision 6 | pub const SCALE: usize = 18; 7 | /// Identity 8 | pub const WAD: u64 = 1_000_000_000_000_000_000; 9 | /// Half of identity 10 | pub const HALF_WAD: u64 = 500_000_000_000_000_000; 11 | /// Scale for percentages 12 | pub const PERCENT_SCALER: u64 = 10_000_000_000_000_000; 13 | /// Scale for basis points 14 | pub const BPS_SCALER: u64 = 100_000_000_000_000; 15 | 16 | /// Try to subtract, return an error on underflow 17 | pub trait TrySub: Sized { 18 | /// Subtract 19 | fn try_sub(self, rhs: Self) -> Result; 20 | } 21 | 22 | /// Subtract and set to zero on underflow 23 | pub trait SaturatingSub: Sized { 24 | /// Subtract 25 | fn saturating_sub(self, rhs: Self) -> Self; 26 | } 27 | 28 | /// Try to subtract, return an error on overflow 29 | pub trait TryAdd: Sized { 30 | /// Add 31 | fn try_add(self, rhs: Self) -> Result; 32 | } 33 | 34 | /// Try to divide, return an error on overflow or divide by zero 35 | pub trait TryDiv: Sized { 36 | /// Divide 37 | fn try_div(self, rhs: RHS) -> Result; 38 | } 39 | 40 | /// Try to multiply, return an error on overflow 41 | pub trait TryMul: Sized { 42 | /// Multiply 43 | fn try_mul(self, rhs: RHS) -> Result; 44 | } 45 | -------------------------------------------------------------------------------- /token-lending/program/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solend-program" 3 | version = "2.0.2" 4 | description = "Solend Program" 5 | authors = ["Solend Maintainers "] 6 | repository = "https://github.com/solendprotocol/solana-program-library" 7 | license = "Apache-2.0" 8 | edition = "2018" 9 | 10 | [features] 11 | no-entrypoint = [] 12 | test-bpf = [] 13 | 14 | [dependencies] 15 | bytemuck = "1.5.1" 16 | # pyth-sdk-solana = "0.8.0" 17 | # pyth-solana-receiver-sdk = "0.3.0" 18 | solana-program = "=1.16.20" 19 | solend-sdk = { path = "../sdk" } 20 | oracles = { path = "../oracles" } 21 | spl-token = { version = "3.3.0", features=["no-entrypoint"] } 22 | static_assertions = "1.1.0" 23 | 24 | [dev-dependencies] 25 | anchor-lang = "0.28.0" 26 | assert_matches = "1.5.0" 27 | bytemuck = "1.5.1" 28 | base64 = "0.13" 29 | log = "0.4.14" 30 | proptest = "1.0" 31 | solana-program-test = "=1.16.20" 32 | solana-sdk = "=1.16.20" 33 | serde = ">=1.0.140" 34 | serde_yaml = "0.8" 35 | thiserror = "1.0" 36 | bincode = "1.3.3" 37 | borsh = "0.10.3" 38 | pyth-sdk-solana = "0.8.0" 39 | pyth-solana-receiver-sdk = "0.3.0" 40 | switchboard-on-demand = "0.1.12" 41 | switchboard-program = "0.2.0" 42 | switchboard-v2 = "0.1.3" 43 | 44 | [lib] 45 | crate-type = ["cdylib", "lib"] 46 | name = "solend_program" 47 | 48 | [profile.release] 49 | lto = "fat" 50 | codegen-units = 1 51 | 52 | [profile.release.build-override] 53 | opt-level = 3 54 | incremental = false 55 | codegen-units = 1 56 | -------------------------------------------------------------------------------- /ci/fuzz.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | usage() { 6 | exitcode=0 7 | if [[ -n "$1" ]]; then 8 | exitcode=1 9 | echo "Error: $*" 10 | fi 11 | echo "Usage: $0 [fuzz-target] [run-time-in-seconds]" 12 | exit $exitcode 13 | } 14 | 15 | fuzz_target=$1 16 | if [[ -z $fuzz_target ]]; then 17 | usage "No fuzz target provided" 18 | fi 19 | 20 | run_time=$2 21 | if [[ -z $2 ]]; then 22 | usage "No runtime provided" 23 | fi 24 | 25 | HFUZZ_RUN_ARGS="--run_time $run_time --exit_upon_crash" cargo hfuzz run $fuzz_target 26 | 27 | # Until https://github.com/rust-fuzz/honggfuzz-rs/issues/16 is resolved, 28 | # hfuzz does not return an error code on crash, so look for a crash artifact 29 | exit_status=0 30 | for crash_file in ./hfuzz_workspace/"$fuzz_target"/*.fuzz; do 31 | # Check if the glob gets expanded to existing files. 32 | if [[ -e "$crash_file" ]]; then 33 | echo "Error: .fuzz file $crash_file found, reproduce locally with the hexdump:" 34 | od -t x1 "$crash_file" 35 | crash_file_base=$(basename $crash_file) 36 | hex_output_filename=hex_"$crash_file_base" 37 | echo "Copy / paste this output into a normal file (e.g. $hex_output_filename)" 38 | echo "Reconstruct the binary file using:" 39 | echo "xxd -r $hex_output_filename > $crash_file_base" 40 | echo "To reproduce the problem, run:" 41 | echo "cargo hfuzz run-debug $fuzz_target $crash_file_base" 42 | exit_status=1 43 | fi 44 | done 45 | 46 | exit $exit_status 47 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | # Validate your changes with: 2 | # 3 | # $ curl -F 'data=@.mergify.yml' https://gh.mergify.io/validate/ 4 | # 5 | # https://doc.mergify.io/ 6 | pull_request_rules: 7 | - name: label changes from community 8 | conditions: 9 | - author≠@core-contributors 10 | - author≠mergify[bot] 11 | - author≠dependabot[bot] 12 | actions: 13 | label: 14 | add: 15 | - community 16 | - name: automatic merge (squash) on CI success 17 | conditions: 18 | - check-success=Travis CI - Pull Request 19 | - check-success=all_github_action_checks 20 | - "#status-failure=0" 21 | - "#status-neutral=0" 22 | - label=automerge 23 | - author≠@dont-squash-my-commits 24 | actions: 25 | merge: 26 | method: squash 27 | # Join the dont-squash-my-commits group if you won't like your commits squashed 28 | - name: automatic merge (rebase) on CI success 29 | conditions: 30 | - check-success=Travis CI - Pull Request 31 | - check-success=all_github_action_checks 32 | - "#status-failure=0" 33 | - "#status-neutral=0" 34 | - label=automerge 35 | - author=@dont-squash-my-commits 36 | actions: 37 | merge: 38 | method: rebase 39 | - name: remove automerge label on CI failure 40 | conditions: 41 | - label=automerge 42 | - "#status-failure!=0" 43 | actions: 44 | label: 45 | remove: 46 | - automerge 47 | comment: 48 | message: automerge label removed due to a CI failure 49 | - name: remove outdated reviews 50 | conditions: 51 | - base=master 52 | actions: 53 | dismiss_reviews: 54 | changes_requested: true 55 | -------------------------------------------------------------------------------- /token-lending/program/tests/fixtures/README.md: -------------------------------------------------------------------------------- 1 | # fixtures 2 | 3 | ### SOL / SRM Aggregator Accounts 4 | 5 | ```shell 6 | solana config set --url https://api.devnet.solana.com 7 | 8 | # Pyth product: SOL/USD 9 | solana account 3Mnn2fX6rQyUsyELYms1sBJyChWofzSNRoqYzvgMVz5E --output-file 3Mnn2fX6rQyUsyELYms1sBJyChWofzSNRoqYzvgMVz5E.bin 10 | # Pyth price: SOL/USD 11 | solana account J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix --output-file J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix.bin 12 | # Switchboard price: SOL/USD 13 | solana account AdtRGGhmqvom3Jemp5YNrxd9q9unX36BZk1pujkkXijL --output-file AdtRGGhmqvom3Jemp5YNrxd9q9unX36BZk1pujkkXijL.bin 14 | # Switchboardv2 price: SOL/USD 15 | solana account GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR --output-file GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR.bin 16 | # Pyth product: SRM/USD 17 | solana account 6MEwdxe4g1NeAF9u6KDG14anJpFsVEa2cvr5H6iriFZ8 --output-file 6MEwdxe4g1NeAF9u6KDG14anJpFsVEa2cvr5H6iriFZ8.bin 18 | # Pyth price: SRM/USD 19 | solana account 992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs --output-file 992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs.bin 20 | # Switchboard price: SRM/USD 21 | solana account BAoygKcKN7wk8yKzLD6sxzUQUqLvhBV1rjMA4UJqfZuH --output-file BAoygKcKN7wk8yKzLD6sxzUQUqLvhBV1rjMA4UJqfZuH.bin 22 | # Switchboardv2 price: SRM/USD 23 | solana account CUgoqwiQ4wCt6Tthkrgx5saAEpLBjPCdHshVa4Pbfcx2 --output-file CUgoqwiQ4wCt6Tthkrgx5saAEpLBjPCdHshVa4Pbfcx2.bin 24 | # pyth v2 sol price 25 | solana account 7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE --output-file 7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE.bin 26 | # SB on demand WEN/USD oracle 27 | solana account GcNZRMqGSEyEULZnLDD3ParcHTgFBrNfUdUCDtThP55e --output-file GcNZRMqGSEyEULZnLDD3ParcHTgFBrNfUdUCDtThP55e.bin 28 | ``` 29 | -------------------------------------------------------------------------------- /token-lending/sdk/src/state/last_update.rs: -------------------------------------------------------------------------------- 1 | use crate::error::LendingError; 2 | use solana_program::{clock::Slot, program_error::ProgramError}; 3 | use std::cmp::Ordering; 4 | 5 | /// Number of slots to consider stale after 6 | pub const STALE_AFTER_SLOTS_ELAPSED: u64 = 1; 7 | 8 | /// Last update state 9 | #[derive(Clone, Debug, Default)] 10 | pub struct LastUpdate { 11 | /// Last slot when updated 12 | pub slot: Slot, 13 | /// True when marked stale, false when slot updated 14 | pub stale: bool, 15 | } 16 | 17 | impl LastUpdate { 18 | /// Create new last update 19 | pub fn new(slot: Slot) -> Self { 20 | Self { slot, stale: true } 21 | } 22 | 23 | /// Return slots elapsed since given slot 24 | pub fn slots_elapsed(&self, slot: Slot) -> Result { 25 | let slots_elapsed = slot 26 | .checked_sub(self.slot) 27 | .ok_or(LendingError::MathOverflow)?; 28 | Ok(slots_elapsed) 29 | } 30 | 31 | /// Set last update slot 32 | pub fn update_slot(&mut self, slot: Slot) { 33 | self.slot = slot; 34 | self.stale = false; 35 | } 36 | 37 | /// Set stale to true 38 | pub fn mark_stale(&mut self) { 39 | self.stale = true; 40 | } 41 | 42 | /// Check if marked stale or last update slot is too long ago 43 | pub fn is_stale(&self, slot: Slot) -> Result { 44 | Ok(self.stale || self.slots_elapsed(slot)? >= STALE_AFTER_SLOTS_ELAPSED) 45 | } 46 | } 47 | 48 | impl PartialEq for LastUpdate { 49 | fn eq(&self, other: &Self) -> bool { 50 | self.slot == other.slot 51 | } 52 | } 53 | 54 | impl PartialOrd for LastUpdate { 55 | fn partial_cmp(&self, other: &Self) -> Option { 56 | self.slot.partial_cmp(&other.slot) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ci/rust-version.sh: -------------------------------------------------------------------------------- 1 | # 2 | # This file maintains the rust versions for use by CI. 3 | # 4 | # Obtain the environment variables without any automatic toolchain updating: 5 | # $ source ci/rust-version.sh 6 | # 7 | # Obtain the environment variables updating both stable and nightly, only stable, or 8 | # only nightly: 9 | # $ source ci/rust-version.sh all 10 | # $ source ci/rust-version.sh stable 11 | # $ source ci/rust-version.sh nightly 12 | 13 | # Then to build with either stable or nightly: 14 | # $ cargo +"$rust_stable" build 15 | # $ cargo +"$rust_nightly" build 16 | # 17 | 18 | if [[ -n $RUST_STABLE_VERSION ]]; then 19 | stable_version="$RUST_STABLE_VERSION" 20 | else 21 | stable_version=1.76.0 22 | fi 23 | 24 | if [[ -n $RUST_NIGHTLY_VERSION ]]; then 25 | nightly_version="$RUST_NIGHTLY_VERSION" 26 | else 27 | nightly_version=2022-04-01 28 | fi 29 | 30 | 31 | export rust_stable="$stable_version" 32 | export rust_stable_docker_image=solanalabs/rust:"$stable_version" 33 | 34 | export rust_nightly=nightly-"$nightly_version" 35 | export rust_nightly_docker_image=solanalabs/rust-nightly:"$nightly_version" 36 | 37 | [[ -z $1 ]] || ( 38 | 39 | rustup_install() { 40 | declare toolchain=$1 41 | if ! cargo +"$toolchain" -V > /dev/null; then 42 | echo "$0: Missing toolchain? Installing...: $toolchain" >&2 43 | rustup install "$toolchain" 44 | cargo +"$toolchain" -V 45 | fi 46 | } 47 | 48 | set -e 49 | cd "$(dirname "${BASH_SOURCE[0]}")" 50 | case $1 in 51 | stable) 52 | rustup_install "$rust_stable" 53 | ;; 54 | nightly) 55 | rustup_install "$rust_nightly" 56 | ;; 57 | all) 58 | rustup_install "$rust_stable" 59 | rustup_install "$rust_nightly" 60 | ;; 61 | *) 62 | echo "$0: Note: ignoring unknown argument: $1" >&2 63 | ;; 64 | esac 65 | ) 66 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-token-lending.yml: -------------------------------------------------------------------------------- 1 | name: Token Lending Pull Request 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - "token-lending/**" 7 | - "token/**" 8 | push: 9 | branches: [master] 10 | paths: 11 | - "token-lending/**" 12 | - "token/**" 13 | 14 | jobs: 15 | cargo-test-bpf: 16 | runs-on: ubuntu-latest-16-cores 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Set env vars 21 | run: | 22 | source ci/rust-version.sh 23 | echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV 24 | source ci/solana-version.sh 25 | echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV 26 | 27 | - uses: actions-rs/toolchain@v1 28 | with: 29 | toolchain: ${{ env.RUST_STABLE }} 30 | override: true 31 | profile: minimal 32 | 33 | - uses: actions/cache@v4 34 | with: 35 | path: | 36 | ~/.cargo/registry 37 | ~/.cargo/git 38 | key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}} 39 | 40 | - uses: actions/cache@v4 41 | with: 42 | path: | 43 | ~/.cargo/bin/rustfilt 44 | key: cargo-bpf-bins-${{ runner.os }} 45 | 46 | - uses: actions/cache@v4 47 | with: 48 | path: | 49 | ~/.cache 50 | key: solana-${{ env.SOLANA_VERSION }} 51 | 52 | - name: Install dependencies 53 | run: | 54 | ./ci/install-build-deps.sh 55 | ./ci/install-program-deps.sh 56 | echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH 57 | 58 | - name: Build and test 59 | run: ./ci/cargo-test-bpf.sh token-lending 60 | 61 | - name: Upload programs 62 | uses: actions/upload-artifact@v4 63 | with: 64 | name: token-lending-programs 65 | path: "target/deploy/*.so" 66 | if-no-files-found: error 67 | -------------------------------------------------------------------------------- /token-lending/sdk/src/state/lending_market_metadata.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | use crate::error::LendingError; 4 | use bytemuck::checked::try_from_bytes; 5 | use bytemuck::{Pod, Zeroable}; 6 | use solana_program::program_error::ProgramError; 7 | use solana_program::pubkey::Pubkey; 8 | use static_assertions::{assert_eq_size, const_assert}; 9 | 10 | /// market name size 11 | pub const MARKET_NAME_SIZE: usize = 50; 12 | 13 | /// market description size 14 | pub const MARKET_DESCRIPTION_SIZE: usize = 300; 15 | 16 | /// market image url size 17 | pub const MARKET_IMAGE_URL_SIZE: usize = 250; 18 | 19 | /// padding size 20 | pub const PADDING_SIZE: usize = 100; 21 | 22 | /// Lending market state 23 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 24 | #[repr(C)] 25 | pub struct LendingMarketMetadata { 26 | /// Bump seed 27 | pub bump_seed: u8, 28 | /// Market name null padded 29 | pub market_name: [u8; MARKET_NAME_SIZE], 30 | /// Market description null padded 31 | pub market_description: [u8; MARKET_DESCRIPTION_SIZE], 32 | /// Market image url 33 | pub market_image_url: [u8; MARKET_IMAGE_URL_SIZE], 34 | /// Lookup Tables 35 | pub lookup_tables: [Pubkey; 4], 36 | /// Padding 37 | pub padding: [u8; PADDING_SIZE], 38 | } 39 | 40 | impl LendingMarketMetadata { 41 | /// Create a LendingMarketMetadata referernce from a slice 42 | pub fn new_from_bytes(data: &[u8]) -> Result<&LendingMarketMetadata, ProgramError> { 43 | try_from_bytes::(&data[1..]).map_err(|_| { 44 | msg!("Failed to deserialize LendingMarketMetadata"); 45 | LendingError::InstructionUnpackError.into() 46 | }) 47 | } 48 | } 49 | 50 | unsafe impl Zeroable for LendingMarketMetadata {} 51 | unsafe impl Pod for LendingMarketMetadata {} 52 | 53 | assert_eq_size!( 54 | LendingMarketMetadata, 55 | [u8; MARKET_NAME_SIZE 56 | + MARKET_DESCRIPTION_SIZE 57 | + MARKET_IMAGE_URL_SIZE 58 | + 4 * 32 59 | + PADDING_SIZE 60 | + 1], 61 | ); 62 | 63 | // transaction size limit check 64 | const_assert!(std::mem::size_of::() <= 850); 65 | -------------------------------------------------------------------------------- /token-lending/sdk/src/state/mod.rs: -------------------------------------------------------------------------------- 1 | //! State types 2 | 3 | mod last_update; 4 | mod lending_market; 5 | mod lending_market_metadata; 6 | mod obligation; 7 | mod rate_limiter; 8 | mod reserve; 9 | 10 | pub use last_update::*; 11 | pub use lending_market::*; 12 | pub use lending_market_metadata::*; 13 | pub use obligation::*; 14 | pub use rate_limiter::*; 15 | pub use reserve::*; 16 | 17 | use crate::math::{Decimal, WAD}; 18 | use solana_program::{msg, program_error::ProgramError}; 19 | 20 | /// Collateral tokens are initially valued at a ratio of 5:1 (collateral:liquidity) 21 | // @FIXME: restore to 5 22 | pub const INITIAL_COLLATERAL_RATIO: u64 = 1; 23 | const INITIAL_COLLATERAL_RATE: u64 = INITIAL_COLLATERAL_RATIO * WAD; 24 | 25 | /// Current version of the program and all new accounts created 26 | pub const PROGRAM_VERSION: u8 = 1; 27 | 28 | /// Accounts are created with data zeroed out, so uninitialized state instances 29 | /// will have the version set to 0. 30 | pub const UNINITIALIZED_VERSION: u8 = 0; 31 | 32 | /// Number of slots per year 33 | // 2 (slots per second) * 60 * 60 * 24 * 365 = 63072000 34 | pub const SLOTS_PER_YEAR: u64 = 63072000; 35 | 36 | // Helpers 37 | fn pack_decimal(decimal: Decimal, dst: &mut [u8; 16]) { 38 | *dst = decimal 39 | .to_scaled_val() 40 | .expect("Decimal cannot be packed") 41 | .to_le_bytes(); 42 | } 43 | 44 | fn unpack_decimal(src: &[u8; 16]) -> Decimal { 45 | Decimal::from_scaled_val(u128::from_le_bytes(*src)) 46 | } 47 | 48 | fn pack_bool(boolean: bool, dst: &mut [u8; 1]) { 49 | *dst = (boolean as u8).to_le_bytes() 50 | } 51 | 52 | fn unpack_bool(src: &[u8; 1]) -> Result { 53 | match u8::from_le_bytes(*src) { 54 | 0 => Ok(false), 55 | 1 => Ok(true), 56 | _ => { 57 | msg!("Boolean cannot be unpacked"); 58 | Err(ProgramError::InvalidAccountData) 59 | } 60 | } 61 | } 62 | 63 | #[cfg(test)] 64 | mod test { 65 | use super::*; 66 | 67 | #[test] 68 | fn initial_collateral_rate_sanity() { 69 | assert_eq!( 70 | INITIAL_COLLATERAL_RATIO.checked_mul(WAD).unwrap(), 71 | INITIAL_COLLATERAL_RATE 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /token-lending/program/tests/donate_to_reserve.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "test-bpf")] 2 | use crate::solend_program_test::custom_scenario; 3 | 4 | use crate::solend_program_test::User; 5 | 6 | use crate::solend_program_test::BalanceChecker; 7 | 8 | use crate::solend_program_test::PriceArgs; 9 | use crate::solend_program_test::ReserveArgs; 10 | use crate::solend_program_test::TokenBalanceChange; 11 | 12 | mod helpers; 13 | 14 | use helpers::*; 15 | use solana_program_test::*; 16 | use solend_sdk::state::Reserve; 17 | 18 | use std::collections::HashSet; 19 | 20 | #[tokio::test] 21 | async fn test_donate_to_reserve() { 22 | let (mut test, lending_market, reserves, _obligations, _users, _) = custom_scenario( 23 | &[ReserveArgs { 24 | mint: usdc_mint::id(), 25 | config: test_reserve_config(), 26 | liquidity_amount: 100_000 * FRACTIONAL_TO_USDC, 27 | price: PriceArgs { 28 | price: 10, 29 | conf: 0, 30 | expo: -1, 31 | ema_price: 10, 32 | ema_conf: 1, 33 | }, 34 | }], 35 | &[], 36 | ) 37 | .await; 38 | 39 | let whale = User::new_with_balances( 40 | &mut test, 41 | &[(&usdc_mint::id(), 100_000 * FRACTIONAL_TO_USDC)], 42 | ) 43 | .await; 44 | 45 | let balance_checker = BalanceChecker::start(&mut test, &[&whale, &reserves[0]]).await; 46 | 47 | lending_market 48 | .donate_to_reserve( 49 | &mut test, 50 | &reserves[0], 51 | &whale, 52 | 100_000 * FRACTIONAL_TO_USDC, 53 | ) 54 | .await 55 | .unwrap(); 56 | 57 | let reserve_post = test.load_account::(reserves[0].pubkey).await; 58 | 59 | assert_eq!( 60 | reserve_post.account.liquidity.available_amount, 61 | 200_000 * FRACTIONAL_TO_USDC 62 | ); 63 | 64 | let (balance_changes, _) = balance_checker.find_balance_changes(&mut test).await; 65 | let expected_balance_changes = HashSet::from([ 66 | TokenBalanceChange { 67 | token_account: whale.get_account(&usdc_mint::id()).unwrap(), 68 | mint: usdc_mint::id(), 69 | diff: -(100_000 * FRACTIONAL_TO_USDC as i128), 70 | }, 71 | TokenBalanceChange { 72 | token_account: reserves[0].account.liquidity.supply_pubkey, 73 | mint: usdc_mint::id(), 74 | diff: 100_000 * FRACTIONAL_TO_USDC as i128, 75 | }, 76 | ]); 77 | 78 | assert_eq!(balance_changes, expected_balance_changes); 79 | } 80 | -------------------------------------------------------------------------------- /token-lending/program/tests/init_lending_market.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "test-bpf")] 2 | 3 | mod helpers; 4 | 5 | use helpers::solend_program_test::{SolendProgramTest, User}; 6 | use helpers::*; 7 | use oracles::{pyth_mainnet, switchboard_v2_mainnet}; 8 | use solana_program::instruction::InstructionError; 9 | use solana_program_test::*; 10 | use solana_sdk::signature::Keypair; 11 | use solana_sdk::signer::Signer; 12 | use solana_sdk::transaction::TransactionError; 13 | use solend_program::error::LendingError; 14 | use solend_program::instruction::init_lending_market; 15 | use solend_program::state::{LendingMarket, RateLimiter, PROGRAM_VERSION}; 16 | 17 | #[tokio::test] 18 | async fn test_success() { 19 | let mut test = SolendProgramTest::start_new().await; 20 | test.advance_clock_by_slots(1000).await; 21 | 22 | let lending_market_owner = User::new_with_balances(&mut test, &[]).await; 23 | 24 | let lending_market = test 25 | .init_lending_market(&lending_market_owner, &Keypair::new()) 26 | .await 27 | .unwrap(); 28 | assert_eq!( 29 | lending_market.account, 30 | LendingMarket { 31 | version: PROGRAM_VERSION, 32 | bump_seed: lending_market.account.bump_seed, // TODO test this field 33 | owner: lending_market_owner.keypair.pubkey(), 34 | quote_currency: QUOTE_CURRENCY, 35 | token_program_id: spl_token::id(), 36 | oracle_program_id: pyth_mainnet::id(), 37 | switchboard_oracle_program_id: switchboard_v2_mainnet::id(), 38 | rate_limiter: RateLimiter::default(), 39 | whitelisted_liquidator: None, 40 | risk_authority: lending_market_owner.keypair.pubkey(), 41 | } 42 | ); 43 | } 44 | 45 | #[tokio::test] 46 | async fn test_already_initialized() { 47 | let mut test = SolendProgramTest::start_new().await; 48 | test.advance_clock_by_slots(1000).await; 49 | 50 | let lending_market_owner = User::new_with_balances(&mut test, &[]).await; 51 | 52 | let keypair = Keypair::new(); 53 | test.init_lending_market(&lending_market_owner, &keypair) 54 | .await 55 | .unwrap(); 56 | 57 | test.advance_clock_by_slots(1).await; 58 | 59 | let res = test 60 | .process_transaction( 61 | &[init_lending_market( 62 | solend_program::id(), 63 | lending_market_owner.keypair.pubkey(), 64 | QUOTE_CURRENCY, 65 | keypair.pubkey(), 66 | pyth_mainnet::id(), 67 | switchboard_v2_mainnet::id(), 68 | )], 69 | None, 70 | ) 71 | .await 72 | .unwrap_err() 73 | .unwrap(); 74 | 75 | assert_eq!( 76 | res, 77 | TransactionError::InstructionError( 78 | 0, 79 | InstructionError::Custom(LendingError::AlreadyInitialized as u32) 80 | ) 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /ci/env.sh: -------------------------------------------------------------------------------- 1 | # 2 | # Normalized CI environment variables 3 | # 4 | # |source| me 5 | # 6 | 7 | if [[ -n $CI ]]; then 8 | export CI=1 9 | if [[ -n $TRAVIS ]]; then 10 | export CI_BRANCH=$TRAVIS_BRANCH 11 | export CI_BASE_BRANCH=$TRAVIS_BRANCH 12 | export CI_BUILD_ID=$TRAVIS_BUILD_ID 13 | export CI_COMMIT=$TRAVIS_COMMIT 14 | export CI_JOB_ID=$TRAVIS_JOB_ID 15 | if [[ $TRAVIS_PULL_REQUEST != false ]]; then 16 | export CI_PULL_REQUEST=true 17 | else 18 | export CI_PULL_REQUEST= 19 | fi 20 | export CI_OS_NAME=$TRAVIS_OS_NAME 21 | export CI_REPO_SLUG=$TRAVIS_REPO_SLUG 22 | export CI_TAG=$TRAVIS_TAG 23 | elif [[ -n $BUILDKITE ]]; then 24 | export CI_BRANCH=$BUILDKITE_BRANCH 25 | export CI_BUILD_ID=$BUILDKITE_BUILD_ID 26 | export CI_COMMIT=$BUILDKITE_COMMIT 27 | export CI_JOB_ID=$BUILDKITE_JOB_ID 28 | # The standard BUILDKITE_PULL_REQUEST environment variable is always "false" due 29 | # to how solana-ci-gate is used to trigger PR builds rather than using the 30 | # standard Buildkite PR trigger. 31 | if [[ $CI_BRANCH =~ pull/* ]]; then 32 | export CI_BASE_BRANCH=$BUILDKITE_PULL_REQUEST_BASE_BRANCH 33 | export CI_PULL_REQUEST=true 34 | else 35 | export CI_BASE_BRANCH=$BUILDKITE_BRANCH 36 | export CI_PULL_REQUEST= 37 | fi 38 | export CI_OS_NAME=linux 39 | if [[ -n $BUILDKITE_TRIGGERED_FROM_BUILD_PIPELINE_SLUG ]]; then 40 | # The solana-secondary pipeline should use the slug of the pipeline that 41 | # triggered it 42 | export CI_REPO_SLUG=$BUILDKITE_ORGANIZATION_SLUG/$BUILDKITE_TRIGGERED_FROM_BUILD_PIPELINE_SLUG 43 | else 44 | export CI_REPO_SLUG=$BUILDKITE_ORGANIZATION_SLUG/$BUILDKITE_PIPELINE_SLUG 45 | fi 46 | # TRIGGERED_BUILDKITE_TAG is a workaround to propagate BUILDKITE_TAG into 47 | # the solana-secondary pipeline 48 | if [[ -n $TRIGGERED_BUILDKITE_TAG ]]; then 49 | export CI_TAG=$TRIGGERED_BUILDKITE_TAG 50 | else 51 | export CI_TAG=$BUILDKITE_TAG 52 | fi 53 | elif [[ -n $APPVEYOR ]]; then 54 | export CI_BRANCH=$APPVEYOR_REPO_BRANCH 55 | export CI_BUILD_ID=$APPVEYOR_BUILD_ID 56 | export CI_COMMIT=$APPVEYOR_REPO_COMMIT 57 | export CI_JOB_ID=$APPVEYOR_JOB_ID 58 | if [[ -n $APPVEYOR_PULL_REQUEST_NUMBER ]]; then 59 | export CI_PULL_REQUEST=true 60 | else 61 | export CI_PULL_REQUEST= 62 | fi 63 | if [[ $CI_LINUX = True ]]; then 64 | export CI_OS_NAME=linux 65 | else 66 | export CI_OS_NAME=windows 67 | fi 68 | export CI_REPO_SLUG=$APPVEYOR_REPO_NAME 69 | export CI_TAG=$APPVEYOR_REPO_TAG_NAME 70 | fi 71 | else 72 | export CI= 73 | export CI_BRANCH= 74 | export CI_BUILD_ID= 75 | export CI_COMMIT= 76 | export CI_JOB_ID= 77 | export CI_OS_NAME= 78 | export CI_PULL_REQUEST= 79 | export CI_REPO_SLUG= 80 | export CI_TAG= 81 | fi 82 | 83 | cat < (SolendProgramTest, Info, User) { 19 | let (test, lending_market, _, _, _, user) = 20 | setup_world(&test_reserve_config(), &test_reserve_config()).await; 21 | 22 | (test, lending_market, user) 23 | } 24 | 25 | #[tokio::test] 26 | async fn test_success() { 27 | let (mut test, lending_market, user) = setup().await; 28 | 29 | let obligation = lending_market 30 | .init_obligation(&mut test, Keypair::new(), &user) 31 | .await 32 | .expect("This should succeed"); 33 | 34 | assert_eq!( 35 | obligation.account, 36 | Obligation { 37 | version: PROGRAM_VERSION, 38 | last_update: LastUpdate { 39 | slot: 1000, 40 | stale: true 41 | }, 42 | lending_market: lending_market.pubkey, 43 | owner: user.keypair.pubkey(), 44 | deposits: Vec::new(), 45 | borrows: Vec::new(), 46 | deposited_value: Decimal::zero(), 47 | borrowed_value: Decimal::zero(), 48 | unweighted_borrowed_value: Decimal::zero(), 49 | borrowed_value_upper_bound: Decimal::zero(), 50 | allowed_borrow_value: Decimal::zero(), 51 | unhealthy_borrow_value: Decimal::zero(), 52 | super_unhealthy_borrow_value: Decimal::zero(), 53 | borrowing_isolated_asset: false, 54 | closeable: false, 55 | } 56 | ); 57 | } 58 | 59 | #[tokio::test] 60 | async fn test_already_initialized() { 61 | let (mut test, lending_market, user) = setup().await; 62 | 63 | let keypair = Keypair::new(); 64 | let keypair_clone = Keypair::from_bytes(&keypair.to_bytes().clone()).unwrap(); 65 | 66 | lending_market 67 | .init_obligation(&mut test, keypair, &user) 68 | .await 69 | .expect("This should succeed"); 70 | 71 | test.advance_clock_by_slots(1).await; 72 | 73 | let res = test 74 | .process_transaction( 75 | &[init_obligation( 76 | solend_program::id(), 77 | keypair_clone.pubkey(), 78 | lending_market.pubkey, 79 | user.keypair.pubkey(), 80 | )], 81 | Some(&[&user.keypair]), 82 | ) 83 | .await 84 | .unwrap_err() 85 | .unwrap(); 86 | 87 | assert_eq!( 88 | res, 89 | TransactionError::InstructionError( 90 | 0, 91 | InstructionError::Custom(LendingError::AlreadyInitialized as u32) 92 | ) 93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /token-lending/program/tests/helpers/genesis.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use solana_program::bpf_loader_upgradeable; 3 | use solana_program_test::*; 4 | use solana_sdk::{ 5 | account::Account, 6 | bpf_loader_upgradeable::UpgradeableLoaderState, 7 | pubkey::Pubkey, 8 | signature::{read_keypair_file, Signer}, 9 | }; 10 | use std::{collections::HashMap, fs::File, io::Write, path::Path}; 11 | 12 | /// An account where the data is encoded as a Base64 string. 13 | #[derive(Serialize, Deserialize, Debug)] 14 | pub struct Base64Account { 15 | pub balance: u64, 16 | pub owner: String, 17 | pub data: String, 18 | pub executable: bool, 19 | } 20 | 21 | impl From for Base64Account { 22 | fn from(account: Account) -> Self { 23 | Self { 24 | owner: account.owner.to_string(), 25 | balance: account.lamports, 26 | executable: account.executable, 27 | data: base64::encode(&account.data), 28 | } 29 | } 30 | } 31 | 32 | #[derive(Default)] 33 | pub struct GenesisAccounts(HashMap); 34 | 35 | impl GenesisAccounts { 36 | pub fn insert_upgradeable_program(&mut self, program_id: Pubkey, filename: &str) { 37 | let program_file = 38 | find_file(filename).unwrap_or_else(|| panic!("couldn't find {}", filename)); 39 | let program_data = read_file(program_file); 40 | let upgrade_authority_keypair = 41 | read_keypair_file("tests/fixtures/lending_market_owner.json").unwrap(); 42 | 43 | let programdata_address = 44 | Pubkey::find_program_address(&[program_id.as_ref()], &bpf_loader_upgradeable::id()).0; 45 | let programdata_data_offset = UpgradeableLoaderState::size_of_programdata_metadata(); 46 | let programdata_space = 2 * program_data.len() + programdata_data_offset; 47 | let mut programdata_account = Account::new_data_with_space( 48 | u32::MAX as u64, 49 | &UpgradeableLoaderState::ProgramData { 50 | slot: 0, 51 | upgrade_authority_address: Some(upgrade_authority_keypair.pubkey()), 52 | }, 53 | programdata_space, 54 | &bpf_loader_upgradeable::id(), 55 | ) 56 | .unwrap(); 57 | 58 | programdata_account.data 59 | [programdata_data_offset..programdata_data_offset + program_data.len()] 60 | .copy_from_slice(&program_data[..]); 61 | 62 | self.0 63 | .insert(programdata_address.to_string(), programdata_account.into()); 64 | 65 | let mut program_account = Account::new_data( 66 | u32::MAX as u64, 67 | &UpgradeableLoaderState::Program { 68 | programdata_address, 69 | }, 70 | &bpf_loader_upgradeable::id(), 71 | ) 72 | .unwrap(); 73 | program_account.executable = true; 74 | 75 | self.0 76 | .insert(program_id.to_string(), program_account.into()); 77 | } 78 | 79 | pub async fn fetch_and_insert(&mut self, banks_client: &mut BanksClient, pubkey: Pubkey) { 80 | let mut account: Account = banks_client.get_account(pubkey).await.unwrap().unwrap(); 81 | account.lamports = u32::MAX as u64; 82 | self.0.insert(pubkey.to_string(), account.into()); 83 | } 84 | 85 | pub fn write_yaml(&self) { 86 | let serialized = serde_yaml::to_string(&self.0).unwrap(); 87 | let path = Path::new("../../target/deploy/lending_accounts.yml"); 88 | let mut file = File::create(path).unwrap(); 89 | file.write_all(&serialized.into_bytes()).unwrap(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /token-lending/program/tests/redeem_fees.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "test-bpf")] 2 | 3 | mod helpers; 4 | 5 | use crate::solend_program_test::scenario_1; 6 | use crate::solend_program_test::BalanceChecker; 7 | use crate::solend_program_test::PriceArgs; 8 | use crate::solend_program_test::TokenBalanceChange; 9 | use solana_program::native_token::LAMPORTS_PER_SOL; 10 | use solend_program::state::LastUpdate; 11 | use solend_program::state::ReserveLiquidity; 12 | use solend_program::state::{Reserve, ReserveConfig}; 13 | use std::collections::HashSet; 14 | 15 | use helpers::*; 16 | use solana_program_test::*; 17 | use solend_program::{ 18 | math::{Decimal, TrySub}, 19 | state::SLOTS_PER_YEAR, 20 | }; 21 | 22 | #[tokio::test] 23 | async fn test_success() { 24 | let (mut test, lending_market, _, wsol_reserve, user, _, _) = scenario_1( 25 | &test_reserve_config(), 26 | &ReserveConfig { 27 | protocol_take_rate: 10, 28 | ..test_reserve_config() 29 | }, 30 | ) 31 | .await; 32 | 33 | test.advance_clock_by_slots(SLOTS_PER_YEAR).await; 34 | 35 | test.set_price( 36 | &wsol_mint::id(), 37 | &PriceArgs { 38 | price: 10, 39 | expo: 0, 40 | conf: 0, 41 | ema_price: 10, 42 | ema_conf: 0, 43 | }, 44 | ) 45 | .await; 46 | 47 | lending_market 48 | .refresh_reserve(&mut test, &wsol_reserve) 49 | .await 50 | .unwrap(); 51 | 52 | // deposit some liquidity so we can actually redeem the fees later 53 | lending_market 54 | .deposit(&mut test, &wsol_reserve, &user, LAMPORTS_PER_SOL) 55 | .await 56 | .unwrap(); 57 | 58 | let wsol_reserve = test.load_account::(wsol_reserve.pubkey).await; 59 | 60 | // redeem fees 61 | let balance_checker = BalanceChecker::start(&mut test, &[&wsol_reserve]).await; 62 | 63 | lending_market 64 | .redeem_fees(&mut test, &wsol_reserve) 65 | .await 66 | .unwrap(); 67 | 68 | let expected_fees = wsol_reserve.account.calculate_redeem_fees().unwrap(); 69 | 70 | // check token balances 71 | let (balance_changes, mint_supply_changes) = 72 | balance_checker.find_balance_changes(&mut test).await; 73 | let expected_balance_changes = HashSet::from([ 74 | TokenBalanceChange { 75 | token_account: wsol_reserve.account.config.fee_receiver, 76 | mint: wsol_mint::id(), 77 | diff: expected_fees as i128, 78 | }, 79 | TokenBalanceChange { 80 | token_account: wsol_reserve.account.liquidity.supply_pubkey, 81 | mint: wsol_mint::id(), 82 | diff: -(expected_fees as i128), 83 | }, 84 | ]); 85 | assert_eq!(balance_changes, expected_balance_changes); 86 | assert_eq!(mint_supply_changes, HashSet::new()); 87 | 88 | // check program state 89 | let wsol_reserve_post = test.load_account::(wsol_reserve.pubkey).await; 90 | assert_eq!( 91 | wsol_reserve_post.account, 92 | Reserve { 93 | last_update: LastUpdate { 94 | slot: 1000 + SLOTS_PER_YEAR, 95 | stale: true 96 | }, 97 | liquidity: ReserveLiquidity { 98 | available_amount: wsol_reserve.account.liquidity.available_amount - expected_fees, 99 | accumulated_protocol_fees_wads: wsol_reserve 100 | .account 101 | .liquidity 102 | .accumulated_protocol_fees_wads 103 | .try_sub(Decimal::from(expected_fees)) 104 | .unwrap(), 105 | ..wsol_reserve.account.liquidity 106 | }, 107 | ..wsol_reserve.account 108 | } 109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /token-lending/oracles/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod pyth; 2 | pub mod switchboard; 3 | 4 | use crate::pyth::get_pyth_price_unchecked; 5 | use crate::pyth::get_pyth_pull_price; 6 | use crate::pyth::get_pyth_pull_price_unchecked; 7 | use crate::switchboard::get_switchboard_price; 8 | use crate::switchboard::get_switchboard_price_on_demand; 9 | use crate::switchboard::get_switchboard_price_v2; 10 | use solana_program::{ 11 | account_info::AccountInfo, msg, program_error::ProgramError, sysvar::clock::Clock, 12 | }; 13 | use solend_sdk::error::LendingError; 14 | use solend_sdk::math::Decimal; 15 | 16 | pub enum OracleType { 17 | Pyth, 18 | Switchboard, 19 | PythPull, 20 | SbOnDemand, 21 | } 22 | 23 | pub fn get_oracle_type(oracle_info: &AccountInfo) -> Result { 24 | if *oracle_info.owner == pyth_mainnet::id() { 25 | return Ok(OracleType::Pyth); 26 | } else if *oracle_info.owner == pyth_pull_mainnet::id() { 27 | return Ok(OracleType::PythPull); 28 | } else if *oracle_info.owner == switchboard_v2_mainnet::id() { 29 | return Ok(OracleType::Switchboard); 30 | } else if *oracle_info.owner == switchboard_on_demand_mainnet::id() { 31 | return Ok(OracleType::SbOnDemand); 32 | } 33 | 34 | msg!( 35 | "Could not find oracle type for {:?} with owner {:?}", 36 | oracle_info.key, 37 | oracle_info.owner 38 | ); 39 | Err(LendingError::InvalidOracleConfig.into()) 40 | } 41 | 42 | pub fn get_single_price( 43 | oracle_account_info: &AccountInfo, 44 | clock: &Clock, 45 | ) -> Result<(Decimal, Option), ProgramError> { 46 | match get_oracle_type(oracle_account_info)? { 47 | OracleType::Pyth => { 48 | let price = pyth::get_pyth_price(oracle_account_info, clock)?; 49 | Ok((price.0, Some(price.1))) 50 | } 51 | OracleType::PythPull => { 52 | let price = get_pyth_pull_price(oracle_account_info, clock)?; 53 | Ok((price.0, Some(price.1))) 54 | } 55 | OracleType::Switchboard => { 56 | let price = get_switchboard_price(oracle_account_info, clock)?; 57 | Ok((price, None)) 58 | } 59 | OracleType::SbOnDemand => { 60 | let price = get_switchboard_price(oracle_account_info, clock)?; 61 | Ok((price, None)) 62 | } 63 | } 64 | } 65 | 66 | pub fn get_single_price_unchecked( 67 | oracle_account_info: &AccountInfo, 68 | clock: &Clock, 69 | ) -> Result { 70 | match get_oracle_type(oracle_account_info)? { 71 | OracleType::Pyth => get_pyth_price_unchecked(oracle_account_info), 72 | OracleType::PythPull => get_pyth_pull_price_unchecked(oracle_account_info), 73 | OracleType::Switchboard => get_switchboard_price_v2(oracle_account_info, clock, false), 74 | OracleType::SbOnDemand => get_switchboard_price_on_demand(oracle_account_info, clock, true), 75 | } 76 | } 77 | 78 | /// Mainnet program id for Switchboard v2. 79 | pub mod switchboard_v2_mainnet { 80 | solana_program::declare_id!("SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f"); 81 | } 82 | 83 | /// Devnet program id for Switchboard v2. 84 | pub mod switchboard_v2_devnet { 85 | solana_program::declare_id!("2TfB33aLaneQb5TNVwyDz3jSZXS6jdW2ARw1Dgf84XCG"); 86 | } 87 | 88 | /// Mainnet program id for Switchboard On-Demand Oracle. 89 | pub mod switchboard_on_demand_mainnet { 90 | solana_program::declare_id!("SBondMDrcV3K4kxZR1HNVT7osZxAHVHgYXL5Ze1oMUv"); 91 | } 92 | 93 | /// Devnet program id for Switchboard On-Demand Oracle. 94 | pub mod switchboard_on_demand_devnet { 95 | solana_program::declare_id!("SBondMDrcV3K4kxZR1HNVT7osZxAHVHgYXL5Ze1oMUv"); 96 | } 97 | 98 | /// Mainnet program id for pyth 99 | pub mod pyth_mainnet { 100 | solana_program::declare_id!("FsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epH"); 101 | } 102 | 103 | /// Mainnet program id for pyth 104 | pub mod pyth_pull_mainnet { 105 | solana_program::declare_id!("rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ"); 106 | } 107 | -------------------------------------------------------------------------------- /token-lending/cli/README.md: -------------------------------------------------------------------------------- 1 | # SPL Token Lending CLI 2 | 3 | A basic command line interface for initializing lending markets and reserves for SPL Token Lending. 4 | 5 | See https://spl.solana.com/token-lending for more details 6 | 7 | ## Install the CLI 8 | ```shell 9 | cargo install solend-program-cli 10 | ``` 11 | 12 | ## Deploy a lending program (optional) 13 | 14 | This is optional! You can simply add your own market and reserves to the existing [on-chain programs](../README.md#on-chain-programs). 15 | 16 | If you want to deploy your own program, follow [this guide](../README.md#deploy-a-lending-program-optional) and note the program ID. 17 | 18 | ## Create a lending market 19 | 20 | A lending market is a collection of reserves that can be configured to borrow and lend with each other. 21 | 22 | The lending market owner must sign to add reserves. 23 | 24 | ### Usage 25 | ```shell 26 | solend-program \ 27 | --program PUBKEY \ 28 | --fee-payer SIGNER \ 29 | create-market \ 30 | --market-owner PUBKEY 31 | ``` 32 | - `--program` is the lending program ID. 33 | - `--fee-payer` will sign to pay transaction fees. 34 | - `--market-owner` is the lending market owner pubkey. 35 | 36 | Run `solend-program create-market --help` for more details and options. 37 | 38 | ### Example 39 | ```shell 40 | solend-program \ 41 | --program 6TvznH3B2e3p2mbhufNBpgSrLx6UkgvxtVQvopEZ2kuH \ 42 | --fee-payer owner.json \ 43 | create-market \ 44 | --market-owner JAgN4SZLNeCo9KTnr8EWt4FzEV1UDgHkcZwkVtWtfp6P 45 | 46 | # Creating lending market 7uX9ywsk1X2j6wLoywMDVQLNWAqhDpVqZzL4qm4CuMMT 47 | # Signature: 51mi4Ve42h4PQ1RXjfz141T6KCdqnB3UDyhEejviVHrX4SnQCMx86TZa9CWUT3efFYkkmfmseG5ZQr2TZTHJ8S95 48 | ``` 49 | Note the lending market pubkey (e.g. `7uX9ywsk1X2j6wLoywMDVQLNWAqhDpVqZzL4qm4CuMMT`). You'll use this to add reserves. 50 | 51 | ## Add a reserve to your market 52 | 53 | A reserve is a liquidity pool that can be deposited into, borrowed from, and optionally used as collateral for borrows. 54 | 55 | ### Usage 56 | ```shell 57 | solend-program \ 58 | --program PUBKEY \ 59 | --fee-payer SIGNER \ 60 | add-reserve \ 61 | --market-owner SIGNER \ 62 | --source-owner SIGNER \ 63 | --market PUBKEY \ 64 | --source PUBKEY \ 65 | --amount DECIMAL_AMOUNT \ 66 | --pyth-product PUBKEY \ 67 | --pyth-price PUBKEY 68 | ``` 69 | - `--program` is the lending program ID. 70 | - `--fee-payer` will sign to pay transaction fees. 71 | - `--market-owner` will sign as the lending market owner. 72 | - `--source-owner` will sign as the source liquidity owner. 73 | - `--market` is the lending market pubkey. 74 | - `--source` is the SPL Token account pubkey (owned by `--source-owner`). 75 | - `--amount` is the amount of tokens to deposit. 76 | - `--pyth-product` and `--pyth-price` are oracle 77 | accounts [provided by Pyth](https://pyth.network/developers/consumers/accounts). 78 | 79 | Run `solend-program add-reserve --help` for more details and options. 80 | 81 | ### Example 82 | ```shell 83 | solend-program \ 84 | --program 6TvznH3B2e3p2mbhufNBpgSrLx6UkgvxtVQvopEZ2kuH \ 85 | --fee-payer owner.json \ 86 | add-reserve \ 87 | --market-owner owner.json \ 88 | --source-owner owner.json \ 89 | --market 7uX9ywsk1X2j6wLoywMDVQLNWAqhDpVqZzL4qm4CuMMT \ 90 | --source AJ2sgpgj6ZeQazPPiDyTYqN9vbj58QMaZQykB9Sr6XY \ 91 | --amount 5.0 \ 92 | --pyth-product 8yrQMUyJRnCJ72NWwMiPV9dNGw465Z8bKUvnUC8P5L6F \ 93 | --pyth-price BdgHsXrH1mXqhdosXavYxZgX6bGqTdj5mh2sxDhF8bJy 94 | 95 | # Adding reserve 69BwFhpQBzZfcp9MCj9V8TLvdv9zGfQQPQbb8dUHsaEa 96 | # Signature: 2yKHnmBSqBpbGdsxW75nnmZMys1bZMbHiczdZitMeQHYdpis4eVhuMWGE29hhgtHpNDjdPj5YVbqkWoAEBw1WaU 97 | # Signature: 33x8gbn2RkiA5844eCZq151DuVrYTvUoF1bQ5xA3mqkibJZaJja2hj8RoyjKZpZqg2ckcSKMAeqWbMeWC6vAySQS 98 | # Signature: 3dk79hSgzFhxPrmctYnS5dxRhojfKkDwwLxEda9bTXqVELHSL4ux8au4jwvL8xuraVhaZAmugCn4TA1YCfLM4sVL 99 | ``` 100 | 101 | Note the reserve pubkey (e.g. `69BwFhpQBzZfcp9MCj9V8TLvdv9zGfQQPQbb8dUHsaEa`). You'll use this to deposit liquidity, redeem collateral, borrow, repay, and liquidate. 102 | -------------------------------------------------------------------------------- /token-lending/program/tests/helpers/mock_switchboard_pull.rs: -------------------------------------------------------------------------------- 1 | /// mock oracle prices in tests with this program. 2 | use solana_program::{ 3 | account_info::AccountInfo, 4 | clock::Clock, 5 | entrypoint::ProgramResult, 6 | instruction::{AccountMeta, Instruction}, 7 | msg, 8 | pubkey::Pubkey, 9 | sysvar::Sysvar, 10 | }; 11 | 12 | use switchboard_on_demand::PullFeedAccountData; 13 | 14 | use borsh::{BorshDeserialize, BorshSerialize}; 15 | use spl_token::solana_program::{account_info::next_account_info, program_error::ProgramError}; 16 | use thiserror::Error; 17 | 18 | #[derive(BorshSerialize, BorshDeserialize)] 19 | pub enum MockSwitchboardPullInstruction { 20 | /// Accounts: 21 | /// 0: AggregatorAccount 22 | InitSwitchboard, 23 | 24 | /// Accounts: 25 | /// 0: AggregatorAccount 26 | SetSwitchboardPrice { price: i64, expo: i32 }, 27 | } 28 | 29 | pub fn process_instruction( 30 | program_id: &Pubkey, 31 | accounts: &[AccountInfo], 32 | instruction_data: &[u8], 33 | ) -> ProgramResult { 34 | Processor::process(program_id, accounts, instruction_data) 35 | } 36 | 37 | pub struct Processor; 38 | impl Processor { 39 | pub fn process( 40 | _program_id: &Pubkey, 41 | accounts: &[AccountInfo], 42 | instruction_data: &[u8], 43 | ) -> ProgramResult { 44 | let instruction = MockSwitchboardPullInstruction::try_from_slice(instruction_data)?; 45 | let account_info_iter = &mut accounts.iter().peekable(); 46 | 47 | match instruction { 48 | MockSwitchboardPullInstruction::InitSwitchboard => { 49 | msg!("Mock Switchboard Pull: Init Switchboard"); 50 | let switchboard_feed = next_account_info(account_info_iter)?; 51 | let mut data = switchboard_feed.try_borrow_mut_data()?; 52 | 53 | data[0..8].copy_from_slice(&PullFeedAccountData::discriminator()); 54 | 55 | Ok(()) 56 | } 57 | MockSwitchboardPullInstruction::SetSwitchboardPrice { price, expo } => { 58 | msg!("Mock Switchboard Pull: Set Switchboard price"); 59 | let switchboard_feed = next_account_info(account_info_iter)?; 60 | 61 | let mut data = switchboard_feed.try_borrow_mut_data()?; 62 | 63 | let scaled = (price as i128) * 10i128.pow((18 + expo) as u32); 64 | 65 | let result_offset = 8 + 2256; 66 | data[result_offset..(result_offset + 16)].copy_from_slice(&scaled.to_le_bytes()); 67 | data[(result_offset + 104)..(result_offset + 112)] 68 | .copy_from_slice(&Clock::get()?.slot.to_le_bytes()); 69 | 70 | Ok(()) 71 | } 72 | } 73 | } 74 | } 75 | 76 | #[derive(Error, Debug, Copy, Clone)] 77 | pub enum MockSwitchboardPullError { 78 | /// Invalid instruction 79 | #[error("Invalid Instruction")] 80 | InvalidInstruction, 81 | #[error("The account is not currently owned by the program")] 82 | IncorrectProgramId, 83 | #[error("Failed to deserialize")] 84 | FailedToDeserialize, 85 | } 86 | 87 | impl From for ProgramError { 88 | fn from(e: MockSwitchboardPullError) -> Self { 89 | ProgramError::Custom(e as u32) 90 | } 91 | } 92 | 93 | pub fn set_switchboard_price( 94 | program_id: Pubkey, 95 | switchboard_feed: Pubkey, 96 | price: i64, 97 | expo: i32, 98 | ) -> Instruction { 99 | let data = MockSwitchboardPullInstruction::SetSwitchboardPrice { price, expo } 100 | .try_to_vec() 101 | .unwrap(); 102 | Instruction { 103 | program_id, 104 | accounts: vec![AccountMeta::new(switchboard_feed, false)], 105 | data, 106 | } 107 | } 108 | 109 | pub fn init_switchboard(program_id: Pubkey, switchboard_feed: Pubkey) -> Instruction { 110 | let data = MockSwitchboardPullInstruction::InitSwitchboard 111 | .try_to_vec() 112 | .unwrap(); 113 | Instruction { 114 | program_id, 115 | accounts: vec![AccountMeta::new(switchboard_feed, false)], 116 | data, 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /token-lending/program/tests/update_metadata.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "test-bpf")] 2 | 3 | mod helpers; 4 | 5 | use crate::solend_program_test::custom_scenario; 6 | use solend_sdk::NULL_PUBKEY; 7 | 8 | use helpers::*; 9 | 10 | use solana_program::pubkey::Pubkey; 11 | use solana_program::system_instruction::transfer; 12 | use solana_program_test::*; 13 | use solana_sdk::native_token::LAMPORTS_PER_SOL; 14 | 15 | use solana_sdk::signer::Signer; 16 | 17 | use solend_program::state::{ 18 | LendingMarketMetadata, MARKET_DESCRIPTION_SIZE, MARKET_IMAGE_URL_SIZE, PADDING_SIZE, 19 | }; 20 | use solend_sdk::state::MARKET_NAME_SIZE; 21 | 22 | #[tokio::test] 23 | async fn test_success() { 24 | let (mut test, lending_market, _reserves, _obligations, _users, lending_market_owner) = 25 | custom_scenario(&[], &[]).await; 26 | 27 | let instructions = [transfer( 28 | &test.context.payer.pubkey(), 29 | &lending_market_owner.keypair.pubkey(), 30 | LAMPORTS_PER_SOL, 31 | )]; 32 | test.process_transaction(&instructions, None).await.unwrap(); 33 | 34 | lending_market 35 | .update_metadata( 36 | &mut test, 37 | &lending_market_owner, 38 | LendingMarketMetadata { 39 | bump_seed: 0, // gets filled in automatically 40 | market_name: [2u8; MARKET_NAME_SIZE], 41 | market_description: [3u8; MARKET_DESCRIPTION_SIZE], 42 | market_image_url: [4u8; MARKET_IMAGE_URL_SIZE], 43 | lookup_tables: [NULL_PUBKEY, NULL_PUBKEY, NULL_PUBKEY, NULL_PUBKEY], 44 | padding: [5u8; PADDING_SIZE], 45 | }, 46 | ) 47 | .await 48 | .unwrap(); 49 | 50 | let metadata_seeds = &[lending_market.pubkey.as_ref(), b"MetaData"]; 51 | let (metadata_key, _bump_seed) = 52 | Pubkey::find_program_address(metadata_seeds, &solend_program::id()); 53 | 54 | let lending_market_metadata = test 55 | .load_zeroable_account::(metadata_key) 56 | .await; 57 | 58 | let (_, bump_seed) = Pubkey::find_program_address( 59 | &[&lending_market.pubkey.to_bytes()[..32], b"MetaData"], 60 | &solend_program::id(), 61 | ); 62 | 63 | assert_eq!( 64 | lending_market_metadata.account, 65 | LendingMarketMetadata { 66 | bump_seed, 67 | market_name: [2u8; MARKET_NAME_SIZE], 68 | market_description: [3u8; MARKET_DESCRIPTION_SIZE], 69 | market_image_url: [4u8; MARKET_IMAGE_URL_SIZE], 70 | lookup_tables: [NULL_PUBKEY, NULL_PUBKEY, NULL_PUBKEY, NULL_PUBKEY,], 71 | padding: [5u8; PADDING_SIZE], 72 | } 73 | ); 74 | 75 | lending_market 76 | .update_metadata( 77 | &mut test, 78 | &lending_market_owner, 79 | LendingMarketMetadata { 80 | bump_seed: 0, // gets filled in automatically 81 | market_name: [6u8; MARKET_NAME_SIZE], 82 | market_description: [7u8; MARKET_DESCRIPTION_SIZE], 83 | market_image_url: [8u8; MARKET_IMAGE_URL_SIZE], 84 | lookup_tables: [NULL_PUBKEY, NULL_PUBKEY, NULL_PUBKEY, NULL_PUBKEY], 85 | padding: [9u8; PADDING_SIZE], 86 | }, 87 | ) 88 | .await 89 | .unwrap(); 90 | 91 | let lending_market_metadata = test 92 | .load_zeroable_account::(metadata_key) 93 | .await; 94 | 95 | let (_, bump_seed) = Pubkey::find_program_address( 96 | &[&lending_market.pubkey.to_bytes()[..32], b"MetaData"], 97 | &solend_program::id(), 98 | ); 99 | 100 | assert_eq!( 101 | lending_market_metadata.account, 102 | LendingMarketMetadata { 103 | bump_seed, 104 | market_name: [6u8; MARKET_NAME_SIZE], 105 | market_description: [7u8; MARKET_DESCRIPTION_SIZE], 106 | market_image_url: [8u8; MARKET_IMAGE_URL_SIZE], 107 | lookup_tables: [NULL_PUBKEY, NULL_PUBKEY, NULL_PUBKEY, NULL_PUBKEY], 108 | padding: [9u8; PADDING_SIZE], 109 | } 110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /token-lending/program/tests/repay_obligation_liquidity.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "test-bpf")] 2 | 3 | mod helpers; 4 | 5 | use crate::solend_program_test::scenario_1; 6 | use std::collections::HashSet; 7 | 8 | use helpers::solend_program_test::{BalanceChecker, TokenBalanceChange}; 9 | use helpers::*; 10 | use solana_program::native_token::LAMPORTS_PER_SOL; 11 | use solana_program_test::*; 12 | 13 | use solend_program::math::TryDiv; 14 | use solend_program::state::{LastUpdate, ObligationLiquidity, ReserveLiquidity, SLOTS_PER_YEAR}; 15 | use solend_program::{ 16 | math::{Decimal, TryAdd, TryMul, TrySub}, 17 | state::{Obligation, Reserve}, 18 | }; 19 | 20 | #[tokio::test] 21 | async fn test_success() { 22 | let (mut test, lending_market, usdc_reserve, wsol_reserve, user, obligation, _) = 23 | scenario_1(&test_reserve_config(), &test_reserve_config()).await; 24 | 25 | test.advance_clock_by_slots(1).await; 26 | 27 | let balance_checker = 28 | BalanceChecker::start(&mut test, &[&usdc_reserve, &user, &wsol_reserve]).await; 29 | 30 | lending_market 31 | .repay_obligation_liquidity( 32 | &mut test, 33 | &wsol_reserve, 34 | &obligation, 35 | &user, 36 | 10 * LAMPORTS_PER_SOL, 37 | ) 38 | .await 39 | .unwrap(); 40 | 41 | // check token balances 42 | let (balance_changes, mint_supply_changes) = 43 | balance_checker.find_balance_changes(&mut test).await; 44 | let expected_balance_changes = HashSet::from([ 45 | TokenBalanceChange { 46 | token_account: user.get_account(&wsol_mint::id()).unwrap(), 47 | mint: wsol_mint::id(), 48 | diff: -(10 * LAMPORTS_PER_SOL as i128), 49 | }, 50 | TokenBalanceChange { 51 | token_account: wsol_reserve.account.liquidity.supply_pubkey, 52 | mint: wsol_mint::id(), 53 | diff: (10 * LAMPORTS_PER_SOL as i128), 54 | }, 55 | ]); 56 | assert_eq!(balance_changes, expected_balance_changes); 57 | assert_eq!(mint_supply_changes, HashSet::new()); 58 | 59 | // check program state 60 | let wsol_reserve_post = test.load_account::(wsol_reserve.pubkey).await; 61 | 62 | // 1 + 0.3/SLOTS_PER_YEAR 63 | let new_cumulative_borrow_rate = Decimal::one() 64 | .try_add( 65 | Decimal::from_percent(wsol_reserve.account.config.max_borrow_rate) 66 | .try_div(Decimal::from(SLOTS_PER_YEAR)) 67 | .unwrap(), 68 | ) 69 | .unwrap(); 70 | let new_borrowed_amount_wads = new_cumulative_borrow_rate 71 | .try_mul(Decimal::from(10 * LAMPORTS_PER_SOL)) 72 | .unwrap() 73 | .try_sub(Decimal::from(10 * LAMPORTS_TO_SOL)) 74 | .unwrap(); 75 | 76 | assert_eq!( 77 | wsol_reserve_post.account, 78 | Reserve { 79 | last_update: LastUpdate { 80 | slot: 1001, 81 | stale: true 82 | }, 83 | liquidity: ReserveLiquidity { 84 | available_amount: 10 * LAMPORTS_PER_SOL, 85 | borrowed_amount_wads: new_borrowed_amount_wads, 86 | cumulative_borrow_rate_wads: new_cumulative_borrow_rate, 87 | ..wsol_reserve.account.liquidity 88 | }, 89 | ..wsol_reserve.account 90 | } 91 | ); 92 | 93 | let obligation_post = test.load_account::(obligation.pubkey).await; 94 | assert_eq!( 95 | obligation_post.account, 96 | Obligation { 97 | // we don't require obligation to be refreshed for repay 98 | last_update: LastUpdate { 99 | slot: 1000, 100 | stale: true 101 | }, 102 | borrows: [ObligationLiquidity { 103 | borrow_reserve: wsol_reserve.pubkey, 104 | cumulative_borrow_rate_wads: new_cumulative_borrow_rate, 105 | borrowed_amount_wads: new_borrowed_amount_wads, 106 | ..obligation.account.borrows[0] 107 | }] 108 | .to_vec(), 109 | ..obligation.account 110 | } 111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /token-lending/program/tests/deposit_obligation_collateral.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "test-bpf")] 2 | 3 | mod helpers; 4 | 5 | use std::collections::HashSet; 6 | 7 | use helpers::solend_program_test::{ 8 | setup_world, BalanceChecker, Info, SolendProgramTest, TokenBalanceChange, User, 9 | }; 10 | use helpers::test_reserve_config; 11 | 12 | use solana_program::instruction::InstructionError; 13 | use solana_program_test::*; 14 | use solana_sdk::signature::Keypair; 15 | use solana_sdk::transaction::TransactionError; 16 | use solend_program::math::Decimal; 17 | use solend_program::state::{LastUpdate, LendingMarket, Obligation, ObligationCollateral, Reserve}; 18 | 19 | async fn setup() -> ( 20 | SolendProgramTest, 21 | Info, 22 | Info, 23 | User, 24 | Info, 25 | ) { 26 | let (mut test, lending_market, usdc_reserve, _, _, user) = 27 | setup_world(&test_reserve_config(), &test_reserve_config()).await; 28 | 29 | let obligation = lending_market 30 | .init_obligation(&mut test, Keypair::new(), &user) 31 | .await 32 | .expect("This should succeed"); 33 | 34 | lending_market 35 | .deposit(&mut test, &usdc_reserve, &user, 1_000_000) 36 | .await 37 | .expect("This should succeed"); 38 | 39 | let usdc_reserve = test.load_account(usdc_reserve.pubkey).await; 40 | 41 | (test, lending_market, usdc_reserve, user, obligation) 42 | } 43 | 44 | #[tokio::test] 45 | async fn test_success() { 46 | let (mut test, lending_market, usdc_reserve, user, obligation) = setup().await; 47 | 48 | let balance_checker = BalanceChecker::start(&mut test, &[&usdc_reserve, &user]).await; 49 | 50 | lending_market 51 | .deposit_obligation_collateral(&mut test, &usdc_reserve, &obligation, &user, 1_000_000) 52 | .await 53 | .expect("This should succeed"); 54 | 55 | // check balance changes 56 | let (balance_changes, mint_supply_changes) = 57 | balance_checker.find_balance_changes(&mut test).await; 58 | let expected_balance_changes = HashSet::from([ 59 | TokenBalanceChange { 60 | token_account: user 61 | .get_account(&usdc_reserve.account.collateral.mint_pubkey) 62 | .unwrap(), 63 | mint: usdc_reserve.account.collateral.mint_pubkey, 64 | diff: -1_000_000, 65 | }, 66 | TokenBalanceChange { 67 | token_account: usdc_reserve.account.collateral.supply_pubkey, 68 | mint: usdc_reserve.account.collateral.mint_pubkey, 69 | diff: 1_000_000, 70 | }, 71 | ]); 72 | 73 | assert_eq!(balance_changes, expected_balance_changes); 74 | assert_eq!(mint_supply_changes, HashSet::new()); 75 | 76 | // check program state changes 77 | let lending_market_post = test.load_account(lending_market.pubkey).await; 78 | assert_eq!(lending_market, lending_market_post); 79 | 80 | let usdc_reserve_post = test.load_account(usdc_reserve.pubkey).await; 81 | assert_eq!(usdc_reserve, usdc_reserve_post); 82 | 83 | let obligation_post = test.load_account::(obligation.pubkey).await; 84 | assert_eq!( 85 | obligation_post.account, 86 | Obligation { 87 | last_update: LastUpdate { 88 | slot: 1000, 89 | stale: true, 90 | }, 91 | deposits: vec![ObligationCollateral { 92 | deposit_reserve: usdc_reserve.pubkey, 93 | deposited_amount: 1_000_000, 94 | market_value: Decimal::zero(), // this field only gets updated on a refresh 95 | attributed_borrow_value: Decimal::zero() 96 | }], 97 | ..obligation.account 98 | } 99 | ); 100 | } 101 | 102 | #[tokio::test] 103 | async fn test_fail_deposit_too_much() { 104 | let (mut test, lending_market, usdc_reserve, user, obligation) = setup().await; 105 | 106 | let res = lending_market 107 | .deposit_obligation_collateral(&mut test, &usdc_reserve, &obligation, &user, 1_000_001) 108 | .await 109 | .err() 110 | .unwrap() 111 | .unwrap(); 112 | 113 | match res { 114 | // InsufficientFunds 115 | TransactionError::InstructionError(1, InstructionError::Custom(1)) => (), 116 | // LendingError::TokenTransferFailed 117 | TransactionError::InstructionError(1, InstructionError::Custom(17)) => (), 118 | e => panic!("unexpected error: {:#?}", e), 119 | }; 120 | } 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana Lending Program 2 | 3 | 4 | The Solend lending protocol is based on the token-lending program authored by [Solana labs](https://github.com/solana-labs/solana-program-library). The Solend implementation can be found [here](https://github.com/solendprotocol/solana-program-library/tree/mainnet/token-lending/audit). 5 | 6 | [![Build status][travis-image]][travis-url] 7 | 8 | [travis-image]: 9 | https://travis-ci.org/solana-labs/solana-program-library.svg?branch=master 10 | [travis-url]: https://travis-ci.org/solana-labs/solana-program-library 11 | 12 | ## Contributing/Building 13 | 14 | The Solend protocol is open source with a focus on developer friendliness and integrations. 15 | 16 | Solend is made for developers to build on top of. Check out our developer portal to get involved. 17 | 18 | [Screen Shot 2022-01-09 at 5 54 30 PM](https://dev.solend.fi/) 19 | 20 | # Solana Program Library 21 | 22 | The Solana Program Library (SPL) is a collection of on-chain programs targeting 23 | the [Sealevel parallel 24 | runtime](https://medium.com/solana-labs/sealevel-parallel-processing-thousands-of-smart-contracts-d814b378192). 25 | These programs are tested against Solana's implementation of Sealevel, 26 | solana-runtime, and deployed to its mainnet. As others implement Sealevel, we 27 | will graciously accept patches to ensure the programs here are portable across 28 | all implementations. 29 | 30 | Full documentation is available at https://spl.solana.com 31 | 32 | ## Development 33 | 34 | ### Environment Setup 35 | 36 | 1. Install the latest Rust stable from https://rustup.rs/ 37 | 2. Install Solana v1.6.1 or later from https://docs.solana.com/cli/install-solana-cli-tools 38 | 3. Install the `libudev` development package for your distribution (`libudev-dev` on Debian-derived distros, `libudev-devel` on Redhat-derived). 39 | 40 | ### Build 41 | 42 | The normal cargo build is available for building programs against your host machine: 43 | ``` 44 | $ cargo build 45 | ``` 46 | 47 | To build a specific program, such as SPL Token, for the Solana BPF target: 48 | ``` 49 | $ cd token/program 50 | $ cargo build-bpf 51 | ``` 52 | 53 | ### Test 54 | 55 | Unit tests contained within all projects can be run with: 56 | ```bash 57 | $ cargo test # <-- runs host-based tests 58 | $ cargo test-bpf # <-- runs BPF program tests 59 | ``` 60 | 61 | To run a specific program's tests, such as SPL Token: 62 | ``` 63 | $ cd token/program 64 | $ cargo test # <-- runs host-based tests 65 | $ cargo test-bpf # <-- runs BPF program tests 66 | ``` 67 | 68 | Integration testing may be performed via the per-project .js bindings. See the 69 | [token program's js project](token/js) for an example. 70 | 71 | ### Clippy 72 | ```bash 73 | $ cargo clippy 74 | ``` 75 | 76 | ### Coverage 77 | ```bash 78 | $ ./coverage.sh # Please help! Coverage build currently fails on MacOS due to an XCode `grcov` mismatch... 79 | ``` 80 | 81 | 82 | ## Release Process 83 | SPL programs are currently tagged and released manually. Each program is 84 | versioned independently of the others, with all new development occurring on 85 | master. Once a program is tested and deemed ready for release: 86 | 87 | ### Bump Version 88 | 89 | * Increment the version number in the program's Cargo.toml 90 | * Generate a new program ID and replace in `/program-id.md` and `/src/lib.rs` 91 | * Run `cargo build-bpf ` to update relevant C bindings. (Note the 92 | location of the generated `spl_.so` for attaching to the Github 93 | release.) 94 | * Open a PR with these version changes and merge after passing CI. 95 | 96 | ### Create Github tag 97 | 98 | Program tags are of the form `-vX.Y.Z`. 99 | Create the new tag at the version-bump commit and push to the 100 | solana-program-library repository, eg: 101 | 102 | ``` 103 | $ git tag token-v1.0.0 b24bfe7 104 | $ git push upstream --tags 105 | ``` 106 | 107 | ### Publish Github release 108 | 109 | * Go to [GitHub Releases UI](https://github.com/solana-labs/solana-program-library/releases) 110 | * Click "Draft new release", and enter the new tag in the "Tag version" box. 111 | * Title the release "SPL vX.Y.Z", complete the description, and attach the `spl_.so` binary 112 | * Click "Publish release" 113 | 114 | ### Publish to Crates.io 115 | 116 | Navigate to the program directory and run `cargo package` 117 | to test the build. Then run `cargo publish`. 118 | -------------------------------------------------------------------------------- /token-lending/program/tests/helpers/mock_switchboard.rs: -------------------------------------------------------------------------------- 1 | /// mock oracle prices in tests with this program. 2 | use solana_program::{ 3 | account_info::AccountInfo, 4 | clock::Clock, 5 | entrypoint::ProgramResult, 6 | instruction::{AccountMeta, Instruction}, 7 | msg, 8 | pubkey::Pubkey, 9 | sysvar::Sysvar, 10 | }; 11 | use std::cell::RefMut; 12 | 13 | use switchboard_v2::{AggregatorAccountData, SwitchboardDecimal}; 14 | 15 | use borsh::{BorshDeserialize, BorshSerialize}; 16 | use spl_token::solana_program::{account_info::next_account_info, program_error::ProgramError}; 17 | use thiserror::Error; 18 | 19 | #[derive(BorshSerialize, BorshDeserialize)] 20 | pub enum MockSwitchboardInstruction { 21 | /// Accounts: 22 | /// 0: AggregatorAccount 23 | InitSwitchboard, 24 | 25 | /// Accounts: 26 | /// 0: AggregatorAccount 27 | SetSwitchboardPrice { price: i64, expo: i32 }, 28 | } 29 | 30 | pub fn process_instruction( 31 | program_id: &Pubkey, 32 | accounts: &[AccountInfo], 33 | instruction_data: &[u8], 34 | ) -> ProgramResult { 35 | Processor::process(program_id, accounts, instruction_data) 36 | } 37 | 38 | pub struct Processor; 39 | impl Processor { 40 | pub fn process( 41 | _program_id: &Pubkey, 42 | accounts: &[AccountInfo], 43 | instruction_data: &[u8], 44 | ) -> ProgramResult { 45 | let instruction = MockSwitchboardInstruction::try_from_slice(instruction_data)?; 46 | let account_info_iter = &mut accounts.iter().peekable(); 47 | 48 | match instruction { 49 | MockSwitchboardInstruction::InitSwitchboard => { 50 | msg!("Mock Switchboard: Init Switchboard"); 51 | let switchboard_feed = next_account_info(account_info_iter)?; 52 | let mut data = switchboard_feed.try_borrow_mut_data()?; 53 | 54 | let discriminator = [217, 230, 65, 101, 201, 162, 27, 125]; 55 | data[0..8].copy_from_slice(&discriminator); 56 | Ok(()) 57 | } 58 | MockSwitchboardInstruction::SetSwitchboardPrice { price, expo } => { 59 | msg!("Mock Switchboard: Set Switchboard price"); 60 | let switchboard_feed = next_account_info(account_info_iter)?; 61 | let data = switchboard_feed.try_borrow_mut_data()?; 62 | 63 | let mut aggregator_account: RefMut = 64 | RefMut::map(data, |data| { 65 | bytemuck::from_bytes_mut( 66 | &mut data[8..std::mem::size_of::() + 8], 67 | ) 68 | }); 69 | 70 | aggregator_account.min_oracle_results = 1; 71 | aggregator_account.latest_confirmed_round.num_success = 1; 72 | aggregator_account.latest_confirmed_round.result = SwitchboardDecimal { 73 | mantissa: price as i128, 74 | scale: expo as u32, 75 | }; 76 | aggregator_account.latest_confirmed_round.round_open_slot = Clock::get()?.slot; 77 | 78 | Ok(()) 79 | } 80 | } 81 | } 82 | } 83 | 84 | #[derive(Error, Debug, Copy, Clone)] 85 | pub enum MockSwitchboardError { 86 | /// Invalid instruction 87 | #[error("Invalid Instruction")] 88 | InvalidInstruction, 89 | #[error("The account is not currently owned by the program")] 90 | IncorrectProgramId, 91 | #[error("Failed to deserialize")] 92 | FailedToDeserialize, 93 | } 94 | 95 | impl From for ProgramError { 96 | fn from(e: MockSwitchboardError) -> Self { 97 | ProgramError::Custom(e as u32) 98 | } 99 | } 100 | 101 | pub fn set_switchboard_price( 102 | program_id: Pubkey, 103 | switchboard_feed: Pubkey, 104 | price: i64, 105 | expo: i32, 106 | ) -> Instruction { 107 | let data = MockSwitchboardInstruction::SetSwitchboardPrice { price, expo } 108 | .try_to_vec() 109 | .unwrap(); 110 | Instruction { 111 | program_id, 112 | accounts: vec![AccountMeta::new(switchboard_feed, false)], 113 | data, 114 | } 115 | } 116 | 117 | pub fn init_switchboard(program_id: Pubkey, switchboard_feed: Pubkey) -> Instruction { 118 | let data = MockSwitchboardInstruction::InitSwitchboard 119 | .try_to_vec() 120 | .unwrap(); 121 | Instruction { 122 | program_id, 123 | accounts: vec![AccountMeta::new(switchboard_feed, false)], 124 | data, 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /token-lending/program/tests/obligation_end_to_end.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "test-bpf")] 2 | 3 | use crate::solend_program_test::TokenBalanceChange; 4 | use solend_program::math::TryMul; 5 | use solend_program::math::TrySub; 6 | use solend_program::state::ReserveConfig; 7 | use solend_program::state::ReserveFees; 8 | mod helpers; 9 | 10 | use std::collections::HashSet; 11 | 12 | use crate::solend_program_test::setup_world; 13 | use crate::solend_program_test::BalanceChecker; 14 | use crate::solend_program_test::Info; 15 | use crate::solend_program_test::SolendProgramTest; 16 | use crate::solend_program_test::User; 17 | use helpers::*; 18 | use solana_program_test::*; 19 | use solana_sdk::signature::Keypair; 20 | use solend_program::math::Decimal; 21 | use solend_program::state::LendingMarket; 22 | use solend_program::state::Reserve; 23 | 24 | async fn setup() -> ( 25 | SolendProgramTest, 26 | Info, 27 | Info, 28 | Info, 29 | User, 30 | ) { 31 | let (test, lending_market, usdc_reserve, wsol_reserve, _, user) = setup_world( 32 | &test_reserve_config(), 33 | &ReserveConfig { 34 | fees: ReserveFees { 35 | borrow_fee_wad: 100_000_000_000, 36 | flash_loan_fee_wad: 0, 37 | host_fee_percentage: 20, 38 | }, 39 | ..test_reserve_config() 40 | }, 41 | ) 42 | .await; 43 | 44 | (test, lending_market, usdc_reserve, wsol_reserve, user) 45 | } 46 | 47 | #[tokio::test] 48 | async fn test_success() { 49 | let (mut test, lending_market, usdc_reserve, wsol_reserve, user) = setup().await; 50 | 51 | let host_fee_receiver = User::new_with_balances(&mut test, &[(&wsol_mint::id(), 0)]).await; 52 | let obligation = lending_market 53 | .init_obligation(&mut test, Keypair::new(), &user) 54 | .await 55 | .unwrap(); 56 | 57 | let balance_checker = BalanceChecker::start( 58 | &mut test, 59 | &[&usdc_reserve, &wsol_reserve, &user, &host_fee_receiver], 60 | ) 61 | .await; 62 | 63 | lending_market 64 | .deposit_reserve_liquidity_and_obligation_collateral( 65 | &mut test, 66 | &usdc_reserve, 67 | &obligation, 68 | &user, 69 | 100 * FRACTIONAL_TO_USDC, 70 | ) 71 | .await 72 | .unwrap(); 73 | 74 | let obligation = test.load_account(obligation.pubkey).await; 75 | lending_market 76 | .borrow_obligation_liquidity( 77 | &mut test, 78 | &wsol_reserve, 79 | &obligation, 80 | &user, 81 | host_fee_receiver.get_account(&wsol_mint::id()), 82 | LAMPORTS_TO_SOL / 2, 83 | ) 84 | .await 85 | .unwrap(); 86 | 87 | lending_market 88 | .repay_obligation_liquidity(&mut test, &wsol_reserve, &obligation, &user, u64::MAX) 89 | .await 90 | .unwrap(); 91 | 92 | let obligation = test.load_account(obligation.pubkey).await; 93 | lending_market 94 | .withdraw_obligation_collateral_and_redeem_reserve_collateral( 95 | &mut test, 96 | &usdc_reserve, 97 | &obligation, 98 | &user, 99 | 100 * FRACTIONAL_TO_USDC, 100 | ) 101 | .await 102 | .unwrap(); 103 | 104 | let (balance_changes, mint_supply_changes) = 105 | balance_checker.find_balance_changes(&mut test).await; 106 | let borrow_fee = Decimal::from(LAMPORTS_TO_SOL / 2) 107 | .try_mul(Decimal::from_scaled_val( 108 | wsol_reserve.account.config.fees.borrow_fee_wad as u128, 109 | )) 110 | .unwrap(); 111 | let host_fee = borrow_fee 112 | .try_mul(Decimal::from_percent( 113 | wsol_reserve.account.config.fees.host_fee_percentage, 114 | )) 115 | .unwrap(); 116 | 117 | let expected_balance_changes = HashSet::from([ 118 | TokenBalanceChange { 119 | token_account: user.get_account(&wsol_mint::id()).unwrap(), 120 | mint: wsol_mint::id(), 121 | diff: -(borrow_fee.try_round_u64().unwrap() as i128), 122 | }, 123 | TokenBalanceChange { 124 | token_account: host_fee_receiver.get_account(&wsol_mint::id()).unwrap(), 125 | mint: wsol_mint::id(), 126 | diff: host_fee.try_round_u64().unwrap() as i128, 127 | }, 128 | TokenBalanceChange { 129 | token_account: wsol_reserve.account.config.fee_receiver, 130 | mint: wsol_mint::id(), 131 | diff: borrow_fee 132 | .try_sub(host_fee) 133 | .unwrap() 134 | .try_round_u64() 135 | .unwrap() as i128, 136 | }, 137 | ]); 138 | assert_eq!(balance_changes, expected_balance_changes); 139 | assert_eq!(mint_supply_changes, HashSet::new()); 140 | } 141 | -------------------------------------------------------------------------------- /token-lending/program/tests/deposit_reserve_liquidity_and_obligation_collateral.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "test-bpf")] 2 | 3 | mod helpers; 4 | 5 | use crate::solend_program_test::MintSupplyChange; 6 | use std::collections::HashSet; 7 | 8 | use helpers::solend_program_test::{ 9 | setup_world, BalanceChecker, Info, SolendProgramTest, TokenBalanceChange, User, 10 | }; 11 | use helpers::*; 12 | use solana_program_test::*; 13 | use solana_sdk::signature::Keypair; 14 | 15 | use solend_program::math::Decimal; 16 | use solend_program::state::{ 17 | LastUpdate, LendingMarket, Obligation, ObligationCollateral, Reserve, ReserveCollateral, 18 | ReserveLiquidity, 19 | }; 20 | 21 | async fn setup() -> ( 22 | SolendProgramTest, 23 | Info, 24 | Info, 25 | User, 26 | Info, 27 | ) { 28 | let (mut test, lending_market, usdc_reserve, _, _, user) = 29 | setup_world(&test_reserve_config(), &test_reserve_config()).await; 30 | 31 | let obligation = lending_market 32 | .init_obligation(&mut test, Keypair::new(), &user) 33 | .await 34 | .expect("This should succeed"); 35 | 36 | (test, lending_market, usdc_reserve, user, obligation) 37 | } 38 | 39 | #[tokio::test] 40 | async fn test_success() { 41 | let (mut test, lending_market, usdc_reserve, user, obligation) = setup().await; 42 | 43 | test.advance_clock_by_slots(1).await; 44 | 45 | let balance_checker = BalanceChecker::start(&mut test, &[&usdc_reserve, &user]).await; 46 | 47 | // deposit 48 | lending_market 49 | .deposit_reserve_liquidity_and_obligation_collateral( 50 | &mut test, 51 | &usdc_reserve, 52 | &obligation, 53 | &user, 54 | 1_000_000, 55 | ) 56 | .await 57 | .expect("this should succeed"); 58 | 59 | // check token balances 60 | let (token_balance_changes, mint_supply_changes) = 61 | balance_checker.find_balance_changes(&mut test).await; 62 | 63 | assert_eq!( 64 | token_balance_changes, 65 | HashSet::from([ 66 | TokenBalanceChange { 67 | token_account: user.get_account(&usdc_mint::id()).unwrap(), 68 | mint: usdc_mint::id(), 69 | diff: -1_000_000, 70 | }, 71 | TokenBalanceChange { 72 | token_account: usdc_reserve.account.collateral.supply_pubkey, 73 | mint: usdc_reserve.account.collateral.mint_pubkey, 74 | diff: 1_000_000, 75 | }, 76 | TokenBalanceChange { 77 | token_account: usdc_reserve.account.liquidity.supply_pubkey, 78 | mint: usdc_reserve.account.liquidity.mint_pubkey, 79 | diff: 1_000_000, 80 | }, 81 | ]), 82 | "{:#?}", 83 | token_balance_changes 84 | ); 85 | 86 | assert_eq!( 87 | mint_supply_changes, 88 | HashSet::from([MintSupplyChange { 89 | mint: usdc_reserve.account.collateral.mint_pubkey, 90 | diff: 1_000_000, 91 | },]), 92 | "{:#?}", 93 | mint_supply_changes 94 | ); 95 | 96 | // check program state 97 | let lending_market_post = test 98 | .load_account::(lending_market.pubkey) 99 | .await; 100 | assert_eq!(lending_market.account, lending_market_post.account); 101 | 102 | let usdc_reserve_post = test.load_account::(usdc_reserve.pubkey).await; 103 | assert_eq!( 104 | usdc_reserve_post.account, 105 | Reserve { 106 | last_update: LastUpdate { 107 | slot: 1001, 108 | stale: false, 109 | }, 110 | liquidity: ReserveLiquidity { 111 | available_amount: usdc_reserve.account.liquidity.available_amount + 1_000_000, 112 | ..usdc_reserve.account.liquidity 113 | }, 114 | collateral: ReserveCollateral { 115 | mint_total_supply: usdc_reserve.account.collateral.mint_total_supply + 1_000_000, 116 | ..usdc_reserve.account.collateral 117 | }, 118 | ..usdc_reserve.account 119 | } 120 | ); 121 | 122 | let obligation_post = test.load_account::(obligation.pubkey).await; 123 | assert_eq!( 124 | obligation_post.account, 125 | Obligation { 126 | last_update: LastUpdate { 127 | slot: 1000, 128 | stale: true 129 | }, 130 | deposits: [ObligationCollateral { 131 | deposit_reserve: usdc_reserve.pubkey, 132 | deposited_amount: 1_000_000, 133 | market_value: Decimal::zero(), 134 | attributed_borrow_value: Decimal::zero() 135 | }] 136 | .to_vec(), 137 | ..obligation.account 138 | } 139 | ); 140 | } 141 | -------------------------------------------------------------------------------- /token-lending/cli/src/lending_state.rs: -------------------------------------------------------------------------------- 1 | use solana_program::instruction::Instruction; 2 | use solend_sdk::instruction::{ 3 | refresh_obligation, refresh_reserve, withdraw_obligation_collateral, 4 | }; 5 | use solend_sdk::state::{Obligation, Reserve}; 6 | 7 | use solana_client::rpc_client::RpcClient; 8 | use solana_program::program_pack::Pack; 9 | use solana_program::pubkey::Pubkey; 10 | use spl_associated_token_account::get_associated_token_address; 11 | use std::collections::HashSet; 12 | 13 | pub struct SolendState { 14 | lending_program_id: Pubkey, 15 | obligation_pubkey: Pubkey, 16 | obligation: Obligation, 17 | reserves: Vec<(Pubkey, Reserve)>, 18 | } 19 | 20 | impl SolendState { 21 | pub fn new( 22 | lending_program_id: Pubkey, 23 | obligation_pubkey: Pubkey, 24 | rpc_client: &RpcClient, 25 | ) -> Self { 26 | let obligation = { 27 | let data = rpc_client.get_account(&obligation_pubkey).unwrap(); 28 | Obligation::unpack(&data.data).unwrap() 29 | }; 30 | 31 | // get reserve pubkeys 32 | let reserve_pubkeys: Vec = { 33 | let mut r = HashSet::new(); 34 | r.extend(obligation.deposits.iter().map(|d| d.deposit_reserve)); 35 | r.extend(obligation.borrows.iter().map(|b| b.borrow_reserve)); 36 | r.into_iter().collect() 37 | }; 38 | 39 | // get reserve accounts 40 | let reserves: Vec<(Pubkey, Reserve)> = rpc_client 41 | .get_multiple_accounts(&reserve_pubkeys) 42 | .unwrap() 43 | .into_iter() 44 | .zip(reserve_pubkeys.iter()) 45 | .map(|(account, pubkey)| (*pubkey, Reserve::unpack(&account.unwrap().data).unwrap())) 46 | .collect(); 47 | 48 | assert!(reserve_pubkeys.len() == reserves.len()); 49 | 50 | Self { 51 | lending_program_id, 52 | obligation_pubkey, 53 | obligation, 54 | reserves, 55 | } 56 | } 57 | 58 | pub fn find_reserve_by_key(&self, pubkey: Pubkey) -> Option<&Reserve> { 59 | self.reserves.iter().find_map( 60 | |(p, reserve)| { 61 | if pubkey == *p { 62 | Some(reserve) 63 | } else { 64 | None 65 | } 66 | }, 67 | ) 68 | } 69 | 70 | fn get_refresh_instructions(&self) -> Vec { 71 | let mut instructions = Vec::new(); 72 | instructions.extend(self.reserves.iter().map(|(pubkey, reserve)| { 73 | refresh_reserve( 74 | self.lending_program_id, 75 | *pubkey, 76 | reserve.liquidity.pyth_oracle_pubkey, 77 | reserve.liquidity.switchboard_oracle_pubkey, 78 | reserve.config.extra_oracle_pubkey, 79 | ) 80 | })); 81 | 82 | let reserve_pubkeys: Vec = { 83 | let mut r = Vec::new(); 84 | r.extend(self.obligation.deposits.iter().map(|d| d.deposit_reserve)); 85 | r.extend(self.obligation.borrows.iter().map(|b| b.borrow_reserve)); 86 | r 87 | }; 88 | 89 | // refresh obligation 90 | instructions.push(refresh_obligation( 91 | self.lending_program_id, 92 | self.obligation_pubkey, 93 | reserve_pubkeys, 94 | )); 95 | 96 | instructions 97 | } 98 | 99 | /// withdraw obligation ctokens to owner's ata 100 | pub fn withdraw( 101 | &self, 102 | withdraw_reserve_pubkey: &Pubkey, 103 | collateral_amount: u64, 104 | ) -> Vec { 105 | let mut instructions = self.get_refresh_instructions(); 106 | 107 | // find repay, withdraw reserve states 108 | let withdraw_reserve = self 109 | .reserves 110 | .iter() 111 | .find_map(|(pubkey, reserve)| { 112 | if withdraw_reserve_pubkey == pubkey { 113 | Some(reserve) 114 | } else { 115 | None 116 | } 117 | }) 118 | .unwrap(); 119 | 120 | instructions.push(withdraw_obligation_collateral( 121 | self.lending_program_id, 122 | collateral_amount, 123 | withdraw_reserve.collateral.supply_pubkey, 124 | get_associated_token_address( 125 | &self.obligation.owner, 126 | &withdraw_reserve.collateral.mint_pubkey, 127 | ), 128 | *withdraw_reserve_pubkey, 129 | self.obligation_pubkey, 130 | withdraw_reserve.lending_market, 131 | self.obligation.owner, 132 | self.obligation 133 | .deposits 134 | .iter() 135 | .map(|d| d.deposit_reserve) 136 | .collect(), 137 | )); 138 | 139 | instructions 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - "docs/**" 7 | push: 8 | branches: [master, upcoming] 9 | paths-ignore: 10 | - "docs/**" 11 | 12 | jobs: 13 | all_github_action_checks: 14 | runs-on: ubuntu-latest 15 | needs: 16 | - rustfmt 17 | - clippy 18 | - cargo-build-test 19 | steps: 20 | - run: echo "Done" 21 | 22 | rustfmt: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | 27 | - name: Set env vars 28 | run: | 29 | source ci/rust-version.sh 30 | echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV 31 | 32 | - uses: actions-rs/toolchain@v1 33 | with: 34 | toolchain: ${{ env.RUST_STABLE }} 35 | override: true 36 | profile: minimal 37 | components: rustfmt 38 | 39 | - name: Run fmt 40 | uses: actions-rs/cargo@v1 41 | with: 42 | command: fmt 43 | args: --all -- --check 44 | 45 | clippy: 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@v2 49 | 50 | - name: Set env vars 51 | run: | 52 | source ci/rust-version.sh 53 | echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV 54 | 55 | - uses: actions-rs/toolchain@v1 56 | with: 57 | toolchain: ${{ env.RUST_STABLE }} 58 | override: true 59 | profile: minimal 60 | components: clippy 61 | 62 | - uses: actions/cache@v4 63 | with: 64 | path: | 65 | ~/.cargo/registry 66 | ~/.cargo/git 67 | target 68 | key: cargo-clippy-${{ hashFiles('**/Cargo.lock') }} 69 | restore-keys: | 70 | cargo-clippy- 71 | 72 | - name: Install dependencies 73 | run: ./ci/install-build-deps.sh 74 | 75 | - name: Run clippy 76 | uses: actions-rs/cargo@v1 77 | with: 78 | command: clippy 79 | args: --workspace --all-targets -- --deny=warnings 80 | 81 | cargo-build-test: 82 | runs-on: ubuntu-latest 83 | steps: 84 | - uses: actions/checkout@v2 85 | 86 | - name: Set env vars 87 | run: | 88 | source ci/rust-version.sh 89 | echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV 90 | source ci/solana-version.sh 91 | echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV 92 | 93 | - uses: actions-rs/toolchain@v1 94 | with: 95 | toolchain: ${{ env.RUST_STABLE }} 96 | override: true 97 | profile: minimal 98 | 99 | - uses: actions/cache@v4 100 | with: 101 | path: | 102 | ~/.cargo/registry 103 | ~/.cargo/git 104 | # target # Removed due to build dependency caching conflicts 105 | key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}} 106 | 107 | - uses: actions/cache@v4 108 | with: 109 | path: | 110 | ~/.cargo/bin/rustfilt 111 | key: cargo-bpf-bins-${{ runner.os }} 112 | 113 | - uses: actions/cache@v4 114 | with: 115 | path: | 116 | ~/.cache 117 | key: solana-${{ env.SOLANA_VERSION }} 118 | 119 | - name: Install dependencies 120 | run: | 121 | ./ci/install-build-deps.sh 122 | ./ci/install-program-deps.sh 123 | echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH 124 | 125 | - name: Build and test 126 | run: ./ci/cargo-build-test.sh 127 | 128 | cargo-coverage: 129 | runs-on: ubuntu-latest-16-cores 130 | steps: 131 | - uses: actions/checkout@v2 132 | 133 | - name: Set env vars 134 | run: | 135 | source ci/rust-version.sh 136 | echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV 137 | source ci/solana-version.sh 138 | echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV 139 | 140 | - uses: actions-rs/toolchain@v1 141 | with: 142 | toolchain: ${{ env.RUST_STABLE }} 143 | override: true 144 | profile: minimal 145 | 146 | - uses: actions/cache@v4 147 | with: 148 | path: | 149 | ~/.cargo/registry 150 | ~/.cargo/git 151 | # target # Removed due to build dependency caching conflicts 152 | key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE }} 153 | 154 | - name: Install dependencies 155 | run: | 156 | ./ci/install-build-deps.sh 157 | ./ci/install-program-deps.sh 158 | echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH 159 | 160 | - name: run test coverage 161 | run: ./coverage.sh 162 | 163 | - name: Codecov 164 | uses: codecov/codecov-action@v3.1.0 165 | with: 166 | directory: target/coverage/ 167 | -------------------------------------------------------------------------------- /token-lending/README.md: -------------------------------------------------------------------------------- 1 | # Solend Lending program 2 | 3 | The Solend lending protocol is based on the token-lending program authored by [Solana labs](https://github.com/solana-labs/solana-program-library). 4 | 5 | ## Contributing/Building 6 | 7 | The Solend protocol is open source with a focus on developer friendliness and integrations. 8 | 9 | Solend is made for developers to build on top of. Check out our developer portal to understand more and get involved. 10 | 11 | [ 12 | Screen Shot 2022-01-09 at 5 54 30 PM](https://dev.solend.fi/). 13 | 14 | 15 | ### On-chain programs 16 | 17 | Please note that only the lending program deployed to devnet is currently operational. 18 | 19 | | Cluster | Program Address | 20 | | --- | --- | 21 | | Mainnet Beta | [`So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo`](https://explorer.solana.com/address/So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo) | 22 | | Devnet | [`ALend7Ketfx5bxh6ghsCDXAoDrhvEmsXT3cynB6aPLgx`](https://explorer.solana.com/address/ALend7Ketfx5bxh6ghsCDXAoDrhvEmsXT3cynB6aPLgx?cluster=devnet) | 23 | 24 | ### Deploy a lending program (optional) 25 | 26 | This is optional! You can skip these steps and use the [Token Lending CLI](./cli/README.md) with one of the on-chain programs listed above to create a lending market and add reserves to it. 27 | 28 | 1. [Install the Solana CLI](https://docs.solana.com/cli/install-solana-cli-tools) 29 | 30 | 1. Install the Token and Token Lending CLIs: 31 | ```shell 32 | cargo install spl-token-cli 33 | cargo install solend-program-cli 34 | ``` 35 | 36 | 1. Clone the SPL repo: 37 | ```shell 38 | git clone https://github.com/solana-labs/solana-program-library.git 39 | ``` 40 | 41 | 1. Go to the new directory: 42 | ```shell 43 | cd solana-program-library 44 | ``` 45 | 46 | 1. Generate a keypair for yourself: 47 | ```shell 48 | solana-keygen new -o owner.json 49 | 50 | # Wrote new keypair to owner.json 51 | # ================================================================================ 52 | # pubkey: JAgN4SZLNeCo9KTnr8EWt4FzEV1UDgHkcZwkVtWtfp6P 53 | # ================================================================================ 54 | # Save this seed phrase and your BIP39 passphrase to recover your new keypair: 55 | # your seed words here never share them not even with your mom 56 | # ================================================================================ 57 | ``` 58 | This pubkey will be the owner of the lending market that can add reserves to it. 59 | 60 | 1. Generate a keypair for the program: 61 | ```shell 62 | solana-keygen new -o lending.json 63 | 64 | # Wrote new keypair to lending.json 65 | # ============================================================================ 66 | # pubkey: 6TvznH3B2e3p2mbhufNBpgSrLx6UkgvxtVQvopEZ2kuH 67 | # ============================================================================ 68 | # Save this seed phrase and your BIP39 passphrase to recover your new keypair: 69 | # your seed words here never share them not even with your mom 70 | # ============================================================================ 71 | ``` 72 | This pubkey will be your Program ID. 73 | 74 | 1. Open `./token-lending/program/src/lib.rs` in your editor. In the line 75 | ```rust 76 | solana_program::declare_id!("6TvznH3B2e3p2mbhufNBpgSrLx6UkgvxtVQvopEZ2kuH"); 77 | ``` 78 | replace the Program ID with yours. 79 | 80 | 1. Build the program binaries: 81 | ```shell 82 | cargo build 83 | cargo build-bpf 84 | ``` 85 | 86 | 1. Prepare to deploy to devnet: 87 | ```shell 88 | solana config set --url https://api.devnet.solana.com 89 | ``` 90 | 91 | 1. Score yourself some sweet SOL: 92 | ```shell 93 | solana airdrop -k owner.json 10 94 | solana airdrop -k owner.json 10 95 | solana airdrop -k owner.json 10 96 | ``` 97 | You'll use this for transaction fees, rent for your program accounts, and initial reserve liquidity. 98 | 99 | 1. Deploy the program: 100 | ```shell 101 | solana program deploy \ 102 | -k owner.json \ 103 | --program-id lending.json \ 104 | target/deploy/solend_program.so 105 | 106 | # Program Id: 6TvznH3B2e3p2mbhufNBpgSrLx6UkgvxtVQvopEZ2kuH 107 | ``` 108 | If the deployment doesn't succeed, follow [this guide](https://docs.solana.com/cli/deploy-a-program#resuming-a-failed-deploy) to resume it. 109 | 110 | 1. Wrap some of your SOL as an SPL Token: 111 | ```shell 112 | spl-token wrap \ 113 | --fee-payer owner.json \ 114 | 10.0 \ 115 | -- owner.json 116 | 117 | # Wrapping 10 SOL into AJ2sgpgj6ZeQazPPiDyTYqN9vbj58QMaZQykB9Sr6XY 118 | ``` 119 | You'll use this for initial reserve liquidity. Note the SPL Token account pubkey (e.g. `AJ2sgpgj6ZeQazPPiDyTYqN9vbj58QMaZQykB9Sr6XY`). 120 | 121 | 1. Use the [Token Lending CLI](./cli/README.md) to create a lending market and add reserves to it. 122 | -------------------------------------------------------------------------------- /token-lending/program/tests/redeem_reserve_collateral.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "test-bpf")] 2 | 3 | mod helpers; 4 | 5 | use crate::solend_program_test::MintSupplyChange; 6 | use solend_sdk::math::Decimal; 7 | use std::collections::HashSet; 8 | 9 | use helpers::solend_program_test::{ 10 | setup_world, BalanceChecker, Info, SolendProgramTest, TokenBalanceChange, User, 11 | }; 12 | use helpers::*; 13 | use solana_program::instruction::InstructionError; 14 | use solana_program_test::*; 15 | use solana_sdk::transaction::TransactionError; 16 | use solend_program::state::{ 17 | LastUpdate, LendingMarket, Reserve, ReserveCollateral, ReserveLiquidity, 18 | }; 19 | 20 | pub async fn setup() -> (SolendProgramTest, Info, Info, User) { 21 | let (mut test, lending_market, usdc_reserve, _, _, user) = 22 | setup_world(&test_reserve_config(), &test_reserve_config()).await; 23 | 24 | lending_market 25 | .deposit(&mut test, &usdc_reserve, &user, 1_000_000) 26 | .await 27 | .expect("this should succeed"); 28 | 29 | let lending_market = test 30 | .load_account::(lending_market.pubkey) 31 | .await; 32 | 33 | let usdc_reserve = test.load_account::(usdc_reserve.pubkey).await; 34 | 35 | (test, lending_market, usdc_reserve, user) 36 | } 37 | 38 | #[tokio::test] 39 | async fn test_success() { 40 | let (mut test, lending_market, usdc_reserve, user) = setup().await; 41 | 42 | let balance_checker = BalanceChecker::start(&mut test, &[&usdc_reserve, &user]).await; 43 | 44 | lending_market 45 | .redeem(&mut test, &usdc_reserve, &user, 1_000_000) 46 | .await 47 | .expect("This should succeed"); 48 | 49 | // check token balances 50 | let (balance_changes, mint_supply_changes) = 51 | balance_checker.find_balance_changes(&mut test).await; 52 | 53 | assert_eq!( 54 | balance_changes, 55 | HashSet::from([ 56 | TokenBalanceChange { 57 | token_account: user.get_account(&usdc_mint::id()).unwrap(), 58 | mint: usdc_mint::id(), 59 | diff: 1_000_000, 60 | }, 61 | TokenBalanceChange { 62 | token_account: user 63 | .get_account(&usdc_reserve.account.collateral.mint_pubkey) 64 | .unwrap(), 65 | mint: usdc_reserve.account.collateral.mint_pubkey, 66 | diff: -1_000_000, 67 | }, 68 | TokenBalanceChange { 69 | token_account: usdc_reserve.account.liquidity.supply_pubkey, 70 | mint: usdc_reserve.account.liquidity.mint_pubkey, 71 | diff: -1_000_000, 72 | }, 73 | ]), 74 | "{:#?}", 75 | balance_changes 76 | ); 77 | assert_eq!( 78 | mint_supply_changes, 79 | HashSet::from([MintSupplyChange { 80 | mint: usdc_reserve.account.collateral.mint_pubkey, 81 | diff: -1_000_000, 82 | },]), 83 | "{:#?}", 84 | mint_supply_changes 85 | ); 86 | 87 | // check program state changes 88 | let lending_market_post = test 89 | .load_account::(lending_market.pubkey) 90 | .await; 91 | assert_eq!( 92 | lending_market_post.account, 93 | LendingMarket { 94 | rate_limiter: { 95 | let mut rate_limiter = lending_market.account.rate_limiter; 96 | rate_limiter.update(1000, Decimal::from(1u64)).unwrap(); 97 | rate_limiter 98 | }, 99 | ..lending_market.account 100 | } 101 | ); 102 | 103 | let usdc_reserve_post = test.load_account::(usdc_reserve.pubkey).await; 104 | assert_eq!( 105 | usdc_reserve_post.account, 106 | Reserve { 107 | last_update: LastUpdate { 108 | slot: 1000, 109 | stale: true 110 | }, 111 | liquidity: ReserveLiquidity { 112 | available_amount: usdc_reserve.account.liquidity.available_amount - 1_000_000, 113 | ..usdc_reserve.account.liquidity 114 | }, 115 | collateral: ReserveCollateral { 116 | mint_total_supply: usdc_reserve.account.collateral.mint_total_supply - 1_000_000, 117 | ..usdc_reserve.account.collateral 118 | }, 119 | rate_limiter: { 120 | let mut rate_limiter = usdc_reserve.account.rate_limiter; 121 | rate_limiter 122 | .update(1000, Decimal::from(1_000_000u64)) 123 | .unwrap(); 124 | 125 | rate_limiter 126 | }, 127 | ..usdc_reserve.account 128 | } 129 | ); 130 | } 131 | 132 | #[tokio::test] 133 | async fn test_fail_redeem_too_much() { 134 | let (mut test, lending_market, usdc_reserve, user) = setup().await; 135 | 136 | let res = lending_market 137 | .redeem(&mut test, &usdc_reserve, &user, 1_000_001) 138 | .await 139 | .err() 140 | .unwrap() 141 | .unwrap(); 142 | 143 | match res { 144 | // TokenError::Insufficient Funds 145 | TransactionError::InstructionError(2, InstructionError::Custom(1)) => (), 146 | // LendingError::TokenBurnFailed 147 | TransactionError::InstructionError(2, InstructionError::Custom(19)) => (), 148 | _ => panic!("Unexpected error: {:#?}", res), 149 | }; 150 | } 151 | -------------------------------------------------------------------------------- /token-lending/program/tests/helpers/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | pub mod flash_loan_proxy; 4 | pub mod flash_loan_receiver; 5 | pub mod genesis; 6 | pub mod mock_pyth; 7 | pub mod mock_pyth_pull; 8 | pub mod mock_switchboard; 9 | pub mod mock_switchboard_pull; 10 | pub mod solend_program_test; 11 | 12 | use bytemuck::{cast_slice_mut, from_bytes_mut, try_cast_slice_mut, Pod, PodCastError}; 13 | 14 | use solana_program::{program_option::COption, program_pack::Pack, pubkey::Pubkey}; 15 | use solana_program_test::*; 16 | use solana_sdk::{ 17 | account::Account, 18 | signature::{Keypair, Signer}, 19 | }; 20 | use solend_program::state::{ReserveConfig, ReserveFees, ReserveType}; 21 | 22 | use spl_token::state::Mint; 23 | 24 | use std::mem::size_of; 25 | use switchboard_v2::AggregatorAccountData; 26 | 27 | pub const QUOTE_CURRENCY: [u8; 32] = 28 | *b"USD\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; 29 | 30 | pub const LAMPORTS_TO_SOL: u64 = 1_000_000_000; 31 | pub const FRACTIONAL_TO_USDC: u64 = 1_000_000; 32 | 33 | pub fn reserve_config_no_fees() -> ReserveConfig { 34 | ReserveConfig { 35 | optimal_utilization_rate: 80, 36 | max_utilization_rate: 80, 37 | loan_to_value_ratio: 50, 38 | liquidation_bonus: 0, 39 | max_liquidation_bonus: 0, 40 | liquidation_threshold: 55, 41 | max_liquidation_threshold: 65, 42 | min_borrow_rate: 0, 43 | optimal_borrow_rate: 0, 44 | max_borrow_rate: 0, 45 | super_max_borrow_rate: 0, 46 | fees: ReserveFees { 47 | borrow_fee_wad: 0, 48 | flash_loan_fee_wad: 0, 49 | host_fee_percentage: 0, 50 | }, 51 | deposit_limit: u64::MAX, 52 | borrow_limit: u64::MAX, 53 | fee_receiver: Keypair::new().pubkey(), 54 | protocol_liquidation_fee: 0, 55 | protocol_take_rate: 0, 56 | added_borrow_weight_bps: 0, 57 | reserve_type: ReserveType::Regular, 58 | scaled_price_offset_bps: 0, 59 | extra_oracle_pubkey: None, 60 | attributed_borrow_limit_open: u64::MAX, 61 | attributed_borrow_limit_close: u64::MAX, 62 | } 63 | } 64 | 65 | pub fn test_reserve_config() -> ReserveConfig { 66 | ReserveConfig { 67 | optimal_utilization_rate: 80, 68 | max_utilization_rate: 80, 69 | loan_to_value_ratio: 50, 70 | liquidation_bonus: 4, 71 | max_liquidation_bonus: 4, 72 | liquidation_threshold: 55, 73 | max_liquidation_threshold: 65, 74 | min_borrow_rate: 0, 75 | optimal_borrow_rate: 4, 76 | max_borrow_rate: 30, 77 | super_max_borrow_rate: 30, 78 | fees: ReserveFees { 79 | borrow_fee_wad: 0, 80 | flash_loan_fee_wad: 0, 81 | host_fee_percentage: 0, 82 | }, 83 | deposit_limit: u64::MAX, 84 | borrow_limit: u64::MAX, 85 | fee_receiver: Keypair::new().pubkey(), 86 | protocol_liquidation_fee: 10, 87 | protocol_take_rate: 0, 88 | added_borrow_weight_bps: 0, 89 | reserve_type: ReserveType::Regular, 90 | scaled_price_offset_bps: 0, 91 | extra_oracle_pubkey: None, 92 | attributed_borrow_limit_open: u64::MAX, 93 | attributed_borrow_limit_close: u64::MAX, 94 | } 95 | } 96 | 97 | pub mod usdc_mint { 98 | solana_program::declare_id!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); 99 | } 100 | 101 | pub mod usdt_mint { 102 | solana_program::declare_id!("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"); 103 | } 104 | 105 | pub mod wsol_mint { 106 | // fake mint, not the real wsol bc i can't mint wsol programmatically 107 | solana_program::declare_id!("So1m5eppzgokXLBt9Cg8KCMPWhHfTzVaGh26Y415MRG"); 108 | } 109 | 110 | pub mod msol_mint { 111 | // fake mint, not the real wsol bc i can't mint wsol programmatically 112 | solana_program::declare_id!("mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So"); 113 | } 114 | 115 | pub mod bonk_mint { 116 | solana_program::declare_id!("bonk99WdRCGrh56xQaeQuRMpMHgiNZEfVoZ53DJAoHS"); 117 | } 118 | 119 | pub trait AddPacked { 120 | fn add_packable_account( 121 | &mut self, 122 | pubkey: Pubkey, 123 | amount: u64, 124 | data: &T, 125 | owner: &Pubkey, 126 | ); 127 | } 128 | 129 | impl AddPacked for ProgramTest { 130 | fn add_packable_account( 131 | &mut self, 132 | pubkey: Pubkey, 133 | amount: u64, 134 | data: &T, 135 | owner: &Pubkey, 136 | ) { 137 | let mut account = Account::new(amount, T::get_packed_len(), owner); 138 | data.pack_into_slice(&mut account.data); 139 | self.add_account(pubkey, account); 140 | } 141 | } 142 | 143 | pub struct TestMint { 144 | pub pubkey: Pubkey, 145 | pub authority: Keypair, 146 | pub decimals: u8, 147 | } 148 | 149 | pub fn load_mut(data: &mut [u8]) -> Result<&mut T, PodCastError> { 150 | let size = size_of::(); 151 | Ok(from_bytes_mut(cast_slice_mut::( 152 | try_cast_slice_mut(&mut data[0..size])?, 153 | ))) 154 | } 155 | 156 | fn add_mint(test: &mut ProgramTest, mint: Pubkey, decimals: u8, authority: Pubkey) { 157 | test.add_packable_account( 158 | mint, 159 | u32::MAX as u64, 160 | &Mint { 161 | is_initialized: true, 162 | mint_authority: COption::Some(authority), 163 | decimals, 164 | ..Mint::default() 165 | }, 166 | &spl_token::id(), 167 | ); 168 | } 169 | -------------------------------------------------------------------------------- /token-lending/program/tests/withdraw_obligation_collateral.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "test-bpf")] 2 | 3 | mod helpers; 4 | 5 | use crate::solend_program_test::scenario_1; 6 | use helpers::solend_program_test::{BalanceChecker, TokenBalanceChange}; 7 | use helpers::*; 8 | use solend_sdk::math::Decimal; 9 | 10 | use solana_program_test::*; 11 | 12 | use solend_program::state::{LastUpdate, Obligation, ObligationCollateral, Reserve}; 13 | use std::collections::HashSet; 14 | use std::u64; 15 | 16 | #[tokio::test] 17 | async fn test_success_withdraw_fixed_amount() { 18 | let (mut test, lending_market, usdc_reserve, wsol_reserve, user, obligation, _) = 19 | scenario_1(&test_reserve_config(), &test_reserve_config()).await; 20 | 21 | let balance_checker = 22 | BalanceChecker::start(&mut test, &[&usdc_reserve, &user, &wsol_reserve]).await; 23 | 24 | lending_market 25 | .withdraw_obligation_collateral(&mut test, &usdc_reserve, &obligation, &user, 1_000_000) 26 | .await 27 | .unwrap(); 28 | 29 | let (balance_changes, mint_supply_changes) = 30 | balance_checker.find_balance_changes(&mut test).await; 31 | let expected_balance_changes = HashSet::from([ 32 | TokenBalanceChange { 33 | token_account: user 34 | .get_account(&usdc_reserve.account.collateral.mint_pubkey) 35 | .unwrap(), 36 | mint: usdc_reserve.account.collateral.mint_pubkey, 37 | diff: 1_000_000, 38 | }, 39 | TokenBalanceChange { 40 | token_account: usdc_reserve.account.collateral.supply_pubkey, 41 | mint: usdc_reserve.account.collateral.mint_pubkey, 42 | diff: -1_000_000, 43 | }, 44 | ]); 45 | assert_eq!(balance_changes, expected_balance_changes); 46 | assert_eq!(mint_supply_changes, HashSet::new()); 47 | 48 | let usdc_reserve_post = test.load_account::(usdc_reserve.pubkey).await; 49 | assert_eq!(usdc_reserve_post.account, usdc_reserve.account); 50 | 51 | let obligation_post = test.load_account::(obligation.pubkey).await; 52 | assert_eq!( 53 | obligation_post.account, 54 | Obligation { 55 | last_update: LastUpdate { 56 | slot: 1000, 57 | stale: true 58 | }, 59 | deposits: [ObligationCollateral { 60 | deposit_reserve: usdc_reserve.pubkey, 61 | deposited_amount: 100_000_000_000 - 1_000_000, 62 | market_value: Decimal::from(99_999u64), 63 | ..obligation.account.deposits[0] 64 | }] 65 | .to_vec(), 66 | deposited_value: Decimal::from(99_999u64), 67 | ..obligation.account 68 | } 69 | ); 70 | } 71 | 72 | #[tokio::test] 73 | async fn test_success_withdraw_max() { 74 | let (mut test, lending_market, usdc_reserve, wsol_reserve, user, obligation, _) = 75 | scenario_1(&test_reserve_config(), &test_reserve_config()).await; 76 | 77 | let balance_checker = 78 | BalanceChecker::start(&mut test, &[&usdc_reserve, &user, &wsol_reserve]).await; 79 | 80 | lending_market 81 | .withdraw_obligation_collateral(&mut test, &usdc_reserve, &obligation, &user, u64::MAX) 82 | .await 83 | .unwrap(); 84 | 85 | // we are borrowing 10 SOL @ $10 with an ltv of 0.5, so the debt has to be collateralized by 86 | // exactly 200cUSDC. 87 | let sol_borrowed = obligation.account.borrows[0] 88 | .borrowed_amount_wads 89 | .try_ceil_u64() 90 | .unwrap() 91 | / LAMPORTS_TO_SOL; 92 | let expected_remaining_collateral = sol_borrowed * 10 * 2 * FRACTIONAL_TO_USDC; 93 | 94 | let (balance_changes, mint_supply_changes) = 95 | balance_checker.find_balance_changes(&mut test).await; 96 | let expected_balance_changes = HashSet::from([ 97 | TokenBalanceChange { 98 | token_account: user 99 | .get_account(&usdc_reserve.account.collateral.mint_pubkey) 100 | .unwrap(), 101 | mint: usdc_reserve.account.collateral.mint_pubkey, 102 | diff: (100_000 * FRACTIONAL_TO_USDC - expected_remaining_collateral) as i128, 103 | }, 104 | TokenBalanceChange { 105 | token_account: usdc_reserve.account.collateral.supply_pubkey, 106 | mint: usdc_reserve.account.collateral.mint_pubkey, 107 | diff: -((100_000_000_000 - expected_remaining_collateral) as i128), 108 | }, 109 | ]); 110 | assert_eq!(balance_changes, expected_balance_changes); 111 | assert_eq!(mint_supply_changes, HashSet::new()); 112 | 113 | let usdc_reserve_post = test.load_account::(usdc_reserve.pubkey).await; 114 | assert_eq!(usdc_reserve_post.account, usdc_reserve.account); 115 | 116 | let obligation_post = test.load_account::(obligation.pubkey).await; 117 | assert_eq!( 118 | obligation_post.account, 119 | Obligation { 120 | last_update: LastUpdate { 121 | slot: 1000, 122 | stale: true 123 | }, 124 | deposits: [ObligationCollateral { 125 | deposit_reserve: usdc_reserve.pubkey, 126 | deposited_amount: expected_remaining_collateral, 127 | market_value: Decimal::from(200u64), 128 | ..obligation.account.deposits[0] 129 | }] 130 | .to_vec(), 131 | deposited_value: Decimal::from(200u64), 132 | ..obligation.account 133 | } 134 | ); 135 | } 136 | -------------------------------------------------------------------------------- /token-lending/program/tests/helpers/flash_loan_receiver.rs: -------------------------------------------------------------------------------- 1 | use solana_program::{ 2 | account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg, pubkey::Pubkey, 3 | }; 4 | 5 | use crate::helpers::flash_loan_receiver::FlashLoanReceiverError::InvalidInstruction; 6 | use spl_token::{ 7 | solana_program::{ 8 | account_info::next_account_info, program::invoke_signed, program_error::ProgramError, 9 | program_pack::Pack, 10 | }, 11 | state::Account, 12 | }; 13 | use std::cmp::min; 14 | use std::convert::TryInto; 15 | use thiserror::Error; 16 | 17 | pub enum FlashLoanReceiverInstruction { 18 | /// Receive a flash loan and perform user-defined operation and finally return the fund back. 19 | /// 20 | /// Accounts expected: 21 | /// 22 | /// 0. `[writable]` Source liquidity (matching the destination from above). 23 | /// 1. `[writable]` Destination liquidity (matching the source from above). 24 | /// 2. `[]` Token program id 25 | /// .. `[any]` Additional accounts provided to the lending program's `FlashLoan` instruction above. 26 | ReceiveFlashLoan { 27 | /// The amount that is loaned 28 | amount: u64, 29 | }, 30 | } 31 | 32 | entrypoint!(process_instruction); 33 | pub fn process_instruction( 34 | program_id: &Pubkey, 35 | accounts: &[AccountInfo], 36 | instruction_data: &[u8], 37 | ) -> ProgramResult { 38 | Processor::process(program_id, accounts, instruction_data) 39 | } 40 | 41 | pub struct Processor; 42 | impl Processor { 43 | pub fn process( 44 | program_id: &Pubkey, 45 | accounts: &[AccountInfo], 46 | instruction_data: &[u8], 47 | ) -> ProgramResult { 48 | let instruction = FlashLoanReceiverInstruction::unpack(instruction_data)?; 49 | 50 | match instruction { 51 | FlashLoanReceiverInstruction::ReceiveFlashLoan { amount } => { 52 | msg!("Instruction: Receive Flash Loan"); 53 | Self::process_receive_flash_loan(accounts, amount, program_id) 54 | } 55 | } 56 | } 57 | 58 | fn process_receive_flash_loan( 59 | accounts: &[AccountInfo], 60 | amount: u64, 61 | program_id: &Pubkey, 62 | ) -> ProgramResult { 63 | let account_info_iter = &mut accounts.iter(); 64 | let source_liquidity_token_account_info = next_account_info(account_info_iter)?; 65 | let destination_liquidity_token_account_info = next_account_info(account_info_iter)?; 66 | let token_program_id = next_account_info(account_info_iter)?; 67 | let program_derived_account_info = next_account_info(account_info_iter)?; 68 | 69 | let destination_liquidity_token_account = Account::unpack_from_slice( 70 | &source_liquidity_token_account_info.try_borrow_mut_data()?, 71 | )?; 72 | let (expected_program_derived_account_pubkey, bump_seed) = 73 | Pubkey::find_program_address(&[b"flashloan"], program_id); 74 | 75 | if &expected_program_derived_account_pubkey != program_derived_account_info.key { 76 | msg!("Supplied program derived account doesn't match with expectation.") 77 | } 78 | 79 | if destination_liquidity_token_account.owner != expected_program_derived_account_pubkey { 80 | msg!("Destination liquidity token account is not owned by the program"); 81 | return Err(ProgramError::IncorrectProgramId); 82 | } 83 | 84 | let balance_in_token_account = 85 | Account::unpack_from_slice(&source_liquidity_token_account_info.try_borrow_data()?)? 86 | .amount; 87 | let transfer_ix = spl_token::instruction::transfer( 88 | token_program_id.key, 89 | source_liquidity_token_account_info.key, 90 | destination_liquidity_token_account_info.key, 91 | &expected_program_derived_account_pubkey, 92 | &[], 93 | min(balance_in_token_account, amount), 94 | )?; 95 | 96 | invoke_signed( 97 | &transfer_ix, 98 | &[ 99 | source_liquidity_token_account_info.clone(), 100 | destination_liquidity_token_account_info.clone(), 101 | program_derived_account_info.clone(), 102 | token_program_id.clone(), 103 | ], 104 | &[&[&b"flashloan"[..], &[bump_seed]]], 105 | )?; 106 | 107 | Ok(()) 108 | } 109 | } 110 | 111 | impl FlashLoanReceiverInstruction { 112 | pub fn unpack(input: &[u8]) -> Result { 113 | let (tag, rest) = input.split_first().ok_or(InvalidInstruction)?; 114 | 115 | Ok(match tag { 116 | 0 => Self::ReceiveFlashLoan { 117 | amount: Self::unpack_amount(rest)?, 118 | }, 119 | _ => return Err(InvalidInstruction.into()), 120 | }) 121 | } 122 | 123 | fn unpack_amount(input: &[u8]) -> Result { 124 | let amount = input 125 | .get(..8) 126 | .and_then(|slice| slice.try_into().ok()) 127 | .map(u64::from_le_bytes) 128 | .ok_or(InvalidInstruction)?; 129 | Ok(amount) 130 | } 131 | } 132 | 133 | #[derive(Error, Debug, Copy, Clone)] 134 | pub enum FlashLoanReceiverError { 135 | /// Invalid instruction 136 | #[error("Invalid Instruction")] 137 | InvalidInstruction, 138 | #[error("The account is not currently owned by the program")] 139 | IncorrectProgramId, 140 | } 141 | 142 | impl From for ProgramError { 143 | fn from(e: FlashLoanReceiverError) -> Self { 144 | ProgramError::Custom(e as u32) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /token-lending/program/tests/deposit_reserve_liquidity.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "test-bpf")] 2 | 3 | mod helpers; 4 | 5 | use crate::solend_program_test::MintSupplyChange; 6 | use solend_program::state::ReserveConfig; 7 | use std::collections::HashSet; 8 | 9 | use helpers::solend_program_test::{ 10 | setup_world, BalanceChecker, Info, SolendProgramTest, TokenBalanceChange, User, 11 | }; 12 | use helpers::*; 13 | use solana_program::instruction::InstructionError; 14 | use solana_program_test::*; 15 | use solana_sdk::transaction::TransactionError; 16 | use solend_program::error::LendingError; 17 | use solend_program::state::{ 18 | LastUpdate, LendingMarket, Reserve, ReserveCollateral, ReserveLiquidity, 19 | }; 20 | 21 | async fn setup() -> (SolendProgramTest, Info, Info, User) { 22 | let (test, lending_market, usdc_reserve, _, _, user) = setup_world( 23 | &ReserveConfig { 24 | deposit_limit: 100_000 * FRACTIONAL_TO_USDC, 25 | ..test_reserve_config() 26 | }, 27 | &test_reserve_config(), 28 | ) 29 | .await; 30 | 31 | (test, lending_market, usdc_reserve, user) 32 | } 33 | 34 | #[tokio::test] 35 | async fn test_success() { 36 | let (mut test, lending_market, usdc_reserve, user) = setup().await; 37 | 38 | let balance_checker = BalanceChecker::start(&mut test, &[&usdc_reserve, &user]).await; 39 | 40 | // deposit 41 | lending_market 42 | .deposit(&mut test, &usdc_reserve, &user, 1_000_000) 43 | .await 44 | .expect("this should succeed"); 45 | 46 | // check token balances 47 | let (token_balance_changes, mint_supply_changes) = 48 | balance_checker.find_balance_changes(&mut test).await; 49 | 50 | assert_eq!( 51 | token_balance_changes, 52 | HashSet::from([ 53 | TokenBalanceChange { 54 | token_account: user.get_account(&usdc_mint::id()).unwrap(), 55 | mint: usdc_mint::id(), 56 | diff: -1_000_000, 57 | }, 58 | TokenBalanceChange { 59 | token_account: user 60 | .get_account(&usdc_reserve.account.collateral.mint_pubkey) 61 | .unwrap(), 62 | mint: usdc_reserve.account.collateral.mint_pubkey, 63 | diff: 1_000_000, 64 | }, 65 | TokenBalanceChange { 66 | token_account: usdc_reserve.account.liquidity.supply_pubkey, 67 | mint: usdc_reserve.account.liquidity.mint_pubkey, 68 | diff: 1_000_000, 69 | }, 70 | ]), 71 | "{:#?}", 72 | token_balance_changes 73 | ); 74 | 75 | assert_eq!( 76 | mint_supply_changes, 77 | HashSet::from([MintSupplyChange { 78 | mint: usdc_reserve.account.collateral.mint_pubkey, 79 | diff: 1_000_000, 80 | },]), 81 | "{:#?}", 82 | mint_supply_changes 83 | ); 84 | 85 | // check program state 86 | let lending_market_post = test 87 | .load_account::(lending_market.pubkey) 88 | .await; 89 | assert_eq!(lending_market.account, lending_market_post.account); 90 | 91 | let usdc_reserve_post = test.load_account::(usdc_reserve.pubkey).await; 92 | let expected_usdc_reserve_post = Reserve { 93 | last_update: LastUpdate { 94 | slot: 1000, 95 | stale: true, 96 | }, 97 | liquidity: ReserveLiquidity { 98 | available_amount: usdc_reserve.account.liquidity.available_amount + 1_000_000, 99 | ..usdc_reserve.account.liquidity 100 | }, 101 | collateral: ReserveCollateral { 102 | mint_total_supply: usdc_reserve.account.collateral.mint_total_supply + 1_000_000, 103 | ..usdc_reserve.account.collateral 104 | }, 105 | ..usdc_reserve.account 106 | }; 107 | assert_eq!( 108 | usdc_reserve_post.account, expected_usdc_reserve_post, 109 | "{:#?} {:#?}", 110 | usdc_reserve_post.account, expected_usdc_reserve_post 111 | ); 112 | } 113 | 114 | #[tokio::test] 115 | async fn test_fail_exceed_deposit_limit() { 116 | let (mut test, lending_market, usdc_reserve, user) = setup().await; 117 | 118 | let res = lending_market 119 | .deposit(&mut test, &usdc_reserve, &user, 200_000_000_000) 120 | .await 121 | .err() 122 | .unwrap() 123 | .unwrap(); 124 | 125 | assert_eq!( 126 | res, 127 | TransactionError::InstructionError( 128 | 1, 129 | InstructionError::Custom(LendingError::InvalidAmount as u32) 130 | ) 131 | ); 132 | } 133 | 134 | #[tokio::test] 135 | async fn test_fail_deposit_too_much() { 136 | let (mut test, lending_market, usdc_reserve, user) = setup().await; 137 | 138 | // drain original user's funds first 139 | { 140 | let new_user = User::new_with_balances(&mut test, &[(&usdc_mint::id(), 0)]).await; 141 | user.transfer( 142 | &usdc_mint::id(), 143 | new_user.get_account(&usdc_mint::id()).unwrap(), 144 | 1_000_000_000_000, 145 | &mut test, 146 | ) 147 | .await; 148 | } 149 | 150 | // deposit more than user owns 151 | let res = lending_market 152 | .deposit(&mut test, &usdc_reserve, &user, 1) 153 | .await 154 | .err() 155 | .unwrap() 156 | .unwrap(); 157 | 158 | match res { 159 | // InsufficientFunds 160 | TransactionError::InstructionError(1, InstructionError::Custom(1)) => (), 161 | // LendingError::TokenTransferFailed 162 | TransactionError::InstructionError(1, InstructionError::Custom(17)) => (), 163 | e => panic!("unexpected error: {:#?}", e), 164 | }; 165 | } 166 | -------------------------------------------------------------------------------- /token-lending/program/tests/set_lending_market_owner.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "test-bpf")] 2 | 3 | mod helpers; 4 | 5 | use crate::solend_program_test::setup_world; 6 | use crate::solend_program_test::Info; 7 | use crate::solend_program_test::SolendProgramTest; 8 | use crate::solend_program_test::User; 9 | use helpers::*; 10 | use solana_program::instruction::{AccountMeta, Instruction}; 11 | use solana_program_test::*; 12 | use solana_sdk::{ 13 | instruction::InstructionError, 14 | pubkey::Pubkey, 15 | signature::{Keypair, Signer}, 16 | transaction::TransactionError, 17 | }; 18 | use solend_program::state::LendingMarket; 19 | use solend_program::state::RateLimiterConfig; 20 | use solend_sdk::state::RateLimiter; 21 | 22 | use solend_program::{error::LendingError, instruction::LendingInstruction}; 23 | 24 | async fn setup() -> (SolendProgramTest, Info, User) { 25 | let (test, lending_market, _usdc_reserve, _, lending_market_owner, _user) = 26 | setup_world(&test_reserve_config(), &test_reserve_config()).await; 27 | 28 | (test, lending_market, lending_market_owner) 29 | } 30 | 31 | #[tokio::test] 32 | async fn test_success() { 33 | let (mut test, lending_market, lending_market_owner) = setup().await; 34 | let new_owner = Keypair::new(); 35 | let new_risk_authority = Keypair::new(); 36 | let new_config = RateLimiterConfig { 37 | max_outflow: 100, 38 | window_duration: 5, 39 | }; 40 | 41 | let whitelisted_liquidator = Pubkey::new_unique(); 42 | lending_market 43 | .set_lending_market_owner_and_config( 44 | &mut test, 45 | &lending_market_owner, 46 | &new_owner.pubkey(), 47 | new_config, 48 | Some(whitelisted_liquidator), 49 | new_risk_authority.pubkey(), 50 | ) 51 | .await 52 | .unwrap(); 53 | 54 | let lending_market_post = test 55 | .load_account::(lending_market.pubkey) 56 | .await; 57 | 58 | assert_eq!( 59 | lending_market_post.account, 60 | LendingMarket { 61 | owner: new_owner.pubkey(), 62 | rate_limiter: RateLimiter::new(new_config, 1000), 63 | whitelisted_liquidator: Some(whitelisted_liquidator), 64 | ..lending_market_post.account 65 | } 66 | ); 67 | } 68 | 69 | #[tokio::test] 70 | async fn test_risk_authority_can_set_only_rate_limiter() { 71 | let (mut test, lending_market, lending_market_owner) = setup().await; 72 | let new_owner = Keypair::new(); 73 | 74 | // set risk authority 75 | let risk_authority = Keypair::new(); 76 | lending_market 77 | .set_lending_market_owner_and_config( 78 | &mut test, 79 | &lending_market_owner, 80 | &lending_market_owner.keypair.pubkey(), 81 | lending_market.account.rate_limiter.config, 82 | None, 83 | risk_authority.pubkey(), 84 | ) 85 | .await 86 | .unwrap(); 87 | 88 | test.advance_clock_by_slots(1).await; 89 | 90 | let new_rate_limiter_config = RateLimiterConfig { 91 | max_outflow: 0, 92 | window_duration: 1, 93 | }; 94 | 95 | let lending_market = test 96 | .load_account::(lending_market.pubkey) 97 | .await; 98 | 99 | test.process_transaction( 100 | &[Instruction { 101 | program_id: solend_program::id(), 102 | accounts: vec![ 103 | AccountMeta::new(lending_market.pubkey, false), 104 | AccountMeta::new_readonly(risk_authority.pubkey(), true), 105 | ], 106 | data: LendingInstruction::SetLendingMarketOwnerAndConfig { 107 | new_owner: new_owner.pubkey(), 108 | rate_limiter_config: new_rate_limiter_config, 109 | whitelisted_liquidator: None, 110 | risk_authority: new_owner.pubkey(), 111 | } 112 | .pack(), 113 | }], 114 | Some(&[&risk_authority]), 115 | ) 116 | .await 117 | .unwrap(); 118 | 119 | let lending_market_post = test 120 | .load_account::(lending_market.pubkey) 121 | .await; 122 | 123 | assert_eq!( 124 | lending_market_post.account, 125 | LendingMarket { 126 | rate_limiter: RateLimiter::new(new_rate_limiter_config, 1001), // only thing that changed 127 | ..lending_market.account 128 | } 129 | ); 130 | } 131 | 132 | #[tokio::test] 133 | async fn test_invalid_owner() { 134 | let (mut test, lending_market, _lending_market_owner) = setup().await; 135 | let invalid_owner = User::new_with_keypair(Keypair::new()); 136 | let new_owner = Keypair::new(); 137 | let new_risk_authority = Keypair::new(); 138 | 139 | let res = lending_market 140 | .set_lending_market_owner_and_config( 141 | &mut test, 142 | &invalid_owner, 143 | &new_owner.pubkey(), 144 | RateLimiterConfig::default(), 145 | None, 146 | new_risk_authority.pubkey(), 147 | ) 148 | .await 149 | .unwrap_err() 150 | .unwrap(); 151 | 152 | assert_eq!( 153 | res, 154 | TransactionError::InstructionError( 155 | 0, 156 | InstructionError::Custom(LendingError::InvalidMarketOwner as u32) 157 | ) 158 | ); 159 | } 160 | 161 | #[tokio::test] 162 | async fn test_owner_not_signer() { 163 | let (mut test, lending_market, _lending_market_owner) = setup().await; 164 | let new_owner = Pubkey::new_unique(); 165 | let new_risk_authority = Keypair::new(); 166 | let res = test 167 | .process_transaction( 168 | &[Instruction { 169 | program_id: solend_program::id(), 170 | accounts: vec![ 171 | AccountMeta::new(lending_market.pubkey, false), 172 | AccountMeta::new_readonly(lending_market.account.owner, false), 173 | ], 174 | data: LendingInstruction::SetLendingMarketOwnerAndConfig { 175 | new_owner, 176 | rate_limiter_config: RateLimiterConfig::default(), 177 | whitelisted_liquidator: None, 178 | risk_authority: new_risk_authority.pubkey(), 179 | } 180 | .pack(), 181 | }], 182 | None, 183 | ) 184 | .await 185 | .unwrap_err() 186 | .unwrap(); 187 | 188 | assert_eq!( 189 | res, 190 | TransactionError::InstructionError( 191 | 0, 192 | InstructionError::Custom(LendingError::InvalidSigner as u32) 193 | ) 194 | ); 195 | } 196 | -------------------------------------------------------------------------------- /token-lending/program/tests/outflow_rate_limits.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "test-bpf")] 2 | 3 | use solana_program::instruction::InstructionError; 4 | use solana_sdk::native_token::LAMPORTS_PER_SOL; 5 | use solana_sdk::signature::Signer; 6 | use solana_sdk::signer::keypair::Keypair; 7 | use solana_sdk::transaction::TransactionError; 8 | 9 | mod helpers; 10 | 11 | use helpers::solend_program_test::{setup_world, Info, SolendProgramTest, User}; 12 | use solend_sdk::error::LendingError; 13 | 14 | use solend_sdk::state::{LendingMarket, RateLimiterConfig, Reserve, ReserveConfig}; 15 | 16 | use helpers::*; 17 | 18 | use solana_program_test::*; 19 | 20 | use solend_sdk::state::Obligation; 21 | 22 | async fn setup( 23 | wsol_reserve_config: &ReserveConfig, 24 | ) -> ( 25 | SolendProgramTest, 26 | Info, 27 | Info, 28 | Info, 29 | User, 30 | Info, 31 | User, 32 | User, 33 | User, 34 | ) { 35 | let (mut test, lending_market, usdc_reserve, wsol_reserve, lending_market_owner, user) = 36 | setup_world(&test_reserve_config(), wsol_reserve_config).await; 37 | 38 | let obligation = lending_market 39 | .init_obligation(&mut test, Keypair::new(), &user) 40 | .await 41 | .expect("This should succeed"); 42 | 43 | lending_market 44 | .deposit(&mut test, &usdc_reserve, &user, 100_000_000) 45 | .await 46 | .expect("This should succeed"); 47 | 48 | let usdc_reserve = test.load_account(usdc_reserve.pubkey).await; 49 | 50 | lending_market 51 | .deposit_obligation_collateral(&mut test, &usdc_reserve, &obligation, &user, 100_000_000) 52 | .await 53 | .expect("This should succeed"); 54 | 55 | let wsol_depositor = User::new_with_balances( 56 | &mut test, 57 | &[ 58 | (&wsol_mint::id(), 5 * LAMPORTS_PER_SOL), 59 | (&wsol_reserve.account.collateral.mint_pubkey, 0), 60 | ], 61 | ) 62 | .await; 63 | 64 | lending_market 65 | .deposit( 66 | &mut test, 67 | &wsol_reserve, 68 | &wsol_depositor, 69 | 5 * LAMPORTS_PER_SOL, 70 | ) 71 | .await 72 | .unwrap(); 73 | 74 | // populate market price correctly 75 | lending_market 76 | .refresh_reserve(&mut test, &wsol_reserve) 77 | .await 78 | .unwrap(); 79 | 80 | // populate deposit value correctly. 81 | let obligation = test.load_account::(obligation.pubkey).await; 82 | lending_market 83 | .refresh_obligation(&mut test, &obligation) 84 | .await 85 | .unwrap(); 86 | 87 | let lending_market = test.load_account(lending_market.pubkey).await; 88 | let usdc_reserve = test.load_account(usdc_reserve.pubkey).await; 89 | let wsol_reserve = test.load_account(wsol_reserve.pubkey).await; 90 | let obligation = test.load_account::(obligation.pubkey).await; 91 | 92 | let host_fee_receiver = User::new_with_balances(&mut test, &[(&wsol_mint::id(), 0)]).await; 93 | ( 94 | test, 95 | lending_market, 96 | usdc_reserve, 97 | wsol_reserve, 98 | user, 99 | obligation, 100 | host_fee_receiver, 101 | lending_market_owner, 102 | wsol_depositor, 103 | ) 104 | } 105 | 106 | #[tokio::test] 107 | async fn test_outflow_reserve() { 108 | let ( 109 | mut test, 110 | lending_market, 111 | usdc_reserve, 112 | wsol_reserve, 113 | user, 114 | obligation, 115 | host_fee_receiver, 116 | lending_market_owner, 117 | wsol_depositor, 118 | ) = setup(&ReserveConfig { 119 | ..test_reserve_config() 120 | }) 121 | .await; 122 | 123 | // ie, within 10 slots, the maximum outflow is $10 124 | lending_market 125 | .set_lending_market_owner_and_config( 126 | &mut test, 127 | &lending_market_owner, 128 | &lending_market_owner.keypair.pubkey(), 129 | RateLimiterConfig { 130 | window_duration: 10, 131 | max_outflow: 10, 132 | }, 133 | None, 134 | lending_market.account.risk_authority, 135 | ) 136 | .await 137 | .unwrap(); 138 | 139 | // borrow max amount 140 | lending_market 141 | .borrow_obligation_liquidity( 142 | &mut test, 143 | &wsol_reserve, 144 | &obligation, 145 | &user, 146 | host_fee_receiver.get_account(&wsol_mint::id()), 147 | LAMPORTS_PER_SOL, 148 | ) 149 | .await 150 | .unwrap(); 151 | 152 | // for the next 10 slots, we shouldn't be able to withdraw, borrow, or redeem anything. 153 | let cur_slot = test.get_clock().await.slot; 154 | for _ in cur_slot..(cur_slot + 10) { 155 | let res = lending_market 156 | .borrow_obligation_liquidity( 157 | &mut test, 158 | &wsol_reserve, 159 | &obligation, 160 | &user, 161 | host_fee_receiver.get_account(&wsol_mint::id()), 162 | 1, 163 | ) 164 | .await 165 | .err() 166 | .unwrap() 167 | .unwrap(); 168 | 169 | assert_eq!( 170 | res, 171 | TransactionError::InstructionError( 172 | 1, 173 | InstructionError::Custom(LendingError::OutflowRateLimitExceeded as u32) 174 | ) 175 | ); 176 | 177 | let res = lending_market 178 | .withdraw_obligation_collateral_and_redeem_reserve_collateral( 179 | &mut test, 180 | &usdc_reserve, 181 | &obligation, 182 | &user, 183 | 1, 184 | ) 185 | .await 186 | .err() 187 | .unwrap() 188 | .unwrap(); 189 | 190 | assert_eq!( 191 | res, 192 | TransactionError::InstructionError( 193 | 1, 194 | InstructionError::Custom(LendingError::WithdrawTooLarge as u32) 195 | ) 196 | ); 197 | 198 | let res = lending_market 199 | .redeem(&mut test, &wsol_reserve, &wsol_depositor, 1) 200 | .await 201 | .err() 202 | .unwrap() 203 | .unwrap(); 204 | 205 | assert_eq!( 206 | res, 207 | TransactionError::InstructionError( 208 | 2, 209 | InstructionError::Custom(LendingError::OutflowRateLimitExceeded as u32) 210 | ) 211 | ); 212 | 213 | test.advance_clock_by_slots(1).await; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /token-lending/program/tests/helpers/mock_pyth_pull.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{AccountDeserialize, AccountSerialize}; 2 | use pyth_solana_receiver_sdk::price_update::{PriceFeedMessage, PriceUpdateV2, VerificationLevel}; 3 | /// mock oracle prices in tests with this program. 4 | use solana_program::{ 5 | account_info::AccountInfo, 6 | clock::Clock, 7 | entrypoint::ProgramResult, 8 | instruction::{AccountMeta, Instruction}, 9 | msg, 10 | pubkey::Pubkey, 11 | sysvar::Sysvar, 12 | }; 13 | 14 | use borsh::{BorshDeserialize, BorshSerialize}; 15 | use spl_token::solana_program::{account_info::next_account_info, program_error::ProgramError}; 16 | use thiserror::Error; 17 | 18 | #[derive(BorshSerialize, BorshDeserialize)] 19 | pub enum MockPythPullInstruction { 20 | /// Accounts: 21 | /// 0: PriceAccount (uninitialized) 22 | Init, 23 | 24 | /// Accounts: 25 | /// 0: PriceAccount 26 | SetPrice { 27 | price: i64, 28 | conf: u64, 29 | expo: i32, 30 | ema_price: i64, 31 | ema_conf: u64, 32 | }, 33 | } 34 | 35 | pub fn process_instruction( 36 | program_id: &Pubkey, 37 | accounts: &[AccountInfo], 38 | instruction_data: &[u8], 39 | ) -> ProgramResult { 40 | Processor::process(program_id, accounts, instruction_data) 41 | } 42 | 43 | fn account_deserialize( 44 | account: &AccountInfo<'_>, 45 | ) -> Result> { 46 | let data = account.clone().data.borrow().to_owned(); 47 | let mut data: &[u8] = &data; 48 | 49 | let user: T = T::try_deserialize(&mut data)?; 50 | 51 | Ok(user) 52 | } 53 | 54 | pub struct Processor; 55 | impl Processor { 56 | pub fn process( 57 | _program_id: &Pubkey, 58 | accounts: &[AccountInfo], 59 | instruction_data: &[u8], 60 | ) -> ProgramResult { 61 | let instruction = MockPythPullInstruction::try_from_slice(instruction_data)?; 62 | let account_info_iter = &mut accounts.iter().peekable(); 63 | 64 | match instruction { 65 | MockPythPullInstruction::Init => { 66 | msg!("Mock Pyth Pull: Init"); 67 | 68 | let price_account_info = next_account_info(account_info_iter)?; 69 | 70 | // write PriceAccount 71 | let price_update_v2 = PriceUpdateV2 { 72 | write_authority: Pubkey::new_unique(), 73 | verification_level: VerificationLevel::Full, 74 | price_message: PriceFeedMessage { 75 | feed_id: [1u8; 32], 76 | price: 1, 77 | conf: 1, 78 | exponent: 1, 79 | publish_time: 1, 80 | prev_publish_time: 1, 81 | ema_price: 1, 82 | ema_conf: 1, 83 | }, 84 | posted_slot: 0, 85 | }; 86 | 87 | // let mut data = price_account_info.try_borrow_mut_data()?; 88 | let mut buf = Vec::new(); 89 | price_update_v2.try_serialize(&mut buf)?; 90 | msg!("buf: {:?}", buf.len()); 91 | 92 | let mut buf_sized = [0u8; PriceUpdateV2::LEN]; 93 | buf_sized[0..buf.len()].copy_from_slice(&buf); 94 | 95 | price_account_info 96 | .try_borrow_mut_data()? 97 | .copy_from_slice(&buf_sized); 98 | 99 | Ok(()) 100 | } 101 | MockPythPullInstruction::SetPrice { 102 | price, 103 | conf, 104 | expo, 105 | ema_price, 106 | ema_conf, 107 | } => { 108 | msg!("Mock Pyth Pull: Set price"); 109 | let price_account_info = next_account_info(account_info_iter)?; 110 | 111 | let mut price_feed_account: PriceUpdateV2 = account_deserialize(price_account_info) 112 | .map_err(|e| { 113 | msg!("Failed to deserialize account: {:?}", e); 114 | MockPythPullError::FailedToDeserialize 115 | })?; 116 | 117 | price_feed_account.price_message.price = price; 118 | price_feed_account.price_message.conf = conf; 119 | price_feed_account.price_message.exponent = expo; 120 | price_feed_account.price_message.ema_price = ema_price; 121 | price_feed_account.price_message.ema_conf = ema_conf; 122 | price_feed_account.price_message.publish_time = Clock::get()?.unix_timestamp; 123 | 124 | price_feed_account.verification_level = VerificationLevel::Full; 125 | price_feed_account.posted_slot = Clock::get()?.slot; 126 | 127 | let mut buf = Vec::new(); 128 | price_feed_account.try_serialize(&mut buf)?; 129 | 130 | let mut buf_sized = [0u8; PriceUpdateV2::LEN]; 131 | buf_sized[0..buf.len()].copy_from_slice(&buf); 132 | 133 | price_account_info 134 | .try_borrow_mut_data()? 135 | .copy_from_slice(&buf_sized); 136 | 137 | Ok(()) 138 | } 139 | } 140 | } 141 | } 142 | 143 | #[derive(Error, Debug, Copy, Clone)] 144 | pub enum MockPythPullError { 145 | /// Invalid instruction 146 | #[error("Invalid Instruction")] 147 | InvalidInstruction, 148 | #[error("The account is not currently owned by the program")] 149 | IncorrectProgramId, 150 | #[error("Failed to deserialize")] 151 | FailedToDeserialize, 152 | } 153 | 154 | impl From for ProgramError { 155 | fn from(e: MockPythPullError) -> Self { 156 | ProgramError::Custom(e as u32) 157 | } 158 | } 159 | 160 | pub fn init(program_id: Pubkey, price_account_pubkey: Pubkey) -> Instruction { 161 | let data = MockPythPullInstruction::Init.try_to_vec().unwrap(); 162 | Instruction { 163 | program_id, 164 | accounts: vec![AccountMeta::new(price_account_pubkey, false)], 165 | data, 166 | } 167 | } 168 | 169 | pub fn set_price( 170 | program_id: Pubkey, 171 | price_account_pubkey: Pubkey, 172 | price: i64, 173 | conf: u64, 174 | expo: i32, 175 | ema_price: i64, 176 | ema_conf: u64, 177 | ) -> Instruction { 178 | let data = MockPythPullInstruction::SetPrice { 179 | price, 180 | conf, 181 | expo, 182 | ema_price, 183 | ema_conf, 184 | } 185 | .try_to_vec() 186 | .unwrap(); 187 | Instruction { 188 | program_id, 189 | accounts: vec![AccountMeta::new(price_account_pubkey, false)], 190 | data, 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /token-lending/oracles/src/switchboard.rs: -------------------------------------------------------------------------------- 1 | use crate::get_oracle_type; 2 | use crate::OracleType; 3 | use solend_sdk::math::TryDiv; 4 | use solend_sdk::math::TryMul; 5 | 6 | use crate::{ 7 | switchboard_on_demand_devnet, switchboard_on_demand_mainnet, switchboard_v2_devnet, 8 | switchboard_v2_mainnet, 9 | }; 10 | use solana_program::{ 11 | account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError, 12 | sysvar::clock::Clock, 13 | }; 14 | use solend_sdk::{error::LendingError, math::Decimal}; 15 | use std::result::Result; 16 | 17 | use switchboard_on_demand::on_demand::accounts::pull_feed::PullFeedAccountData as SbOnDemandFeed; 18 | use switchboard_v2::AggregatorAccountData; 19 | 20 | pub fn get_switchboard_price( 21 | switchboard_feed_info: &AccountInfo, 22 | clock: &Clock, 23 | ) -> Result { 24 | if *switchboard_feed_info.key == solend_sdk::NULL_PUBKEY { 25 | return Err(LendingError::NullOracleConfig.into()); 26 | } 27 | if switchboard_feed_info.owner == &switchboard_v2_mainnet::id() 28 | || switchboard_feed_info.owner == &switchboard_v2_devnet::id() 29 | { 30 | return get_switchboard_price_v2(switchboard_feed_info, clock, true); 31 | } 32 | 33 | if switchboard_feed_info.owner == &switchboard_on_demand_devnet::id() 34 | || switchboard_feed_info.owner == &switchboard_on_demand_mainnet::id() 35 | { 36 | return get_switchboard_price_on_demand(switchboard_feed_info, clock, true); 37 | } 38 | Err(LendingError::NullOracleConfig.into()) 39 | } 40 | 41 | pub fn get_switchboard_price_on_demand( 42 | switchboard_feed_info: &AccountInfo, 43 | clock: &Clock, 44 | check_staleness: bool, 45 | ) -> Result { 46 | const STALE_AFTER_SLOTS_ELAPSED: u64 = 240; 47 | let data = switchboard_feed_info.try_borrow_data()?; 48 | let feed = SbOnDemandFeed::parse(data).map_err(|_| ProgramError::InvalidAccountData)?; 49 | let slots_elapsed = clock 50 | .slot 51 | .checked_sub(feed.result.slot) 52 | .ok_or(LendingError::MathOverflow)?; 53 | if check_staleness && slots_elapsed >= STALE_AFTER_SLOTS_ELAPSED { 54 | msg!("Switchboard oracle price is stale"); 55 | return Err(LendingError::InvalidOracleConfig.into()); 56 | } 57 | let price_desc = feed.value().ok_or(ProgramError::InvalidAccountData)?; 58 | if price_desc.mantissa() < 0 { 59 | msg!("Switchboard oracle price is negative which is not allowed"); 60 | return Err(LendingError::InvalidOracleConfig.into()); 61 | } 62 | let price_mantissa = Decimal::from(price_desc.mantissa() as u128); 63 | let exp = Decimal::from((10u128).checked_pow(price_desc.scale()).unwrap()); 64 | let price = price_mantissa.try_div(exp)?; 65 | 66 | let range_desc = feed.range().ok_or(ProgramError::InvalidAccountData)?; 67 | if range_desc.mantissa() < 0 { 68 | msg!("Switchboard oracle price range is negative which is not allowed"); 69 | return Err(LendingError::InvalidOracleConfig.into()); 70 | } 71 | let range_mantissa = Decimal::from(range_desc.mantissa() as u128); 72 | let range_exp = Decimal::from((10u128).checked_pow(range_desc.scale()).unwrap()); 73 | let range = range_mantissa.try_div(range_exp)?; 74 | 75 | if range.try_mul(10_u64)? > price { 76 | msg!( 77 | "Oracle price range is too wide. price: {}, conf: {}", 78 | price, 79 | range, 80 | ); 81 | return Err(LendingError::InvalidOracleConfig.into()); 82 | } 83 | 84 | Ok(price) 85 | } 86 | 87 | pub fn get_switchboard_price_v2( 88 | switchboard_feed_info: &AccountInfo, 89 | clock: &Clock, 90 | check_staleness: bool, 91 | ) -> Result { 92 | const STALE_AFTER_SLOTS_ELAPSED: u64 = 240; 93 | let data = &switchboard_feed_info.try_borrow_data()?; 94 | let feed = AggregatorAccountData::new_from_bytes(data)?; 95 | 96 | let slots_elapsed = clock 97 | .slot 98 | .checked_sub(feed.latest_confirmed_round.round_open_slot) 99 | .ok_or(LendingError::MathOverflow)?; 100 | if check_staleness && slots_elapsed >= STALE_AFTER_SLOTS_ELAPSED { 101 | msg!("Switchboard oracle price is stale"); 102 | return Err(LendingError::InvalidOracleConfig.into()); 103 | } 104 | 105 | let price_switchboard_desc = feed.get_result()?; 106 | if price_switchboard_desc.mantissa < 0 { 107 | msg!("Switchboard oracle price is negative which is not allowed"); 108 | return Err(LendingError::InvalidOracleConfig.into()); 109 | } 110 | let price = Decimal::from(price_switchboard_desc.mantissa as u128); 111 | let exp = Decimal::from((10u128).checked_pow(price_switchboard_desc.scale).unwrap()); 112 | price.try_div(exp) 113 | } 114 | 115 | pub fn validate_switchboard_keys(switchboard_feed_info: &AccountInfo) -> ProgramResult { 116 | if *switchboard_feed_info.key == solend_sdk::NULL_PUBKEY { 117 | return Ok(()); 118 | } 119 | 120 | match get_oracle_type(switchboard_feed_info)? { 121 | OracleType::Switchboard => validate_switchboard_v2_keys(switchboard_feed_info), 122 | OracleType::SbOnDemand => validate_sb_on_demand_keys(switchboard_feed_info), 123 | _ => Err(LendingError::InvalidOracleConfig.into()), 124 | } 125 | } 126 | 127 | /// validates switchboard AccountInfo 128 | fn validate_switchboard_v2_keys(switchboard_feed_info: &AccountInfo) -> ProgramResult { 129 | if *switchboard_feed_info.key == solend_sdk::NULL_PUBKEY { 130 | return Ok(()); 131 | } 132 | if switchboard_feed_info.owner != &switchboard_v2_mainnet::id() 133 | && switchboard_feed_info.owner != &switchboard_v2_devnet::id() 134 | { 135 | msg!("Switchboard account provided is not owned by the switchboard oracle program"); 136 | return Err(LendingError::InvalidOracleConfig.into()); 137 | } 138 | 139 | let data = &switchboard_feed_info.try_borrow_data()?; 140 | AggregatorAccountData::new_from_bytes(data)?; 141 | 142 | Ok(()) 143 | } 144 | 145 | /// validates switchboard on-demand AccountInfo 146 | pub fn validate_sb_on_demand_keys(switchboard_feed_info: &AccountInfo) -> ProgramResult { 147 | if *switchboard_feed_info.key == solend_sdk::NULL_PUBKEY { 148 | return Ok(()); 149 | } 150 | 151 | if switchboard_feed_info.owner != &switchboard_on_demand_mainnet::id() 152 | && switchboard_feed_info.owner != &switchboard_on_demand_devnet::id() 153 | { 154 | msg!("Switchboard account provided is not owned by the switchboard oracle program"); 155 | return Err(LendingError::InvalidOracleConfig.into()); 156 | } 157 | 158 | let data = switchboard_feed_info.try_borrow_data()?; 159 | SbOnDemandFeed::parse(data).map_err(|_| ProgramError::InvalidAccountData)?; 160 | 161 | Ok(()) 162 | } 163 | -------------------------------------------------------------------------------- /token-lending/program/tests/helpers/mock_pyth.rs: -------------------------------------------------------------------------------- 1 | use pyth_sdk_solana::state::{ 2 | AccountType, PriceAccount, PriceStatus, ProductAccount, Rational, MAGIC, PROD_ACCT_SIZE, 3 | PROD_ATTR_SIZE, VERSION_2, 4 | }; 5 | /// mock oracle prices in tests with this program. 6 | use solana_program::{ 7 | account_info::AccountInfo, 8 | clock::Clock, 9 | entrypoint::ProgramResult, 10 | instruction::{AccountMeta, Instruction}, 11 | msg, 12 | pubkey::Pubkey, 13 | sysvar::Sysvar, 14 | }; 15 | 16 | use borsh::{BorshDeserialize, BorshSerialize}; 17 | use spl_token::solana_program::{account_info::next_account_info, program_error::ProgramError}; 18 | use thiserror::Error; 19 | 20 | use super::{load_mut, QUOTE_CURRENCY}; 21 | 22 | #[derive(BorshSerialize, BorshDeserialize)] 23 | pub enum MockPythInstruction { 24 | /// Accounts: 25 | /// 0: PriceAccount (uninitialized) 26 | /// 1: ProductAccount (uninitialized) 27 | Init, 28 | 29 | /// Accounts: 30 | /// 0: PriceAccount 31 | SetPrice { 32 | price: i64, 33 | conf: u64, 34 | expo: i32, 35 | ema_price: i64, 36 | ema_conf: u64, 37 | }, 38 | } 39 | 40 | pub fn process_instruction( 41 | program_id: &Pubkey, 42 | accounts: &[AccountInfo], 43 | instruction_data: &[u8], 44 | ) -> ProgramResult { 45 | Processor::process(program_id, accounts, instruction_data) 46 | } 47 | 48 | pub struct Processor; 49 | impl Processor { 50 | pub fn process( 51 | _program_id: &Pubkey, 52 | accounts: &[AccountInfo], 53 | instruction_data: &[u8], 54 | ) -> ProgramResult { 55 | let instruction = MockPythInstruction::try_from_slice(instruction_data)?; 56 | let account_info_iter = &mut accounts.iter().peekable(); 57 | 58 | match instruction { 59 | MockPythInstruction::Init => { 60 | msg!("Mock Pyth: Init"); 61 | 62 | let price_account_info = next_account_info(account_info_iter)?; 63 | let product_account_info = next_account_info(account_info_iter)?; 64 | 65 | // write PriceAccount 66 | let price_account = PriceAccount { 67 | magic: MAGIC, 68 | ver: VERSION_2, 69 | atype: AccountType::Price as u32, 70 | size: 240, // PC_PRICE_T_COMP_OFFSET from pyth_client repo 71 | ..PriceAccount::default() 72 | }; 73 | 74 | let mut data = price_account_info.try_borrow_mut_data()?; 75 | data.copy_from_slice(bytemuck::bytes_of(&price_account)); 76 | 77 | // write ProductAccount 78 | let attr = { 79 | let mut attr: Vec = Vec::new(); 80 | let quote_currency = b"quote_currency"; 81 | attr.push(quote_currency.len() as u8); 82 | attr.extend(quote_currency); 83 | attr.push(QUOTE_CURRENCY.len() as u8); 84 | attr.extend(QUOTE_CURRENCY); 85 | 86 | let mut buf = [0; PROD_ATTR_SIZE]; 87 | buf[0..attr.len()].copy_from_slice(&attr); 88 | 89 | buf 90 | }; 91 | 92 | let product_account = ProductAccount { 93 | magic: MAGIC, 94 | ver: VERSION_2, 95 | atype: AccountType::Product as u32, 96 | size: PROD_ACCT_SIZE as u32, 97 | px_acc: *price_account_info.key, 98 | attr, 99 | }; 100 | 101 | let mut data = product_account_info.try_borrow_mut_data()?; 102 | data.copy_from_slice(bytemuck::bytes_of(&product_account)); 103 | 104 | Ok(()) 105 | } 106 | MockPythInstruction::SetPrice { 107 | price, 108 | conf, 109 | expo, 110 | ema_price, 111 | ema_conf, 112 | } => { 113 | msg!("Mock Pyth: Set price"); 114 | let price_account_info = next_account_info(account_info_iter)?; 115 | let data = &mut price_account_info.try_borrow_mut_data()?; 116 | let price_account: &mut PriceAccount = load_mut(data).unwrap(); 117 | 118 | price_account.agg.price = price; 119 | price_account.agg.conf = conf; 120 | price_account.expo = expo; 121 | 122 | price_account.ema_price = Rational { 123 | val: ema_price, 124 | // these fields don't matter 125 | numer: 1, 126 | denom: 1, 127 | }; 128 | 129 | price_account.ema_conf = Rational { 130 | val: ema_conf as i64, 131 | numer: 1, 132 | denom: 1, 133 | }; 134 | 135 | price_account.last_slot = Clock::get()?.slot; 136 | price_account.agg.pub_slot = Clock::get()?.slot; 137 | price_account.agg.status = PriceStatus::Trading; 138 | 139 | Ok(()) 140 | } 141 | } 142 | } 143 | } 144 | 145 | #[derive(Error, Debug, Copy, Clone)] 146 | pub enum MockPythError { 147 | /// Invalid instruction 148 | #[error("Invalid Instruction")] 149 | InvalidInstruction, 150 | #[error("The account is not currently owned by the program")] 151 | IncorrectProgramId, 152 | #[error("Failed to deserialize")] 153 | FailedToDeserialize, 154 | } 155 | 156 | impl From for ProgramError { 157 | fn from(e: MockPythError) -> Self { 158 | ProgramError::Custom(e as u32) 159 | } 160 | } 161 | 162 | pub fn init( 163 | program_id: Pubkey, 164 | price_account_pubkey: Pubkey, 165 | product_account_pubkey: Pubkey, 166 | ) -> Instruction { 167 | let data = MockPythInstruction::Init.try_to_vec().unwrap(); 168 | Instruction { 169 | program_id, 170 | accounts: vec![ 171 | AccountMeta::new(price_account_pubkey, false), 172 | AccountMeta::new(product_account_pubkey, false), 173 | ], 174 | data, 175 | } 176 | } 177 | 178 | pub fn set_price( 179 | program_id: Pubkey, 180 | price_account_pubkey: Pubkey, 181 | price: i64, 182 | conf: u64, 183 | expo: i32, 184 | ema_price: i64, 185 | ema_conf: u64, 186 | ) -> Instruction { 187 | let data = MockPythInstruction::SetPrice { 188 | price, 189 | conf, 190 | expo, 191 | ema_price, 192 | ema_conf, 193 | } 194 | .try_to_vec() 195 | .unwrap(); 196 | Instruction { 197 | program_id, 198 | accounts: vec![AccountMeta::new(price_account_pubkey, false)], 199 | data, 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /token-lending/sdk/src/math/rate.rs: -------------------------------------------------------------------------------- 1 | //! Math for preserving precision of ratios and percentages. 2 | //! 3 | //! Usages and their ranges include: 4 | //! - Collateral exchange ratio <= 5.0 5 | //! - Loan to value ratio <= 0.9 6 | //! - Max borrow rate <= 2.56 7 | //! - Percentages <= 1.0 8 | //! 9 | //! Rates are internally scaled by a WAD (10^18) to preserve 10 | //! precision up to 18 decimal places. Rates are sized to support 11 | //! both serialization and precise math for the full range of 12 | //! unsigned 8-bit integers. The underlying representation is a 13 | //! u128 rather than u192 to reduce compute cost while losing 14 | //! support for arithmetic operations at the high end of u8 range. 15 | 16 | #![allow(clippy::assign_op_pattern)] 17 | #![allow(clippy::ptr_offset_with_cast)] 18 | #![allow(clippy::reversed_empty_ranges)] 19 | #![allow(clippy::manual_range_contains)] 20 | 21 | use crate::{ 22 | error::LendingError, 23 | math::{common::*, decimal::Decimal}, 24 | }; 25 | use solana_program::program_error::ProgramError; 26 | use std::{convert::TryFrom, fmt}; 27 | use uint::construct_uint; 28 | 29 | // U128 with 128 bits consisting of 2 x 64-bit words 30 | construct_uint! { 31 | pub struct U128(2); 32 | } 33 | 34 | /// Small decimal values, precise to 18 digits 35 | #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Eq, Ord)] 36 | pub struct Rate(pub U128); 37 | 38 | impl Rate { 39 | /// One 40 | pub fn one() -> Self { 41 | Self(Self::wad()) 42 | } 43 | 44 | /// Zero 45 | pub fn zero() -> Self { 46 | Self(U128::from(0)) 47 | } 48 | 49 | // OPTIMIZE: use const slice when fixed in BPF toolchain 50 | fn wad() -> U128 { 51 | U128::from(WAD) 52 | } 53 | 54 | /// Create scaled decimal from percent value 55 | pub fn from_percent(percent: u8) -> Self { 56 | Self(U128::from(percent as u64 * PERCENT_SCALER)) 57 | } 58 | 59 | /// Create scaled decimal from percent value 60 | pub fn from_percent_u64(percent: u64) -> Self { 61 | Self(U128::from(percent) * PERCENT_SCALER) 62 | } 63 | 64 | /// Return raw scaled value 65 | #[allow(clippy::wrong_self_convention)] 66 | pub fn to_scaled_val(&self) -> u128 { 67 | self.0.as_u128() 68 | } 69 | 70 | /// Create decimal from scaled value 71 | pub fn from_scaled_val(scaled_val: u64) -> Self { 72 | Self(U128::from(scaled_val)) 73 | } 74 | 75 | /// Calculates base^exp 76 | pub fn try_pow(&self, mut exp: u64) -> Result { 77 | let mut base = *self; 78 | let mut ret = if exp % 2 != 0 { 79 | base 80 | } else { 81 | Rate(Self::wad()) 82 | }; 83 | 84 | while exp > 0 { 85 | exp /= 2; 86 | base = base.try_mul(base)?; 87 | 88 | if exp % 2 != 0 { 89 | ret = ret.try_mul(base)?; 90 | } 91 | } 92 | 93 | Ok(ret) 94 | } 95 | } 96 | 97 | impl fmt::Display for Rate { 98 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 99 | let mut scaled_val = self.0.to_string(); 100 | if scaled_val.len() <= SCALE { 101 | scaled_val.insert_str(0, &vec!["0"; SCALE - scaled_val.len()].join("")); 102 | scaled_val.insert_str(0, "0."); 103 | } else { 104 | scaled_val.insert(scaled_val.len() - SCALE, '.'); 105 | } 106 | f.write_str(&scaled_val) 107 | } 108 | } 109 | 110 | impl TryFrom for Rate { 111 | type Error = ProgramError; 112 | fn try_from(decimal: Decimal) -> Result { 113 | Ok(Self(U128::from(decimal.to_scaled_val()?))) 114 | } 115 | } 116 | 117 | impl TryAdd for Rate { 118 | fn try_add(self, rhs: Self) -> Result { 119 | Ok(Self( 120 | self.0 121 | .checked_add(rhs.0) 122 | .ok_or(LendingError::MathOverflow)?, 123 | )) 124 | } 125 | } 126 | 127 | impl TrySub for Rate { 128 | fn try_sub(self, rhs: Self) -> Result { 129 | Ok(Self( 130 | self.0 131 | .checked_sub(rhs.0) 132 | .ok_or(LendingError::MathOverflow)?, 133 | )) 134 | } 135 | } 136 | 137 | impl TryDiv for Rate { 138 | fn try_div(self, rhs: u64) -> Result { 139 | Ok(Self( 140 | self.0 141 | .checked_div(U128::from(rhs)) 142 | .ok_or(LendingError::MathOverflow)?, 143 | )) 144 | } 145 | } 146 | 147 | impl TryDiv for Rate { 148 | fn try_div(self, rhs: Self) -> Result { 149 | Ok(Self( 150 | self.0 151 | .checked_mul(Self::wad()) 152 | .ok_or(LendingError::MathOverflow)? 153 | .checked_div(rhs.0) 154 | .ok_or(LendingError::MathOverflow)?, 155 | )) 156 | } 157 | } 158 | 159 | impl TryMul for Rate { 160 | fn try_mul(self, rhs: u64) -> Result { 161 | Ok(Self( 162 | self.0 163 | .checked_mul(U128::from(rhs)) 164 | .ok_or(LendingError::MathOverflow)?, 165 | )) 166 | } 167 | } 168 | 169 | impl TryMul for Rate { 170 | fn try_mul(self, rhs: Self) -> Result { 171 | Ok(Self( 172 | self.0 173 | .checked_mul(rhs.0) 174 | .ok_or(LendingError::MathOverflow)? 175 | .checked_div(Self::wad()) 176 | .ok_or(LendingError::MathOverflow)?, 177 | )) 178 | } 179 | } 180 | 181 | #[cfg(test)] 182 | mod test { 183 | use super::*; 184 | use std::convert::TryInto; 185 | 186 | #[test] 187 | fn test_scaled_val() { 188 | assert_eq!(Rate::from_percent(50).to_scaled_val(), HALF_WAD as u128); 189 | } 190 | 191 | #[test] 192 | fn checked_pow() { 193 | assert_eq!(Rate::one(), Rate::one().try_pow(u64::MAX).unwrap()); 194 | assert_eq!( 195 | Rate::from_percent(200).try_pow(7).unwrap(), 196 | Decimal::from(128u64).try_into().unwrap() 197 | ); 198 | } 199 | 200 | #[test] 201 | fn test_display() { 202 | assert_eq!( 203 | Rate::one().try_div(3u64).unwrap().to_string(), 204 | "0.333333333333333333" 205 | ); 206 | } 207 | 208 | #[test] 209 | fn test_basic_arithmetic() { 210 | assert_eq!( 211 | Rate::one().try_add(Rate::one()).unwrap(), 212 | Rate::from_scaled_val(2 * WAD) 213 | ); 214 | 215 | assert_eq!(Rate::one().try_sub(Rate::one()).unwrap(), Rate::zero()); 216 | 217 | assert_eq!( 218 | Rate::from_percent(240) 219 | .try_mul(Rate::from_percent(50)) 220 | .unwrap(), 221 | Rate::from_percent(120) 222 | ); 223 | assert_eq!( 224 | Rate::from_percent(240).try_mul(10).unwrap(), 225 | Decimal::from(24u64).try_into().unwrap() 226 | ); 227 | 228 | assert_eq!( 229 | Rate::from_percent(240) 230 | .try_div(Rate::from_percent(60)) 231 | .unwrap(), 232 | Rate::from_scaled_val(4 * WAD) 233 | ); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /token-lending/sdk/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types 2 | 3 | use num_derive::FromPrimitive; 4 | use num_traits::FromPrimitive; 5 | use solana_program::{decode_error::DecodeError, program_error::ProgramError}; 6 | use solana_program::{msg, program_error::PrintProgramError}; 7 | use thiserror::Error; 8 | 9 | /// Errors that may be returned by the TokenLending program. 10 | #[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] 11 | pub enum LendingError { 12 | // 0 13 | /// Invalid instruction data passed in. 14 | #[error("Failed to unpack instruction data")] 15 | InstructionUnpackError, 16 | /// The account cannot be initialized because it is already in use. 17 | #[error("Account is already initialized")] 18 | AlreadyInitialized, 19 | /// Lamport balance below rent-exempt threshold. 20 | #[error("Lamport balance below rent-exempt threshold")] 21 | NotRentExempt, 22 | /// The program address provided doesn't match the value generated by the program. 23 | #[error("Market authority is invalid")] 24 | InvalidMarketAuthority, 25 | /// Expected a different market owner 26 | #[error("Market owner is invalid")] 27 | InvalidMarketOwner, 28 | 29 | // 5 30 | /// The owner of the input isn't set to the program address generated by the program. 31 | #[error("Input account owner is not the program address")] 32 | InvalidAccountOwner, 33 | /// The owner of the account input isn't set to the correct token program id. 34 | #[error("Input token account is not owned by the correct token program id")] 35 | InvalidTokenOwner, 36 | /// Expected an SPL Token account 37 | #[error("Input token account is not valid")] 38 | InvalidTokenAccount, 39 | /// Expected an SPL Token mint 40 | #[error("Input token mint account is not valid")] 41 | InvalidTokenMint, 42 | /// Expected a different SPL Token program 43 | #[error("Input token program account is not valid")] 44 | InvalidTokenProgram, 45 | 46 | // 10 47 | /// Invalid amount, must be greater than zero 48 | #[error("Input amount is invalid")] 49 | InvalidAmount, 50 | /// Invalid config value 51 | #[error("Input config value is invalid")] 52 | InvalidConfig, 53 | /// Invalid config value 54 | #[error("Input account must be a signer")] 55 | InvalidSigner, 56 | /// Invalid account input 57 | #[error("Invalid account input")] 58 | InvalidAccountInput, 59 | /// Math operation overflow 60 | #[error("Math operation overflow")] 61 | MathOverflow, 62 | 63 | // 15 64 | /// Token initialize mint failed 65 | #[error("Token initialize mint failed")] 66 | TokenInitializeMintFailed, 67 | /// Token initialize account failed 68 | #[error("Token initialize account failed")] 69 | TokenInitializeAccountFailed, 70 | /// Token transfer failed 71 | #[error("Token transfer failed")] 72 | TokenTransferFailed, 73 | /// Token mint to failed 74 | #[error("Token mint to failed")] 75 | TokenMintToFailed, 76 | /// Token burn failed 77 | #[error("Token burn failed")] 78 | TokenBurnFailed, 79 | 80 | // 20 81 | /// Insufficient liquidity available 82 | #[error("Insufficient liquidity available")] 83 | InsufficientLiquidity, 84 | /// This reserve's collateral cannot be used for borrows 85 | #[error("Input reserve has collateral disabled")] 86 | ReserveCollateralDisabled, 87 | /// Reserve state stale 88 | #[error("Reserve state needs to be refreshed")] 89 | ReserveStale, 90 | /// Withdraw amount too small 91 | #[error("Withdraw amount too small")] 92 | WithdrawTooSmall, 93 | /// Withdraw amount too large 94 | #[error("Withdraw amount too large")] 95 | WithdrawTooLarge, 96 | 97 | // 25 98 | /// Borrow amount too small 99 | #[error("Borrow amount too small to receive liquidity after fees")] 100 | BorrowTooSmall, 101 | /// Borrow amount too large 102 | #[error("Borrow amount too large for deposited collateral")] 103 | BorrowTooLarge, 104 | /// Repay amount too small 105 | #[error("Repay amount too small to transfer liquidity")] 106 | RepayTooSmall, 107 | /// Liquidation amount too small 108 | #[error("Liquidation amount too small to receive collateral")] 109 | LiquidationTooSmall, 110 | /// Cannot liquidate healthy obligations 111 | #[error("Cannot liquidate healthy obligations")] 112 | ObligationHealthy, 113 | 114 | // 30 115 | /// Obligation state stale 116 | #[error("Obligation state needs to be refreshed")] 117 | ObligationStale, 118 | /// Obligation reserve limit exceeded 119 | #[error("Obligation reserve limit exceeded")] 120 | ObligationReserveLimit, 121 | /// Expected a different obligation owner 122 | #[error("Obligation owner is invalid")] 123 | InvalidObligationOwner, 124 | /// Obligation deposits are empty 125 | #[error("Obligation deposits are empty")] 126 | ObligationDepositsEmpty, 127 | /// Obligation borrows are empty 128 | #[error("Obligation borrows are empty")] 129 | ObligationBorrowsEmpty, 130 | 131 | // 35 132 | /// Obligation deposits have zero value 133 | #[error("Obligation deposits have zero value")] 134 | ObligationDepositsZero, 135 | /// Obligation borrows have zero value 136 | #[error("Obligation borrows have zero value")] 137 | ObligationBorrowsZero, 138 | /// Invalid obligation collateral 139 | #[error("Invalid obligation collateral")] 140 | InvalidObligationCollateral, 141 | /// Invalid obligation liquidity 142 | #[error("Invalid obligation liquidity")] 143 | InvalidObligationLiquidity, 144 | /// Obligation collateral is empty 145 | #[error("Obligation collateral is empty")] 146 | ObligationCollateralEmpty, 147 | 148 | // 40 149 | /// Obligation liquidity is empty 150 | #[error("Obligation liquidity is empty")] 151 | ObligationLiquidityEmpty, 152 | /// Negative interest rate 153 | #[error("Interest rate is negative")] 154 | NegativeInterestRate, 155 | /// Oracle config is invalid 156 | #[error("Input oracle config is invalid")] 157 | InvalidOracleConfig, 158 | /// Expected a different flash loan receiver program 159 | #[error("Input flash loan receiver program account is not valid")] 160 | InvalidFlashLoanReceiverProgram, 161 | /// Not enough liquidity after flash loan 162 | #[error("Not enough liquidity after flash loan")] 163 | NotEnoughLiquidityAfterFlashLoan, 164 | 165 | // 45 166 | /// Null oracle config 167 | #[error("Null oracle config")] 168 | NullOracleConfig, 169 | /// Insufficent protocol fees to redeem or no liquidity availible to process redeem 170 | #[error("Insufficent protocol fees to claim or no liquidity availible")] 171 | InsufficientProtocolFeesToRedeem, 172 | /// No cpi flash borrows allowed 173 | #[error("No cpi flash borrows allowed")] 174 | FlashBorrowCpi, 175 | /// No corresponding repay found for flash borrow 176 | #[error("No corresponding repay found for flash borrow")] 177 | NoFlashRepayFound, 178 | /// Invalid flash repay found for borrow 179 | #[error("Invalid repay found")] 180 | InvalidFlashRepay, 181 | 182 | // 50 183 | /// No cpi flash repays allowed 184 | #[error("No cpi flash repays allowed")] 185 | FlashRepayCpi, 186 | /// Multiple flash borrows not allowed in the same transaction 187 | #[error("Multiple flash borrows not allowed in the same transaction")] 188 | MultipleFlashBorrows, 189 | /// Flash loans are disabled for this reserve 190 | #[error("Flash loans are disabled for this reserve")] 191 | FlashLoansDisabled, 192 | /// Deprecated instruction 193 | #[error("Instruction is deprecated")] 194 | DeprecatedInstruction, 195 | /// Outflow Rate Limit Exceeded 196 | #[error("Outflow Rate Limit Exceeded")] 197 | OutflowRateLimitExceeded, 198 | 199 | // 55 200 | /// Not a whitelisted liquidator 201 | #[error("Not a whitelisted liquidator")] 202 | NotWhitelistedLiquidator, 203 | /// Isolated Tier Asset Violation 204 | #[error("Isolated Tier Asset Violation")] 205 | IsolatedTierAssetViolation, 206 | /// Borrow Attribution Limit Exceeded 207 | #[error("Borrow Attribution Limit Exceeded")] 208 | BorrowAttributionLimitExceeded, 209 | /// Borrow Attribution Limit Not Exceeded 210 | #[error("Borrow Attribution Limit Not Exceeded")] 211 | BorrowAttributionLimitNotExceeded, 212 | } 213 | 214 | impl From for ProgramError { 215 | fn from(e: LendingError) -> Self { 216 | ProgramError::Custom(e as u32) 217 | } 218 | } 219 | 220 | impl DecodeError for LendingError { 221 | fn type_of() -> &'static str { 222 | "Lending Error" 223 | } 224 | } 225 | 226 | impl PrintProgramError for LendingError { 227 | fn print(&self) 228 | where 229 | E: 'static + std::error::Error + DecodeError + PrintProgramError + FromPrimitive, 230 | { 231 | msg!(&self.to_string()); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /deploy_token_lending.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "Running deploy script..."; 3 | SOLANA_CONFIG=$1; 4 | PROGRAM_ID=$2; 5 | # Get OWNER from keypair_path key of the solana config file 6 | OWNER=`grep 'keypair_path:' $SOLANA_CONFIG | awk '{print $2}'`; 7 | MARKET_OWNER=`solana --config $SOLANA_CONFIG address`; 8 | 9 | target/debug/spl-token --config $SOLANA_CONFIG unwrap; 10 | 11 | set -e; 12 | echo "Using Solana config filepath: $SOLANA_CONFIG"; 13 | echo "Program ID: $PROGRAM_ID"; 14 | echo "Owner: $OWNER"; 15 | echo "Market Owner $MARKET_OWNER"; 16 | 17 | solana config set --url https://api.devnet.solana.com; 18 | 19 | solana airdrop 10 $MARKET_OWNER; 20 | SOURCE=`target/debug/spl-token --config $SOLANA_CONFIG wrap 10 2>&1 | head -n1 | awk '{print $NF}'`; 21 | 22 | solana program --config $SOLANA_CONFIG deploy \ 23 | --program-id $PROGRAM_ID \ 24 | target/deploy/solend_program.so; 25 | 26 | echo "Creating Lending Market"; 27 | CREATE_MARKET_OUTPUT=`target/debug/solend-program create-market \ 28 | --fee-payer $OWNER \ 29 | --market-owner $MARKET_OWNER \ 30 | --verbose`; 31 | 32 | echo "$CREATE_MARKET_OUTPUT"; 33 | MARKET_ADDR=`echo $CREATE_MARKET_OUTPUT | head -n1 | awk '{print $4}'`; 34 | AUTHORITY_ADDR=`echo $CREATE_MARKET_OUTPUT | grep "Authority Address" | awk '{print $NF}'`; 35 | 36 | echo "Creating SOL reserve"; 37 | SOL_RESERVE_OUTPUT=`target/debug/solend-program add-reserve \ 38 | --fee-payer $OWNER \ 39 | --market-owner $OWNER \ 40 | --source-owner $OWNER \ 41 | --market $MARKET_ADDR \ 42 | --source $SOURCE \ 43 | --amount 5 \ 44 | --pyth-product 3Mnn2fX6rQyUsyELYms1sBJyChWofzSNRoqYzvgMVz5E \ 45 | --pyth-price J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix \ 46 | --switchboard-feed AdtRGGhmqvom3Jemp5YNrxd9q9unX36BZk1pujkkXijL \ 47 | --optimal-utilization-rate 80 \ 48 | --loan-to-value-ratio 75 \ 49 | --liquidation-bonus 5 \ 50 | --liquidation-threshold 80 \ 51 | --min-borrow-rate 0 \ 52 | --optimal-borrow-rate 12 \ 53 | --max-borrow-rate 150 \ 54 | --host-fee-percentage 50 \ 55 | --deposit-limit 40000 \ 56 | --verbose`; 57 | echo "$SOL_RESERVE_OUTPUT"; 58 | 59 | # USDC Reserve 60 | echo "Creating USDC Reserve"; 61 | USDC_TOKEN_MINT=`target/debug/spl-token --config $SOLANA_CONFIG create-token --decimals 6 | awk '{print $3}'`; 62 | echo "USDC MINT: $USDC_TOKEN_MINT" 63 | USDC_TOKEN_ACCOUNT=`target/debug/spl-token --config $SOLANA_CONFIG create-account $USDC_TOKEN_MINT | awk '{print $3}'`; 64 | target/debug/spl-token --config $SOLANA_CONFIG mint $USDC_TOKEN_MINT 30000000; 65 | 66 | USDC_RESERVE_OUTPUT=`target/debug/solend-program add-reserve \ 67 | --fee-payer $OWNER \ 68 | --market-owner $OWNER \ 69 | --source-owner $OWNER \ 70 | --market $MARKET_ADDR \ 71 | --source $USDC_TOKEN_ACCOUNT \ 72 | --amount 500000 \ 73 | --pyth-product 6NpdXrQEpmDZ3jZKmM2rhdmkd3H6QAk23j2x8bkXcHKA \ 74 | --pyth-price 5SSkXsEKQepHHAewytPVwdej4epN1nxgLVM84L4KXgy7 \ 75 | --switchboard-feed CZx29wKMUxaJDq6aLVQTdViPL754tTR64NAgQBUGxxHb \ 76 | --optimal-utilization-rate 80 \ 77 | --loan-to-value-ratio 75 \ 78 | --liquidation-bonus 5 \ 79 | --liquidation-threshold 80 \ 80 | --min-borrow-rate 0 \ 81 | --optimal-borrow-rate 8 \ 82 | --max-borrow-rate 50 \ 83 | --host-fee-percentage 50 \ 84 | --deposit-limit 1000000 \ 85 | --verbose`; 86 | echo "$USDC_RESERVE_OUTPUT"; 87 | 88 | # ETH Reserve 89 | echo "Creating ETH Reserve" 90 | ETH_TOKEN_MINT=`target/debug/spl-token --config $SOLANA_CONFIG create-token --decimals 6 | awk '{print $3}'`; 91 | echo "ETH MINT: $ETH_TOKEN_MINT" 92 | ETH_TOKEN_ACCOUNT=`target/debug/spl-token --config $SOLANA_CONFIG create-account $ETH_TOKEN_MINT | awk '{print $3}'`; 93 | target/debug/spl-token --config $SOLANA_CONFIG mint $ETH_TOKEN_MINT 8000000; 94 | 95 | ETH_RESERVE_OUTPUT=`target/debug/solend-program add-reserve \ 96 | --fee-payer $OWNER \ 97 | --market-owner $OWNER \ 98 | --source-owner $OWNER \ 99 | --market $MARKET_ADDR \ 100 | --source $ETH_TOKEN_ACCOUNT \ 101 | --amount 250 \ 102 | --pyth-product 2ciUuGZiee5macAMeQ7bHGTJtwcYTgnt6jdmQnnKZrfu \ 103 | --pyth-price EdVCmQ9FSPcVe5YySXDPCRmc8aDQLKJ9xvYBMZPie1Vw \ 104 | --switchboard-feed QJc2HgGhdtW4e7zjvLB1TGRuwEpTre2agU5Lap2UqYz \ 105 | --optimal-utilization-rate 80 \ 106 | --loan-to-value-ratio 75 \ 107 | --liquidation-bonus 5 \ 108 | --liquidation-threshold 80 \ 109 | --min-borrow-rate 0 \ 110 | --optimal-borrow-rate 8 \ 111 | --max-borrow-rate 100 \ 112 | --host-fee-percentage 50 \ 113 | --deposit-limit 500 \ 114 | --verbose`; 115 | echo "$ETH_RESERVE_OUTPUT"; 116 | 117 | 118 | echo "Creating BTC Reserve" 119 | BTC_TOKEN_MINT=`target/debug/spl-token --config $SOLANA_CONFIG create-token --decimals 6 | awk '{print $3}'`; 120 | echo "BTC MINT: $BTC_TOKEN_MINT" 121 | BTC_TOKEN_ACCOUNT=`target/debug/spl-token --config $SOLANA_CONFIG create-account $BTC_TOKEN_MINT | awk '{print $3}'`; 122 | target/debug/spl-token --config $SOLANA_CONFIG mint $BTC_TOKEN_MINT 8000000; 123 | 124 | BTC_RESERVE_OUTPUT=`target/debug/solend-program add-reserve \ 125 | --fee-payer $OWNER \ 126 | --market-owner $OWNER \ 127 | --source-owner $OWNER \ 128 | --market $MARKET_ADDR \ 129 | --source $BTC_TOKEN_ACCOUNT \ 130 | --amount 15 \ 131 | --pyth-product 3m1y5h2uv7EQL3KaJZehvAJa4yDNvgc5yAdL9KPMKwvk\ 132 | --pyth-price HovQMDrbAgAYPCmHVSrezcSmkMtXSSUsLDFANExrZh2J \ 133 | --switchboard-feed 74YzQPGUT9VnjrBz8MuyDLKgKpbDqGot5xZJvTtMi6Ng \ 134 | --optimal-utilization-rate 80 \ 135 | --loan-to-value-ratio 75 \ 136 | --liquidation-bonus 5 \ 137 | --liquidation-threshold 80 \ 138 | --min-borrow-rate 0 \ 139 | --optimal-borrow-rate 8 \ 140 | --max-borrow-rate 100 \ 141 | --host-fee-percentage 50 \ 142 | --deposit-limit 30 \ 143 | --verbose`; 144 | echo "$BTC_RESERVE_OUTPUT"; 145 | 146 | target/debug/spl-token --config $SOLANA_CONFIG unwrap; 147 | 148 | # Export variables for new config.ts file 149 | CONFIG_TEMPLATE_FILE="https://raw.githubusercontent.com/solendprotocol/common/master/src/devnet_template.json" 150 | # Token Mints 151 | export USDC_MINT_ADDRESS="$USDC_TOKEN_MINT"; 152 | export ETH_MINT_ADDRESS="$ETH_TOKEN_MINT"; 153 | export BTC_MINT_ADDRESS="$BTC_TOKEN_MINT"; 154 | 155 | # Main Market 156 | export MAIN_MARKET_ADDRESS="$MARKET_ADDR"; 157 | export MAIN_MARKET_AUTHORITY_ADDRESS="$AUTHORITY_ADDR"; 158 | 159 | # Reserves 160 | export SOL_RESERVE_ADDRESS=`echo "$SOL_RESERVE_OUTPUT" | grep "Adding reserve" | awk '{print $NF}'`; 161 | export SOL_RESERVE_COLLATERAL_MINT_ADDRESS=`echo "$SOL_RESERVE_OUTPUT" | grep "Adding collateral mint" | awk '{print $NF}'`; 162 | export SOL_RESERVE_COLLATERAL_SUPPLY_ADDRESS=`echo "$SOL_RESERVE_OUTPUT" | grep "Adding collateral supply" | awk '{print $NF}'`; 163 | export SOL_RESERVE_LIQUIDITY_ADDRESS=`echo "$SOL_RESERVE_OUTPUT" | grep "Adding liquidity supply" | awk '{print $NF}'`; 164 | export SOL_RESERVE_LIQUIDITY_FEE_RECEIVER_ADDRESS=`echo "$SOL_RESERVE_OUTPUT" | grep "Adding liquidity fee receiver" | awk '{print $NF}'`; 165 | 166 | export USDC_RESERVE_ADDRESS=`echo "$USDC_RESERVE_OUTPUT" | grep "Adding reserve" | awk '{print $NF}'`; 167 | export USDC_RESERVE_COLLATERAL_MINT_ADDRESS=`echo "$USDC_RESERVE_OUTPUT" | grep "Adding collateral mint" | awk '{print $NF}'`; 168 | export USDC_RESERVE_COLLATERAL_SUPPLY_ADDRESS=`echo "$USDC_RESERVE_OUTPUT" | grep "Adding collateral supply" | awk '{print $NF}'`; 169 | export USDC_RESERVE_LIQUIDITY_ADDRESS=`echo "$USDC_RESERVE_OUTPUT" | grep "Adding liquidity supply" | awk '{print $NF}'`; 170 | export USDC_RESERVE_LIQUIDITY_FEE_RECEIVER_ADDRESS=`echo "$USDC_RESERVE_OUTPUT" | grep "Adding liquidity fee receiver" | awk '{print $NF}'`; 171 | 172 | export ETH_RESERVE_ADDRESS=`echo "$ETH_RESERVE_OUTPUT" | grep "Adding reserve" | awk '{print $NF}'`; 173 | export ETH_RESERVE_COLLATERAL_MINT_ADDRESS=`echo "$ETH_RESERVE_OUTPUT" | grep "Adding collateral mint" | awk '{print $NF}'`; 174 | export ETH_RESERVE_COLLATERAL_SUPPLY_ADDRESS=`echo "$ETH_RESERVE_OUTPUT" | grep "Adding collateral supply" | awk '{print $NF}'`; 175 | export ETH_RESERVE_LIQUIDITY_ADDRESS=`echo "$ETH_RESERVE_OUTPUT" | grep "Adding liquidity supply" | awk '{print $NF}'`; 176 | export ETH_RESERVE_LIQUIDITY_FEE_RECEIVER_ADDRESS=`echo "$ETH_RESERVE_OUTPUT" | grep "Adding liquidity fee receiver" | awk '{print $NF}'`; 177 | 178 | export BTC_RESERVE_ADDRESS=`echo "$BTC_RESERVE_OUTPUT" | grep "Adding reserve" | awk '{print $NF}'`; 179 | export BTC_RESERVE_COLLATERAL_MINT_ADDRESS=`echo "$BTC_RESERVE_OUTPUT" | grep "Adding collateral mint" | awk '{print $NF}'`; 180 | export BTC_RESERVE_COLLATERAL_SUPPLY_ADDRESS=`echo "$BTC_RESERVE_OUTPUT" | grep "Adding collateral supply" | awk '{print $NF}'`; 181 | export BTC_RESERVE_LIQUIDITY_ADDRESS=`echo "$BTC_RESERVE_OUTPUT" | grep "Adding liquidity supply" | awk '{print $NF}'`; 182 | export BTC_RESERVE_LIQUIDITY_FEE_RECEIVER_ADDRESS=`echo "$BTC_RESERVE_OUTPUT" | grep "Adding liquidity fee receiver" | awk '{print $NF}'`; 183 | 184 | # Run templating command 185 | curl $CONFIG_TEMPLATE_FILE | envsubst -------------------------------------------------------------------------------- /token-lending/sdk/src/state/lending_market.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; 3 | use solana_program::{ 4 | msg, 5 | program_error::ProgramError, 6 | program_pack::{IsInitialized, Pack, Sealed}, 7 | pubkey::{Pubkey, PUBKEY_BYTES}, 8 | }; 9 | 10 | /// Lending market state 11 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 12 | pub struct LendingMarket { 13 | /// Version of lending market 14 | pub version: u8, 15 | /// Bump seed for derived authority address 16 | pub bump_seed: u8, 17 | /// Owner authority which can add new reserves 18 | pub owner: Pubkey, 19 | /// Currency market prices are quoted in 20 | /// e.g. "USD" null padded (`*b"USD\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"`) or a SPL token mint pubkey 21 | pub quote_currency: [u8; 32], 22 | /// Token program id 23 | pub token_program_id: Pubkey, 24 | /// Oracle (Pyth) program id 25 | pub oracle_program_id: Pubkey, 26 | /// Oracle (Switchboard) program id 27 | pub switchboard_oracle_program_id: Pubkey, 28 | /// Outflow rate limiter denominated in dollars 29 | pub rate_limiter: RateLimiter, 30 | /// whitelisted liquidator 31 | pub whitelisted_liquidator: Option, 32 | /// risk authority (additional pubkey used for setting params) 33 | pub risk_authority: Pubkey, 34 | } 35 | 36 | impl LendingMarket { 37 | /// Create a new lending market 38 | pub fn new(params: InitLendingMarketParams) -> Self { 39 | let mut lending_market = Self::default(); 40 | Self::init(&mut lending_market, params); 41 | lending_market 42 | } 43 | 44 | /// Initialize a lending market 45 | pub fn init(&mut self, params: InitLendingMarketParams) { 46 | self.version = PROGRAM_VERSION; 47 | self.bump_seed = params.bump_seed; 48 | self.owner = params.owner; 49 | self.quote_currency = params.quote_currency; 50 | self.token_program_id = params.token_program_id; 51 | self.oracle_program_id = params.oracle_program_id; 52 | self.switchboard_oracle_program_id = params.switchboard_oracle_program_id; 53 | self.rate_limiter = RateLimiter::default(); 54 | self.whitelisted_liquidator = None; 55 | self.risk_authority = params.owner; 56 | } 57 | } 58 | 59 | /// Initialize a lending market 60 | pub struct InitLendingMarketParams { 61 | /// Bump seed for derived authority address 62 | pub bump_seed: u8, 63 | /// Owner authority which can add new reserves 64 | pub owner: Pubkey, 65 | /// Currency market prices are quoted in 66 | /// e.g. "USD" null padded (`*b"USD\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"`) or a SPL token mint pubkey 67 | pub quote_currency: [u8; 32], 68 | /// Token program id 69 | pub token_program_id: Pubkey, 70 | /// Oracle (Pyth) program id 71 | pub oracle_program_id: Pubkey, 72 | /// Oracle (Switchboard) program id 73 | pub switchboard_oracle_program_id: Pubkey, 74 | } 75 | 76 | impl Sealed for LendingMarket {} 77 | impl IsInitialized for LendingMarket { 78 | fn is_initialized(&self) -> bool { 79 | self.version != UNINITIALIZED_VERSION 80 | } 81 | } 82 | 83 | const LENDING_MARKET_LEN: usize = 290; // 1 + 1 + 32 + 32 + 32 + 32 + 32 + 56 + 32 + 40 84 | impl Pack for LendingMarket { 85 | const LEN: usize = LENDING_MARKET_LEN; 86 | 87 | fn pack_into_slice(&self, output: &mut [u8]) { 88 | let output = array_mut_ref![output, 0, LENDING_MARKET_LEN]; 89 | #[allow(clippy::ptr_offset_with_cast)] 90 | let ( 91 | version, 92 | bump_seed, 93 | owner, 94 | quote_currency, 95 | token_program_id, 96 | oracle_program_id, 97 | switchboard_oracle_program_id, 98 | rate_limiter, 99 | whitelisted_liquidator, 100 | risk_authority, 101 | _padding, 102 | ) = mut_array_refs![ 103 | output, 104 | 1, 105 | 1, 106 | PUBKEY_BYTES, 107 | 32, 108 | PUBKEY_BYTES, 109 | PUBKEY_BYTES, 110 | PUBKEY_BYTES, 111 | RATE_LIMITER_LEN, 112 | PUBKEY_BYTES, 113 | PUBKEY_BYTES, 114 | 8 115 | ]; 116 | 117 | *version = self.version.to_le_bytes(); 118 | *bump_seed = self.bump_seed.to_le_bytes(); 119 | owner.copy_from_slice(self.owner.as_ref()); 120 | quote_currency.copy_from_slice(self.quote_currency.as_ref()); 121 | token_program_id.copy_from_slice(self.token_program_id.as_ref()); 122 | oracle_program_id.copy_from_slice(self.oracle_program_id.as_ref()); 123 | switchboard_oracle_program_id.copy_from_slice(self.switchboard_oracle_program_id.as_ref()); 124 | self.rate_limiter.pack_into_slice(rate_limiter); 125 | match self.whitelisted_liquidator { 126 | Some(pubkey) => { 127 | whitelisted_liquidator.copy_from_slice(pubkey.as_ref()); 128 | } 129 | None => { 130 | whitelisted_liquidator.copy_from_slice(&[0u8; 32]); 131 | } 132 | } 133 | risk_authority.copy_from_slice(self.risk_authority.as_ref()); 134 | } 135 | 136 | /// Unpacks a byte buffer into a [LendingMarketInfo](struct.LendingMarketInfo.html) 137 | fn unpack_from_slice(input: &[u8]) -> Result { 138 | let input = array_ref![input, 0, LENDING_MARKET_LEN]; 139 | #[allow(clippy::ptr_offset_with_cast)] 140 | let ( 141 | version, 142 | bump_seed, 143 | owner, 144 | quote_currency, 145 | token_program_id, 146 | oracle_program_id, 147 | switchboard_oracle_program_id, 148 | rate_limiter, 149 | whitelisted_liquidator, 150 | risk_authority, 151 | _padding, 152 | ) = array_refs![ 153 | input, 154 | 1, 155 | 1, 156 | PUBKEY_BYTES, 157 | 32, 158 | PUBKEY_BYTES, 159 | PUBKEY_BYTES, 160 | PUBKEY_BYTES, 161 | RATE_LIMITER_LEN, 162 | PUBKEY_BYTES, 163 | PUBKEY_BYTES, 164 | 8 165 | ]; 166 | 167 | let version = u8::from_le_bytes(*version); 168 | if version > PROGRAM_VERSION { 169 | msg!("Lending market version does not match lending program version"); 170 | return Err(ProgramError::InvalidAccountData); 171 | } 172 | 173 | let owner_pubkey = Pubkey::new_from_array(*owner); 174 | Ok(Self { 175 | version, 176 | bump_seed: u8::from_le_bytes(*bump_seed), 177 | owner: owner_pubkey, 178 | quote_currency: *quote_currency, 179 | token_program_id: Pubkey::new_from_array(*token_program_id), 180 | oracle_program_id: Pubkey::new_from_array(*oracle_program_id), 181 | switchboard_oracle_program_id: Pubkey::new_from_array(*switchboard_oracle_program_id), 182 | rate_limiter: RateLimiter::unpack_from_slice(rate_limiter)?, 183 | whitelisted_liquidator: if whitelisted_liquidator == &[0u8; 32] { 184 | None 185 | } else { 186 | Some(Pubkey::new_from_array(*whitelisted_liquidator)) 187 | }, 188 | // the risk authority can equal [0; 32] when the program is upgraded to v2.0.2. in that 189 | // case, we set the risk authority to be the owner. This isn't strictly necessary, but 190 | // better to be safe i guess. 191 | risk_authority: if *risk_authority == [0; 32] { 192 | owner_pubkey 193 | } else { 194 | Pubkey::new_from_array(*risk_authority) 195 | }, 196 | }) 197 | } 198 | } 199 | 200 | #[cfg(test)] 201 | mod test { 202 | use super::*; 203 | use rand::Rng; 204 | 205 | #[test] 206 | fn pack_and_unpack_lending_market() { 207 | let mut rng = rand::thread_rng(); 208 | let lending_market = LendingMarket { 209 | version: PROGRAM_VERSION, 210 | bump_seed: rng.gen(), 211 | owner: Pubkey::new_unique(), 212 | quote_currency: [rng.gen(); 32], 213 | token_program_id: Pubkey::new_unique(), 214 | oracle_program_id: Pubkey::new_unique(), 215 | switchboard_oracle_program_id: Pubkey::new_unique(), 216 | rate_limiter: rand_rate_limiter(), 217 | whitelisted_liquidator: if rng.gen_bool(0.5) { 218 | None 219 | } else { 220 | Some(Pubkey::new_unique()) 221 | }, 222 | risk_authority: Pubkey::new_unique(), 223 | }; 224 | 225 | let mut packed = vec![0u8; LendingMarket::LEN]; 226 | LendingMarket::pack(lending_market.clone(), &mut packed).unwrap(); 227 | let unpacked = LendingMarket::unpack_from_slice(&packed).unwrap(); 228 | assert_eq!(unpacked, lending_market); 229 | } 230 | } 231 | --------------------------------------------------------------------------------