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..411797a3 100644 --- a/src/reward_supplier/errors.cairo +++ b/src/reward_supplier/errors.cairo @@ -5,6 +5,7 @@ pub enum Error { ON_RECEIVE_NOT_FROM_STARKGATE, UNEXPECTED_TOKEN, BLOCK_DURATION_OVERFLOW, + INVALID_MIN_MAX_BLOCK_DURATION, } impl DescribableError of Describable { @@ -13,6 +14,7 @@ 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_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 6f0a0145..c36cca80 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 }