lending: Command Line Interface (#1676)
This commit is contained in:
parent
346075743e
commit
3591f80616
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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.
|
|
@ -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"
|
|
@ -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.
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue