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,19 +18,20 @@ 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- NON_MONOTONIC_TIME , ORDER_IS_NOT_EXPIRED , STALE_TIME , TRADE_ASSET_NOT_SYNTHETIC ,
23- TRANSFER_FAILED ,
21+ AMOUNT_OVERFLOW , FORCED_WAIT_REQUIRED , INVALID_INTEREST_RATE , INVALID_ZERO_TIMEOUT ,
22+ LENGTH_MISMATCH , NON_MONOTONIC_TIME , ORDER_IS_NOT_EXPIRED , STALE_TIME ,
23+ TRADE_ASSET_NOT_SYNTHETIC , TRANSFER_FAILED , ZERO_MAX_INTEREST_RATE ,
2424 };
2525 use perpetuals :: core :: events;
2626 use perpetuals :: core :: interface :: {ICore , Settlement };
2727 use perpetuals :: core :: types :: asset :: AssetId ;
28+ use perpetuals :: core :: types :: asset :: synthetic :: AssetType ;
2829 use perpetuals :: core :: types :: balance :: Balance ;
2930 use perpetuals :: core :: types :: order :: {ForcedTrade , LimitOrder , Order };
3031 use perpetuals :: core :: types :: position :: {PositionDiff , PositionId , PositionTrait };
3132 use perpetuals :: core :: types :: price :: PriceMulTrait ;
3233 use perpetuals :: core :: types :: vault :: ConvertPositionToVault ;
33- use perpetuals :: core :: value_risk_calculator :: PositionTVTR ;
34+ use perpetuals :: core :: value_risk_calculator :: { PositionTVTR , calculate_position_pnl} ;
3435 use starknet :: event :: EventEmitter ;
3536 use starknet :: storage :: {
3637 StorageMapReadAccess , StoragePointerReadAccess , StoragePointerWriteAccess ,
@@ -46,10 +47,13 @@ pub mod Core {
4647 use starkware_utils :: components :: roles :: RolesComponent :: InternalTrait as RolesInternal ;
4748 use starkware_utils :: components :: roles :: interface :: IRoles ;
4849 use starkware_utils :: hash :: message_hash :: OffchainMessageHash ;
50+ use starkware_utils :: math :: abs :: Abs ;
51+ use starkware_utils :: math :: utils :: mul_wide_and_floor_div;
4952 use starkware_utils :: signature :: stark :: {PublicKey , Signature };
5053 use starkware_utils :: storage :: iterable_map :: {
5154 IterableMapIntoIterImpl , IterableMapReadAccessImpl , IterableMapWriteAccessImpl ,
5255 };
56+ use starkware_utils :: storage :: utils :: AddToStorage ;
5357 use starkware_utils :: time :: time :: {Time , TimeDelta , Timestamp };
5458 use crate :: core :: components :: assets :: interface :: IAssets ;
5559 use crate :: core :: components :: deleverage :: deleverage_manager :: IDeleverageManagerDispatcherTrait ;
@@ -65,7 +69,6 @@ pub mod Core {
6569 use crate :: core :: components :: vaults :: vaults :: {IVaults , Vaults as VaultsComponent };
6670 use crate :: core :: components :: vaults :: vaults_contract :: IVaultExternalDispatcherTrait ;
6771 use crate :: core :: components :: withdrawal :: withdrawal_manager :: IWithdrawalManagerDispatcherTrait ;
68- use crate :: core :: types :: asset :: synthetic :: AssetType ;
6972 use crate :: core :: utils :: {validate_signature, validate_trade};
7073
7174
@@ -163,6 +166,10 @@ pub mod Core {
163166 // Cost for executing forced actions.
164167 premium_cost : u64 ,
165168 system_time : Timestamp ,
169+ // Maximum interest rate per second (32-bit fixed-point with 32-bit fractional part).
170+ // Example: max_interest_rate_per_sec = 1000000 means the rate is 1000000 / 2^32 ≈
171+ // 0.000232 per second, which is approximately 7.3% per year.
172+ max_interest_rate_per_sec : u32 ,
166173 }
167174
168175 #[event]
@@ -228,6 +235,7 @@ pub mod Core {
228235 insurance_fund_position_owner_public_key : PublicKey ,
229236 forced_action_timelock : u64 ,
230237 premium_cost : u64 ,
238+ max_interest_rate_per_sec : u32 ,
231239 ) {
232240 self . roles. initialize (: governance_admin );
233241 self . replaceability. initialize (: upgrade_delay );
@@ -250,6 +258,8 @@ pub mod Core {
250258 assert (forced_action_timelock . is_non_zero (), INVALID_ZERO_TIMEOUT );
251259 self . forced_action_timelock. write (TimeDelta { seconds : forced_action_timelock });
252260 self . premium_cost. write (premium_cost );
261+ assert (max_interest_rate_per_sec . is_non_zero (), ZERO_MAX_INTEREST_RATE );
262+ self . max_interest_rate_per_sec. write (max_interest_rate_per_sec );
253263 }
254264
255265 #[abi(embed_v0)]
@@ -994,6 +1004,38 @@ pub mod Core {
9941004 fn get_system_time (self : @ ContractState ) -> Timestamp {
9951005 self . system_time. read ()
9961006 }
1007+
1008+ fn apply_interests (
1009+ ref self : ContractState ,
1010+ operator_nonce : u64 ,
1011+ position_ids : Span <PositionId >,
1012+ interest_amounts : Span <i64 >,
1013+ ) {
1014+ assert (position_ids . len () == interest_amounts . len (), LENGTH_MISMATCH );
1015+ self . pausable. assert_not_paused ();
1016+ self . assets. validate_assets_integrity ();
1017+ self . operator_nonce. use_checked_nonce (: operator_nonce );
1018+
1019+ // Read once and pass as arguments to avoid redundant storage reads
1020+ let current_time = Time :: now ();
1021+ let max_interest_rate_per_sec = self . max_interest_rate_per_sec. read ();
1022+ let interest_rate_scale : u64 = 2_u64 . pow (32 );
1023+
1024+ let mut i : usize = 0 ;
1025+ for position_id in position_ids {
1026+ let interest_amount = * interest_amounts [i ];
1027+ self
1028+ . _apply_interest_to_position (
1029+ position_id : * position_id ,
1030+ : interest_amount ,
1031+ : current_time ,
1032+ : max_interest_rate_per_sec ,
1033+ : interest_rate_scale ,
1034+ );
1035+ i += 1 ;
1036+ }
1037+ }
1038+
9971039 fn liquidate_spot_asset (
9981040 ref self : ContractState ,
9991041 operator_nonce : u64 ,
@@ -1175,5 +1217,83 @@ pub mod Core {
11751217 fn _is_vault (ref self : ContractState , vault_position : PositionId ) -> bool {
11761218 self . vaults. is_vault_position (vault_position )
11771219 }
1220+
1221+ /// Calculates the position PnL (profit and loss) as the total value of synthetic assets
1222+ /// plus base collateral. Similar to TV calculation but without vault and spot assets.
1223+ /// Includes funding ticks in the calculation.
1224+ fn _calculate_position_pnl (
1225+ ref self : ContractState , position_id : PositionId , collateral_balance : Balance ,
1226+ ) -> i64 {
1227+ let position = self . positions. get_position_snapshot (: position_id );
1228+
1229+ // Use existing function to derive funding delta and unchanged assets
1230+ // This already calculates funding and builds AssetBalanceInfo array
1231+ let (funding_delta , unchanged_assets ) = self
1232+ . positions
1233+ . derive_funding_delta_and_unchanged_assets (
1234+ : position , position_diff : Default :: default (),
1235+ );
1236+
1237+ // Filter to only include synthetic assets (exclude vault and spot)
1238+ let mut synthetic_assets = array! [];
1239+ for asset in unchanged_assets {
1240+ let asset_config = self . assets. get_asset_config (* asset . id);
1241+ if asset_config . asset_type == AssetType :: SYNTHETIC {
1242+ synthetic_assets . append (* asset );
1243+ }
1244+ }
1245+
1246+ let collateral_balance_with_funding = collateral_balance + funding_delta ;
1247+ calculate_position_pnl (
1248+ assets : synthetic_assets . span (),
1249+ collateral_balance : collateral_balance_with_funding ,
1250+ )
1251+ }
1252+
1253+ fn _apply_interest_to_position (
1254+ ref self : ContractState ,
1255+ position_id : PositionId ,
1256+ interest_amount : i64 ,
1257+ current_time : Timestamp ,
1258+ max_interest_rate_per_sec : u32 ,
1259+ interest_rate_scale : u64 ,
1260+ ) {
1261+ // Check that position exists
1262+ let position = self . positions. get_position_mut (: position_id );
1263+
1264+ let previous_timestamp = position . last_interest_applied_time. read ();
1265+
1266+ // Calculate position PnL (total value of synthetic assets + base collateral)
1267+ let pnl = self . _calculate_position_pnl (position_id , position . collateral_balance. read ());
1268+
1269+ // Validate interest rate
1270+ if pnl . is_non_zero () && previous_timestamp . is_non_zero () {
1271+ // Calculate time difference
1272+ let time_diff : u64 = current_time . seconds - previous_timestamp . seconds;
1273+
1274+ // Calculate maximum allowed change: |pnl| * time_diff *
1275+ // max_interest_rate_per_sec / 2^32.
1276+ let balance_time_product : u128 = pnl . abs (). into () * time_diff . into ();
1277+ let max_allowed_change = mul_wide_and_floor_div (
1278+ balance_time_product ,
1279+ max_interest_rate_per_sec . into (),
1280+ interest_rate_scale . into (),
1281+ )
1282+ . expect (AMOUNT_OVERFLOW );
1283+
1284+ // Check: |interest_amount| <= max_allowed_change
1285+ assert (interest_amount . abs (). into () <= max_allowed_change , INVALID_INTEREST_RATE );
1286+
1287+ // Apply interest
1288+ position . collateral_balance. add_and_write (interest_amount . into ());
1289+ } else {
1290+ // If old balance is zero, only allow zero interest.
1291+ // If `previous_timestamp` is zero, this indicates the first interest calculation,
1292+ // and the interest amount is required to be zero.
1293+ assert (interest_amount . is_zero (), INVALID_INTEREST_RATE );
1294+ }
1295+
1296+ position . last_interest_applied_time. write (current_time );
1297+ }
11781298 }
11791299}
0 commit comments