Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 5d8cb55

Browse files
lending: Add instruction for changing reserve configuration after initialization (#3349)
* lending: Add instruction for configuring reserve params * lending: (fix) remove duplicate import in modify_reserve_config test * modify_reserve_config: refactor pack and unpack of ReserveConfig into helper func; refactor validate_reserve_config as method; refine tests
1 parent 271d235 commit 5d8cb55

File tree

5 files changed

+612
-90
lines changed

5 files changed

+612
-90
lines changed

token-lending/program/src/instruction.rs

Lines changed: 123 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,19 @@ pub enum LendingInstruction {
308308
/// The amount that is to be borrowed - u64::MAX for up to 100% of available liquidity
309309
amount: u64,
310310
},
311+
312+
// 14
313+
/// Modify the ReserveConfig parameters of an already initialized Reserve account
314+
///
315+
/// Accounts expected by this instruction:
316+
///
317+
/// 0. `[writable]` Reserve account
318+
/// 1. `[]` Lending market account
319+
/// 2. `[signer]` Lending market owner
320+
ModifyReserveConfig {
321+
/// Reserve configuration updated values
322+
new_config: ReserveConfig,
323+
},
311324
}
312325

313326
impl LendingInstruction {
@@ -331,32 +344,10 @@ impl LendingInstruction {
331344
}
332345
2 => {
333346
let (liquidity_amount, rest) = Self::unpack_u64(rest)?;
334-
let (optimal_utilization_rate, rest) = Self::unpack_u8(rest)?;
335-
let (loan_to_value_ratio, rest) = Self::unpack_u8(rest)?;
336-
let (liquidation_bonus, rest) = Self::unpack_u8(rest)?;
337-
let (liquidation_threshold, rest) = Self::unpack_u8(rest)?;
338-
let (min_borrow_rate, rest) = Self::unpack_u8(rest)?;
339-
let (optimal_borrow_rate, rest) = Self::unpack_u8(rest)?;
340-
let (max_borrow_rate, rest) = Self::unpack_u8(rest)?;
341-
let (borrow_fee_wad, rest) = Self::unpack_u64(rest)?;
342-
let (flash_loan_fee_wad, rest) = Self::unpack_u64(rest)?;
343-
let (host_fee_percentage, _rest) = Self::unpack_u8(rest)?;
347+
let config = Self::unpack_reserve_config(rest)?;
344348
Self::InitReserve {
345349
liquidity_amount,
346-
config: ReserveConfig {
347-
optimal_utilization_rate,
348-
loan_to_value_ratio,
349-
liquidation_bonus,
350-
liquidation_threshold,
351-
min_borrow_rate,
352-
optimal_borrow_rate,
353-
max_borrow_rate,
354-
fees: ReserveFees {
355-
borrow_fee_wad,
356-
flash_loan_fee_wad,
357-
host_fee_percentage,
358-
},
359-
},
350+
config,
360351
}
361352
}
362353
3 => Self::RefreshReserve,
@@ -394,6 +385,10 @@ impl LendingInstruction {
394385
let (amount, _rest) = Self::unpack_u64(rest)?;
395386
Self::FlashLoan { amount }
396387
}
388+
14 => {
389+
let new_config = Self::unpack_reserve_config(rest)?;
390+
Self::ModifyReserveConfig { new_config }
391+
}
397392
_ => {
398393
msg!("Instruction cannot be unpacked");
399394
return Err(LendingError::InstructionUnpackError.into());
@@ -453,6 +448,34 @@ impl LendingInstruction {
453448
Ok((pk, rest))
454449
}
455450

451+
fn unpack_reserve_config(input: &[u8]) -> Result<ReserveConfig, ProgramError> {
452+
let (optimal_utilization_rate, rest) = Self::unpack_u8(input)?;
453+
let (loan_to_value_ratio, rest) = Self::unpack_u8(rest)?;
454+
let (liquidation_bonus, rest) = Self::unpack_u8(rest)?;
455+
let (liquidation_threshold, rest) = Self::unpack_u8(rest)?;
456+
let (min_borrow_rate, rest) = Self::unpack_u8(rest)?;
457+
let (optimal_borrow_rate, rest) = Self::unpack_u8(rest)?;
458+
let (max_borrow_rate, rest) = Self::unpack_u8(rest)?;
459+
let (borrow_fee_wad, rest) = Self::unpack_u64(rest)?;
460+
let (flash_loan_fee_wad, rest) = Self::unpack_u64(rest)?;
461+
let (host_fee_percentage, _rest) = Self::unpack_u8(rest)?;
462+
463+
Ok(ReserveConfig {
464+
optimal_utilization_rate,
465+
loan_to_value_ratio,
466+
liquidation_bonus,
467+
liquidation_threshold,
468+
min_borrow_rate,
469+
optimal_borrow_rate,
470+
max_borrow_rate,
471+
fees: ReserveFees {
472+
borrow_fee_wad,
473+
flash_loan_fee_wad,
474+
host_fee_percentage,
475+
},
476+
})
477+
}
478+
456479
/// Packs a [LendingInstruction](enum.LendingInstruction.html) into a byte buffer.
457480
pub fn pack(&self) -> Vec<u8> {
458481
let mut buf = Vec::with_capacity(size_of::<Self>());
@@ -471,35 +494,11 @@ impl LendingInstruction {
471494
}
472495
Self::InitReserve {
473496
liquidity_amount,
474-
config:
475-
ReserveConfig {
476-
optimal_utilization_rate,
477-
loan_to_value_ratio,
478-
liquidation_bonus,
479-
liquidation_threshold,
480-
min_borrow_rate,
481-
optimal_borrow_rate,
482-
max_borrow_rate,
483-
fees:
484-
ReserveFees {
485-
borrow_fee_wad,
486-
flash_loan_fee_wad,
487-
host_fee_percentage,
488-
},
489-
},
497+
config,
490498
} => {
491499
buf.push(2);
492500
buf.extend_from_slice(&liquidity_amount.to_le_bytes());
493-
buf.extend_from_slice(&optimal_utilization_rate.to_le_bytes());
494-
buf.extend_from_slice(&loan_to_value_ratio.to_le_bytes());
495-
buf.extend_from_slice(&liquidation_bonus.to_le_bytes());
496-
buf.extend_from_slice(&liquidation_threshold.to_le_bytes());
497-
buf.extend_from_slice(&min_borrow_rate.to_le_bytes());
498-
buf.extend_from_slice(&optimal_borrow_rate.to_le_bytes());
499-
buf.extend_from_slice(&max_borrow_rate.to_le_bytes());
500-
buf.extend_from_slice(&borrow_fee_wad.to_le_bytes());
501-
buf.extend_from_slice(&flash_loan_fee_wad.to_le_bytes());
502-
buf.extend_from_slice(&host_fee_percentage.to_le_bytes());
501+
Self::extend_buffer_from_reserve_config(&mut buf, &config);
503502
}
504503
Self::RefreshReserve => {
505504
buf.push(3);
@@ -542,9 +541,27 @@ impl LendingInstruction {
542541
buf.push(13);
543542
buf.extend_from_slice(&amount.to_le_bytes());
544543
}
544+
Self::ModifyReserveConfig { new_config } => {
545+
buf.push(14);
546+
Self::extend_buffer_from_reserve_config(&mut buf, &new_config);
547+
}
545548
}
546549
buf
547550
}
551+
552+
// Helper function to pack a ReserveConfig into a Vec<u8> buffer
553+
fn extend_buffer_from_reserve_config(buf: &mut Vec<u8>, config: &ReserveConfig) {
554+
buf.extend_from_slice(&config.optimal_utilization_rate.to_le_bytes());
555+
buf.extend_from_slice(&config.loan_to_value_ratio.to_le_bytes());
556+
buf.extend_from_slice(&config.liquidation_bonus.to_le_bytes());
557+
buf.extend_from_slice(&config.liquidation_threshold.to_le_bytes());
558+
buf.extend_from_slice(&config.min_borrow_rate.to_le_bytes());
559+
buf.extend_from_slice(&config.optimal_borrow_rate.to_le_bytes());
560+
buf.extend_from_slice(&config.max_borrow_rate.to_le_bytes());
561+
buf.extend_from_slice(&config.fees.borrow_fee_wad.to_le_bytes());
562+
buf.extend_from_slice(&config.fees.flash_loan_fee_wad.to_le_bytes());
563+
buf.extend_from_slice(&config.fees.host_fee_percentage.to_le_bytes());
564+
}
548565
}
549566

550567
/// Creates an 'InitLendingMarket' instruction.
@@ -982,6 +999,27 @@ pub fn flash_loan(
982999
}
9831000
}
9841001

