Skip to content

Commit 9fe1ce6

Browse files
committed
add cli tool
1 parent 425cf43 commit 9fe1ce6

File tree

2 files changed

+338
-4
lines changed

2 files changed

+338
-4
lines changed

token-lending/cli/src/main.rs

Lines changed: 338 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use {
2424
},
2525
spl_token_lending::{
2626
self,
27-
instruction::{init_lending_market, init_reserve},
27+
instruction::{init_lending_market, init_reserve, update_reserve_config},
2828
math::WAD,
2929
state::{LendingMarket, Reserve, ReserveConfig, ReserveFees},
3030
},
@@ -40,6 +40,39 @@ struct Config {
4040
dry_run: bool,
4141
}
4242

43+
/// Reserve config with optional fields
44+
struct PartialReserveConfig {
45+
/// Optimal utilization rate, as a percentage
46+
pub optimal_utilization_rate: Option<u8>,
47+
/// Target ratio of the value of borrows to deposits, as a percentage
48+
/// 0 if use as collateral is disabled
49+
pub loan_to_value_ratio: Option<u8>,
50+
/// Bonus a liquidator gets when repaying part of an unhealthy obligation, as a percentage
51+
pub liquidation_bonus: Option<u8>,
52+
/// Loan to value ratio at which an obligation can be liquidated, as a percentage
53+
pub liquidation_threshold: Option<u8>,
54+
/// Min borrow APY
55+
pub min_borrow_rate: Option<u8>,
56+
/// Optimal (utilization) borrow APY
57+
pub optimal_borrow_rate: Option<u8>,
58+
/// Max borrow APY
59+
pub max_borrow_rate: Option<u8>,
60+
/// Program owner fees assessed, separate from gains due to interest accrual
61+
pub fees: PartialReserveFees,
62+
/// Deposit limit
63+
pub deposit_limit: Option<u64>,
64+
}
65+
66+
/// Reserve Fees with optional fields
67+
struct PartialReserveFees {
68+
pub borrow_fee_wad: Option<u64>,
69+
/// Fee for flash loan, expressed as a Wad.
70+
/// 0.3% (Aave flash loan fee) = 3_000_000_000_000_000
71+
pub flash_loan_fee_wad: Option<u64>,
72+
/// Amount of fee going to host account, if provided in liquidate and repay
73+
pub host_fee_percentage: Option<u8>,
74+
}
75+
4376
type Error = Box<dyn std::error::Error>;
4477
type CommandResult = Result<(), Error>;
4578

