lending: Command Line Interface (#1676)

This commit is contained in:
Jordan Sexton 2021-06-23 16:04:43 -05:00 committed by GitHub
parent 346075743e
commit 3591f80616
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1000 additions and 11 deletions

15
Cargo.lock generated
View File

@ -3997,6 +3997,21 @@ dependencies = [
"uint",
]
[[package]]
name = "spl-token-lending-cli"
version = "0.1.0"
dependencies = [
"clap",
"solana-clap-utils",
"solana-cli-config",
"solana-client",
"solana-logger",
"solana-program",
"solana-sdk",
"spl-token 3.1.1",
"spl-token-lending",
]
[[package]]
name = "spl-token-swap"
version = "2.1.0"

View File

@ -17,6 +17,7 @@ members = [
"shared-memory/program",
"stake-pool/cli",
"stake-pool/program",
"token-lending/cli",
"token-lending/program",
"token-swap/program",
"token-swap/program/fuzz",

View File

@ -1,4 +1,4 @@
# Token-lending program
# Token Lending program
A lending protocol for the Token program on the Solana blockchain inspired by Aave and Compound.
@ -6,10 +6,111 @@ Full documentation is available at https://spl.solana.com/token-lending
Web3 bindings are available in the `./js` directory.
### On-Chain Programs
### On-chain programs
Please note that only the lending program deployed to devnet is currently operational.
| Cluster | Program Address |
| --- | --- |
| Mainnet Beta | [`LendZqTs8gn5CTSJU1jWKhKuVpjJGom45nnwPb2AMTi`](https://explorer.solana.com/address/LendZqTs7gn5CTSJU1jWKhKuVpjJGom45nnwPb2AMTi) |
| Testnet | [`LendZqTs8gn5CTSJU1jWKhKuVpjJGom45nnwPb2AMTi`](https://explorer.solana.com/address/LendZqTs8gn5CTSJU1jWKhKuVpjJGom45nnwPb2AMTi?cluster=testnet) |
| Devnet | [`LendZqTs8gn5CTSJU1jWKhKuVpjJGom45nnwPb2AMTi`](https://explorer.solana.com/address/LendZqTs8gn5CTSJU1jWKhKuVpjJGom45nnwPb2AMTi?cluster=devnet) |
| Testnet | [`6TvznH3B2e3p2mbhufNBpgSrLx6UkgvxtVQvopEZ2kuH`](https://explorer.solana.com/address/6TvznH3B2e3p2mbhufNBpgSrLx6UkgvxtVQvopEZ2kuH?cluster=testnet) |
| Devnet | [`6TvznH3B2e3p2mbhufNBpgSrLx6UkgvxtVQvopEZ2kuH`](https://explorer.solana.com/address/6TvznH3B2e3p2mbhufNBpgSrLx6UkgvxtVQvopEZ2kuH?cluster=devnet) |
### Deploy a lending program (optional)
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.
1. [Install the Solana CLI](https://docs.solana.com/cli/install-solana-cli-tools)
1. Install the Token and Token Lending CLIs:
```shell
cargo install spl-token-cli
cargo install spl-token-lending-cli
```
1. Clone the SPL repo:
```shell
git clone https://github.com/solana-labs/solana-program-library.git
```
1. Go to the new directory:
```shell
cd solana-program-library
```
1. Generate a keypair for yourself:
```shell
solana-keygen new -o owner.json
# Wrote new keypair to owner.json
# ================================================================================
# pubkey: JAgN4SZLNeCo9KTnr8EWt4FzEV1UDgHkcZwkVtWtfp6P
# ================================================================================
# Save this seed phrase and your BIP39 passphrase to recover your new keypair:
# your seed words here never share them not even with your mom
# ================================================================================
```
This pubkey will be the owner of the lending market that can add reserves to it.
1. Generate a keypair for the program:
```shell
solana-keygen new -o lending.json
# Wrote new keypair to lending.json
# ============================================================================
# pubkey: 6TvznH3B2e3p2mbhufNBpgSrLx6UkgvxtVQvopEZ2kuH
# ============================================================================
# Save this seed phrase and your BIP39 passphrase to recover your new keypair:
# your seed words here never share them not even with your mom
# ============================================================================
```
This pubkey will be your Program ID.
1. Open `./token-lending/program/src/lib.rs` in your editor. In the line
```rust
solana_program::declare_id!("6TvznH3B2e3p2mbhufNBpgSrLx6UkgvxtVQvopEZ2kuH");
```
replace the Program ID with yours.
1. Build the program binaries:
```shell
cargo build
cargo build-bpf
```
1. Prepare to deploy to devnet:
```shell
solana config set --url https://api.devnet.solana.com
```
1. Score yourself some sweet SOL:
```shell
solana airdrop -k owner.json 10
solana airdrop -k owner.json 10
solana airdrop -k owner.json 10
```
You'll use this for transaction fees, rent for your program accounts, and initial reserve liquidity.
1. Deploy the program:
```shell
solana program deploy \
-k owner.json \
--program-id lending.json \
target/deploy/spl_token_lending.so
# Program Id: 6TvznH3B2e3p2mbhufNBpgSrLx6UkgvxtVQvopEZ2kuH
```
If the deployment doesn't succeed, follow [this guide](https://docs.solana.com/cli/deploy-a-program#resuming-a-failed-deploy) to resume it.
1. Wrap some of your SOL as an SPL Token:
```shell
spl-token wrap \
--fee-payer owner.json \
10.0 \
-- owner.json
# Wrapping 10 SOL into AJ2sgpgj6ZeQazPPiDyTYqN9vbj58QMaZQykB9Sr6XY
```
You'll use this for initial reserve liquidity. Note the SPL Token account pubkey (e.g. `AJ2sgpgj6ZeQazPPiDyTYqN9vbj58QMaZQykB9Sr6XY`).
1. Use the [Token Lending CLI](./cli/README.md) to create a lending market and add reserves to it.

View File

@ -0,0 +1,24 @@
[package]
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
description = "SPL Token Lending CLI"
edition = "2018"
homepage = "https://spl.solana.com/token-lending"
license = "Apache-2.0"
name = "spl-token-lending-cli"
repository = "https://github.com/solana-labs/solana-program-library"
version = "0.1.0"
[dependencies]
clap = "2.33.3"
solana-clap-utils = "1.6.6"
solana-cli-config = "1.6.6"
solana-client = "1.6.6"
solana-logger = "1.6.6"
solana-sdk = "1.6.6"
solana-program = "1.6.6"
spl-token-lending = { path="../program", features = [ "no-entrypoint" ] }
spl-token = { path="../../token/program", features = [ "no-entrypoint" ] }
[[bin]]
name = "spl-token-lending"
path = "src/main.rs"

101
token-lending/cli/README.md Normal file
View File

@ -0,0 +1,101 @@
# SPL Token Lending CLI
A basic command line interface for initializing lending markets and reserves for SPL Token Lending.
See https://spl.solana.com/token-lending for more details
## Install the CLI
```shell
cargo install spl-token-lending-cli
```
## Deploy a lending program (optional)
This is optional! You can simply add your own market and reserves to the existing [on-chain programs](../README.md#On-chain-programs).
If you want to deploy your own program, follow [this guide](../README.md#Deploy-a-lending-program) and note the program ID.
## Create a lending market
A lending market is a collection of reserves that can be configured to borrow and lend with each other.
The lending market owner must sign to add reserves.
### Usage
```shell
spl-token-lending \
--program PUBKEY \
--fee-payer SIGNER \
create-market \
--market-owner PUBKEY
```
- `--program` is the lending program ID.
- `--fee-payer` will sign to pay transaction fees.
- `--market-owner` is the lending market owner pubkey.
Run `spl-token-lending create-market --help` for more details and options.
### Example
```shell
spl-token-lending \
--program 6TvznH3B2e3p2mbhufNBpgSrLx6UkgvxtVQvopEZ2kuH \
--fee-payer owner.json \
create-market \
--market-owner JAgN4SZLNeCo9KTnr8EWt4FzEV1UDgHkcZwkVtWtfp6P
# Creating lending market 7uX9ywsk1X2j6wLoywMDVQLNWAqhDpVqZzL4qm4CuMMT
# Signature: 51mi4Ve42h4PQ1RXjfz141T6KCdqnB3UDyhEejviVHrX4SnQCMx86TZa9CWUT3efFYkkmfmseG5ZQr2TZTHJ8S95
```
Note the lending market pubkey (e.g. `7uX9ywsk1X2j6wLoywMDVQLNWAqhDpVqZzL4qm4CuMMT`). You'll use this to add reserves.
## Add a reserve to your market
A reserve is a liquidity pool that can be deposited into, borrowed from, and optionally used as collateral for borrows.
### Usage
```shell
spl-token-lending \
--program PUBKEY \
--fee-payer SIGNER \
add-reserve \
--market-owner SIGNER \
--source-owner SIGNER \
--market PUBKEY \
--source PUBKEY \
--amount DECIMAL_AMOUNT \
--pyth-product PUBKEY \
--pyth-price PUBKEY
```
- `--program` is the lending program ID.
- `--fee-payer` will sign to pay transaction fees.
- `--market-owner` will sign as the lending market owner.
- `--source-owner` will sign as the source liquidity owner.
- `--market` is the lending market pubkey.
- `--source` is the SPL Token account pubkey (owned by `--source-owner`).
- `--amount` is the amount of tokens to deposit.
- `--pyth-product` and `--pyth-price` are oracle
accounts [provided by Pyth](https://pyth.network/developers/consumers/accounts).
Run `spl-token-lending add-reserve --help` for more details and options.
### Example
```shell
spl-token-lending \
--program 6TvznH3B2e3p2mbhufNBpgSrLx6UkgvxtVQvopEZ2kuH \
--fee-payer owner.json \
add-reserve \
--market-owner owner.json \
--source-owner owner.json \
--market 7uX9ywsk1X2j6wLoywMDVQLNWAqhDpVqZzL4qm4CuMMT \
--source AJ2sgpgj6ZeQazPPiDyTYqN9vbj58QMaZQykB9Sr6XY \
--amount 5.0 \
--pyth-product 8yrQMUyJRnCJ72NWwMiPV9dNGw465Z8bKUvnUC8P5L6F \
--pyth-price BdgHsXrH1mXqhdosXavYxZgX6bGqTdj5mh2sxDhF8bJy
# Adding reserve 69BwFhpQBzZfcp9MCj9V8TLvdv9zGfQQPQbb8dUHsaEa
# Signature: 2yKHnmBSqBpbGdsxW75nnmZMys1bZMbHiczdZitMeQHYdpis4eVhuMWGE29hhgtHpNDjdPj5YVbqkWoAEBw1WaU
# Signature: 33x8gbn2RkiA5844eCZq151DuVrYTvUoF1bQ5xA3mqkibJZaJja2hj8RoyjKZpZqg2ckcSKMAeqWbMeWC6vAySQS
# Signature: 3dk79hSgzFhxPrmctYnS5dxRhojfKkDwwLxEda9bTXqVELHSL4ux8au4jwvL8xuraVhaZAmugCn4TA1YCfLM4sVL
```
Note the reserve pubkey (e.g. `69BwFhpQBzZfcp9MCj9V8TLvdv9zGfQQPQbb8dUHsaEa`). You'll use this to deposit liquidity, redeem collateral, borrow, repay, and liquidate.

View File

@ -0,0 +1,731 @@
use {
clap::{
crate_description, crate_name, crate_version, value_t, App, AppSettings, Arg, ArgMatches,
SubCommand,
},
solana_clap_utils::{
fee_payer::fee_payer_arg,
input_parsers::{keypair_of, pubkey_of, value_of},
input_validators::{is_amount, is_keypair, is_parsable, is_pubkey, is_url},
keypair::signer_from_path,
},
solana_client::rpc_client::RpcClient,
solana_program::{native_token::lamports_to_sol, program_pack::Pack, pubkey::Pubkey},
solana_sdk::{
commitment_config::CommitmentConfig,
signature::{Keypair, Signer},
system_instruction,
transaction::Transaction,
},
spl_token::{
instruction::{approve, revoke},
state::{Account as Token, Mint},
ui_amount_to_amount,
},
spl_token_lending::{
self,
instruction::{init_lending_market, init_reserve},
math::WAD,
state::{LendingMarket, Reserve, ReserveConfig, ReserveFees},
},
std::{borrow::Borrow, process::exit, str::FromStr},
system_instruction::create_account,
};
struct Config {
rpc_client: RpcClient,
fee_payer: Box<dyn Signer>,
lending_program_id: Pubkey,
verbose: bool,
dry_run: bool,
}
type Error = Box<dyn std::error::Error>;
type CommandResult = Result<(), Error>;
const PYTH_PROGRAM_ID: &str = "5mkqGkkWSaSk2NL9p4XptwEQu4d5jFTJiurbbzdqYexF";
fn main() {
solana_logger::setup_with_default("solana=info");
let default_lending_program_id: &str = &spl_token_lending::id().to_string();
let matches = App::new(crate_name!())
.about(crate_description!())
.version(crate_version!())
.setting(AppSettings::SubcommandRequiredElseHelp)
.arg({
let arg = Arg::with_name("config_file")
.short("C")
.long("config")
.value_name("PATH")
.takes_value(true)
.global(true)
.help("Configuration file to use");
if let Some(ref config_file) = *solana_cli_config::CONFIG_FILE {
arg.default_value(&config_file)
} else {
arg
}
})
.arg(
Arg::with_name("json_rpc_url")
.long("url")
.value_name("URL")
.takes_value(true)
.validator(is_url)
.help("JSON RPC URL for the cluster. Default from the configuration file."),
)
.arg(
fee_payer_arg()
.short("p")
.global(true)
)
.arg(
Arg::with_name("lending_program_id")
.long("program")
.validator(is_pubkey)
.value_name("PUBKEY")
.takes_value(true)
.required(true)
.default_value(default_lending_program_id)
.help("Lending program ID"),
)
.arg(
Arg::with_name("verbose")
.long("verbose")
.short("v")
.takes_value(false)
.global(true)
.help("Show additional information"),
)
.arg(
Arg::with_name("dry_run")
.long("dry-run")
.takes_value(false)
.global(true)
.help("Simulate transaction instead of executing"),
)
.subcommand(
SubCommand::with_name("create-market")
.about("Create a new lending market")
.arg(
Arg::with_name("lending_market_owner")
.long("market-owner")
.validator(is_pubkey)
.value_name("PUBKEY")
.takes_value(true)
.required(true)
.help("Owner that can add reserves to the market"),
)
.arg(
Arg::with_name("oracle_program_id")
.long("oracle")
.validator(is_pubkey)
.value_name("PUBKEY")
.takes_value(true)
.required(true)
.default_value(PYTH_PROGRAM_ID)
.help("Oracle (Pyth) program ID for quoting market prices"),
)
.arg(
Arg::with_name("quote_currency")
.long("quote")
.value_name("STRING")
.takes_value(true)
.required(true)
.default_value("USD")
.help("Currency market prices are quoted in"),
),
)
.subcommand(
SubCommand::with_name("add-reserve")
.about("Add a reserve to a lending market")
// @TODO: use is_valid_signer
.arg(
Arg::with_name("lending_market_owner")
.long("market-owner")
.validator(is_keypair)
.value_name("KEYPAIR")
.takes_value(true)
.required(true)
.help("Owner of the lending market"),
)
// @TODO: use is_valid_signer
.arg(
Arg::with_name("source_liquidity_owner")
.long("source-owner")
.validator(is_keypair)
.value_name("KEYPAIR")
.takes_value(true)
.required(true)
.help("Owner of the SPL Token account to deposit initial liquidity from"),
)
.arg(
Arg::with_name("lending_market")
.long("market")
.validator(is_pubkey)
.value_name("PUBKEY")
.takes_value(true)
.required(true)
.help("Lending market address"),
)
.arg(
Arg::with_name("source_liquidity")
.long("source")
.validator(is_pubkey)
.value_name("PUBKEY")
.takes_value(true)
.required(true)
.help("SPL Token account to deposit initial liquidity from"),
)
// @TODO: use is_amount_or_all
.arg(
Arg::with_name("liquidity_amount")
.long("amount")
.validator(is_amount)
.value_name("DECIMAL_AMOUNT")
.takes_value(true)
.required(true)
.help("Initial amount of liquidity to deposit into the new reserve"),
)
.arg(
Arg::with_name("pyth_product")
.long("pyth-product")
.validator(is_pubkey)
.value_name("PUBKEY")
.takes_value(true)
.required(true)
.help("Pyth product account: https://pyth.network/developers/consumers/accounts"),
)
.arg(
Arg::with_name("pyth_price")
.long("pyth-price")
.validator(is_pubkey)
.value_name("PUBKEY")
.takes_value(true)
.required(true)
.help("Pyth price account: https://pyth.network/developers/consumers/accounts"),
)
.arg(
Arg::with_name("optimal_utilization_rate")
.long("optimal-utilization-rate")
.validator(is_parsable::<u8>)
.value_name("INTEGER_PERCENT")
.takes_value(true)
.required(true)
.default_value("80")
.help("Optimal utilization rate: [0, 100]"),
)
.arg(
Arg::with_name("loan_to_value_ratio")
.long("loan-to-value-ratio")
.validator(is_parsable::<u8>)
.value_name("INTEGER_PERCENT")
.takes_value(true)
.required(true)
.default_value("50")
.help("Target ratio of the value of borrows to deposits: [0, 100)"),
)
.arg(
Arg::with_name("liquidation_bonus")
.long("liquidation-bonus")
.validator(is_parsable::<u8>)
.value_name("INTEGER_PERCENT")
.takes_value(true)
.required(true)
.default_value("5")
.help("Bonus a liquidator gets when repaying part of an unhealthy obligation: [0, 100]"),
)
.arg(
Arg::with_name("liquidation_threshold")
.long("liquidation-threshold")
.validator(is_parsable::<u8>)
.value_name("INTEGER_PERCENT")
.takes_value(true)
.required(true)
.default_value("55")
.help("Loan to value ratio at which an obligation can be liquidated: (LTV, 100]"),
)
.arg(
Arg::with_name("min_borrow_rate")
.long("min-borrow-rate")
.validator(is_parsable::<u8>)
.value_name("INTEGER_PERCENT")
.takes_value(true)
.required(true)
.default_value("0")
.help("Min borrow APY: min <= optimal <= max"),
)
.arg(
Arg::with_name("optimal_borrow_rate")
.long("optimal-borrow-rate")
.validator(is_parsable::<u8>)
.value_name("INTEGER_PERCENT")
.takes_value(true)
.required(true)
.default_value("4")
.help("Optimal (utilization) borrow APY: min <= optimal <= max"),
)
.arg(
Arg::with_name("max_borrow_rate")
.long("max-borrow-rate")
.validator(is_parsable::<u8>)
.value_name("INTEGER_PERCENT")
.takes_value(true)
.required(true)
.default_value("30")
.help("Max borrow APY: min <= optimal <= max"),
)
.arg(
Arg::with_name("borrow_fee")
.long("borrow-fee")
.validator(is_parsable::<f64>)
.value_name("DECIMAL_PERCENT")
.takes_value(true)
.required(true)
.default_value("0.00001")
.help("Fee assessed on borrow, expressed as a percentage: [0, 1)"),
)
.arg(
Arg::with_name("flash_loan_fee")
.long("flash-loan-fee")
.validator(is_parsable::<f64>)
.value_name("DECIMAL_PERCENT")
.takes_value(true)
.required(true)
.default_value(".3")
.help("Fee assessed for flash loans, expressed as a percentage: [0, 1)"),
)
.arg(
Arg::with_name("host_fee_percentage")
.long("host-fee-percentage")
.validator(is_parsable::<u8>)
.value_name("INTEGER_PERCENT")
.takes_value(true)
.required(true)
.default_value("20")
.help("Amount of fee going to host account: [0, 100]"),
)
)
.get_matches();
let mut wallet_manager = None;
let config = {
let cli_config = if let Some(config_file) = matches.value_of("config_file") {
solana_cli_config::Config::load(config_file).unwrap_or_default()
} else {
solana_cli_config::Config::default()
};
let json_rpc_url = value_t!(matches, "json_rpc_url", String)
.unwrap_or_else(|_| cli_config.json_rpc_url.clone());
let fee_payer = signer_from_path(
&matches,
matches
.value_of("fee_payer")
.unwrap_or(&cli_config.keypair_path),
"fee_payer",
&mut wallet_manager,
)
.unwrap_or_else(|e| {
eprintln!("error: {}", e);
exit(1);
});
let lending_program_id = pubkey_of(&matches, "lending_program_id").unwrap();
let verbose = matches.is_present("verbose");
let dry_run = matches.is_present("dry_run");
Config {
rpc_client: RpcClient::new_with_commitment(json_rpc_url, CommitmentConfig::confirmed()),
fee_payer,
lending_program_id,
verbose,
dry_run,
}
};
let _ = match matches.subcommand() {
("create-market", Some(arg_matches)) => {
let lending_market_owner = pubkey_of(arg_matches, "lending_market_owner").unwrap();
let quote_currency = quote_currency_of(arg_matches, "quote_currency").unwrap();
let oracle_program_id = pubkey_of(arg_matches, "oracle_program_id").unwrap();
command_create_lending_market(
&config,
lending_market_owner,
quote_currency,
oracle_program_id,
)
}
("add-reserve", Some(arg_matches)) => {
let lending_market_owner_keypair =
keypair_of(arg_matches, "lending_market_owner").unwrap();
let source_liquidity_owner_keypair =
keypair_of(arg_matches, "source_liquidity_owner").unwrap();
let lending_market_pubkey = pubkey_of(arg_matches, "lending_market").unwrap();
let source_liquidity_pubkey = pubkey_of(arg_matches, "source_liquidity").unwrap();
let ui_amount = value_of(arg_matches, "liquidity_amount").unwrap();
let pyth_product_pubkey = pubkey_of(arg_matches, "pyth_product").unwrap();
let pyth_price_pubkey = pubkey_of(arg_matches, "pyth_price").unwrap();
let optimal_utilization_rate =
value_of(arg_matches, "optimal_utilization_rate").unwrap();
let loan_to_value_ratio = value_of(arg_matches, "loan_to_value_ratio").unwrap();
let liquidation_bonus = value_of(arg_matches, "liquidation_bonus").unwrap();
let liquidation_threshold = value_of(arg_matches, "liquidation_threshold").unwrap();
let min_borrow_rate = value_of(arg_matches, "min_borrow_rate").unwrap();
let optimal_borrow_rate = value_of(arg_matches, "optimal_borrow_rate").unwrap();
let max_borrow_rate = value_of(arg_matches, "max_borrow_rate").unwrap();
let borrow_fee = value_of::<f64>(arg_matches, "borrow_fee").unwrap();
let flash_loan_fee = value_of::<f64>(arg_matches, "flash_loan_fee").unwrap();
let host_fee_percentage = value_of(arg_matches, "host_fee_percentage").unwrap();
let borrow_fee_wad = (borrow_fee * WAD as f64) as u64;
let flash_loan_fee_wad = (flash_loan_fee * WAD as f64) as u64;
command_add_reserve(
&config,
ui_amount,
ReserveConfig {
optimal_utilization_rate,
loan_to_value_ratio,
liquidation_bonus,
liquidation_threshold,
min_borrow_rate,
optimal_borrow_rate,
max_borrow_rate,
fees: ReserveFees {
borrow_fee_wad,
flash_loan_fee_wad,
host_fee_percentage,
},
},
source_liquidity_pubkey,
source_liquidity_owner_keypair,
lending_market_pubkey,
lending_market_owner_keypair,
pyth_product_pubkey,
pyth_price_pubkey,
)
}
_ => unreachable!(),
}
.map_err(|err| {
eprintln!("{}", err);
exit(1);
});
}
// COMMANDS
fn command_create_lending_market(
config: &Config,
lending_market_owner: Pubkey,
quote_currency: [u8; 32],
oracle_program_id: Pubkey,
) -> CommandResult {
let lending_market_keypair = Keypair::new();
println!(
"Creating lending market {}",
lending_market_keypair.pubkey()
);
let lending_market_balance = config
.rpc_client
.get_minimum_balance_for_rent_exemption(LendingMarket::LEN)?;
let mut transaction = Transaction::new_with_payer(
&[
// Account for the lending market
create_account(
&config.fee_payer.pubkey(),
&lending_market_keypair.pubkey(),
lending_market_balance,
LendingMarket::LEN as u64,
&config.lending_program_id,
),
// Initialize lending market account
init_lending_market(
config.lending_program_id,
lending_market_owner,
quote_currency,
lending_market_keypair.pubkey(),
oracle_program_id,
),
],
Some(&config.fee_payer.pubkey()),
);
let (recent_blockhash, fee_calculator) = config.rpc_client.get_recent_blockhash()?;
check_fee_payer_balance(
config,
lending_market_balance + fee_calculator.calculate_fee(&transaction.message()),
)?;
transaction.sign(
&vec![config.fee_payer.as_ref(), &lending_market_keypair],
recent_blockhash,
);
send_transaction(&config, transaction)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn command_add_reserve(
config: &Config,
ui_amount: f64,
reserve_config: ReserveConfig,
source_liquidity_pubkey: Pubkey,
source_liquidity_owner_keypair: Keypair,
lending_market_pubkey: Pubkey,
lending_market_owner_keypair: Keypair,
pyth_product_pubkey: Pubkey,
pyth_price_pubkey: Pubkey,
) -> CommandResult {
let source_liquidity_account = config.rpc_client.get_account(&source_liquidity_pubkey)?;
let source_liquidity = Token::unpack_from_slice(source_liquidity_account.data.borrow())?;
let source_liquidity_mint_account = config.rpc_client.get_account(&source_liquidity.mint)?;
let source_liquidity_mint =
Mint::unpack_from_slice(source_liquidity_mint_account.data.borrow())?;
let liquidity_amount = ui_amount_to_amount(ui_amount, source_liquidity_mint.decimals);
let reserve_keypair = Keypair::new();
let collateral_mint_keypair = Keypair::new();
let collateral_supply_keypair = Keypair::new();
let liquidity_supply_keypair = Keypair::new();
let liquidity_fee_receiver_keypair = Keypair::new();
let user_collateral_keypair = Keypair::new();
let user_transfer_authority_keypair = Keypair::new();
println!("Adding reserve {}", reserve_keypair.pubkey());
if config.verbose {
println!(
"Adding collateral mint {}",
collateral_mint_keypair.pubkey()
);
println!(
"Adding collateral supply {}",
collateral_supply_keypair.pubkey()
);
println!(
"Adding liquidity supply {}",
liquidity_supply_keypair.pubkey()
);
println!(
"Adding liquidity fee receiver {}",
liquidity_fee_receiver_keypair.pubkey()
);
println!(
"Adding user collateral {}",
user_collateral_keypair.pubkey()
);
println!(
"Adding user transfer authority {}",
user_transfer_authority_keypair.pubkey()
);
}
let reserve_balance = config
.rpc_client
.get_minimum_balance_for_rent_exemption(Reserve::LEN)?;
let collateral_mint_balance = config
.rpc_client
.get_minimum_balance_for_rent_exemption(Mint::LEN)?;
let token_account_balance = config
.rpc_client
.get_minimum_balance_for_rent_exemption(Token::LEN)?;
let collateral_supply_balance = token_account_balance;
let user_collateral_balance = token_account_balance;
let liquidity_supply_balance = token_account_balance;
let liquidity_fee_receiver_balance = token_account_balance;
let total_balance = reserve_balance
+ collateral_mint_balance
+ collateral_supply_balance
+ user_collateral_balance
+ liquidity_supply_balance
+ liquidity_fee_receiver_balance;
let mut transaction_1 = Transaction::new_with_payer(
&[
create_account(
&config.fee_payer.pubkey(),
&reserve_keypair.pubkey(),
reserve_balance,
Reserve::LEN as u64,
&config.lending_program_id,
),
create_account(
&config.fee_payer.pubkey(),
&collateral_mint_keypair.pubkey(),
collateral_mint_balance,
Mint::LEN as u64,
&spl_token::id(),
),
create_account(
&config.fee_payer.pubkey(),
&collateral_supply_keypair.pubkey(),
collateral_supply_balance,
Token::LEN as u64,
&spl_token::id(),
),
create_account(
&config.fee_payer.pubkey(),
&user_collateral_keypair.pubkey(),
user_collateral_balance,
Token::LEN as u64,
&spl_token::id(),
),
],
Some(&config.fee_payer.pubkey()),
);
let mut transaction_2 = Transaction::new_with_payer(
&[
create_account(
&config.fee_payer.pubkey(),
&liquidity_supply_keypair.pubkey(),
liquidity_supply_balance,
Token::LEN as u64,
&spl_token::id(),
),
create_account(
&config.fee_payer.pubkey(),
&liquidity_fee_receiver_keypair.pubkey(),
liquidity_fee_receiver_balance,
Token::LEN as u64,
&spl_token::id(),
),
],
Some(&config.fee_payer.pubkey()),
);
let mut transaction_3 = Transaction::new_with_payer(
&[
approve(
&spl_token::id(),
&source_liquidity_pubkey,
&user_transfer_authority_keypair.pubkey(),
&source_liquidity_owner_keypair.pubkey(),
&[],
liquidity_amount,
)
.unwrap(),
init_reserve(
config.lending_program_id,
liquidity_amount,
reserve_config,
source_liquidity_pubkey,
user_collateral_keypair.pubkey(),
reserve_keypair.pubkey(),
source_liquidity.mint,
liquidity_supply_keypair.pubkey(),
liquidity_fee_receiver_keypair.pubkey(),
collateral_mint_keypair.pubkey(),
collateral_supply_keypair.pubkey(),
pyth_product_pubkey,
pyth_price_pubkey,
lending_market_pubkey,
lending_market_owner_keypair.pubkey(),
user_transfer_authority_keypair.pubkey(),
),
revoke(
&spl_token::id(),
&source_liquidity_pubkey,
&source_liquidity_owner_keypair.pubkey(),
&[],
)
.unwrap(),
],
Some(&config.fee_payer.pubkey()),
);
let (recent_blockhash, fee_calculator) = config.rpc_client.get_recent_blockhash()?;
check_fee_payer_balance(
config,
total_balance
+ fee_calculator.calculate_fee(&transaction_1.message())
+ fee_calculator.calculate_fee(&transaction_2.message())
+ fee_calculator.calculate_fee(&transaction_3.message()),
)?;
transaction_1.sign(
&vec![
config.fee_payer.as_ref(),
&reserve_keypair,
&collateral_mint_keypair,
&collateral_supply_keypair,
&user_collateral_keypair,
],
recent_blockhash,
);
transaction_2.sign(
&vec![
config.fee_payer.as_ref(),
&liquidity_supply_keypair,
&liquidity_fee_receiver_keypair,
],
recent_blockhash,
);
transaction_3.sign(
&vec![
config.fee_payer.as_ref(),
&source_liquidity_owner_keypair,
&lending_market_owner_keypair,
&user_transfer_authority_keypair,
],
recent_blockhash,
);
send_transaction(&config, transaction_1)?;
send_transaction(&config, transaction_2)?;
send_transaction(&config, transaction_3)?;
Ok(())
}
// HELPERS
fn check_fee_payer_balance(config: &Config, required_balance: u64) -> Result<(), Error> {
let balance = config.rpc_client.get_balance(&config.fee_payer.pubkey())?;
if balance < required_balance {
Err(format!(
"Fee payer, {}, has insufficient balance: {} required, {} available",
config.fee_payer.pubkey(),
lamports_to_sol(required_balance),
lamports_to_sol(balance)
)
.into())
} else {
Ok(())
}
}
fn send_transaction(
config: &Config,
transaction: Transaction,
) -> solana_client::client_error::Result<()> {
if config.dry_run {
let result = config.rpc_client.simulate_transaction(&transaction)?;
println!("Simulate result: {:?}", result);
} else {
let signature = config
.rpc_client
.send_and_confirm_transaction_with_spinner(&transaction)?;
println!("Signature: {}", signature);
}
Ok(())
}
fn quote_currency_of(matches: &ArgMatches<'_>, name: &str) -> Option<[u8; 32]> {
if let Some(value) = matches.value_of(name) {
if value == "USD" {
Some(*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")
} else if value.len() <= 32 {
let mut bytes32 = [0u8; 32];
bytes32[0..value.len()].clone_from_slice(value.as_bytes());
Some(bytes32)
} else {
Some(Pubkey::from_str(value).unwrap().to_bytes())
}
} else {
None
}
}

View File

@ -13,4 +13,4 @@ pub mod state;
// Export current sdk types for downstream users building with a different sdk version
pub use solana_program;
solana_program::declare_id!("TokenLending1111111111111111111111111111111");
solana_program::declare_id!("6TvznH3B2e3p2mbhufNBpgSrLx6UkgvxtVQvopEZ2kuH");

View File

@ -1277,6 +1277,7 @@ fn process_repay_obligation_liquidity(
msg!("Repay reserve liquidity supply must be used as the destination liquidity provided");
return Err(LendingError::InvalidAccountInput.into());
}
// @FIXME: why is it necessary to refresh before repay?
if repay_reserve.last_update.is_stale(clock.slot)? {
msg!("Repay reserve is stale and must be refreshed in the current slot");
return Err(LendingError::ReserveStale.into());
@ -1291,6 +1292,7 @@ fn process_repay_obligation_liquidity(
msg!("Obligation lending market does not match the lending market provided");
return Err(LendingError::InvalidAccountInput.into());
}
// @FIXME: why is it necessary to refresh before repay?
if obligation.last_update.is_stale(clock.slot)? {
msg!("Obligation is stale and must be refreshed in the current slot");
return Err(LendingError::ObligationStale.into());
@ -1859,6 +1861,20 @@ fn spl_token_init_mint(params: TokenInitializeMintParams<'_, '_>) -> ProgramResu
result.map_err(|_| LendingError::TokenInitializeMintFailed.into())
}
/// Invoke signed unless signers seeds are empty
#[inline(always)]
fn invoke_optionally_signed(
instruction: &Instruction,
account_infos: &[AccountInfo],
authority_signer_seeds: &[&[u8]],
) -> ProgramResult {
if authority_signer_seeds.is_empty() {
invoke(instruction, account_infos)
} else {
invoke_signed(instruction, account_infos, &[authority_signer_seeds])
}
}
/// Issue a spl_token `Transfer` instruction.
#[inline(always)]
fn spl_token_transfer(params: TokenTransferParams<'_, '_>) -> ProgramResult {
@ -1870,7 +1886,7 @@ fn spl_token_transfer(params: TokenTransferParams<'_, '_>) -> ProgramResult {
amount,
authority_signer_seeds,
} = params;
let result = invoke_signed(
let result = invoke_optionally_signed(
&spl_token::instruction::transfer(
token_program.key,
source.key,
@ -1880,7 +1896,7 @@ fn spl_token_transfer(params: TokenTransferParams<'_, '_>) -> ProgramResult {
amount,
)?,
&[source, destination, authority, token_program],
&[authority_signer_seeds],
authority_signer_seeds,
);
result.map_err(|_| LendingError::TokenTransferFailed.into())
}
@ -1895,7 +1911,7 @@ fn spl_token_mint_to(params: TokenMintToParams<'_, '_>) -> ProgramResult {
amount,
authority_signer_seeds,
} = params;
let result = invoke_signed(
let result = invoke_optionally_signed(
&spl_token::instruction::mint_to(
token_program.key,
mint.key,
@ -1905,7 +1921,7 @@ fn spl_token_mint_to(params: TokenMintToParams<'_, '_>) -> ProgramResult {
amount,
)?,
&[mint, destination, authority, token_program],
&[authority_signer_seeds],
authority_signer_seeds,
);
result.map_err(|_| LendingError::TokenMintToFailed.into())
}
@ -1921,7 +1937,7 @@ fn spl_token_burn(params: TokenBurnParams<'_, '_>) -> ProgramResult {
amount,
authority_signer_seeds,
} = params;
let result = invoke_signed(
let result = invoke_optionally_signed(
&spl_token::instruction::burn(
token_program.key,
source.key,
@ -1931,7 +1947,7 @@ fn spl_token_burn(params: TokenBurnParams<'_, '_>) -> ProgramResult {
amount,
)?,
&[source, mint, authority, token_program],
&[authority_signer_seeds],
authority_signer_seeds,
);
result.map_err(|_| LendingError::TokenBurnFailed.into())
}