1002+
/// Creates a 'ModifyReserveConfig` instruction.
1003+
#[allow(clippy::too_many_arguments)]
1004+
pub fn modify_reserve_config(
1005+
program_id: Pubkey,
1006+
config: ReserveConfig,
1007+
reserve_pubkey: Pubkey,
1008+
lending_market_pubkey: Pubkey,
1009+
lending_market_owner_pubkey: Pubkey,
1010+
) -> Instruction {
1011+
let accounts = vec![
1012+
AccountMeta::new(reserve_pubkey, false),
1013+
AccountMeta::new(lending_market_pubkey, false),
1014+
AccountMeta::new(lending_market_owner_pubkey, true),
1015+
];
1016+
Instruction {
1017+
program_id,
1018+
accounts,
1019+
data: LendingInstruction::ModifyReserveConfig { new_config: config }.pack(),
1020+
}
1021+
}
1022+
9851023
#[cfg(test)]
9861024
mod tests {
9871025
use super::*;
@@ -1386,4 +1424,39 @@ mod tests {
13861424
LendingInstruction::FlashLoan { amount }.pack()
13871425
);
13881426
}
1427+
1428+
#[test]
1429+
fn test_modify_reserve_config() {
1430+
let program_id = Pubkey::new_unique();
1431+
let config = ReserveConfig {
1432+
optimal_utilization_rate: 60,
1433+
loan_to_value_ratio: 1,
1434+
liquidation_bonus: 10,
1435+
liquidation_threshold: 5,
1436+
min_borrow_rate: 2,
1437+
optimal_borrow_rate: 4,
1438+
max_borrow_rate: 10,
1439+
fees: ReserveFees {
1440+
borrow_fee_wad: 1,
1441+
flash_loan_fee_wad: 3,
1442+
host_fee_percentage: 1,
1443+
},
1444+
};
1445+
let reserve_pubkey = Pubkey::new_unique();
1446+
let lending_market_pubkey = Pubkey::new_unique();
1447+
let lending_market_owner_pubkey = Pubkey::new_unique();
1448+
let instruction = modify_reserve_config(
1449+
program_id,
1450+
config,
1451+
reserve_pubkey,
1452+
lending_market_pubkey,
1453+
lending_market_owner_pubkey,
1454+
);
1455+
assert_eq!(instruction.program_id, program_id);
1456+
assert_eq!(instruction.accounts.len(), 3);
1457+
assert_eq!(
1458+
instruction.data,
1459+
LendingInstruction::ModifyReserveConfig { new_config: config }.pack()
1460+
);
1461+
}
13891462
}

