11#[starknet:: contract]
22pub mod Core {
33 use core :: dict :: {Felt252Dict , Felt252DictTrait };
4- use core :: num :: traits :: Zero ;
4+ use core :: num :: traits :: { Pow , Zero } ;
55 use core :: panic_with_felt252 ;
66 use openzeppelin :: access :: accesscontrol :: AccessControlComponent ;
77 use openzeppelin :: interfaces :: erc20 :: IERC20DispatcherTrait ;
@@ -18,8 +18,9 @@ pub mod Core {
1818 FEE_POSITION , InternalTrait as PositionsInternalTrait ,
1919 };
2020 use perpetuals :: core :: errors :: {
21- AMOUNT_OVERFLOW , FORCED_WAIT_REQUIRED , INVALID_ZERO_TIMEOUT , LENGTH_MISMATCH ,
22- ORDER_IS_NOT_EXPIRED , TRADE_ASSET_NOT_SYNTHETIC , TRANSFER_FAILED ,
21+ AMOUNT_OVERFLOW , FORCED_WAIT_REQUIRED , INVALID_INTEREST_RATE , INVALID_ZERO_TIMEOUT ,
22+ LENGTH_MISMATCH , ORDER_IS_NOT_EXPIRED , TRADE_ASSET_NOT_SYNTHETIC , TRANSFER_FAILED ,
23+ ZERO_MAX_INTEREST_RATE ,
2324 };
2425 use perpetuals :: core :: events;
2526 use perpetuals :: core :: interface :: {ICore , Settlement };
@@ -45,6 +46,8 @@ pub mod Core {
4546 use starkware_utils :: components :: roles :: RolesComponent :: InternalTrait as RolesInternal ;
4647 use starkware_utils :: components :: roles :: interface :: IRoles ;
4748 use starkware_utils :: hash :: message_hash :: OffchainMessageHash ;
49+ use starkware_utils :: math :: abs :: Abs ;
50+ use starkware_utils :: math :: utils :: mul_wide_and_floor_div;
4851 use starkware_utils :: signature :: stark :: {PublicKey , Signature };
4952 use starkware_utils :: storage :: iterable_map :: {
5053 IterableMapIntoIterImpl , IterableMapReadAccessImpl , IterableMapWriteAccessImpl ,
@@ -161,6 +164,8 @@ pub mod Core {
161164 forced_action_timelock : TimeDelta ,
162165 // Cost for executing forced actions.
163166 premium_cost : u64 ,
167+ // Maximum interest rate per second (32-bit fixed-point with 32-bit fractional part).
168+ max_interest_rate_per_sec : u32 ,
164169 }
165170
166171 #[event]
@@ -226,6 +231,7 @@ pub mod Core {
226231 insurance_fund_position_owner_public_key : PublicKey ,
227232 forced_action_timelock : u64 ,
228233 premium_cost : u64 ,
234+ max_interest_rate_per_sec : u32 ,
229235 ) {
230236 self . roles. initialize (: governance_admin );
231237 self . replaceability. initialize (: upgrade_delay );
@@ -248,6 +254,8 @@ pub mod Core {
248254 assert (forced_action_timelock . is_non_zero (), INVALID_ZERO_TIMEOUT );
249255 self . forced_action_timelock. write (TimeDelta { seconds : forced_action_timelock });
250256 self . premium_cost. write (premium_cost );
257+ assert (max_interest_rate_per_sec . is_non_zero (), ZERO_MAX_INTEREST_RATE );
258+ self . max_interest_rate_per_sec. write (max_interest_rate_per_sec );
251259 }
252260
253261 #[abi(embed_v0)]
@@ -953,6 +961,36 @@ pub mod Core {
953961 },
954962 );
955963 }
964+ fn apply_interests (
965+ ref self : ContractState ,
966+ operator_nonce : u64 ,
967+ position_ids : Span <PositionId >,
968+ interest_amounts : Span <i64 >,
969+ ) {
970+ assert (position_ids . len () == interest_amounts . len (), LENGTH_MISMATCH );
971+ self . pausable. assert_not_paused ();
972+ self . assets. validate_assets_integrity ();
973+ self . operator_nonce. use_checked_nonce (: operator_nonce );
974+
975+ // Read once and pass as arguments to avoid redundant storage reads
976+ let current_time = Time :: now ();
977+ let max_interest_rate_per_sec = self . max_interest_rate_per_sec. read ();
978+ let interest_rate_scale : u64 = 2_u64 . pow (32 );
979+
980+ let mut i : usize = 0 ;
981+ for position_id in position_ids {
982+ let interest_amount = * interest_amounts [i ];
983+ self
984+ . _apply_interest_to_position (
985+ position_id : * position_id ,
986+ : interest_amount ,
987+ : current_time ,
988+ : max_interest_rate_per_sec ,
989+ : interest_rate_scale ,
990+ );
991+ i += 1 ;
992+ }
993+ }
956994 }
957995
958996 #[generate_trait]
@@ -1106,5 +1144,66 @@ pub mod Core {
11061144 fn _is_vault (ref self : ContractState , vault_position : PositionId ) -> bool {
11071145 self . vaults. is_vault_position (vault_position )
11081146 }
1147+
1148+ fn _apply_interest_to_position (
1149+ ref self : ContractState ,
1150+ position_id : PositionId ,
1151+ interest_amount : i64 ,
1152+ current_time : Timestamp ,
1153+ max_interest_rate_per_sec : u32 ,
1154+ interest_rate_scale : u64 ,
1155+ ) {
1156+ // Check that position exists
1157+ let position = self . positions. get_position_mut (: position_id );
1158+
1159+ // Get old balance and last interest applied time
1160+ let old_balance = position . collateral_balance. read ();
1161+ let prev_ts = position . last_interest_applied_time. read ();
1162+
1163+ // Calculate new balance
1164+ let new_balance = old_balance + interest_amount . into ();
1165+
1166+ // Validate interest rate
1167+ let old_balance_value : i64 = old_balance . into ();
1168+ if old_balance_value . is_non_zero () && prev_ts . is_non_zero () {
1169+ // Calculate time difference
1170+ let time_diff : u64 = if current_time . seconds >= prev_ts . seconds {
1171+ current_time . seconds - prev_ts . seconds
1172+ } else {
1173+ // If current time is before previous time, something is wrong
1174+ panic_with_felt252 (' INVALID_TIMESTAMP' );
1175+ };
1176+
1177+ // Calculate maximum allowed change: |old_balance| * time_diff *
1178+ // max_interest_rate_per_sec / 2^32 Formula: |interest_amount| <= |old_balance| *
1179+ // time_diff * max_interest_rate_per_sec / 2^32
1180+ let old_balance_abs : u128 = old_balance_value . abs (). into ();
1181+ let max_rate : u128 = max_interest_rate_per_sec . into ();
1182+ let time_diff_u128 : u128 = time_diff . into ();
1183+ // Calculate: (|old_balance| * time_diff * max_interest_rate_per_sec) / 2^32
1184+ // To preserve precision, multiply all factors first, then divide.
1185+ // Use mul_wide_and_floor_div to safely compute (balance * time_diff * max_rate) /
1186+ // scale which handles wide multiplication to avoid overflow.
1187+ let balance_time_product = old_balance_abs * time_diff_u128 ;
1188+ let max_allowed_change = mul_wide_and_floor_div (
1189+ balance_time_product , max_rate , interest_rate_scale . into (),
1190+ )
1191+ . expect (AMOUNT_OVERFLOW );
1192+
1193+ // Check: |interest_amount| <= max_allowed_change
1194+ let interest_amount_abs : u128 = interest_amount . abs (). into ();
1195+ assert (interest_amount_abs <= max_allowed_change , INVALID_INTEREST_RATE );
1196+
1197+ // Apply interest
1198+ position . collateral_balance. write (new_balance );
1199+ } else {
1200+ // If old balance is zero, only allow zero interest.
1201+ // If `prev_ts` is zero, this indicates the first interest calculation, and the
1202+ // interest amount is required to be zero.
1203+ assert (interest_amount . is_zero (), INVALID_INTEREST_RATE );
1204+ }
1205+
1206+ position . last_interest_applied_time. write (current_time );
1207+ }
11091208 }
11101209}
0 commit comments