@@ -328,6 +361,137 @@ fn main() {
328361
.help("Deposit limit"),
329362
)
330363
)
364+
.subcommand(
365+
SubCommand::with_name("update-reserve")
366+
.about("Update a reserve config")
367+
.arg(
368+
Arg::with_name("reserve")
369+
.long("reserve")
370+
.validator(is_pubkey)
371+
.value_name("PUBKEY")
372+
.takes_value(true)
373+
.required(true)
374+
.help("Reserve address"),
375+
)
376+
.arg(
377+
Arg::with_name("lending_market_owner")
378+
.long("market-owner")
379+
.validator(is_keypair)
380+
.value_name("KEYPAIR")
381+
.takes_value(true)
382+
.required(true)
383+
.help("Owner of the lending market"),
384+
)
385+
// @TODO: use is_valid_signer
386+
.arg(
387+
Arg::with_name("lending_market")
388+
.long("market")
389+
.validator(is_pubkey)
390+
.value_name("PUBKEY")
391+
.takes_value(true)
392+
.required(true)
393+
.help("Lending market address"),
394+
)
395+
.arg(
396+
Arg::with_name("optimal_utilization_rate")
397+
.long("optimal-utilization-rate")
398+
.validator(is_parsable::<u8>)
399+
.value_name("INTEGER_PERCENT")
400+
.takes_value(true)
401+
.required(false)
402+
.help("Optimal utilization rate: [0, 100]"),
403+
)
404+
.arg(
405+
Arg::with_name("loan_to_value_ratio")
406+
.long("loan-to-value-ratio")
407+
.validator(is_parsable::<u8>)
408+
.value_name("INTEGER_PERCENT")
409+
.takes_value(true)
410+
.required(false)
411+
.help("Target ratio of the value of borrows to deposits: [0, 100)"),
412+
)
413+
.arg(
414+
Arg::with_name("liquidation_bonus")
415+
.long("liquidation-bonus")
416+
.validator(is_parsable::<u8>)
417+
.value_name("INTEGER_PERCENT")
418+
.takes_value(true)
419+
.required(false)
420+
.help("Bonus a liquidator gets when repaying part of an unhealthy obligation: [0, 100]"),
421+
)
422+
.arg(
423+
Arg::with_name("liquidation_threshold")
424+
.long("liquidation-threshold")
425+
.validator(is_parsable::<u8>)
426+
.value_name("INTEGER_PERCENT")
427+
.takes_value(true)
428+
.required(false)
429+
.help("Loan to value ratio at which an obligation can be liquidated: (LTV, 100]"),
430+
)
431+
.arg(
432+
Arg::with_name("min_borrow_rate")
433+
.long("min-borrow-rate")
434+
.validator(is_parsable::<u8>)
435+
.value_name("INTEGER_PERCENT")
436+
.takes_value(true)
437+
.required(false)
438+
.help("Min borrow APY: min <= optimal <= max"),
439+
)
440+
.arg(
441+
Arg::with_name("optimal_borrow_rate")
442+
.long("optimal-borrow-rate")
443+
.validator(is_parsable::<u8>)
444+
.value_name("INTEGER_PERCENT")
445+
.takes_value(true)
446+
.required(false)
447+
.help("Optimal (utilization) borrow APY: min <= optimal <= max"),
448+
)
449+
.arg(
450+
Arg::with_name("max_borrow_rate")
451+
.long("max-borrow-rate")
452+
.validator(is_parsable::<u8>)
453+
.value_name("INTEGER_PERCENT")
454+
.takes_value(true)
455+
.required(false)
456+
.help("Max borrow APY: min <= optimal <= max"),
457+
)
458+
.arg(
459+
Arg::with_name("borrow_fee")
460+
.long("borrow-fee")
461+
.validator(is_parsable::<f64>)
462+
.value_name("DECIMAL_PERCENT")
463+
.takes_value(true)
464+
.required(false)
465+
.help("Fee assessed on borrow, expressed as a percentage: [0, 1)"),
466+
)
467+
.arg(
468+
Arg::with_name("flash_loan_fee")
469+
.long("flash-loan-fee")
470+
.validator(is_parsable::<f64>)
471+
.value_name("DECIMAL_PERCENT")
472+
.takes_value(true)
473+
.required(false)
474+
.help("Fee assessed for flash loans, expressed as a percentage: [0, 1)"),
475+
)
476+
.arg(
477+
Arg::with_name("host_fee_percentage")
478+
.long("host-fee-percentage")
479+
.validator(is_parsable::<u8>)
480+
.value_name("INTEGER_PERCENT")
481+
.takes_value(true)
482+
.required(false)
483+
.help("Amount of fee going to host account: [0, 100]"),
484+
)
485+
.arg(
486+
Arg::with_name("deposit_limit")
487+
.long("deposit-limit")
488+
.validator(is_parsable::<u64>)
489+
.value_name("INTEGER")
490+
.takes_value(true)
491+
.required(false)
492+
.help("Deposit Limit"),
493+
)
494+
)
331495
.get_matches();
332496