token-lending/program/src/processor.rs

Lines changed: 54 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use crate::{
44
error::LendingError,
55
instruction::LendingInstruction,
6-
math::{Decimal, Rate, TryAdd, TryDiv, TryMul, WAD},
6+
math::{Decimal, Rate, TryAdd, TryDiv, TryMul},
77
pyth,
88
state::{
99
CalculateBorrowResult, CalculateLiquidationResult, CalculateRepayResult,
@@ -99,6 +99,10 @@ pub fn process_instruction(
9999
msg!("Instruction: Flash Loan");
100100
process_flash_loan(program_id, amount, accounts)
101101
}
102+
LendingInstruction::ModifyReserveConfig { new_config } => {
103+
msg!("Instruction: Modify Reserve Config");
104+
process_modify_reserve_config(program_id, new_config, accounts)
105+
}
102106
}
103107
}
104108

@@ -173,44 +177,8 @@ fn process_init_reserve(
173177
msg!("Reserve must be initialized with liquidity");
174178
return Err(LendingError::InvalidAmount.into());
175179
}
176-
if config.optimal_utilization_rate > 100 {
177-
msg!("Optimal utilization rate must be in range [0, 100]");
178-
return Err(LendingError::InvalidConfig.into());
179-
}
180-
if config.loan_to_value_ratio >= 100 {
181-
msg!("Loan to value ratio must be in range [0, 100)");
182-
return Err(LendingError::InvalidConfig.into());
183-
}
184-
if config.liquidation_bonus > 100 {
185-
msg!("Liquidation bonus must be in range [0, 100]");
186-
return Err(LendingError::InvalidConfig.into());
187-
}
188-
if config.liquidation_threshold <= config.loan_to_value_ratio
189-
|| config.liquidation_threshold > 100
190-
{
191-
msg!("Liquidation threshold must be in range (LTV, 100]");
192-
return Err(LendingError::InvalidConfig.into());
193-
}
194-
if config.optimal_borrow_rate < config.min_borrow_rate {
195-
msg!("Optimal borrow rate must be >= min borrow rate");
196-
return Err(LendingError::InvalidConfig.into());
197-
}
198-
if config.optimal_borrow_rate > config.max_borrow_rate {
199-
msg!("Optimal borrow rate must be <= max borrow rate");
200-
return Err(LendingError::InvalidConfig.into());
201-
}
202-
if config.fees.borrow_fee_wad >= WAD {
203-
msg!("Borrow fee must be in range [0, 1_000_000_000_000_000_000)");
204-
return Err(LendingError::InvalidConfig.into());
205-
}
206-
if config.fees.flash_loan_fee_wad >= WAD {
207-
msg!("Flash loan fee must be in range [0, 1_000_000_000_000_000_000)");
208-
return Err(LendingError::InvalidConfig.into());
209-
}
210-
if config.fees.host_fee_percentage > 100 {
211-
msg!("Host fee percentage must be in range [0, 100]");
212-
return Err(LendingError::InvalidConfig.into());
213-
}
180+
181+
config.validate()?;
214182

