From b29228ca7d4d012b9e79a617c5eedfb41d7d6658 Mon Sep 17 00:00:00 2001 From: Noa Wolfgor Date: Sun, 5 Oct 2025 19:57:45 +0300 Subject: [PATCH 01/10] feat(staking): calculate block rewards by timestamp --- src/flow_test/flow_ideas.md | 5 ++++ src/flow_test/test.cairo | 8 +----- src/staking/staking.cairo | 27 ++++++++++++++----- src/staking/tests/test.cairo | 42 +++++++++++------------------- src/test_utils.cairo | 50 +++++++++++++++++++++--------------- 5 files changed, 72 insertions(+), 60 deletions(-) diff --git a/src/flow_test/flow_ideas.md b/src/flow_test/flow_ideas.md index 99ecd5da..1f5720d8 100644 --- a/src/flow_test/flow_ideas.md +++ b/src/flow_test/flow_ideas.md @@ -49,3 +49,8 @@ more ideas: - enable token, update rewards, advance epoch, update rewards, advance epoch, update rewards - token does not get rewards until after 2 epochs - same as above with disable (can be implemented together as one test) - enable token A and disable token B, next epoch upgrade, test views and rewards. + +## block rewards by timestamp +- advance blocks with different block times and check the avg is calculated correctly +- update_rewards for blocks in same epoch - same rewards, then advance epoch, different rewards, update rewards for blocks in same epoch - same rewards. +- update rewards is not called every block, still rewards is updated correctly (miss block, miss first block in epoch, miss epoch) \ No newline at end of file diff --git a/src/flow_test/test.cairo b/src/flow_test/test.cairo index 4a01eea2..c7c30a71 100644 --- a/src/flow_test/test.cairo +++ b/src/flow_test/test.cairo @@ -2720,11 +2720,7 @@ fn update_rewards_transition_from_attestation_to_consensus_flow_test() { system.update_rewards(:staker, disable_rewards: false); let rewards = system.staker_claim_rewards(:staker); let (expected_rewards_v3, _) = calculate_staker_strk_rewards_with_balances_v3( - amount_own: stake_amount, - pool_amount: Zero::zero(), - :commission, - :staking_contract, - :minting_curve_contract, + amount_own: stake_amount, pool_amount: Zero::zero(), :commission, :minting_curve_contract, ); assert!(expected_rewards_v3.is_non_zero()); assert!(rewards == expected_rewards_v3); @@ -2873,7 +2869,6 @@ fn update_rewards_disable_rewards_consensus_rewards_flow_test() { amount_own: stake_amount, pool_amount: Zero::zero(), :commission, - staking_contract: system.staking.address, minting_curve_contract: system.minting_curve.address, ); assert!(expected_rewards.is_non_zero()); @@ -2920,7 +2915,6 @@ fn update_rewards_strk_pool_flow_test() { amount_own: stake_amount, pool_amount: delegation_amount, :commission, - staking_contract: system.staking.address, minting_curve_contract: system.minting_curve.address, ); let staker_rewards = system.staker_claim_rewards(:staker); diff --git a/src/staking/staking.cairo b/src/staking/staking.cairo index 12f52328..cc9147a6 100644 --- a/src/staking/staking.cairo +++ b/src/staking/staking.cairo @@ -188,6 +188,10 @@ pub mod Staking { pool_eic_class_hash: ClassHash, /// Map staker address to its version. staker_version: Map, + /// Last epoch for which block rewards were calculated. + last_calculated_epoch: Epoch, + /// Block rewards (STRK, BTC) for the current epoch. + block_rewards: (Amount, Amount), } #[event] @@ -1431,7 +1435,7 @@ pub mod Staking { // Get current block data and update rewards. let reward_supplier_dispatcher = self.reward_supplier_dispatcher.read(); let (strk_block_rewards, btc_block_rewards) = self - .calculate_block_rewards(:reward_supplier_dispatcher); + .calculate_block_rewards(:reward_supplier_dispatcher, :curr_epoch); self ._update_rewards( :staker_address, @@ -1490,13 +1494,24 @@ pub mod Staking { } /// Calculates the rewards for a block in the current epoch (for STRK and BTC). + /// + /// Precondition: `curr_epoch` must be `get_current_epoch()`, it's passed as a param to save + /// storage reads. + // TODO: Consider view? + // TODO: Migration. fn calculate_block_rewards( - self: @ContractState, reward_supplier_dispatcher: IRewardSupplierDispatcher, + ref self: ContractState, + reward_supplier_dispatcher: IRewardSupplierDispatcher, + curr_epoch: Epoch, ) -> (Amount, Amount) { - let (strk_rewards, btc_rewards) = reward_supplier_dispatcher - .calculate_current_epoch_rewards(); - let epoch_len_in_blocks = self.get_epoch_info().epoch_len_in_blocks(); - (strk_rewards / epoch_len_in_blocks.into(), btc_rewards / epoch_len_in_blocks.into()) + if curr_epoch > self.last_calculated_epoch.read() { + self.last_calculated_epoch.write(curr_epoch); + let block_rewards = reward_supplier_dispatcher.update_current_epoch_block_rewards(); + self.block_rewards.write(block_rewards); + block_rewards + } else { + self.block_rewards.read() + } } fn update_commission( diff --git a/src/staking/tests/test.cairo b/src/staking/tests/test.cairo index c697bb0f..468e110c 100644 --- a/src/staking/tests/test.cairo +++ b/src/staking/tests/test.cairo @@ -86,16 +86,17 @@ use starkware_utils_testing::test_utils::{ }; use test_utils::{ StakingInitConfig, advance_block_into_attestation_window, advance_epoch_global, - advance_k_epochs_global, approve, calculate_staker_btc_pool_rewards_v2, - calculate_staker_btc_pool_rewards_v3, calculate_staker_strk_rewards_v2, - calculate_staker_strk_rewards_with_balances_v3, cheat_target_attestation_block_hash, constants, - custom_decimals_token, declare_pool_contract, declare_pool_eic_contract, - declare_staking_contract, declare_staking_eic_contract, deploy_mock_erc20_decimals_contract, - deploy_staking_contract, enter_delegation_pool_for_testing_using_dispatcher, fund, - general_contract_system_deployment, load_from_simple_map, load_one_felt, pause_staking_contract, - setup_btc_token, stake_for_testing_using_dispatcher, stake_from_zero_address, - stake_with_strk_pool_enabled, store_internal_staker_info_v0_to_map, store_to_simple_map, - to_amount_18_decimals, upgrade_implementation, + advance_k_epochs_global, approve, calculate_current_block_rewards_v3, + calculate_staker_btc_pool_rewards_v2, calculate_staker_btc_pool_rewards_v3, + calculate_staker_strk_rewards_v2, calculate_staker_strk_rewards_with_balances_v3, + cheat_target_attestation_block_hash, constants, custom_decimals_token, declare_pool_contract, + declare_pool_eic_contract, declare_staking_contract, declare_staking_eic_contract, + deploy_mock_erc20_decimals_contract, deploy_staking_contract, + enter_delegation_pool_for_testing_using_dispatcher, fund, general_contract_system_deployment, + load_from_simple_map, load_one_felt, pause_staking_contract, setup_btc_token, + stake_for_testing_using_dispatcher, stake_from_zero_address, stake_with_strk_pool_enabled, + store_internal_staker_info_v0_to_map, store_to_simple_map, to_amount_18_decimals, + upgrade_implementation, }; #[test] @@ -3490,10 +3491,7 @@ fn test_update_rewards_only_staker() { contract_address: staking_contract, }; let staking_config_dispatcher = IStakingConfigDispatcher { contract_address: staking_contract }; - let reward_supplier = cfg.staking_contract_info.reward_supplier; - let reward_supplier_dispatcher = IRewardSupplierDispatcher { - contract_address: reward_supplier, - }; + let minting_curve_contract = cfg.reward_supplier.minting_curve_contract; let current_epoch = staking_dispatcher.get_current_epoch(); cheat_caller_address_once( contract_address: staking_contract, caller_address: cfg.test_info.app_governor, @@ -3505,9 +3503,7 @@ fn test_update_rewards_only_staker() { advance_k_epochs_global(); let staker_address = cfg.test_info.staker_address; let staker_info_before = staking_dispatcher.staker_info_v1(:staker_address); - let (strk_epoch_rewards, _) = reward_supplier_dispatcher.calculate_current_epoch_rewards(); - let epoch_len_in_blocks = cfg.staking_contract_info.epoch_info.epoch_len_in_blocks(); - let strk_block_rewards = strk_epoch_rewards / epoch_len_in_blocks.into(); + let (strk_block_rewards, _) = calculate_current_block_rewards_v3(:minting_curve_contract); let staker_info_expected = StakerInfoV1 { unclaimed_rewards_own: strk_block_rewards, ..staker_info_before, }; @@ -3536,10 +3532,7 @@ fn test_update_rewards_miss_blocks() { contract_address: staking_contract, }; let staking_config_dispatcher = IStakingConfigDispatcher { contract_address: staking_contract }; - let reward_supplier = cfg.staking_contract_info.reward_supplier; - let reward_supplier_dispatcher = IRewardSupplierDispatcher { - contract_address: reward_supplier, - }; + let minting_curve_contract = cfg.reward_supplier.minting_curve_contract; let current_epoch = staking_dispatcher.get_current_epoch(); cheat_caller_address_once( contract_address: staking_contract, caller_address: cfg.test_info.app_governor, @@ -3551,9 +3544,7 @@ fn test_update_rewards_miss_blocks() { advance_k_epochs_global(); let staker_address = cfg.test_info.staker_address; let staker_info_before = staking_dispatcher.staker_info_v1(:staker_address); - let (strk_epoch_rewards, _) = reward_supplier_dispatcher.calculate_current_epoch_rewards(); - let epoch_len_in_blocks = cfg.staking_contract_info.epoch_info.epoch_len_in_blocks(); - let strk_block_rewards = strk_epoch_rewards / epoch_len_in_blocks.into(); + let (strk_block_rewards, _) = calculate_current_block_rewards_v3(:minting_curve_contract); let staker_info_expected = StakerInfoV1 { unclaimed_rewards_own: strk_block_rewards * 2, ..staker_info_before, }; @@ -3603,7 +3594,6 @@ fn test_update_rewards_with_strk_pool() { amount_own: stake_amount, pool_amount: delegated_amount, :commission, - :staking_contract, :minting_curve_contract, ); // Assert staker rewards, delegator rewards, and pool balance before update. @@ -3733,7 +3723,6 @@ fn test_update_rewards_with_both_strk_and_btc() { amount_own: stake_amount, pool_amount: strk_delegated_amount, :commission, - :staking_contract, :minting_curve_contract, ); // Same calculation for both BTC pools (both have the same decimals). @@ -3748,7 +3737,6 @@ fn test_update_rewards_with_both_strk_and_btc() { :normalized_pool_balance, :normalized_staker_total_btc_balance, :commission, - :staking_contract, :minting_curve_contract, token_address: btc_token_address, ); diff --git a/src/test_utils.cairo b/src/test_utils.cairo index 4512f56f..785bea1f 100644 --- a/src/test_utils.cairo +++ b/src/test_utils.cairo @@ -27,7 +27,7 @@ use snforge_std::{ }; use staking::attestation::attestation::Attestation::MIN_ATTESTATION_WINDOW; use staking::attestation::interface::{IAttestationDispatcher, IAttestationDispatcherTrait}; -use staking::constants::{K, STARTING_EPOCH, STRK_IN_FRIS, STRK_TOKEN_ADDRESS}; +use staking::constants::{K, SECONDS_IN_YEAR, STARTING_EPOCH, STRK_IN_FRIS, STRK_TOKEN_ADDRESS}; use staking::errors::InternalError; use staking::minting_curve::interface::{ IMintingCurveConfigDispatcher, IMintingCurveConfigDispatcherTrait, IMintingCurveDispatcher, @@ -40,6 +40,7 @@ use staking::pool::pool::Pool; use staking::pool::pool_member_balance_trace::trace::PoolMemberCheckpointTrait; use staking::pool::utils::compute_rewards_rounded_down; use staking::reward_supplier::reward_supplier::RewardSupplier; +use staking::reward_supplier::reward_supplier::RewardSupplier::BLOCK_DURATION_SCALE; use staking::reward_supplier::utils::calculate_btc_rewards; use staking::staking::interface::{ IStakingDispatcher, IStakingDispatcherTrait, IStakingPauseDispatcher, @@ -1121,12 +1122,9 @@ pub(crate) fn calculate_staker_strk_rewards_with_balances_v3( amount_own: Amount, pool_amount: Amount, commission: Commission, - staking_contract: ContractAddress, minting_curve_contract: ContractAddress, ) -> (Amount, Amount) { - let (strk_block_rewards, _) = _calculate_current_block_rewards_v3( - :staking_contract, :minting_curve_contract, - ); + let (strk_block_rewards, _) = calculate_current_block_rewards_v3(:minting_curve_contract); let total_stake = amount_own + pool_amount; // Calculate staker own rewards. let mut staker_rewards = mul_wide_and_div( @@ -1166,7 +1164,6 @@ pub(crate) fn calculate_strk_pool_rewards_v3( amount_own: staker_info.amount_own, pool_amount: pool_info.amount, commission: pool_info.commission, - :staking_contract, :minting_curve_contract, ); pool_rewards @@ -1180,13 +1177,10 @@ pub(crate) fn calculate_staker_btc_pool_rewards_v3( normalized_pool_balance: NormalizedAmount, normalized_staker_total_btc_balance: NormalizedAmount, commission: Commission, - staking_contract: ContractAddress, minting_curve_contract: ContractAddress, token_address: ContractAddress, ) -> (Amount, Amount) { - let (_, btc_block_rewards) = _calculate_current_block_rewards_v3( - :staking_contract, :minting_curve_contract, - ); + let (_, btc_block_rewards) = calculate_current_block_rewards_v3(:minting_curve_contract); // Calculate pool rewards including commission. let pool_rewards_including_commission = mul_wide_and_div( lhs: btc_block_rewards, @@ -1203,17 +1197,33 @@ pub(crate) fn calculate_staker_btc_pool_rewards_v3( } -fn _calculate_current_block_rewards_v3( - staking_contract: ContractAddress, minting_curve_contract: ContractAddress, +/// Calculate block rewards for the current epoch. Use `AVG_BLOCK_TIME` (default value for testing) +/// for `avg_block_time`. +pub(crate) fn calculate_current_block_rewards_v3( + minting_curve_contract: ContractAddress, ) -> (Amount, Amount) { - let (strk_epoch_rewards, btc_epoch_rewards) = _calculate_current_epoch_rewards_v2( - :staking_contract, :minting_curve_contract, - ); - let staking_dispatcher = IStakingDispatcher { contract_address: staking_contract }; - let epoch_len_in_blocks = staking_dispatcher.get_epoch_info().epoch_len_in_blocks(); - let strk_block_rewards = strk_epoch_rewards / epoch_len_in_blocks.into(); - let btc_block_rewards = btc_epoch_rewards / epoch_len_in_blocks.into(); - (strk_block_rewards, btc_block_rewards) + calculate_current_block_rewards_with_avg_block_time_v3( + :minting_curve_contract, avg_block_time: AVG_BLOCK_DURATION * BLOCK_DURATION_SCALE, + ) +} + +/// Calculate block rewards for the current epoch based on the given `avg_block_time`. +pub(crate) fn calculate_current_block_rewards_with_avg_block_time_v3( + minting_curve_contract: ContractAddress, avg_block_time: u64, +) -> (Amount, Amount) { + let minting_curve_dispatcher = IMintingCurveDispatcher { + contract_address: minting_curve_contract, + }; + let yearly_mint = minting_curve_dispatcher.yearly_mint(); + let total_rewards = mul_wide_and_div( + lhs: yearly_mint, + rhs: avg_block_time.into(), + div: BLOCK_DURATION_SCALE.into() * SECONDS_IN_YEAR.into(), + ) + .expect_with_err(err: InternalError::REWARDS_COMPUTATION_OVERFLOW); + let btc_rewards = calculate_btc_rewards(:total_rewards); + let strk_rewards = total_rewards - btc_rewards; + (strk_rewards, btc_rewards) } // ---- Calculate Rewards - Helpers ----- From b28368c3cb2654a5594c7059c97e6678f3588ef6 Mon Sep 17 00:00:00 2001 From: Noa Wolfgor Date: Sun, 5 Oct 2025 22:40:58 +0300 Subject: [PATCH 02/10] feat(staking): add block time config in reward supplier --- docs/spec.md | 43 +++++++++++++ src/reward_supplier/errors.cairo | 6 ++ src/reward_supplier/interface.cairo | 31 +++++++++ src/reward_supplier/reward_supplier.cairo | 43 ++++++++++++- src/reward_supplier/test.cairo | 76 +++++++++++++++++++++-- src/test_utils.cairo | 10 +++ 6 files changed, 203 insertions(+), 6 deletions(-) diff --git a/docs/spec.md b/docs/spec.md index fca9affc..65194d26 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -125,6 +125,8 @@ - [contract\_parameters\_v1](#contract_parameters_v1-2) - [on\_receive](#on_receive) - [get\_alpha](#get_alpha) + - [get\_block\_duration\_config](#get_block_duration_config) + - [set\_block\_duration\_config](#set_block_duration_config) - [Events](#events-2) - [Mint Request](#mint-request) - [Minting Curve Contract](#minting-curve-contract) @@ -237,6 +239,7 @@ - [CONSENSUS\_REWARDS\_IS\_ACTIVE](#consensus_rewards_is_active) - [INVALID\_STAKER](#invalid_staker) - [INVALID\_TOKEN\_DECIMALS](#invalid_token_decimals) + - [INVALID\_MIN\_MAX\_BLOCK\_DURATION](#invalid_min_max_block_duration) - [Structs](#structs) - [StakerPoolInfoV1](#stakerpoolinfov1) - [StakerInfoV1](#stakerinfov1) @@ -254,6 +257,7 @@ - [AttestationInfo](#attestationinfo) - [EpochInfo](#epochinfo) - [MintingCurveContractInfo](#mintingcurvecontractinfo) + - [BlockDurationConfig](#blockdurationconfig) - [Type aliases](#type-aliases) - [Amount](#amount) - [Commission](#commission) @@ -2344,6 +2348,36 @@ Returns the alpha parameter, as percentage, used when computing BTC rewards. #### access control Any address can execute. +### get_block_duration_config +```rust +fn get_block_duration_config(self: @TContractState) -> BlockdurationConfig; +``` +#### description +Returns [BlockdurationConfig](#blockdurationconfig). +#### emits +#### errors +#### pre-condition +#### logic +#### access control +Any address can execute. + +### get_block_duration_config +```rust +fn set_block_duration_config(ref self: TContractState, block_duration_config: BlockdurationConfig); +``` +#### description +Set the block duration configuration. +#### emits +#### errors +1. [ONLY\_APP\_GOVERNOR](#only_app_governor) +2. [INVALID\_MIN\_MAX\_BLOCK\_duration](#invalid_min_max_block_duration) +#### pre-condition +1. 0 < `block_duration_config.weighted_avg_factor` <= 100 +2. 0 < `block_duration_config.min_block_duration` <= `block_duration_config.max_block_duration` +#### logic +#### access control +Only app governor. + ## Events ### Mint Request | data | type | keyed | @@ -2797,6 +2831,9 @@ Only token admin. ### INVALID_STAKER "Staker is invalid for getting rewards" +### INVALID_MIN_MAX_BLOCK_DURATION +"Invalid min/max block duration" + # Structs ### StakerPoolInfoV1 | name | type | @@ -2918,6 +2955,12 @@ Only token admin. | c_num | Inflation | | c_denom | Inflation | +### BlockDurationConfig +| name | type | +| ------------------- | ---- | +| min_block_duration | u64 | +| max_block_duration | u64 | + # Type aliases ### Amount Amount: u128 diff --git a/src/reward_supplier/errors.cairo b/src/reward_supplier/errors.cairo index 2e697f3d..2ee13175 100644 --- a/src/reward_supplier/errors.cairo +++ b/src/reward_supplier/errors.cairo @@ -5,6 +5,9 @@ pub enum Error { ON_RECEIVE_NOT_FROM_STARKGATE, UNEXPECTED_TOKEN, BLOCK_DURATION_OVERFLOW, + INVALID_BLOCK_NUMBER, + INVALID_BLOCK_TIMESTAMP, + INVALID_MIN_MAX_BLOCK_DURATION, } impl DescribableError of Describable { @@ -13,6 +16,9 @@ impl DescribableError of Describable { Error::ON_RECEIVE_NOT_FROM_STARKGATE => "Only StarkGate can call on_receive", Error::UNEXPECTED_TOKEN => "Unexpected token", Error::BLOCK_DURATION_OVERFLOW => "Block duration calculation overflow", + Error::INVALID_BLOCK_NUMBER => "Invalid block number", + Error::INVALID_BLOCK_TIMESTAMP => "Invalid block timestamp", + Error::INVALID_MIN_MAX_BLOCK_DURATION => "Invalid min/max block duration", } } } diff --git a/src/reward_supplier/interface.cairo b/src/reward_supplier/interface.cairo index eb0ddd28..c3dc2049 100644 --- a/src/reward_supplier/interface.cairo +++ b/src/reward_supplier/interface.cairo @@ -78,6 +78,28 @@ pub trait IRewardSupplier { fn contract_parameters_v1(self: @TContractState) -> RewardSupplierInfoV1; /// Returns the alpha parameter, as percentage, used when computing BTC rewards. fn get_alpha(self: @TContractState) -> u128; + /// Returns the block duration configuration. + fn get_block_duration_config(self: @TContractState) -> BlockDurationConfig; +} + +#[starknet::interface] +pub trait IRewardSupplierConfig { + /// Sets the block duration configuration. + /// + /// #### Preconditions: + /// - `block_duration_config.min_block_duration > 0` + /// - `block_duration_config.min_block_duration <= block_duration_config.max_block_duration` + /// + /// #### Errors: + /// - [`ONLY_APP_GOVERNOR`](AccessErrors::ONLY_APP_GOVERNOR) + /// - + /// [`INVALID_MIN_MAX_BLOCK_DURATION`](staking::reward_supplier::errors::Error::INVALID_MIN_MAX_BLOCK_DURATION) + /// + /// #### Access control: + /// Only app governor. + fn set_block_duration_config( + ref self: TContractState, block_duration_config: BlockDurationConfig, + ); } pub mod Events { @@ -95,3 +117,12 @@ pub struct RewardSupplierInfoV1 { pub unclaimed_rewards: Amount, pub l1_pending_requested_amount: Amount, } + +/// Configuration for block duration calculation. +#[derive(Debug, Copy, Drop, Serde, PartialEq, starknet::Store)] +pub struct BlockDurationConfig { + /// Minimum block duration, in units of 1 / BLOCK_DURATION_SCALE seconds. + pub min_block_duration: u64, + /// Maximum block duration, in units of 1 / BLOCK_DURATION_SCALE seconds. + pub max_block_duration: u64, +} diff --git a/src/reward_supplier/reward_supplier.cairo b/src/reward_supplier/reward_supplier.cairo index 8472ce8e..d8787aa2 100644 --- a/src/reward_supplier/reward_supplier.cairo +++ b/src/reward_supplier/reward_supplier.cairo @@ -10,7 +10,9 @@ pub mod RewardSupplier { use staking::errors::{GenericError, InternalError}; use staking::minting_curve::interface::{IMintingCurveDispatcher, IMintingCurveDispatcherTrait}; use staking::reward_supplier::errors::Error; - use staking::reward_supplier::interface::{Events, IRewardSupplier, RewardSupplierInfoV1}; + use staking::reward_supplier::interface::{ + BlockDurationConfig, Events, IRewardSupplier, IRewardSupplierConfig, RewardSupplierInfoV1, + }; use staking::reward_supplier::utils::{calculate_btc_rewards, compute_threshold}; use staking::staking::interface::{IStakingDispatcher, IStakingDispatcherTrait}; use staking::staking::objects::EpochInfoTrait; @@ -33,6 +35,10 @@ pub mod RewardSupplier { pub(crate) const BLOCK_DURATION_SCALE: u64 = 100; /// Default avg block duration. pub(crate) const DEFAULT_AVG_BLOCK_DURATION: u64 = 3 * BLOCK_DURATION_SCALE; + /// Default block duration configuration. + pub(crate) const DEFAULT_BLOCK_DURATION_CONFIG: BlockDurationConfig = BlockDurationConfig { + min_block_duration: 2 * BLOCK_DURATION_SCALE, max_block_duration: 5 * BLOCK_DURATION_SCALE, + }; component!(path: ReplaceabilityComponent, storage: replaceability, event: ReplaceabilityEvent); component!(path: RolesComponent, storage: roles, event: RolesEvent); @@ -76,7 +82,7 @@ pub mod RewardSupplier { l1_reward_supplier: felt252, /// Token bridge address. starkgate_address: ContractAddress, - /// Average block duration in units of 1 / BLOCK_TIME_SCALE seconds. + /// Average block duration in units of 1 / BLOCK_DURATION_SCALE seconds. // TODO: Initial in EIC. // TODO: Setter. // TODO: View? @@ -84,6 +90,9 @@ pub mod RewardSupplier { /// The latest block data used for average block duration calculation. /// Updated at the start of each epoch. block_snapshot: (BlockNumber, Timestamp), + /// Configuration for block duration calculation. + // TODO: Initial in EIC. + block_duration_config: BlockDurationConfig, } #[event] @@ -123,6 +132,7 @@ pub mod RewardSupplier { self.l1_reward_supplier.write(l1_reward_supplier); self.starkgate_address.write(starkgate_address); self.avg_block_duration.write(DEFAULT_AVG_BLOCK_DURATION); + self.block_duration_config.write(DEFAULT_BLOCK_DURATION_CONFIG); } #[abi(embed_v0)] @@ -255,6 +265,35 @@ pub mod RewardSupplier { fn get_alpha(self: @ContractState) -> u128 { ALPHA } + + fn get_block_duration_config(self: @ContractState) -> BlockDurationConfig { + self.block_duration_config.read() + } + } + + #[abi(embed_v0)] + impl RewardSupplierConfigImpl of IRewardSupplierConfig { + fn set_block_duration_config( + ref self: ContractState, block_duration_config: BlockDurationConfig, + ) { + self.roles.only_app_governor(); + // TODO: Emit event? + // Assert that block_time_config is valid. + // TODO: More validations? + assert!( + block_duration_config.min_block_duration > 0, + "{}", + Error::INVALID_MIN_MAX_BLOCK_DURATION, + ); + assert!( + block_duration_config + .min_block_duration <= block_duration_config + .max_block_duration, + "{}", + Error::INVALID_MIN_MAX_BLOCK_DURATION, + ); + self.block_duration_config.write(block_duration_config); + } } #[generate_trait] diff --git a/src/reward_supplier/test.cairo b/src/reward_supplier/test.cairo index 13646686..d3ae5ec8 100644 --- a/src/reward_supplier/test.cairo +++ b/src/reward_supplier/test.cairo @@ -11,13 +11,17 @@ use snforge_std::{ use staking::constants::{ALPHA, ALPHA_DENOMINATOR, SECONDS_IN_YEAR, STRK_IN_FRIS}; use staking::errors::{GenericError, InternalError}; use staking::minting_curve::interface::{IMintingCurveDispatcher, IMintingCurveDispatcherTrait}; +use staking::reward_supplier::errors::Error; use staking::reward_supplier::interface::{ - IRewardSupplier, IRewardSupplierDispatcher, IRewardSupplierDispatcherTrait, - IRewardSupplierSafeDispatcher, IRewardSupplierSafeDispatcherTrait, RewardSupplierInfoV1, + BlockDurationConfig, IRewardSupplier, IRewardSupplierConfigDispatcher, + IRewardSupplierConfigDispatcherTrait, IRewardSupplierConfigSafeDispatcher, + IRewardSupplierConfigSafeDispatcherTrait, IRewardSupplierDispatcher, + IRewardSupplierDispatcherTrait, IRewardSupplierSafeDispatcher, + IRewardSupplierSafeDispatcherTrait, RewardSupplierInfoV1, }; use staking::reward_supplier::reward_supplier::RewardSupplier; use staking::reward_supplier::reward_supplier::RewardSupplier::{ - BLOCK_DURATION_SCALE, DEFAULT_AVG_BLOCK_DURATION, + BLOCK_DURATION_SCALE, DEFAULT_AVG_BLOCK_DURATION, DEFAULT_BLOCK_DURATION_CONFIG, }; use staking::reward_supplier::utils::compute_threshold; use staking::staking::interface::{IStakingDispatcher, IStakingDispatcherTrait}; @@ -41,7 +45,6 @@ use test_utils::{ stake_for_testing_using_dispatcher, }; - #[test] fn test_identity() { assert!(reward_supplier_identity == 'Reward Supplier'); @@ -534,3 +537,68 @@ fn test_update_current_epoch_block_rewards_assertions() { :result, expected_error: InternalError::INVALID_BLOCK_TIMESTAMP.describe(), ); } + +#[test] +fn test_get_block_duration_config() { + let mut cfg: StakingInitConfig = Default::default(); + general_contract_system_deployment(ref :cfg); + let reward_supplier = cfg.staking_contract_info.reward_supplier; + let reward_supplier_dispatcher = IRewardSupplierDispatcher { + contract_address: reward_supplier, + }; + let block_duration_config = reward_supplier_dispatcher.get_block_duration_config(); + assert!(block_duration_config == DEFAULT_BLOCK_DURATION_CONFIG); +} + +#[test] +fn test_set_block_duration_config() { + let mut cfg: StakingInitConfig = Default::default(); + general_contract_system_deployment(ref :cfg); + let reward_supplier = cfg.staking_contract_info.reward_supplier; + let reward_supplier_dispatcher = IRewardSupplierDispatcher { + contract_address: reward_supplier, + }; + let reward_supplier_config_dispatcher = IRewardSupplierConfigDispatcher { + contract_address: reward_supplier, + }; + let app_governor = cfg.test_info.app_governor; + let block_duration_config = BlockDurationConfig { + min_block_duration: 90, max_block_duration: 350, + }; + assert!(reward_supplier_dispatcher.get_block_duration_config() != block_duration_config); + cheat_caller_address_once(contract_address: reward_supplier, caller_address: app_governor); + reward_supplier_config_dispatcher.set_block_duration_config(:block_duration_config); + assert!(reward_supplier_dispatcher.get_block_duration_config() == block_duration_config); +} + +#[test] +#[feature("safe_dispatcher")] +fn test_set_block_duration_config_assertions() { + let mut cfg: StakingInitConfig = Default::default(); + general_contract_system_deployment(ref :cfg); + let reward_supplier = cfg.staking_contract_info.reward_supplier; + let reward_supplier_config_safe_dispatcher = IRewardSupplierConfigSafeDispatcher { + contract_address: reward_supplier, + }; + let app_governor = cfg.test_info.app_governor; + let mut block_duration_config = DEFAULT_BLOCK_DURATION_CONFIG; + // Catch ONLY_APP_GOVERNOR. + let result = reward_supplier_config_safe_dispatcher + .set_block_duration_config(:block_duration_config); + assert_panic_with_error(:result, expected_error: "ONLY_APP_GOVERNOR"); + // Catch INVALID_MIN_MAX_BLOCK_DURATION. + block_duration_config.min_block_duration = block_duration_config.max_block_duration + 1; + cheat_caller_address_once(contract_address: reward_supplier, caller_address: app_governor); + let result = reward_supplier_config_safe_dispatcher + .set_block_duration_config(:block_duration_config); + assert_panic_with_error( + :result, expected_error: Error::INVALID_MIN_MAX_BLOCK_DURATION.describe(), + ); + block_duration_config.min_block_duration = 0; + cheat_caller_address_once(contract_address: reward_supplier, caller_address: app_governor); + let result = reward_supplier_config_safe_dispatcher + .set_block_duration_config(:block_duration_config); + assert_panic_with_error( + :result, expected_error: Error::INVALID_MIN_MAX_BLOCK_DURATION.describe(), + ); +} diff --git a/src/test_utils.cairo b/src/test_utils.cairo index 785bea1f..318a27fb 100644 --- a/src/test_utils.cairo +++ b/src/test_utils.cairo @@ -383,6 +383,16 @@ pub(crate) fn deploy_reward_supplier_contract(cfg: StakingInitConfig) -> Contrac cfg.test_info.governance_admin.serialize(ref calldata); let reward_supplier_contract = snforge_std::declare("RewardSupplier").unwrap().contract_class(); let (reward_supplier_contract_address, _) = reward_supplier_contract.deploy(@calldata).unwrap(); + set_account_as_app_role_admin( + contract: reward_supplier_contract_address, + account: cfg.test_info.app_role_admin, + governance_admin: cfg.test_info.governance_admin, + ); + set_account_as_app_governor( + contract: reward_supplier_contract_address, + account: cfg.test_info.app_governor, + app_role_admin: cfg.test_info.app_role_admin, + ); reward_supplier_contract_address } From 353e2e753bae9da7e4e5f7841bca19cf24e33267 Mon Sep 17 00:00:00 2001 From: Noa Wolfgor Date: Sun, 5 Oct 2025 22:49:53 +0300 Subject: [PATCH 03/10] feat: use block duration config in set_avg_block_durarion to adust the avg --- Scarb.lock | 4 +- Scarb.toml | 4 +- src/flow_test/flow_ideas.md | 3 +- src/reward_supplier/reward_supplier.cairo | 13 +++- src/reward_supplier/test.cairo | 92 ++++++++++++++++++++++- src/test_utils.cairo | 7 ++ 6 files changed, 112 insertions(+), 11 deletions(-) diff --git a/Scarb.lock b/Scarb.lock index 84a0d39d..18a8e809 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -147,7 +147,7 @@ dependencies = [ [[package]] name = "starkware_utils" version = "1.0.0" -source = "git+https://github.com/starkware-libs/starkware-starknet-utils?rev=e1955423808045de868987b8fb0b43f5cbdf5699#e1955423808045de868987b8fb0b43f5cbdf5699" +source = "git+https://github.com/starkware-libs/starkware-starknet-utils?rev=7b46975d5612fb1b920bb248941030bf6c295d44#7b46975d5612fb1b920bb248941030bf6c295d44" dependencies = [ "openzeppelin", ] @@ -155,7 +155,7 @@ dependencies = [ [[package]] name = "starkware_utils_testing" version = "1.0.0" -source = "git+https://github.com/starkware-libs/starkware-starknet-utils?rev=e1955423808045de868987b8fb0b43f5cbdf5699#e1955423808045de868987b8fb0b43f5cbdf5699" +source = "git+https://github.com/starkware-libs/starkware-starknet-utils?rev=7b46975d5612fb1b920bb248941030bf6c295d44#7b46975d5612fb1b920bb248941030bf6c295d44" dependencies = [ "openzeppelin", "snforge_std", diff --git a/Scarb.toml b/Scarb.toml index c26a0ab2..cafaf2d5 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -6,12 +6,12 @@ edition = "2024_07" [dependencies] starknet = "2.12.0" openzeppelin = "2.0.0" -starkware_utils = { git = "https://github.com/starkware-libs/starkware-starknet-utils", rev = "e1955423808045de868987b8fb0b43f5cbdf5699" } +starkware_utils = { git = "https://github.com/starkware-libs/starkware-starknet-utils", rev = "7b46975d5612fb1b920bb248941030bf6c295d44" } [dev-dependencies] assert_macros = "2.12.0" snforge_std = "0.49.0" -starkware_utils_testing = { git = "https://github.com/starkware-libs/starkware-starknet-utils", rev = "e1955423808045de868987b8fb0b43f5cbdf5699" } +starkware_utils_testing = { git = "https://github.com/starkware-libs/starkware-starknet-utils", rev = "7b46975d5612fb1b920bb248941030bf6c295d44" } [scripts] test = "snforge test" diff --git a/src/flow_test/flow_ideas.md b/src/flow_test/flow_ideas.md index 1f5720d8..694d80c6 100644 --- a/src/flow_test/flow_ideas.md +++ b/src/flow_test/flow_ideas.md @@ -53,4 +53,5 @@ more ideas: ## block rewards by timestamp - advance blocks with different block times and check the avg is calculated correctly - update_rewards for blocks in same epoch - same rewards, then advance epoch, different rewards, update rewards for blocks in same epoch - same rewards. -- update rewards is not called every block, still rewards is updated correctly (miss block, miss first block in epoch, miss epoch) \ No newline at end of file +- update rewards is not called every block, still rewards is updated correctly (miss block, miss first block in epoch, miss epoch) +- set block time config and test rewards after diff --git a/src/reward_supplier/reward_supplier.cairo b/src/reward_supplier/reward_supplier.cairo index d8787aa2..3817e4ad 100644 --- a/src/reward_supplier/reward_supplier.cairo +++ b/src/reward_supplier/reward_supplier.cairo @@ -27,8 +27,9 @@ pub mod RewardSupplier { use starkware_utils::erc20::erc20_utils::CheckedIERC20DispatcherTrait; use starkware_utils::errors::OptionAuxTrait; use starkware_utils::interfaces::identity::Identity; - use starkware_utils::math::utils::{ceil_of_division, mul_wide_and_div}; + use starkware_utils::math::utils::{Clamp, ceil_of_division, mul_wide_and_div}; use starkware_utils::time::time::Timestamp; + pub const CONTRACT_IDENTITY: felt252 = 'Reward Supplier'; pub const CONTRACT_VERSION: felt252 = '3.0.0'; /// Scale factor for block duration measurements. 100 implies granularity of 100th of second. @@ -367,11 +368,17 @@ pub mod RewardSupplier { // We calculate `num_blocks` instead of using the configured value to keep the average // accurate even if some calls are missed. let num_blocks = current_block_number - snapshot_block_number; - let calculated_block_duration = mul_wide_and_div( + let mut calculated_block_duration = mul_wide_and_div( lhs: time_delta, rhs: BLOCK_DURATION_SCALE, div: num_blocks, ) .expect_with_err(err: Error::BLOCK_DURATION_OVERFLOW); - // TODO: Adjust calculated_block_duration with MIN_BLOCK_TIME and MAX_BLOCK_TIME. + // Adjust calculated_block_duration with min and max block duration. + let block_duration_config = self.block_duration_config.read(); + calculated_block_duration = calculated_block_duration + .clamp( + block_duration_config.min_block_duration, + block_duration_config.max_block_duration, + ); self.avg_block_duration.write(calculated_block_duration); } } diff --git a/src/reward_supplier/test.cairo b/src/reward_supplier/test.cairo index d3ae5ec8..41f5f971 100644 --- a/src/reward_supplier/test.cairo +++ b/src/reward_supplier/test.cairo @@ -40,9 +40,9 @@ use starkware_utils_testing::test_utils::{ advance_block_number_global, assert_panic_with_error, cheat_caller_address_once, check_identity, }; use test_utils::{ - StakingInitConfig, advance_epoch_global, advance_k_epochs_global, advance_time_global, fund, - general_contract_system_deployment, initialize_reward_supplier_state_from_cfg, load_one_felt, - stake_for_testing_using_dispatcher, + StakingInitConfig, advance_epoch_global, advance_epoch_global_custom_time, + advance_k_epochs_global, advance_time_global, fund, general_contract_system_deployment, + initialize_reward_supplier_state_from_cfg, load_one_felt, stake_for_testing_using_dispatcher, }; #[test] @@ -602,3 +602,89 @@ fn test_set_block_duration_config_assertions() { :result, expected_error: Error::INVALID_MIN_MAX_BLOCK_DURATION.describe(), ); } + +#[test] +fn test_update_current_epoch_block_rewards_with_adjustments() { + let mut cfg: StakingInitConfig = Default::default(); + general_contract_system_deployment(ref :cfg); + stake_for_testing_using_dispatcher(:cfg); + advance_k_epochs_global(); + let reward_supplier = cfg.staking_contract_info.reward_supplier; + let reward_supplier_dispatcher = IRewardSupplierDispatcher { + contract_address: reward_supplier, + }; + let minting_curve_dispatcher = IMintingCurveDispatcher { + contract_address: cfg.reward_supplier.minting_curve_contract, + }; + let staking_contract = cfg.test_info.staking_contract; + // First snapshot, not update avg_block_time. Rewards are calculated using the default avg block + // time. + cheat_caller_address_once(contract_address: reward_supplier, caller_address: staking_contract); + let (_, _) = reward_supplier_dispatcher.update_current_epoch_block_rewards(); + let mut curr_avg_block_time = DEFAULT_AVG_BLOCK_DURATION; + // Adjust avg_block_time to MIN (avg is less than min). + let min_block_time = DEFAULT_BLOCK_DURATION_CONFIG.min_block_duration; + advance_epoch_global_custom_time( + block_time: TimeDelta { seconds: min_block_time / BLOCK_DURATION_SCALE - 1 }, + ); + cheat_caller_address_once(contract_address: reward_supplier, caller_address: staking_contract); + let (strk_rewards, btc_rewards) = reward_supplier_dispatcher + .update_current_epoch_block_rewards(); + // Test avg_block_time. + curr_avg_block_time = min_block_time; + let avg_block_time = load_one_felt( + target: reward_supplier, storage_address: selector!("avg_block_duration"), + ) + .try_into() + .unwrap(); + assert!(avg_block_time == curr_avg_block_time); + // Test rewards. + let yearly_mint = minting_curve_dispatcher.yearly_mint(); + let expected_rewards = mul_wide_and_div( + lhs: yearly_mint, + rhs: curr_avg_block_time.into(), + div: BLOCK_DURATION_SCALE.into() * SECONDS_IN_YEAR.into(), + ) + .expect_with_err(err: InternalError::REWARDS_COMPUTATION_OVERFLOW); + let expected_btc_rewards = mul_wide_and_div( + lhs: expected_rewards, rhs: ALPHA, div: ALPHA_DENOMINATOR, + ) + .unwrap(); + let expected_strk_rewards = expected_rewards - expected_btc_rewards; + assert!(expected_strk_rewards.is_non_zero()); + assert!(expected_btc_rewards.is_non_zero()); + assert!(strk_rewards == expected_strk_rewards); + assert!(btc_rewards == expected_btc_rewards); + // Adjust avg_block_time to MAX (avg is more than max). + let max_block_time = DEFAULT_BLOCK_DURATION_CONFIG.max_block_duration; + advance_epoch_global_custom_time( + block_time: TimeDelta { seconds: max_block_time / BLOCK_DURATION_SCALE + 1 }, + ); + cheat_caller_address_once(contract_address: reward_supplier, caller_address: staking_contract); + let (strk_rewards, btc_rewards) = reward_supplier_dispatcher + .update_current_epoch_block_rewards(); + // Test avg_block_time. + curr_avg_block_time = max_block_time; + let avg_block_time = load_one_felt( + target: reward_supplier, storage_address: selector!("avg_block_duration"), + ) + .try_into() + .unwrap(); + assert!(avg_block_time == curr_avg_block_time); + // Test rewards. + let expected_rewards = mul_wide_and_div( + lhs: yearly_mint, + rhs: curr_avg_block_time.into(), + div: BLOCK_DURATION_SCALE.into() * SECONDS_IN_YEAR.into(), + ) + .expect_with_err(err: InternalError::REWARDS_COMPUTATION_OVERFLOW); + let expected_btc_rewards = mul_wide_and_div( + lhs: expected_rewards, rhs: ALPHA, div: ALPHA_DENOMINATOR, + ) + .unwrap(); + let expected_strk_rewards = expected_rewards - expected_btc_rewards; + assert!(expected_strk_rewards.is_non_zero()); + assert!(expected_btc_rewards.is_non_zero()); + assert!(strk_rewards == expected_strk_rewards); + assert!(btc_rewards == expected_btc_rewards); +} diff --git a/src/test_utils.cairo b/src/test_utils.cairo index 318a27fb..88dcf406 100644 --- a/src/test_utils.cairo +++ b/src/test_utils.cairo @@ -924,6 +924,13 @@ pub(crate) fn advance_k_epochs_global() { } } +/// Advance one epoch with the given `block_time` per block. +pub(crate) fn advance_epoch_global_custom_time(block_time: TimeDelta) { + advance_block_number_global(blocks: EPOCH_LENGTH.into()); + let time = TimeDelta { seconds: block_time.seconds * EPOCH_LENGTH.into() }; + advance_time_global(:time); +} + // ---- Calculate Rewards - V0 (index based) ----- /// Update rewards for STRK pool. From 2793f131950927768437a5b9c2a8753a5461acbc Mon Sep 17 00:00:00 2001 From: Noa Wolfgor Date: Wed, 8 Oct 2025 19:21:38 +0300 Subject: [PATCH 04/10] feat(staking): migration to rewards by timestamp --- src/flow_test/flow_ideas.md | 6 +++ src/flow_test/test.cairo | 8 ++-- src/staking/staking.cairo | 20 ++++++-- src/staking/tests/test.cairo | 88 +++++++++++++++++++++++++++++++++++- src/test_utils.cairo | 9 +++- 5 files changed, 122 insertions(+), 9 deletions(-) diff --git a/src/flow_test/flow_ideas.md b/src/flow_test/flow_ideas.md index 694d80c6..3d0d7fdc 100644 --- a/src/flow_test/flow_ideas.md +++ b/src/flow_test/flow_ideas.md @@ -55,3 +55,9 @@ more ideas: - update_rewards for blocks in same epoch - same rewards, then advance epoch, different rewards, update rewards for blocks in same epoch - same rewards. - update rewards is not called every block, still rewards is updated correctly (miss block, miss first block in epoch, miss epoch) - set block time config and test rewards after + +## rewards by timestamp - migration +- set_consensus_rewards to future epoch, call update_rewards before consensus epoch and after, test rewards. +- set_consensus_rewards to future epoch, call update_rewards only after consensus epoch, test rewards. +- set_consensus_rewards to curr_epoch + 2. test rewards before and after. tets avg block time is update correctly. +- set_consensus_rewards, update_rewards, then set_consensus_rewards to later epoch, update_rewards, then set_consensus_rewards to earlier epoch, update_rewards, test avg block time. diff --git a/src/flow_test/test.cairo b/src/flow_test/test.cairo index c7c30a71..e8b0ee32 100644 --- a/src/flow_test/test.cairo +++ b/src/flow_test/test.cairo @@ -18,11 +18,11 @@ use staking::staking::interface::{ }; use staking::staking::utils::{BTC_WEIGHT_FACTOR, STAKING_POWER_BASE_VALUE, STRK_WEIGHT_FACTOR}; use staking::test_utils::constants::{ - BTC_18D_CONFIG, BTC_5D_CONFIG, BTC_8D_CONFIG, PUBLIC_KEY, STRK_BASE_VALUE, + AVG_BLOCK_DURATION, BTC_18D_CONFIG, BTC_5D_CONFIG, BTC_8D_CONFIG, PUBLIC_KEY, STRK_BASE_VALUE, TEST_MIN_BTC_FOR_REWARDS, }; use staking::test_utils::{ - StakingInitConfig, calculate_staker_btc_pool_rewards_v2, + StakingInitConfig, advance_blocks, calculate_staker_btc_pool_rewards_v2, calculate_staker_strk_rewards_with_balances_v2, calculate_staker_strk_rewards_with_balances_v3, calculate_strk_pool_rewards_with_pool_balance_v2, compute_rewards_per_unit, custom_decimals_token, deploy_mock_erc20_decimals_contract, @@ -2830,7 +2830,7 @@ fn update_rewards_disable_rewards_consensus_rewards_flow_test() { assert_panic_with_error( :result, expected_error: StakingError::REWARDS_ALREADY_UPDATED.describe(), ); - advance_block_number_global(blocks: 1); + advance_blocks(blocks: 1, block_duration: AVG_BLOCK_DURATION); // Disable rewards = false before consensus epoch - no rewards system.update_rewards(:staker, disable_rewards: false); @@ -2860,7 +2860,7 @@ fn update_rewards_disable_rewards_consensus_rewards_flow_test() { assert_panic_with_error( :result, expected_error: StakingError::REWARDS_ALREADY_UPDATED.describe(), ); - advance_block_number_global(blocks: 1); + advance_blocks(blocks: 1, block_duration: AVG_BLOCK_DURATION); // Disable rewards = false with consensus on - rewards system.update_rewards(:staker, disable_rewards: false); diff --git a/src/staking/staking.cairo b/src/staking/staking.cairo index cc9147a6..e04c3db4 100644 --- a/src/staking/staking.cairo +++ b/src/staking/staking.cairo @@ -1278,8 +1278,22 @@ pub mod Staking { fn set_consensus_rewards_first_epoch(ref self: ContractState, epoch_id: Epoch) { self.roles.only_app_governor(); - assert!(epoch_id >= self.get_current_epoch() + 2, "{}", Error::INVALID_EPOCH); + let curr_epoch = self.get_current_epoch(); + assert!(epoch_id >= curr_epoch + 2, "{}", Error::INVALID_EPOCH); assert!(self.is_pre_consensus(), "{}", Error::CONSENSUS_REWARDS_IS_ACTIVE); + // If it's the first time setting the consensus rewards first epoch, initialize block + // rewards. + // We use `curr_epoch + 1` to ensure the average is computed only after completing + // at least one full epoch. This prevents calculating the average over too few blocks, + // since we are currently mid-epoch. The next calculation will therefore occur after + // at least one complete epoch (the next epoch) has passed. + if self.consensus_rewards_first_epoch.read().is_zero() { + self + .calculate_block_rewards( + reward_supplier_dispatcher: self.reward_supplier_dispatcher.read(), + curr_epoch: curr_epoch + 1, + ); + } self.consensus_rewards_first_epoch.write(epoch_id); self .emit( @@ -1496,9 +1510,9 @@ pub mod Staking { /// Calculates the rewards for a block in the current epoch (for STRK and BTC). /// /// Precondition: `curr_epoch` must be `get_current_epoch()`, it's passed as a param to save - /// storage reads. + /// storage reads. This precondition is only violated during the migration to consensus + /// rewards. // TODO: Consider view? - // TODO: Migration. fn calculate_block_rewards( ref self: ContractState, reward_supplier_dispatcher: IRewardSupplierDispatcher, diff --git a/src/staking/tests/test.cairo b/src/staking/tests/test.cairo index 468e110c..389ea7ff 100644 --- a/src/staking/tests/test.cairo +++ b/src/staking/tests/test.cairo @@ -67,7 +67,7 @@ use staking::staking::staking::Staking::{ DEFAULT_EXIT_WAIT_WINDOW, MAX_EXIT_WAIT_WINDOW, V3_PREV_CONTRACT_VERSION, }; use staking::staking::utils::STRK_WEIGHT_FACTOR; -use staking::types::{Epoch, InternalStakerInfoLatest, VecIndex}; +use staking::types::{Amount, Epoch, InternalStakerInfoLatest, VecIndex}; use staking::{event_test_utils, test_utils}; use starknet::class_hash::ClassHash; use starknet::{ContractAddress, Store, get_block_number}; @@ -3441,6 +3441,7 @@ fn test_update_rewards_from_attestation_contract_assertions() { let attestation_contract = cfg.test_info.attestation_contract; let staking_dispatcher = IStakingDispatcher { contract_address: staking_contract }; let staking_config_dispatcher = IStakingConfigDispatcher { contract_address: staking_contract }; + advance_epoch_global(); stake_for_testing_using_dispatcher(:cfg); // Catch CALLER_IS_NOT_ATTESTATION_CONTRACT. @@ -3492,6 +3493,7 @@ fn test_update_rewards_only_staker() { }; let staking_config_dispatcher = IStakingConfigDispatcher { contract_address: staking_contract }; let minting_curve_contract = cfg.reward_supplier.minting_curve_contract; + advance_epoch_global(); let current_epoch = staking_dispatcher.get_current_epoch(); cheat_caller_address_once( contract_address: staking_contract, caller_address: cfg.test_info.app_governor, @@ -3533,6 +3535,7 @@ fn test_update_rewards_miss_blocks() { }; let staking_config_dispatcher = IStakingConfigDispatcher { contract_address: staking_contract }; let minting_curve_contract = cfg.reward_supplier.minting_curve_contract; + advance_epoch_global(); let current_epoch = staking_dispatcher.get_current_epoch(); cheat_caller_address_once( contract_address: staking_contract, caller_address: cfg.test_info.app_governor, @@ -3570,6 +3573,7 @@ fn test_update_rewards_with_strk_pool() { let token = cfg.test_info.strk_token; let token_address = token.contract_address(); let token_dispatcher = IERC20Dispatcher { contract_address: token_address }; + advance_epoch_global(); let current_epoch = staking_dispatcher.get_current_epoch(); cheat_caller_address_once( contract_address: staking_contract, caller_address: cfg.test_info.app_governor, @@ -3658,6 +3662,7 @@ fn test_update_rewards_with_both_strk_and_btc() { let btc_token = cfg.test_info.btc_token; let btc_token_address = btc_token.contract_address(); let token_dispatcher = IERC20Dispatcher { contract_address: token_address }; + advance_epoch_global(); let current_epoch = staking_dispatcher.get_current_epoch(); cheat_caller_address_once( contract_address: staking_contract, caller_address: cfg.test_info.app_governor, @@ -3910,6 +3915,7 @@ fn test_update_rewards_assertions_already_consensus() { }; let staking_config_dispatcher = IStakingConfigDispatcher { contract_address: staking_contract }; let staker_address = cfg.test_info.staker_address; + advance_epoch_global(); let current_epoch = staking_dispatcher.get_current_epoch(); cheat_caller_address_once( contract_address: staking_contract, caller_address: cfg.test_info.app_governor, @@ -4566,6 +4572,9 @@ fn test_set_consensus_rewards_first_epoch() { let staking_safe_config_dispatcher = IStakingConfigSafeDispatcher { contract_address: staking_contract, }; + let minting_curve_contract = cfg.reward_supplier.minting_curve_contract; + stake_for_testing_using_dispatcher(:cfg); + advance_k_epochs_global(); let current_epoch = staking_dispatcher.get_current_epoch(); let mut spy = snforge_std::spy_events(); cheat_caller_address_once( @@ -4578,6 +4587,31 @@ fn test_set_consensus_rewards_first_epoch() { .try_into() .unwrap(); assert!(consensus_rewards_first_epoch == current_epoch + 2); + // Test that block rewards are initialized. + let block_rewards = snforge_std::load( + target: staking_contract, + storage_address: selector!("block_rewards"), + size: Store::<(Amount, Amount)>::size().into(), + ) + .span(); + let strk_block_rewards = (*block_rewards.at(0)).try_into().unwrap(); + let btc_block_rewards = (*block_rewards.at(1)).try_into().unwrap(); + let (expected_strk_block_rewards, expected_btc_block_rewards) = + calculate_current_block_rewards_v3( + :minting_curve_contract, + ); + assert!(expected_strk_block_rewards.is_non_zero()); + assert!(expected_btc_block_rewards.is_non_zero()); + assert!(strk_block_rewards == expected_strk_block_rewards); + assert!(btc_block_rewards == expected_btc_block_rewards); + let last_calculated_epoch = load_one_felt( + target: staking_contract, storage_address: selector!("last_calculated_epoch"), + ) + .try_into() + .unwrap(); + let expected_last_calculated_epoch = current_epoch + 1; + assert!(last_calculated_epoch == expected_last_calculated_epoch); + // Test that event is emitted. let events = spy.get_events().emitted_by(contract_address: staking_contract).events; assert_number_of_events( actual: events.len(), expected: 1, message: "set_consensus_rewards_first_epoch", @@ -4597,6 +4631,23 @@ fn test_set_consensus_rewards_first_epoch() { .try_into() .unwrap(); assert!(consensus_rewards_first_epoch == current_epoch + 4); + // Test that block rewards are initialized. + let block_rewards = snforge_std::load( + target: staking_contract, + storage_address: selector!("block_rewards"), + size: Store::<(Amount, Amount)>::size().into(), + ) + .span(); + let strk_block_rewards = (*block_rewards.at(0)).try_into().unwrap(); + let btc_block_rewards = (*block_rewards.at(1)).try_into().unwrap(); + assert!(strk_block_rewards == expected_strk_block_rewards); + assert!(btc_block_rewards == expected_btc_block_rewards); + let last_calculated_epoch = load_one_felt( + target: staking_contract, storage_address: selector!("last_calculated_epoch"), + ) + .try_into() + .unwrap(); + assert!(last_calculated_epoch == expected_last_calculated_epoch); // Set consensus rewards epoch again with earlier epoch. cheat_caller_address_once( contract_address: staking_contract, caller_address: cfg.test_info.app_governor, @@ -4608,6 +4659,23 @@ fn test_set_consensus_rewards_first_epoch() { .try_into() .unwrap(); assert!(consensus_rewards_first_epoch == current_epoch + 3); + // Test that block rewards are initialized. + let block_rewards = snforge_std::load( + target: staking_contract, + storage_address: selector!("block_rewards"), + size: Store::<(Amount, Amount)>::size().into(), + ) + .span(); + let strk_block_rewards = (*block_rewards.at(0)).try_into().unwrap(); + let btc_block_rewards = (*block_rewards.at(1)).try_into().unwrap(); + assert!(strk_block_rewards == expected_strk_block_rewards); + assert!(btc_block_rewards == expected_btc_block_rewards); + let last_calculated_epoch = load_one_felt( + target: staking_contract, storage_address: selector!("last_calculated_epoch"), + ) + .try_into() + .unwrap(); + assert!(last_calculated_epoch == expected_last_calculated_epoch); // Set consensus rewards epoch again when currently set to current epoch + 1. advance_k_epochs_global(); let current_epoch = staking_dispatcher.get_current_epoch(); @@ -4621,6 +4689,23 @@ fn test_set_consensus_rewards_first_epoch() { .try_into() .unwrap(); assert!(consensus_rewards_first_epoch == current_epoch + 2); + // Test that block rewards are initialized. + let block_rewards = snforge_std::load( + target: staking_contract, + storage_address: selector!("block_rewards"), + size: Store::<(Amount, Amount)>::size().into(), + ) + .span(); + let strk_block_rewards = (*block_rewards.at(0)).try_into().unwrap(); + let btc_block_rewards = (*block_rewards.at(1)).try_into().unwrap(); + assert!(strk_block_rewards == expected_strk_block_rewards); + assert!(btc_block_rewards == expected_btc_block_rewards); + let last_calculated_epoch = load_one_felt( + target: staking_contract, storage_address: selector!("last_calculated_epoch"), + ) + .try_into() + .unwrap(); + assert!(last_calculated_epoch == expected_last_calculated_epoch); // Advance to consensus rewards and test that consensus rewards epoch cannot be changed. advance_k_epochs_global(); let current_epoch = staking_dispatcher.get_current_epoch(); @@ -4643,6 +4728,7 @@ fn test_set_consensus_rewards_first_epoch_assertions() { let staking_safe_config_dispatcher = IStakingConfigSafeDispatcher { contract_address: staking_contract, }; + advance_epoch_global(); let current_epoch = staking_dispatcher.get_current_epoch(); // Catch ONLY_APP_GOVERNOR. diff --git a/src/test_utils.cairo b/src/test_utils.cairo index 88dcf406..184ca62a 100644 --- a/src/test_utils.cairo +++ b/src/test_utils.cairo @@ -67,7 +67,7 @@ use starkware_utils::components::replaceability::interface::{ use starkware_utils::constants::SYMBOL; use starkware_utils::errors::OptionAuxTrait; use starkware_utils::math::utils::{mul_wide_and_ceil_div, mul_wide_and_div}; -use starkware_utils::time::time::{Time, TimeDelta, Timestamp}; +use starkware_utils::time::time::{Seconds, Time, TimeDelta, Timestamp}; use starkware_utils_testing::test_utils::{ advance_block_number_global, cheat_caller_address_once, set_account_as_app_governor, set_account_as_app_role_admin, set_account_as_security_admin, set_account_as_security_agent, @@ -931,6 +931,13 @@ pub(crate) fn advance_epoch_global_custom_time(block_time: TimeDelta) { advance_time_global(:time); } +/// Advance the block number by the given `blocks` and the timestamp by the given `block_duration` +/// in seconds * `blocks`. +pub(crate) fn advance_blocks(blocks: u64, block_duration: Seconds) { + advance_block_number_global(:blocks); + advance_time_global(time: TimeDelta { seconds: block_duration * blocks }); +} + // ---- Calculate Rewards - V0 (index based) ----- /// Update rewards for STRK pool. From af42a28367b40110b835ecf45ca835c34c61199c Mon Sep 17 00:00:00 2001 From: Noa Wolfgor Date: Thu, 9 Oct 2025 13:39:57 +0300 Subject: [PATCH 05/10] feat: reward supplier eic --- src/flow_test/utils.cairo | 35 +++- src/reward_supplier.cairo | 1 + src/reward_supplier/eic.cairo | 49 ++++++ src/reward_supplier/errors.cairo | 2 + src/reward_supplier/reward_supplier.cairo | 2 - src/reward_supplier/test.cairo | 197 +++++++++++++++++++++- src/test_utils.cairo | 13 ++ 7 files changed, 290 insertions(+), 9 deletions(-) create mode 100644 src/reward_supplier/eic.cairo diff --git a/src/flow_test/utils.cairo b/src/flow_test/utils.cairo index 0345db90..8d8ba5c2 100644 --- a/src/flow_test/utils.cairo +++ b/src/flow_test/utils.cairo @@ -34,6 +34,9 @@ use staking::pool::interface_v0::{IPoolV0Dispatcher, IPoolV0DispatcherTrait, Poo use staking::reward_supplier::interface::{ IRewardSupplierDispatcher, IRewardSupplierDispatcherTrait, }; +use staking::reward_supplier::reward_supplier::RewardSupplier::{ + DEFAULT_AVG_BLOCK_DURATION, DEFAULT_BLOCK_DURATION_CONFIG, +}; use staking::staking::interface::{ CommissionCommitment, IStakingConfigDispatcher, IStakingConfigDispatcherTrait, IStakingConsensusDispatcher, IStakingConsensusSafeDispatcher, IStakingDispatcher, @@ -62,9 +65,9 @@ use staking::test_utils::constants::{ }; use staking::test_utils::{ StakingInitConfig, approve, calculate_block_offset, custom_decimals_token, - declare_pool_contract, declare_pool_eic_contract, declare_staking_contract, - declare_staking_eic_contract, deploy_mock_erc20_decimals_contract, fund, load_from_simple_map, - upgrade_implementation, + declare_pool_contract, declare_pool_eic_contract, declare_reward_supplier_contract, + declare_reward_supplier_eic_contract, declare_staking_contract, declare_staking_eic_contract, + deploy_mock_erc20_decimals_contract, fund, load_from_simple_map, upgrade_implementation, }; use staking::types::{ Amount, BlockNumber, Commission, Epoch, Index, Inflation, InternalPoolMemberInfoLatest, @@ -2030,6 +2033,7 @@ pub(crate) impl SystemReplaceabilityV3Impl of SystemReplaceabilityV3Trait { fn upgrade_contracts_implementation_v3(self: SystemState) { self.staking.pause(); self.upgrade_staking_implementation_v3(); + self.upgrade_reward_supplier_implementation_v3(); for staker_address in self.staker_addresses { self.staker_migration(staker_address: *staker_address); } @@ -2054,10 +2058,29 @@ pub(crate) impl SystemReplaceabilityV3Impl of SystemReplaceabilityV3Trait { upgrade_governor: self.staking.roles.upgrade_governor, ); } -} -fn declare_reward_supplier_contract() -> ClassHash { - *snforge_std::declare("RewardSupplier").unwrap().contract_class().class_hash + /// Upgrades the staking contract in the system state with a local implementation. + fn upgrade_reward_supplier_implementation_v3(self: SystemState) { + let eic_data = EICData { + eic_hash: declare_reward_supplier_eic_contract(), + eic_init_data: array![ + DEFAULT_AVG_BLOCK_DURATION.into(), + DEFAULT_BLOCK_DURATION_CONFIG.min_block_duration.into(), + DEFAULT_BLOCK_DURATION_CONFIG.max_block_duration.into(), + ] + .span(), + }; + let implementation_data = ImplementationData { + impl_hash: declare_reward_supplier_contract(), + eic_data: Option::Some(eic_data), + final: false, + }; + upgrade_implementation( + contract_address: self.reward_supplier.address, + :implementation_data, + upgrade_governor: self.reward_supplier.roles.upgrade_governor, + ); + } } fn declare_minting_curve_contract() -> ClassHash { diff --git a/src/reward_supplier.cairo b/src/reward_supplier.cairo index 99a7b2f5..367a185d 100644 --- a/src/reward_supplier.cairo +++ b/src/reward_supplier.cairo @@ -1,3 +1,4 @@ +mod eic; pub mod errors; pub mod interface; pub mod reward_supplier; diff --git a/src/reward_supplier/eic.cairo b/src/reward_supplier/eic.cairo new file mode 100644 index 00000000..00a54c26 --- /dev/null +++ b/src/reward_supplier/eic.cairo @@ -0,0 +1,49 @@ +/// An External Initializer Contract to upgrade a reward supplier contract. +/// This EIC is used to upgrade the reward supplier contract from V2 (BTC) to V3. +#[starknet::contract] +mod RewardSupplierEIC { + use staking::reward_supplier::errors::Error; + use staking::reward_supplier::interface::BlockDurationConfig; + use starknet::storage::StoragePointerWriteAccess; + use starkware_utils::components::replaceability::interface::IEICInitializable; + + #[storage] + struct Storage { + // --- New fields --- + /// Average block duration in units of 1 / BLOCK_DURATION_SCALE seconds. + avg_block_duration: u64, + /// Configuration for block duration calculation. + block_duration_config: BlockDurationConfig, + } + + /// Expected data : [avg_block_duration, min_block_duration, max_block_duration] + #[abi(embed_v0)] + impl EICInitializable of IEICInitializable { + fn eic_initialize(ref self: ContractState, eic_init_data: Span) { + assert(eic_init_data.len() == 3, 'EXPECTED_DATA_LENGTH_3'); + let avg_block_duration: u64 = (*eic_init_data[0]).try_into().unwrap(); + let min_block_duration: u64 = (*eic_init_data[1]).try_into().unwrap(); + let max_block_duration: u64 = (*eic_init_data[2]).try_into().unwrap(); + + // Validate values. + assert!( + min_block_duration <= avg_block_duration + && avg_block_duration <= max_block_duration, + "{}", + Error::INVALID_AVG_BLOCK_DURATION, + ); + assert!(min_block_duration > 0, "{}", Error::INVALID_MIN_MAX_BLOCK_DURATION); + assert!( + min_block_duration <= max_block_duration, + "{}", + Error::INVALID_MIN_MAX_BLOCK_DURATION, + ); + + // Set values. + self.avg_block_duration.write(avg_block_duration); + self + .block_duration_config + .write(BlockDurationConfig { min_block_duration, max_block_duration }); + } + } +} diff --git a/src/reward_supplier/errors.cairo b/src/reward_supplier/errors.cairo index 2ee13175..25ed13f3 100644 --- a/src/reward_supplier/errors.cairo +++ b/src/reward_supplier/errors.cairo @@ -8,6 +8,7 @@ pub enum Error { INVALID_BLOCK_NUMBER, INVALID_BLOCK_TIMESTAMP, INVALID_MIN_MAX_BLOCK_DURATION, + INVALID_AVG_BLOCK_DURATION, } impl DescribableError of Describable { @@ -19,6 +20,7 @@ impl DescribableError of Describable { Error::INVALID_BLOCK_NUMBER => "Invalid block number", Error::INVALID_BLOCK_TIMESTAMP => "Invalid block timestamp", Error::INVALID_MIN_MAX_BLOCK_DURATION => "Invalid min/max block duration", + Error::INVALID_AVG_BLOCK_DURATION => "Invalid avg block duration", } } } diff --git a/src/reward_supplier/reward_supplier.cairo b/src/reward_supplier/reward_supplier.cairo index 3817e4ad..98039122 100644 --- a/src/reward_supplier/reward_supplier.cairo +++ b/src/reward_supplier/reward_supplier.cairo @@ -84,7 +84,6 @@ pub mod RewardSupplier { /// Token bridge address. starkgate_address: ContractAddress, /// Average block duration in units of 1 / BLOCK_DURATION_SCALE seconds. - // TODO: Initial in EIC. // TODO: Setter. // TODO: View? avg_block_duration: u64, @@ -92,7 +91,6 @@ pub mod RewardSupplier { /// Updated at the start of each epoch. block_snapshot: (BlockNumber, Timestamp), /// Configuration for block duration calculation. - // TODO: Initial in EIC. block_duration_config: BlockDurationConfig, } diff --git a/src/reward_supplier/test.cairo b/src/reward_supplier/test.cairo index 41f5f971..33135d67 100644 --- a/src/reward_supplier/test.cairo +++ b/src/reward_supplier/test.cairo @@ -32,6 +32,7 @@ use staking::test_utils::constants::{ }; use staking::types::{Amount, BlockNumber}; use starknet::{ContractAddress, Store}; +use starkware_utils::components::replaceability::interface::{EICData, ImplementationData}; use starkware_utils::errors::{Describable, OptionAuxTrait}; use starkware_utils::math::utils::{ceil_of_division, mul_wide_and_div}; use starkware_utils::time::time::{Time, TimeDelta, Timestamp}; @@ -41,8 +42,10 @@ use starkware_utils_testing::test_utils::{ }; use test_utils::{ StakingInitConfig, advance_epoch_global, advance_epoch_global_custom_time, - advance_k_epochs_global, advance_time_global, fund, general_contract_system_deployment, + advance_k_epochs_global, advance_time_global, declare_reward_supplier_contract, + declare_reward_supplier_eic_contract, fund, general_contract_system_deployment, initialize_reward_supplier_state_from_cfg, load_one_felt, stake_for_testing_using_dispatcher, + upgrade_implementation, }; #[test] @@ -688,3 +691,195 @@ fn test_update_current_epoch_block_rewards_with_adjustments() { assert!(strk_rewards == expected_strk_rewards); assert!(btc_rewards == expected_btc_rewards); } + +#[test] +fn test_reward_supplier_eic() { + let mut cfg: StakingInitConfig = Default::default(); + general_contract_system_deployment(ref :cfg); + let reward_supplier = cfg.staking_contract_info.reward_supplier; + let reward_supplier_dispatcher = IRewardSupplierDispatcher { + contract_address: reward_supplier, + }; + let upgrade_governor = cfg.test_info.upgrade_governor; + let avg_block_duration = load_one_felt( + target: reward_supplier, storage_address: selector!("avg_block_duration"), + ) + .try_into() + .unwrap(); + assert!(avg_block_duration == DEFAULT_AVG_BLOCK_DURATION); + assert!( + reward_supplier_dispatcher.get_block_duration_config() == DEFAULT_BLOCK_DURATION_CONFIG, + ); + + // Upgrade. + let avg_block_duration = DEFAULT_AVG_BLOCK_DURATION - 10; + let min_block_duration = DEFAULT_BLOCK_DURATION_CONFIG.min_block_duration - 10; + let max_block_duration = DEFAULT_BLOCK_DURATION_CONFIG.max_block_duration - 10; + let eic_data = EICData { + eic_hash: declare_reward_supplier_eic_contract(), + eic_init_data: [ + avg_block_duration.into(), min_block_duration.into(), max_block_duration.into(), + ] + .span(), + }; + let implementation_data = ImplementationData { + impl_hash: declare_reward_supplier_contract(), + eic_data: Option::Some(eic_data), + final: false, + }; + start_cheat_block_timestamp_global( + block_timestamp: Time::now().add(delta: Time::days(count: 1)).into(), + ); + upgrade_implementation( + contract_address: reward_supplier, :implementation_data, :upgrade_governor, + ); + + // Test. + let new_avg_block_duration = load_one_felt( + target: reward_supplier, storage_address: selector!("avg_block_duration"), + ) + .try_into() + .unwrap(); + assert!(new_avg_block_duration == avg_block_duration); + assert!( + reward_supplier_dispatcher + .get_block_duration_config() == BlockDurationConfig { + min_block_duration, max_block_duration, + }, + ); +} + +#[test] +#[should_panic(expected: "EIC_LIB_CALL_FAILED")] +fn test_reward_supplier_eic_with_wrong_number_of_data_elements() { + let mut cfg: StakingInitConfig = Default::default(); + general_contract_system_deployment(ref :cfg); + let reward_supplier = cfg.staking_contract_info.reward_supplier; + let upgrade_governor = cfg.test_info.upgrade_governor; + let eic_data = EICData { + eic_hash: declare_reward_supplier_eic_contract(), eic_init_data: [].span(), + }; + let implementation_data = ImplementationData { + impl_hash: declare_reward_supplier_contract(), + eic_data: Option::Some(eic_data), + final: false, + }; + start_cheat_block_timestamp_global( + block_timestamp: Time::now().add(delta: Time::days(count: 1)).into(), + ); + upgrade_implementation( + contract_address: reward_supplier, :implementation_data, :upgrade_governor, + ); +} + +#[test] +#[should_panic(expected: "EIC_LIB_CALL_FAILED")] +fn test_reward_supplier_eic_invalid_avg_block_duration_less_than_min() { + let mut cfg: StakingInitConfig = Default::default(); + general_contract_system_deployment(ref :cfg); + let reward_supplier = cfg.staking_contract_info.reward_supplier; + let upgrade_governor = cfg.test_info.upgrade_governor; + let avg_block_duration = 4; + let min_block_time = 5; + let max_block_time = 10; + let eic_data = EICData { + eic_hash: declare_reward_supplier_eic_contract(), + eic_init_data: [avg_block_duration.into(), min_block_time.into(), max_block_time.into()] + .span(), + }; + let implementation_data = ImplementationData { + impl_hash: declare_reward_supplier_contract(), + eic_data: Option::Some(eic_data), + final: false, + }; + start_cheat_block_timestamp_global( + block_timestamp: Time::now().add(delta: Time::days(count: 1)).into(), + ); + upgrade_implementation( + contract_address: reward_supplier, :implementation_data, :upgrade_governor, + ); +} + +#[test] +#[should_panic(expected: "EIC_LIB_CALL_FAILED")] +fn test_reward_supplier_eic_invalid_avg_block_duration_greater_than_max() { + let mut cfg: StakingInitConfig = Default::default(); + general_contract_system_deployment(ref :cfg); + let reward_supplier = cfg.staking_contract_info.reward_supplier; + let upgrade_governor = cfg.test_info.upgrade_governor; + let avg_block_duration = 14; + let min_block_time = 5; + let max_block_time = 10; + let eic_data = EICData { + eic_hash: declare_reward_supplier_eic_contract(), + eic_init_data: [avg_block_duration.into(), min_block_time.into(), max_block_time.into()] + .span(), + }; + let implementation_data = ImplementationData { + impl_hash: declare_reward_supplier_contract(), + eic_data: Option::Some(eic_data), + final: false, + }; + start_cheat_block_timestamp_global( + block_timestamp: Time::now().add(delta: Time::days(count: 1)).into(), + ); + upgrade_implementation( + contract_address: reward_supplier, :implementation_data, :upgrade_governor, + ); +} + +#[test] +#[should_panic(expected: "EIC_LIB_CALL_FAILED")] +fn test_reward_supplier_eic_invalid_min_zero() { + let mut cfg: StakingInitConfig = Default::default(); + general_contract_system_deployment(ref :cfg); + let reward_supplier = cfg.staking_contract_info.reward_supplier; + let upgrade_governor = cfg.test_info.upgrade_governor; + let avg_block_duration = 5; + let min_block_time = 0; + let max_block_time = 10; + let eic_data = EICData { + eic_hash: declare_reward_supplier_eic_contract(), + eic_init_data: [avg_block_duration.into(), min_block_time.into(), max_block_time.into()] + .span(), + }; + let implementation_data = ImplementationData { + impl_hash: declare_reward_supplier_contract(), + eic_data: Option::Some(eic_data), + final: false, + }; + start_cheat_block_timestamp_global( + block_timestamp: Time::now().add(delta: Time::days(count: 1)).into(), + ); + upgrade_implementation( + contract_address: reward_supplier, :implementation_data, :upgrade_governor, + ); +} + +#[test] +#[should_panic(expected: "EIC_LIB_CALL_FAILED")] +fn test_reward_supplier_eic_invalid_min_max() { + let mut cfg: StakingInitConfig = Default::default(); + general_contract_system_deployment(ref :cfg); + let reward_supplier = cfg.staking_contract_info.reward_supplier; + let upgrade_governor = cfg.test_info.upgrade_governor; + let avg_block_duration = 5; + let min_block_time = 10; + let max_block_time = 9; + let eic_data = EICData { + eic_hash: declare_reward_supplier_eic_contract(), + eic_init_data: [avg_block_duration.into(), min_block_time.into(), max_block_time.into()] + .span(), + }; + let implementation_data = ImplementationData { + impl_hash: declare_reward_supplier_contract(), + eic_data: Option::Some(eic_data), + final: false, + }; + start_cheat_block_timestamp_global( + block_timestamp: Time::now().add(delta: Time::days(count: 1)).into(), + ); + upgrade_implementation( + contract_address: reward_supplier, :implementation_data, :upgrade_governor, + ); +} diff --git a/src/test_utils.cairo b/src/test_utils.cairo index 184ca62a..ed351127 100644 --- a/src/test_utils.cairo +++ b/src/test_utils.cairo @@ -393,6 +393,11 @@ pub(crate) fn deploy_reward_supplier_contract(cfg: StakingInitConfig) -> Contrac account: cfg.test_info.app_governor, app_role_admin: cfg.test_info.app_role_admin, ); + set_account_as_upgrade_governor( + contract: reward_supplier_contract_address, + account: cfg.test_info.upgrade_governor, + governance_admin: cfg.test_info.governance_admin, + ); reward_supplier_contract_address } @@ -424,6 +429,10 @@ pub(crate) fn declare_pool_contract() -> ClassHash { *snforge_std::declare("Pool").unwrap().contract_class().class_hash } +pub(crate) fn declare_reward_supplier_contract() -> ClassHash { + *snforge_std::declare("RewardSupplier").unwrap().contract_class().class_hash +} + pub(crate) fn declare_staking_eic_contract() -> ClassHash { *snforge_std::declare("StakingEIC").unwrap().contract_class().class_hash } @@ -432,6 +441,10 @@ pub(crate) fn declare_pool_eic_contract() -> ClassHash { *snforge_std::declare("PoolEIC").unwrap().contract_class().class_hash } +pub(crate) fn declare_reward_supplier_eic_contract() -> ClassHash { + *snforge_std::declare("RewardSupplierEIC").unwrap().contract_class().class_hash +} + /// Upgrades implementation of the given contract. pub(crate) fn upgrade_implementation( contract_address: ContractAddress, From d6990573ad0cd7f1fda7fdf7e4b21a9e7d617b44 Mon Sep 17 00:00:00 2001 From: Arad Reder Date: Thu, 27 Nov 2025 13:58:10 +0200 Subject: [PATCH 06/10] test: add update_rewards single btc pool flow --- src/flow_test/flow_ideas.md | 2 - src/flow_test/test.cairo | 100 ++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/src/flow_test/flow_ideas.md b/src/flow_test/flow_ideas.md index 3d0d7fdc..5f99880e 100644 --- a/src/flow_test/flow_ideas.md +++ b/src/flow_test/flow_ideas.md @@ -5,9 +5,7 @@ - member change balance and test with view of current epoch balance ## `update_rewards` -- staker with only btc pool. - staker with empty pool (STRK + BTC). -- staker with 2 btc pools with different decimals. - staker immediately/one epoch after he called intent. - update rewards for 2 different blocks in the same epoch - should be same rewards. - Change epoch len in blocks - rewards should be changed. diff --git a/src/flow_test/test.cairo b/src/flow_test/test.cairo index e8b0ee32..cf2ba1a4 100644 --- a/src/flow_test/test.cairo +++ b/src/flow_test/test.cairo @@ -16,6 +16,7 @@ use staking::staking::interface::{ IStakingConsensusDispatcherTrait, IStakingDispatcherTrait, IStakingRewardsManagerSafeDispatcherTrait, }; +use staking::staking::objects::NormalizedAmountTrait; use staking::staking::utils::{BTC_WEIGHT_FACTOR, STAKING_POWER_BASE_VALUE, STRK_WEIGHT_FACTOR}; use staking::test_utils::constants::{ AVG_BLOCK_DURATION, BTC_18D_CONFIG, BTC_5D_CONFIG, BTC_8D_CONFIG, PUBLIC_KEY, STRK_BASE_VALUE, @@ -23,6 +24,7 @@ use staking::test_utils::constants::{ }; use staking::test_utils::{ StakingInitConfig, advance_blocks, calculate_staker_btc_pool_rewards_v2, + StakingInitConfig, calculate_staker_btc_pool_rewards_v2, calculate_staker_btc_pool_rewards_v3, calculate_staker_strk_rewards_with_balances_v2, calculate_staker_strk_rewards_with_balances_v3, calculate_strk_pool_rewards_with_pool_balance_v2, compute_rewards_per_unit, custom_decimals_token, deploy_mock_erc20_decimals_contract, @@ -2927,6 +2929,104 @@ fn update_rewards_strk_pool_flow_test() { assert!(delegator_rewards == expected_pool_rewards); } +/// Flow: +/// Staker stake. +/// Open BTC pool 8 decimals. +/// Open BTC pool 18 decimals. +/// Delegator delegate BTC 8 decimals. +/// Delegator delegate BTC 18 decimals. +/// Start consensus rewards. +/// update_rewards. +/// Test staker rewards. +/// Test delegator rewards. +/// Advance epoch. +/// Test delegator rewards. +#[test] +fn update_rewards_multiple_btc_pools_flow_test() { + let cfg: StakingInitConfig = Default::default(); + let mut system = SystemConfigTrait::basic_stake_flow_cfg(:cfg).deploy(); + let stake_amount = system.staking.get_min_stake(); + let delegation_amount_8 = BTC_8D_CONFIG.min_for_rewards; + let delegation_amount_18 = BTC_18D_CONFIG.min_for_rewards; + + // Setup tokens. + let token_8 = system + .deploy_new_btc_token(name: "BTC_8D_TOKEN", decimals: BTC_8D_CONFIG.decimals); + let token_18 = system + .deploy_new_btc_token(name: "BTC_18D_TOKEN", decimals: BTC_18D_CONFIG.decimals); + system.staking.add_token(token_address: token_8.contract_address()); + system.staking.add_token(token_address: token_18.contract_address()); + system.staking.enable_token(token_address: token_8.contract_address()); + system.staking.enable_token(token_address: token_18.contract_address()); + + // Stake and open pools. + let staker = system.new_staker(amount: stake_amount); + let commission = 200; + let staking_contract = system.staking.address; + let minting_curve_contract = system.minting_curve.address; + system.stake(:staker, amount: stake_amount, pool_enabled: false, :commission); + system.set_commission(:staker, :commission); + let pool_8 = system.set_open_for_delegation(:staker, token_address: token_8.contract_address()); + let pool_18 = system + .set_open_for_delegation(:staker, token_address: token_18.contract_address()); + let delegator_8 = system.new_btc_delegator(amount: delegation_amount_8, token: token_8); + let delegator_18 = system.new_btc_delegator(amount: delegation_amount_18, token: token_18); + system + .delegate_btc( + delegator: delegator_8, pool: pool_8, amount: delegation_amount_8, token: token_8, + ); + system + .delegate_btc( + delegator: delegator_18, pool: pool_18, amount: delegation_amount_18, token: token_18, + ); + + system.start_consensus_rewards(); + system.update_rewards(:staker, disable_rewards: false); + + // Calculate expected rewards. + let (expected_staker_rewards, _) = calculate_staker_strk_rewards_with_balances_v3( + amount_own: stake_amount, + pool_amount: Zero::zero(), + :commission, + :staking_contract, + :minting_curve_contract, + ); + assert!(expected_staker_rewards.is_non_zero()); + let normalized_delegation_amount = NormalizedAmountTrait::from_native_amount( + amount: delegation_amount_8, decimals: BTC_8D_CONFIG.decimals, + ); + let normalized_total_btc_balance = NormalizedAmountTrait::from_native_amount( + amount: delegation_amount_8 * 2, decimals: BTC_8D_CONFIG.decimals, + ); + let (expected_commission_rewards, expected_pool_rewards) = calculate_staker_btc_pool_rewards_v3( + normalized_pool_balance: normalized_delegation_amount, + normalized_staker_total_btc_balance: normalized_total_btc_balance, + :commission, + :staking_contract, + :minting_curve_contract, + token_address: system.btc_token.contract_address(), + ); + assert!(expected_commission_rewards.is_non_zero()); + assert!(expected_pool_rewards.is_non_zero()); + + // Claim rewards - test staker and delegator rewards + let staker_rewards = system.staker_claim_rewards(:staker); + let delegator_rewards_8 = system.delegator_claim_rewards(delegator: delegator_8, pool: pool_8); + let delegator_rewards_18 = system + .delegator_claim_rewards(delegator: delegator_18, pool: pool_18); + assert!(staker_rewards == expected_staker_rewards + expected_commission_rewards * 2); + assert!(delegator_rewards_8.is_zero()); + assert!(delegator_rewards_18.is_zero()); + + // Advance epoch - test delegator rewards + system.advance_epoch(); + let delegator_rewards_8 = system.delegator_claim_rewards(delegator: delegator_8, pool: pool_8); + let delegator_rewards_18 = system + .delegator_claim_rewards(delegator: delegator_18, pool: pool_18); + assert!(delegator_rewards_8 == expected_pool_rewards); + assert!(delegator_rewards_18 == expected_pool_rewards); +} + /// Flow: /// 2 BTC tokens (A and B) added - test tokens /// Enable token A - test tokens From 08880f4af753ca4556e12e93f51424607253f873 Mon Sep 17 00:00:00 2001 From: Arad Reder Date: Thu, 27 Nov 2025 14:04:09 +0200 Subject: [PATCH 07/10] test: add update_rewards empty pools flow --- src/flow_test/flow_ideas.md | 1 - src/flow_test/test.cairo | 45 +++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/flow_test/flow_ideas.md b/src/flow_test/flow_ideas.md index 5f99880e..d68cb5c1 100644 --- a/src/flow_test/flow_ideas.md +++ b/src/flow_test/flow_ideas.md @@ -5,7 +5,6 @@ - member change balance and test with view of current epoch balance ## `update_rewards` -- staker with empty pool (STRK + BTC). - staker immediately/one epoch after he called intent. - update rewards for 2 different blocks in the same epoch - should be same rewards. - Change epoch len in blocks - rewards should be changed. diff --git a/src/flow_test/test.cairo b/src/flow_test/test.cairo index cf2ba1a4..4bfe442a 100644 --- a/src/flow_test/test.cairo +++ b/src/flow_test/test.cairo @@ -3027,6 +3027,51 @@ fn update_rewards_multiple_btc_pools_flow_test() { assert!(delegator_rewards_18 == expected_pool_rewards); } +/// Flow: +/// Start consensus rewards. +/// Staker stake with STRK pool. +/// Open BTC pool. +/// Advance K epochs. +/// update_rewards. +/// Test staker rewards. +#[test] +fn update_rewards_empty_pools_flow_test() { + let cfg: StakingInitConfig = Default::default(); + let mut system = SystemConfigTrait::basic_stake_flow_cfg(:cfg).deploy(); + let stake_amount = system.staking.get_min_stake(); + let staker = system.new_staker(amount: stake_amount); + let commission = 200; + let staking_contract = system.staking.address; + let minting_curve_contract = system.minting_curve.address; + system.stake(:staker, amount: stake_amount, pool_enabled: true, :commission); + let strk_pool = system.staking.get_pool(:staker); + let btc_pool = system + .set_open_for_delegation(:staker, token_address: system.btc_token.contract_address()); + system.start_consensus_rewards(); + + system.update_rewards(:staker, disable_rewards: false); + + // Calculate expected rewards. + let (expected_staker_rewards, _) = calculate_staker_strk_rewards_with_balances_v3( + amount_own: stake_amount, + pool_amount: Zero::zero(), + :commission, + :staking_contract, + :minting_curve_contract, + ); + assert!(expected_staker_rewards.is_non_zero()); + + // Claim rewards - test staker rewards + let staker_rewards = system.staker_claim_rewards(:staker); + assert!(staker_rewards == expected_staker_rewards); + + // Test pool balances + let strk_pool_balance = system.token.balance_of(account: strk_pool); + let btc_pool_balance = system.token.balance_of(account: btc_pool); + assert!(strk_pool_balance.is_zero()); + assert!(btc_pool_balance.is_zero()); +} + /// Flow: /// 2 BTC tokens (A and B) added - test tokens /// Enable token A - test tokens From 8904b34bac109f93a34b952762defad62c8800ff Mon Sep 17 00:00:00 2001 From: Arad Reder Date: Thu, 27 Nov 2025 14:29:03 +0200 Subject: [PATCH 08/10] test: add update_rewards staker intent flow --- src/flow_test/flow_ideas.md | 1 - src/flow_test/test.cairo | 43 +++++++++++++++++++++++++++++++++++++ src/flow_test/utils.cairo | 17 ++++++++++++--- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/flow_test/flow_ideas.md b/src/flow_test/flow_ideas.md index d68cb5c1..68f3ae13 100644 --- a/src/flow_test/flow_ideas.md +++ b/src/flow_test/flow_ideas.md @@ -5,7 +5,6 @@ - member change balance and test with view of current epoch balance ## `update_rewards` -- staker immediately/one epoch after he called intent. - update rewards for 2 different blocks in the same epoch - should be same rewards. - Change epoch len in blocks - rewards should be changed. - disable_rewards = true, advance block, disable_rewards = false, advance block, disable_rewards = true, test rewards. diff --git a/src/flow_test/test.cairo b/src/flow_test/test.cairo index 4bfe442a..53f50083 100644 --- a/src/flow_test/test.cairo +++ b/src/flow_test/test.cairo @@ -3710,3 +3710,46 @@ fn delegate_claim_after_claim_with_rewards_flow_test() { system.advance_epoch(); assert!(system.delegator_claim_rewards(:delegator, :pool).is_zero()); } + +/// Flow: +/// Staker stake. +/// Start consensus rewards. +/// Staker exit intent. +/// update_rewards - test rewards. +/// Advance epoch. +/// update_rewards - test rewards. +/// Advance epoch. +/// update_rewards - test no rewards. +#[test] +fn update_rewards_staker_intent_flow_test() { + let cfg: StakingInitConfig = Default::default(); + let mut system = SystemConfigTrait::basic_stake_flow_cfg(:cfg).deploy(); + let staking_contract = system.staking.address; + let minting_curve_contract = system.minting_curve.address; + let stake_amount = system.staking.get_min_stake(); + let staker = system.new_staker(amount: stake_amount); + let commission = 200; + system.stake(:staker, amount: stake_amount, pool_enabled: false, :commission); + system.start_consensus_rewards(); + + system.staker_exit_intent(:staker); + system.update_rewards(:staker, disable_rewards: false); + let staker_rewards = system.staker_claim_rewards(:staker); + let (expected_rewards, _) = calculate_staker_strk_rewards_with_balances_v3( + amount_own: stake_amount, + pool_amount: Zero::zero(), + :commission, + :staking_contract, + :minting_curve_contract, + ); + assert!(staker_rewards == expected_rewards); + + system.advance_epoch(); + system.update_rewards(:staker, disable_rewards: false); + let staker_rewards = system.staker_claim_rewards(:staker); + assert!(staker_rewards == expected_rewards); + + system.advance_epoch(); + let result = system.safe_update_rewards(:staker, disable_rewards: false); + assert_panic_with_error(:result, expected_error: StakingError::INVALID_STAKER.describe()); +} diff --git a/src/flow_test/utils.cairo b/src/flow_test/utils.cairo index 8d8ba5c2..e25b5f66 100644 --- a/src/flow_test/utils.cairo +++ b/src/flow_test/utils.cairo @@ -44,9 +44,10 @@ use staking::staking::interface::{ IStakingMigrationSafeDispatcher, IStakingPauseDispatcher, IStakingPauseDispatcherTrait, IStakingPoolDispatcher, IStakingPoolSafeDispatcher, IStakingRewardsManagerDispatcher, IStakingRewardsManagerDispatcherTrait, IStakingRewardsManagerSafeDispatcher, - IStakingSafeDispatcher, IStakingSafeDispatcherTrait, IStakingTokenManagerDispatcher, - IStakingTokenManagerDispatcherTrait, IStakingTokenManagerSafeDispatcher, - IStakingTokenManagerSafeDispatcherTrait, StakerInfoV1, StakerInfoV1Trait, StakerPoolInfoV2, + IStakingRewardsManagerSafeDispatcherTrait, IStakingSafeDispatcher, IStakingSafeDispatcherTrait, + IStakingTokenManagerDispatcher, IStakingTokenManagerDispatcherTrait, + IStakingTokenManagerSafeDispatcher, IStakingTokenManagerSafeDispatcherTrait, StakerInfoV1, + StakerInfoV1Trait, StakerPoolInfoV2, }; use staking::staking::objects::{ EpochInfo, EpochInfoTrait, NormalizedAmount, StakerVersion, StakerVersionTrait, @@ -1589,6 +1590,16 @@ pub(crate) impl SystemStakerImpl of SystemStakerTrait { .rewards_manager_dispatcher() .update_rewards(staker_address: staker.staker.address, :disable_rewards); } + + #[feature("safe_dispatcher")] + fn safe_update_rewards( + self: SystemState, staker: Staker, disable_rewards: bool, + ) -> Result<(), Array> { + self + .staking + .rewards_manager_safe_dispatcher() + .update_rewards(staker_address: staker.staker.address, :disable_rewards) + } } /// The `Delegator` struct represents a delegator in the staking system. From 453b96763e1bb42ad988756b0b2d1697df1828cc Mon Sep 17 00:00:00 2001 From: Arad Reder Date: Thu, 27 Nov 2025 14:34:25 +0200 Subject: [PATCH 09/10] test: add update_rewards same epoch flow --- src/flow_test/flow_ideas.md | 1 - src/flow_test/test.cairo | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/flow_test/flow_ideas.md b/src/flow_test/flow_ideas.md index 68f3ae13..2745c5f6 100644 --- a/src/flow_test/flow_ideas.md +++ b/src/flow_test/flow_ideas.md @@ -5,7 +5,6 @@ - member change balance and test with view of current epoch balance ## `update_rewards` -- update rewards for 2 different blocks in the same epoch - should be same rewards. - Change epoch len in blocks - rewards should be changed. - disable_rewards = true, advance block, disable_rewards = false, advance block, disable_rewards = true, test rewards. - update rewards for some different blocks in the same epoch, test rewards of pool member. diff --git a/src/flow_test/test.cairo b/src/flow_test/test.cairo index 53f50083..b2c7813d 100644 --- a/src/flow_test/test.cairo +++ b/src/flow_test/test.cairo @@ -3753,3 +3753,38 @@ fn update_rewards_staker_intent_flow_test() { let result = system.safe_update_rewards(:staker, disable_rewards: false); assert_panic_with_error(:result, expected_error: StakingError::INVALID_STAKER.describe()); } + +/// Flow: +/// Staker stake. +/// Start consensus rewards. +/// update_rewards - test rewards. +/// Advance block. +/// update_rewards - test rewards are the same. +#[test] +fn update_rewards_same_epoch_flow_test() { + let cfg: StakingInitConfig = Default::default(); + let mut system = SystemConfigTrait::basic_stake_flow_cfg(:cfg).deploy(); + let staking_contract = system.staking.address; + let minting_curve_contract = system.minting_curve.address; + let stake_amount = system.staking.get_min_stake(); + let staker = system.new_staker(amount: stake_amount); + let commission = 200; + system.stake(:staker, amount: stake_amount, pool_enabled: false, :commission); + system.start_consensus_rewards(); + + system.update_rewards(:staker, disable_rewards: false); + let staker_rewards = system.staker_claim_rewards(:staker); + let (expected_rewards, _) = calculate_staker_strk_rewards_with_balances_v3( + amount_own: stake_amount, + pool_amount: Zero::zero(), + :commission, + :staking_contract, + :minting_curve_contract, + ); + assert!(staker_rewards == expected_rewards); + + advance_block_number_global(blocks: 1); + system.update_rewards(:staker, disable_rewards: false); + let staker_rewards = system.staker_claim_rewards(:staker); + assert!(staker_rewards == expected_rewards); +} From 4dcf4e26f8fc7fb3b9476744feeeb099deffdb79 Mon Sep 17 00:00:00 2001 From: Arad Reder Date: Thu, 27 Nov 2025 14:52:56 +0200 Subject: [PATCH 10/10] test: add update_rewards epoch length flow --- src/flow_test/flow_ideas.md | 1 - src/flow_test/test.cairo | 45 +++++++++++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/flow_test/flow_ideas.md b/src/flow_test/flow_ideas.md index 2745c5f6..6d62e592 100644 --- a/src/flow_test/flow_ideas.md +++ b/src/flow_test/flow_ideas.md @@ -5,7 +5,6 @@ - member change balance and test with view of current epoch balance ## `update_rewards` -- Change epoch len in blocks - rewards should be changed. - disable_rewards = true, advance block, disable_rewards = false, advance block, disable_rewards = true, test rewards. - update rewards for some different blocks in the same epoch, test rewards of pool member. - staker change balance, attest, change balance, attest, set_v3, change balance, update_rewards, change_balance, update_rewards, test rewards. diff --git a/src/flow_test/test.cairo b/src/flow_test/test.cairo index b2c7813d..b220e60b 100644 --- a/src/flow_test/test.cairo +++ b/src/flow_test/test.cairo @@ -19,13 +19,13 @@ use staking::staking::interface::{ use staking::staking::objects::NormalizedAmountTrait; use staking::staking::utils::{BTC_WEIGHT_FACTOR, STAKING_POWER_BASE_VALUE, STRK_WEIGHT_FACTOR}; use staking::test_utils::constants::{ - AVG_BLOCK_DURATION, BTC_18D_CONFIG, BTC_5D_CONFIG, BTC_8D_CONFIG, PUBLIC_KEY, STRK_BASE_VALUE, - TEST_MIN_BTC_FOR_REWARDS, + AVG_BLOCK_DURATION, BTC_18D_CONFIG, BTC_5D_CONFIG, BTC_8D_CONFIG, EPOCH_DURATION, EPOCH_LENGTH, + PUBLIC_KEY, STRK_BASE_VALUE, TEST_MIN_BTC_FOR_REWARDS, }; use staking::test_utils::{ StakingInitConfig, advance_blocks, calculate_staker_btc_pool_rewards_v2, - StakingInitConfig, calculate_staker_btc_pool_rewards_v2, calculate_staker_btc_pool_rewards_v3, - calculate_staker_strk_rewards_with_balances_v2, calculate_staker_strk_rewards_with_balances_v3, + calculate_staker_btc_pool_rewards_v3, calculate_staker_strk_rewards_with_balances_v2, + calculate_staker_strk_rewards_with_balances_v3, calculate_strk_pool_rewards_with_pool_balance_v2, compute_rewards_per_unit, custom_decimals_token, deploy_mock_erc20_decimals_contract, }; @@ -3788,3 +3788,40 @@ fn update_rewards_same_epoch_flow_test() { let staker_rewards = system.staker_claim_rewards(:staker); assert!(staker_rewards == expected_rewards); } + +/// Flow: +/// Staker stake. +/// Start consensus rewards. +/// update rewards +/// Advance epoch +/// Increase epoch length in blocks. +/// update rewards - test rewards are smaller. +/// Decrease epoch length in blocks. +/// Advance epoch +/// update rewards - test rewards are larger. +#[test] +fn update_rewards_epoch_length_flow_test() { + let cfg: StakingInitConfig = Default::default(); + let mut system = SystemConfigTrait::basic_stake_flow_cfg(:cfg).deploy(); + let stake_amount = system.staking.get_min_stake(); + let staker = system.new_staker(amount: stake_amount); + let commission = 200; + system.stake(:staker, amount: stake_amount, pool_enabled: false, :commission); + system.start_consensus_rewards(); + + system.update_rewards(:staker, disable_rewards: false); + let default_length_rewards = system.staker_claim_rewards(:staker); + + system.staking.set_epoch_info(epoch_duration: EPOCH_DURATION, epoch_length: EPOCH_LENGTH + 1); + system.advance_epoch(); + system.update_rewards(:staker, disable_rewards: false); + let increased_length_rewards = system.staker_claim_rewards(:staker); + + system.staking.set_epoch_info(epoch_duration: EPOCH_DURATION, epoch_length: EPOCH_LENGTH - 1); + system.advance_epoch(); + system.update_rewards(:staker, disable_rewards: false); + let decreased_length_rewards = system.staker_claim_rewards(:staker); + + assert!(increased_length_rewards < default_length_rewards); + assert!(default_length_rewards < decreased_length_rewards); +}