333497
let mut wallet_manager = None;
@@ -432,6 +596,48 @@ fn main() {
432596
switchboard_feed_pubkey,
433597
)
434598
}
599+
("update-reserve", Some(arg_matches)) => {
600+
let reserve_pubkey = pubkey_of(arg_matches, "reserve").unwrap();
601+
let lending_market_owner_keypair =
602+
keypair_of(arg_matches, "lending_market_owner").unwrap();
603+
let lending_market_pubkey = pubkey_of(arg_matches, "lending_market").unwrap();
604+
let optimal_utilization_rate = value_of(arg_matches, "optimal_utilization_rate");
605+
let loan_to_value_ratio = value_of(arg_matches, "loan_to_value_ratio");
606+
let liquidation_bonus = value_of(arg_matches, "liquidation_bonus");
607+
let liquidation_threshold = value_of(arg_matches, "liquidation_threshold");
608+
let min_borrow_rate = value_of(arg_matches, "min_borrow_rate");
609+
let optimal_borrow_rate = value_of(arg_matches, "optimal_borrow_rate");
610+
let max_borrow_rate = value_of(arg_matches, "max_borrow_rate");
611+
let borrow_fee = value_of::<f64>(arg_matches, "borrow_fee");
612+
let flash_loan_fee = value_of::<f64>(arg_matches, "flash_loan_fee");
613+
let host_fee_percentage = value_of(arg_matches, "host_fee_percentage");
614+
let deposit_limit = value_of(arg_matches, "deposit_limit");
615+
616+
let borrow_fee_wad = borrow_fee.map(|fee| (fee * WAD as f64) as u64);
617+
let flash_loan_fee_wad = flash_loan_fee.map(|fee| (fee * WAD as f64) as u64);
618+
619+
command_update_reserve(
620+
&mut config,
621+
PartialReserveConfig {
622+
optimal_utilization_rate,
623+
loan_to_value_ratio,
624+
liquidation_bonus,
625+
liquidation_threshold,
626+
min_borrow_rate,
627+
optimal_borrow_rate,
628+
max_borrow_rate,
629+
fees: PartialReserveFees {
630+
borrow_fee_wad,
631+
flash_loan_fee_wad,
632+
host_fee_percentage,
633+
},
634+
deposit_limit,
635+
},
636+
reserve_pubkey,
637+
lending_market_pubkey,
638+
lending_market_owner_keypair,
639+
)
640+
}
435641
_ => unreachable!(),
436642
}
437643
.map_err(|err| {
@@ -707,6 +913,137 @@ fn command_add_reserve(
707913
Ok(())
708914
}
709915

916+
#[allow(clippy::too_many_arguments)]
917+
fn command_update_reserve(
918+
config: &mut Config,
919+
reserve_config: PartialReserveConfig,
920+
reserve_pubkey: Pubkey,
921+
lending_market_pubkey: Pubkey,
922+
lending_market_owner_keypair: Keypair,
923+
) -> CommandResult {
924+
let reserve_info = config.rpc_client.get_account(&reserve_pubkey)?;
925+
let mut reserve = Reserve::unpack_from_slice(reserve_info.data.borrow())?;
926+
if reserve_config.optimal_utilization_rate.is_some() {
927+
println!(
928+
"Updating optimal_utilization_rate from {} to {}",
929+
reserve_config.optimal_utilization_rate.unwrap(),
930+
reserve.config.optimal_utilization_rate,
931+
);
932+
reserve.config.optimal_utilization_rate = reserve_config.optimal_utilization_rate.unwrap();
933+
}
934+
935+
if reserve_config.loan_to_value_ratio.is_some() {
936+
println!(
937+
"Updating loan_to_value_ratio from {} to {}",
938+
reserve_config.loan_to_value_ratio.unwrap(),
939+
reserve.config.loan_to_value_ratio,
940+
);
941+
reserve.config.loan_to_value_ratio = reserve_config.loan_to_value_ratio.unwrap();
942+
}
943+
944+
if reserve_config.liquidation_bonus.is_some() {
945+
println!(
946+
"Updating liquidation_bonus from {} to {}",
947+
reserve_config.liquidation_bonus.unwrap(),
948+
reserve.config.liquidation_bonus,
949+
);
950+
reserve.config.liquidation_bonus = reserve_config.liquidation_bonus.unwrap();
951+
}
952+
953+
if reserve_config.liquidation_threshold.is_some() {
954+
println!(
955+
"Updating liquidation_threshold from {} to {}",
956+
reserve_config.liquidation_threshold.unwrap(),
957+
reserve.config.liquidation_threshold,
958+
);
959+
reserve.config.liquidation_threshold = reserve_config.liquidation_threshold.unwrap();
960+
}
961+
962+
if reserve_config.min_borrow_rate.is_some() {
963+
println!(
964+
"Updating min_borrow_rate from {} to {}",
965+
reserve_config.min_borrow_rate.unwrap(),
966+
reserve.config.min_borrow_rate,
967+
);
968+
reserve.config.min_borrow_rate = reserve_config.min_borrow_rate.unwrap();
969+
}
970+
971+
if reserve_config.optimal_borrow_rate.is_some() {
972+
println!(
973+
"Updating optimal_borrow_rate from {} to {}",
974+
reserve_config.optimal_borrow_rate.unwrap(),
975+
reserve.config.optimal_borrow_rate,
976+
);
977+
reserve.config.optimal_borrow_rate = reserve_config.optimal_borrow_rate.unwrap();
978+
}
979+
980+
if reserve_config.max_borrow_rate.is_some() {
981+
println!(
982+
"Updating max_borrow_rate from {} to {}",
983+
reserve_config.max_borrow_rate.unwrap(),
984+
reserve.config.max_borrow_rate,
985+
);
986+
reserve.config.max_borrow_rate = reserve_config.max_borrow_rate.unwrap();
987+
}
988+
989+
if reserve_config.fees.borrow_fee_wad.is_some() {
990+
println!(
991+
"Updating borrow_fee_wad from {} to {}",
992+
reserve_config.fees.borrow_fee_wad.unwrap(),
993+
reserve.config.fees.borrow_fee_wad,
994+
);
995+
reserve.config.fees.borrow_fee_wad = reserve_config.fees.borrow_fee_wad.unwrap();
996+
}
997+
998+
if reserve_config.fees.flash_loan_fee_wad.is_some() {
999+
println!(
1000+
"Updating flash_loan_fee_wad from {} to {}",
1001+
reserve_config.fees.flash_loan_fee_wad.unwrap(),
1002+
reserve.config.fees.flash_loan_fee_wad,
1003+
);
1004+
reserve.config.fees.flash_loan_fee_wad = reserve_config.fees.flash_loan_fee_wad.unwrap();
1005+
}
1006+
1007+
if reserve_config.fees.host_fee_percentage.is_some() {
1008+
println!(
1009+
"Updating host_fee_percentage from {} to {}",
1010+
reserve_config.fees.host_fee_percentage.unwrap(),
1011+
reserve.config.fees.host_fee_percentage,
1012+
);
1013+
reserve.config.fees.host_fee_percentage = reserve_config.fees.host_fee_percentage.unwrap();
1014+
}
1015+
1016+
if reserve_config.deposit_limit.is_some() {
1017+
println!(
1018+
"Updating deposit_limit from {} to {}",
1019+
reserve_config.deposit_limit.unwrap(),
1020+
reserve.config.deposit_limit,
1021+
);
1022+
reserve.config.deposit_limit = reserve_config.deposit_limit.unwrap();
1023+
}
1024+
1025+
let mut transaction = Transaction::new_with_payer(
1026+
&[update_reserve_config(
1027+
config.lending_program_id,
1028+
reserve.config,
1029+
reserve_pubkey,
1030+
lending_market_pubkey,
1031+
lending_market_owner_keypair.pubkey(),
1032+
)],
1033+
Some(&config.fee_payer.pubkey()),
1034+
);
1035+
1036+
let (recent_blockhash, fee_calculator) = config.rpc_client.get_recent_blockhash()?;
1037+
check_fee_payer_balance(config, fee_calculator.calculate_fee(transaction.message()))?;
1038+
1039+
transaction.sign(
1040+
&vec![config.fee_payer.as_ref(), &lending_market_owner_keypair],
1041+
recent_blockhash,
1042+
);
1043+
send_transaction(config, transaction)?;
1044+
Ok(())
1045+
}
1046+
7101047
// HELPERS
7111048

7121049
fn check_fee_payer_balance(config: &Config, required_balance: u64) -> Result<(), Error> {

token-lending/program/src/processor.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,13 +195,10 @@ fn process_init_reserve(
195195
config: ReserveConfig,
196196
accounts: &[AccountInfo],
197197
) -> ProgramResult {
198-
<<<<<<< HEAD
199198
if liquidity_amount == 0 {
200199
msg!("Reserve must be initialized with liquidity");
201200
return Err(LendingError::InvalidAmount.into());
202201
}
203-
=======
204-
>>>>>>> Add instruction to update the LTV of a reserve
205202
validate_reserve_config(config)?;
206203
let account_info_iter = &mut accounts.iter().peekable();
207204
let source_liquidity_info = next_account_info(account_info_iter)?;

0 commit comments

Comments
 (0)