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,10 @@ 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+ // Example: max_interest_rate_per_sec = 1000000 means the rate is 1000000 / 2^32 ≈
169+ // 0.000232 per second, which is approximately 7.3% per year.
170+ max_interest_rate_per_sec : u32 ,
164171 }
165172
166173 #[event]
@@ -226,6 +233,7 @@ pub mod Core {
226233 insurance_fund_position_owner_public_key : PublicKey ,
227234 forced_action_timelock : u64 ,
228235 premium_cost : u64 ,
236+ max_interest_rate_per_sec : u32 ,
229237 ) {
230238 self . roles. initialize (: governance_admin );
231239 self . replaceability. initialize (: upgrade_delay );
@@ -248,6 +256,8 @@ pub mod Core {
248256 assert (forced_action_timelock . is_non_zero (), INVALID_ZERO_TIMEOUT );
249257 self . forced_action_timelock. write (TimeDelta { seconds : forced_action_timelock });
250258 self . premium_cost. write (premium_cost );
259+ assert (max_interest_rate_per_sec . is_non_zero (), ZERO_MAX_INTEREST_RATE );
260+ self . max_interest_rate_per_sec. write (max_interest_rate_per_sec );
251261 }
252262
253263 #[abi(embed_v0)]
@@ -953,6 +963,36 @@ pub mod Core {
953963 },
954964 );
955965 }
966+ fn apply_interests (
967+ ref self : ContractState ,
968+ operator_nonce : u64 ,
969+ position_ids : Span <PositionId >,
970+ interest_amounts : Span <i64 >,
971+ ) {
972+ assert (position_ids . len () == interest_amounts . len (), LENGTH_MISMATCH );
973+ self . pausable. assert_not_paused ();
974+ self . assets. validate_assets_integrity ();
975+ self . operator_nonce. use_checked_nonce (: operator_nonce );
976+
977+ // Read once and pass as arguments to avoid redundant storage reads
978+ let current_time = Time :: now ();
979+ let max_interest_rate_per_sec = self . max_interest_rate_per_sec. read ();
980+ let interest_rate_scale : u64 = 2_u64 . pow (32 );
981+
982+ let mut i : usize = 0 ;
983+ for position_id in position_ids {
984+ let interest_amount = * interest_amounts [i ];
985+ self
986+ . _apply_interest_to_position (
987+ position_id : * position_id ,
988+ : interest_amount ,
989+ : current_time ,
990+ : max_interest_rate_per_sec ,
991+ : interest_rate_scale ,
992+ );
993+ i += 1 ;
994+ }
995+ }
956996 }
957997
958998 #[generate_trait]
@@ -1106,5 +1146,66 @@ pub mod Core {
11061146 fn _is_vault (ref self : ContractState , vault_position : PositionId ) -> bool {
11071147 self . vaults. is_vault_position (vault_position )
11081148 }
1149+
1150+ fn _apply_interest_to_position (
1151+ ref self : ContractState ,
1152+ position_id : PositionId ,
1153+ interest_amount : i64 ,
1154+ current_time : Timestamp ,
1155+ max_interest_rate_per_sec : u32 ,
1156+ interest_rate_scale : u64 ,
1157+ ) {
1158+ // Check that position exists
1159+ let position = self . positions. get_position_mut (: position_id );
1160+
1161+ // Get old balance and last interest applied time
1162+ let old_balance = position . collateral_balance. read ();
1163+ let previous_timestamp = position . last_interest_applied_time. read ();
1164+
1165+ // Calculate new balance
1166+ let new_balance = old_balance + interest_amount . into ();
1167+
1168+ // Validate interest rate
1169+ let old_balance_value : i64 = old_balance . into ();
1170+ if old_balance_value . is_non_zero () && previous_timestamp . is_non_zero () {
1171+ // Calculate time difference
1172+ let time_diff : u64 = if current_time . seconds >= previous_timestamp . seconds {
1173+ current_time . seconds - previous_timestamp . seconds
1174+ } else {
1175+ // If current time is before previous time, something is wrong
1176+ panic_with_felt252 (' INVALID_TIMESTAMP' );
1177+ };
1178+
1179+ // Calculate maximum allowed change: |old_balance| * time_diff *
1180+ // max_interest_rate_per_sec / 2^32 Formula: |interest_amount| <= |old_balance| *
1181+ // time_diff * max_interest_rate_per_sec / 2^32
1182+ let old_balance_abs : u128 = old_balance_value . abs (). into ();
1183+ let max_rate : u128 = max_interest_rate_per_sec . into ();
1184+ let time_diff_u128 : u128 = time_diff . into ();
1185+ // Calculate: (|old_balance| * time_diff * max_interest_rate_per_sec) / 2^32
1186+ // To preserve precision, multiply all factors first, then divide.
1187+ // Use mul_wide_and_floor_div to safely compute (balance * time_diff * max_rate) /
1188+ // scale which handles wide multiplication to avoid overflow.
1189+ let balance_time_product = old_balance_abs * time_diff_u128 ;
1190+ let max_allowed_change = mul_wide_and_floor_div (
1191+ balance_time_product , max_rate , interest_rate_scale . into (),
1192+ )
1193+ . expect (AMOUNT_OVERFLOW );
1194+
1195+ // Check: |interest_amount| <= max_allowed_change
1196+ let interest_amount_abs : u128 = interest_amount . abs (). into ();
1197+ assert (interest_amount_abs <= max_allowed_change , INVALID_INTEREST_RATE );
1198+
1199+ // Apply interest
1200+ position . collateral_balance. write (new_balance );
1201+ } else {
1202+ // If old balance is zero, only allow zero interest.
1203+ // If `prev_ts` is zero, this indicates the first interest calculation, and the
1204+ // interest amount is required to be zero.
1205+ assert (interest_amount . is_zero (), INVALID_INTEREST_RATE );
1206+ }
1207+
1208+ position . last_interest_applied_time. write (current_time );
1209+ }
11091210 }
11101211}
0 commit comments