diff --git a/src/interfaces.cairo b/src/interfaces.cairo index 1e18d97..5696a43 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -120,6 +120,10 @@ trait IMarket { fn set_borrow_factor(ref self: TContractState, token: ContractAddress, borrow_factor: felt252); + fn set_reserve_factor( + ref self: TContractState, token: ContractAddress, reserve_factor: felt252 + ); + fn set_debt_limit(ref self: TContractState, token: ContractAddress, limit: felt252); fn transfer_ownership(ref self: TContractState, new_owner: ContractAddress); diff --git a/src/market.cairo b/src/market.cairo index 84ac3a3..70e936c 100644 --- a/src/market.cairo +++ b/src/market.cairo @@ -50,6 +50,7 @@ mod Market { InterestRateModelUpdate: InterestRateModelUpdate, CollateralFactorUpdate: CollateralFactorUpdate, BorrowFactorUpdate: BorrowFactorUpdate, + ReserveFactorUpdate: ReserveFactorUpdate, DebtLimitUpdate: DebtLimitUpdate, Deposit: Deposit, Withdrawal: Withdrawal, @@ -113,6 +114,12 @@ mod Market { borrow_factor: felt252 } + #[derive(Drop, PartialEq, starknet::Event)] + struct ReserveFactorUpdate { + token: ContractAddress, + reserve_factor: felt252 + } + #[derive(Drop, PartialEq, starknet::Event)] struct DebtLimitUpdate { token: ContractAddress, @@ -362,6 +369,12 @@ mod Market { external::set_borrow_factor(ref self, token, borrow_factor) } + fn set_reserve_factor( + ref self: ContractState, token: ContractAddress, reserve_factor: felt252 + ) { + external::set_reserve_factor(ref self, token, reserve_factor) + } + fn set_debt_limit(ref self: ContractState, token: ContractAddress, limit: felt252) { external::set_debt_limit(ref self, token, limit) } diff --git a/src/market/external.cairo b/src/market/external.cairo index 0fa90a1..cbb3023 100644 --- a/src/market/external.cairo +++ b/src/market/external.cairo @@ -327,6 +327,41 @@ fn set_borrow_factor(ref self: ContractState, token: ContractAddress, borrow_fac ); } +fn set_reserve_factor(ref self: ContractState, token: ContractAddress, reserve_factor: felt252) { + ownable::assert_only_owner(@self); + + // Checks reserve_factor range + assert( + Into::<_, u256>::into(reserve_factor) <= safe_decimal_math::SCALE_U256, + errors::RESERVE_FACTOR_RANGE + ); + + internal::assert_reserve_exists(@self, token); + + // Settles interest payments up until this point to prevent retrospective changes. + let UpdatedAccumulators{debt_accumulator: updated_debt_accumulator, .. } = + internal::update_accumulators( + ref self, token + ); + internal::update_rates_and_raw_total_debt( + ref self, + token, // token + updated_debt_accumulator, // updated_debt_accumulator + false, // is_delta_reserve_balance_negative + 0, // abs_delta_reserve_balance + false, // is_delta_raw_total_debt_negative + 0 // abs_delta_raw_total_debt + ); + + self.reserves.write_reserve_factor(token, reserve_factor); + self + .emit( + contract::Event::ReserveFactorUpdate( + contract::ReserveFactorUpdate { token, reserve_factor } + ) + ); +} + fn set_debt_limit(ref self: ContractState, token: ContractAddress, limit: felt252) { ownable::assert_only_owner(@self); diff --git a/src/market/storage.cairo b/src/market/storage.cairo index b25a2ff..20a96c2 100644 --- a/src/market/storage.cairo +++ b/src/market/storage.cairo @@ -110,6 +110,8 @@ trait ReservesStorageShortcuts { fn write_borrow_factor(self: @T, token: ContractAddress, borrow_factor: felt252); + fn write_reserve_factor(self: @T, token: ContractAddress, reserve_factor: felt252); + fn write_debt_limit(self: @T, token: ContractAddress, debt_limit: felt252); fn write_accumulators( @@ -304,6 +306,12 @@ impl ReservesStorageShortcutsImpl of ReservesStorageShortcuts { Store::::write_at_offset(D, base, 5, borrow_factor).expect(E); } + fn write_reserve_factor(self: @Reserves, token: ContractAddress, reserve_factor: felt252) { + let base = self.address(token); + + Store::::write_at_offset(D, base, 6, reserve_factor).expect(E); + } + fn write_debt_limit(self: @Reserves, token: ContractAddress, debt_limit: felt252) { let base = self.address(token); diff --git a/tests/market.cairo b/tests/market.cairo index deef20b..ec9c7db 100644 --- a/tests/market.cairo +++ b/tests/market.cairo @@ -1517,6 +1517,73 @@ fn test_change_borrow_factor() { ); } +#[test] +#[available_gas(90000000)] +fn test_change_reserve_factor() { + let setup = setup_with_loan(); + + // (Copied from `test_rates_changed_on_borrow`) + // Borrowing rate: + // Utilization rate = 22.5 / 10,000 = 0.00225 + // Borrowing rate = 0.05 + 0.2 * 0.00225 / 0.8 = 0.0505625 => 505625 * 10 ** 20 + + starknet::testing::set_block_timestamp(100); + + // Reserve balance is not updated without an actual settlement. We do a noop IRM change here to + // just for triggering the settlement. + setup + .alice + .market_set_interest_rate_model( + setup.market.contract_address, + setup.token_b.contract_address, // token + setup.irm_b.contract_address // interest_rate_model + ); + + // Total interest after 100 seconds: + // Interest = 0.0505625 * 22.5 * 100 / (365 * 86400) = 0.000003607484303652 + // Reserve interest: + // Interest = 0.000003607484303652 * 20% = 0.000000721496860730 + assert_approximatedly_equals( + setup.z_token_b.balanceOf(MOCK_TREASURY_ADDRESS.try_into().unwrap()), 721496860730, 1 + ); + + // Doubles reserve ratio to 40% + setup + .alice + .market_set_reserve_factor( + setup.market.contract_address, + setup.token_b.contract_address, // token + 400000000000000000000000000, // reserve_factor + ); + + // Trigger another settlement after 100 seconds + starknet::testing::set_block_timestamp(200); + setup + .alice + .market_set_interest_rate_model( + setup.market.contract_address, + setup.token_b.contract_address, // token + setup.irm_b.contract_address // interest_rate_model + ); + + // Borrowing rate: + // Utilization rate = 22.500003607484303652 / 10,000.000003607484303652 = 0.002250000359936746267031683 + // Borrowing rate = 0.05 + 0.2 * 0.002250000359936746267031683 / 0.8 = 0.050562500089984186566757920 + // New lending rate: + // Lending rate = 0.050562500089984186566757920 * 0.002250000359936746267031683 = 0.000113765643401766185290610 + // Total interest after 100 seconds: + // Interest = 0.050562500089984186566757920 * 22.500003607484303652 * 100 / (365 * 86400) = 0.000003607484888470 + // Reserve interest: + // Interest = 0.000003607484888470 * 40% = 0.000001442993955388 + // Interest on previous reserve balance: + // Interest = 0.000113765643401766185290610 * 0.000000721496860730 * 100 / (365 * 86400) * (1 - 40%) = 0.000000000000000156 + // New balance: + // Balance = 0.000000721496860730 + 0.000000000000000156 + 0.000001442993955388 = 0.000002164490816274 + assert_approximatedly_equals( + setup.z_token_b.balanceOf(MOCK_TREASURY_ADDRESS.try_into().unwrap()), 2164490816274, 1 + ); +} + #[test] #[available_gas(90000000)] fn test_prelisted_token_may_have_price_source_unset() { diff --git a/tests/mock.cairo b/tests/mock.cairo index 6c3a28b..d49ebf2 100644 --- a/tests/mock.cairo +++ b/tests/mock.cairo @@ -88,6 +88,13 @@ trait IAccount { borrow_factor: felt252 ); + fn market_set_reserve_factor( + ref self: TContractState, + contract_address: ContractAddress, + token: ContractAddress, + reserve_factor: felt252 + ); + fn market_set_debt_limit( ref self: TContractState, contract_address: ContractAddress, diff --git a/tests/mock/account.cairo b/tests/mock/account.cairo index 2fb8940..b1f27da 100644 --- a/tests/mock/account.cairo +++ b/tests/mock/account.cairo @@ -85,6 +85,15 @@ mod Account { IMarketDispatcher { contract_address }.set_borrow_factor(token, borrow_factor) } + fn market_set_reserve_factor( + ref self: ContractState, + contract_address: ContractAddress, + token: ContractAddress, + reserve_factor: felt252 + ) { + IMarketDispatcher { contract_address }.set_reserve_factor(token, reserve_factor) + } + fn market_set_debt_limit( ref self: ContractState, contract_address: ContractAddress,