215183
let account_info_iter = &mut accounts.iter().peekable();
216184
let source_liquidity_info = next_account_info(account_info_iter)?;
@@ -1698,6 +1666,53 @@ fn process_flash_loan(
16981666
Ok(())
16991667
}
17001668

1669+
fn process_modify_reserve_config(
1670+
program_id: &Pubkey,
1671+
new_config: ReserveConfig,
1672+
accounts: &[AccountInfo],
1673+
) -> ProgramResult {
1674+
new_config.validate()?;
1675+
1676+
let account_info_iter = &mut accounts.iter().peekable();
1677+
let reserve_info = next_account_info(account_info_iter)?;
1678+
let lending_market_info = next_account_info(account_info_iter)?;
1679+
let lending_market_owner_info = next_account_info(account_info_iter)?;
1680+
1681+
if reserve_info.owner != program_id {
1682+
msg!("Reserve provided is not owned by the lending program");
1683+
return Err(LendingError::InvalidAccountOwner.into());
1684+
}
1685+
1686+
let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?;
1687+
if lending_market_info.owner != program_id {
1688+
msg!("Lending market provided is not owned by the lending program");
1689+
return Err(LendingError::InvalidAccountOwner.into());
1690+
}
1691+
if &lending_market.owner != lending_market_owner_info.key {
1692+
msg!("Lending market owner does not match the lending market owner provided");
1693+
return Err(LendingError::InvalidMarketOwner.into());
1694+
}
1695+
if !lending_market_owner_info.is_signer {
1696+
msg!("Lending market owner provided must be a signer");
1697+
return Err(LendingError::InvalidSigner.into());
1698+
}
1699+
1700+
let mut reserve = Reserve::unpack(&reserve_info.data.borrow_mut())?;
1701+
// Validate that the reserve account corresponds to the correct lending market,
1702+
// after validating above that the lending market and lending market owner correspond,
1703+
// to prevent one compromised lending market owner from changing configs on other lending markets
1704+
if reserve.lending_market != *lending_market_info.key {
1705+
msg!("Reserve account does not match the lending market");
1706+
return Err(LendingError::InvalidAccountInput.into());
1707+
}
1708+
1709+
reserve.config = new_config;
1710+
1711+
Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?;
1712+
1713+
Ok(())
1714+
}
1715+
17011716
fn assert_rent_exempt(rent: &Rent, account_info: &AccountInfo) -> ProgramResult {
17021717
if !rent.is_exempt(account_info.lamports(), account_info.data_len()) {
17031718
msg!(&rent.minimum_balance(account_info.data_len()).to_string());

0 commit comments

Comments
 (0)