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,6 +47,8 @@ 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 ,
@@ -65,7 +68,6 @@ pub mod Core {
6568 use crate :: core :: components :: vaults :: vaults :: {IVaults , Vaults as VaultsComponent };
6669 use crate :: core :: components :: vaults :: vaults_contract :: IVaultExternalDispatcherTrait ;
6770 use crate :: core :: components :: withdrawal :: withdrawal_manager :: IWithdrawalManagerDispatcherTrait ;
68- use crate :: core :: types :: asset :: synthetic :: AssetType ;
6971 use crate :: core :: utils :: {validate_signature, validate_trade};
7072
7173
@@ -163,6 +165,10 @@ pub mod Core {
163165 // Cost for executing forced actions.
164166 premium_cost : u64 ,
165167 system_time : Timestamp ,
168+ // Maximum interest rate per second (32-bit fixed-point with 32-bit fractional part).
169+ // Example: max_interest_rate_per_sec = 1000000 means the rate is 1000000 / 2^32 ≈
170+ // 0.000232 per second, which is approximately 7.3% per year.
171+ max_interest_rate_per_sec : u32 ,
166172 }
167173
168174 #[event]
@@ -228,6 +234,7 @@ pub mod Core {
228234 insurance_fund_position_owner_public_key : PublicKey ,
229235 forced_action_timelock : u64 ,
230236 premium_cost : u64 ,
237+ max_interest_rate_per_sec : u32 ,
231238 ) {
232239 self . roles. initialize (: governance_admin );
233240 self . replaceability. initialize (: upgrade_delay );
@@ -250,6 +257,8 @@ pub mod Core {
250257 assert (forced_action_timelock . is_non_zero (), INVALID_ZERO_TIMEOUT );
251258 self . forced_action_timelock. write (TimeDelta { seconds : forced_action_timelock });
252259 self . premium_cost. write (premium_cost );
260+ assert (max_interest_rate_per_sec . is_non_zero (), ZERO_MAX_INTEREST_RATE );
261+ self . max_interest_rate_per_sec. write (max_interest_rate_per_sec );
253262 }
254263
255264 #[abi(embed_v0)]
@@ -994,6 +1003,38 @@ pub mod Core {
9941003 fn get_system_time (self : @ ContractState ) -> Timestamp {
9951004 self . system_time. read ()
9961005 }
1006+
1007+ fn apply_interests (
1008+ ref self : ContractState ,
1009+ operator_nonce : u64 ,
1010+ position_ids : Span <PositionId >,
1011+ interest_amounts : Span <i64 >,
1012+ ) {
1013+ assert (position_ids . len () == interest_amounts . len (), LENGTH_MISMATCH );
1014+ self . pausable. assert_not_paused ();
1015+ self . assets. validate_assets_integrity ();
1016+ self . operator_nonce. use_checked_nonce (: operator_nonce );
1017+
1018+ // Read once and pass as arguments to avoid redundant storage reads
1019+ let current_time = Time :: now ();
1020+ let max_interest_rate_per_sec = self . max_interest_rate_per_sec. read ();
1021+ let interest_rate_scale : u64 = 2_u64 . pow (32 );
1022+
1023+ let mut i : usize = 0 ;
1024+ for position_id in position_ids {
1025+ let interest_amount = * interest_amounts [i ];
1026+ self
1027+ . _apply_interest_to_position (
1028+ position_id : * position_id ,
1029+ : interest_amount ,
1030+ : current_time ,
1031+ : max_interest_rate_per_sec ,
1032+ : interest_rate_scale ,
1033+ );
1034+ i += 1 ;
1035+ }
1036+ }
1037+
9971038 fn liquidate_spot_asset (
9981039 ref self : ContractState ,
9991040 operator_nonce : u64 ,
@@ -1175,5 +1216,93 @@ pub mod Core {
11751216 fn _is_vault (ref self : ContractState , vault_position : PositionId ) -> bool {
11761217 self . vaults. is_vault_position (vault_position )
11771218 }
1219+
1220+ /// Calculates the position PnL (profit and loss) as the total value of synthetic assets
1221+ /// plus base collateral. Similar to TV calculation but without vault and spot assets.
1222+ /// Includes funding ticks in the calculation.
1223+ fn _calculate_position_pnl (
1224+ ref self : ContractState , position_id : PositionId , collateral_balance : Balance ,
1225+ ) -> i128 {
1226+ let position = self . positions. get_position_snapshot (: position_id );
1227+
1228+ // Use existing function to derive funding delta and unchanged assets
1229+ // This already calculates funding and builds AssetBalanceInfo array
1230+ let (funding_delta , unchanged_assets ) = self
1231+ . positions
1232+ . derive_funding_delta_and_unchanged_assets (
1233+ : position , position_diff : Default :: default (),
1234+ );
1235+
1236+ // Filter to only include synthetic assets (exclude vault and spot)
1237+ let mut synthetic_assets = array! [];
1238+ for asset in unchanged_assets {
1239+ let asset_config = self . assets. get_asset_config (* asset . id);
1240+ if asset_config . asset_type == AssetType :: SYNTHETIC {
1241+ synthetic_assets . append (* asset );
1242+ }
1243+ }
1244+
1245+ let collateral_balance_with_funding = collateral_balance + funding_delta ;
1246+ calculate_position_pnl (synthetic_assets . span (), collateral_balance_with_funding )
1247+ }
1248+
1249+ fn _apply_interest_to_position (
1250+ ref self : ContractState ,
1251+ position_id : PositionId ,
1252+ interest_amount : i64 ,
1253+ current_time : Timestamp ,
1254+ max_interest_rate_per_sec : u32 ,
1255+ interest_rate_scale : u64 ,
1256+ ) {
1257+ // Check that position exists
1258+ let position = self . positions. get_position_mut (: position_id );
1259+
1260+ // Get old balance and last interest applied time
1261+ let current_balance = position . collateral_balance. read ();
1262+ let previous_timestamp = position . last_interest_applied_time. read ();
1263+
1264+ // Calculate new balance
1265+ let new_balance = current_balance + interest_amount . into ();
1266+
1267+ // Calculate position PnL (total value of synthetic assets + base collateral)
1268+ let pnl = self . _calculate_position_pnl (position_id , current_balance );
1269+
1270+ // Validate interest rate
1271+ let pnl : i64 = pnl . try_into (). expect (AMOUNT_OVERFLOW );
1272+ if pnl . is_non_zero () && previous_timestamp . is_non_zero () {
1273+ // Calculate time difference
1274+ let time_diff : u64 = current_time . seconds - previous_timestamp . seconds;
1275+
1276+ // Calculate maximum allowed change: |old_balance| * time_diff *
1277+ // max_interest_rate_per_sec / 2^32 Formula: |interest_amount| <= |old_balance| *
1278+ // time_diff * max_interest_rate_per_sec / 2^32
1279+ let pnl_abs : u128 = pnl . abs (). into ();
1280+ let max_rate : u128 = max_interest_rate_per_sec . into ();
1281+ let time_diff_u128 : u128 = time_diff . into ();
1282+ // Calculate: (|old_balance| * time_diff * max_interest_rate_per_sec) / 2^32
1283+ // To preserve precision, multiply all factors first, then divide.
1284+ // Use mul_wide_and_floor_div to safely compute (balance * time_diff * max_rate) /
1285+ // scale which handles wide multiplication to avoid overflow.
1286+ let balance_time_product = pnl_abs * time_diff_u128 ;
1287+ let max_allowed_change = mul_wide_and_floor_div (
1288+ balance_time_product , max_rate , interest_rate_scale . into (),
1289+ )
1290+ . expect (AMOUNT_OVERFLOW );
1291+
1292+ // Check: |interest_amount| <= max_allowed_change
1293+ let interest_amount_abs : u128 = interest_amount . abs (). into ();
1294+ assert (interest_amount_abs <= max_allowed_change , INVALID_INTEREST_RATE );
1295+
1296+ // Apply interest
1297+ position . collateral_balance. write (new_balance );
1298+ } else {
1299+ // If old balance is zero, only allow zero interest.
1300+ // If `prev_ts` is zero, this indicates the first interest calculation, and the
1301+ // interest amount is required to be zero.
1302+ assert (interest_amount . is_zero (), INVALID_INTEREST_RATE );
1303+ }
1304+
1305+ position . last_interest_applied_time. write (current_time );
1306+ }
11781307 }
11791308}
0 commit comments