diff --git a/contracts/tgrade-valset/src/contract.rs b/contracts/tgrade-valset/src/contract.rs index 0dd3440..7d19a1b 100644 --- a/contracts/tgrade-valset/src/contract.rs +++ b/contracts/tgrade-valset/src/contract.rs @@ -5,8 +5,8 @@ use std::convert::{TryFrom, TryInto}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Addr, Binary, BlockInfo, CustomQuery, Decimal, Deps, DepsMut, Env, MessageInfo, - Order, QueryRequest, Reply, StdError, StdResult, Timestamp, WasmMsg, + to_binary, Addr, Binary, BlockInfo, Coin, CustomQuery, Decimal, Deps, DepsMut, Env, + MessageInfo, Order, QueryRequest, Reply, StdError, StdResult, Timestamp, WasmMsg, }; use cw2::set_contract_version; @@ -20,10 +20,9 @@ use tg_bindings::{ Pubkey, TgradeMsg, TgradeQuery, TgradeSudoMsg, ToAddress, ValidatorDiff, ValidatorUpdate, ValidatorVoteResponse, }; -use tg_utils::{JailingDuration, SlashMsg, ADMIN}; +use tg_utils::{Duration, JailingDuration, SlashMsg, ADMIN}; use crate::error::ContractError; -use crate::migration::{migrate_jailing_period, migrate_verify_validators}; use crate::msg::{ EpochResponse, ExecuteMsg, InstantiateMsg, InstantiateResponse, JailingEnd, JailingPeriod, ListActiveValidatorsResponse, ListValidatorResponse, ListValidatorSlashingResponse, MigrateMsg, @@ -32,9 +31,9 @@ use crate::msg::{ }; use crate::rewards::pay_block_rewards; use crate::state::{ - export, import, operators, Config, EpochInfo, OperatorInfo, ValidatorInfo, ValidatorSlashing, - ValsetState, BLOCK_SIGNERS, CONFIG, EPOCH, JAIL, VALIDATORS, VALIDATOR_SLASHING, - VALIDATOR_START_HEIGHT, + export, import, operators, Config, DistributionContract, EpochInfo, OperatorInfo, + ValidatorInfo, ValidatorSlashing, ValsetState, BLOCK_SIGNERS, CONFIG, EPOCH, JAIL, VALIDATORS, + VALIDATOR_SLASHING, VALIDATOR_START_HEIGHT, }; // version info for migration info @@ -164,7 +163,28 @@ pub fn execute( ExecuteMsg::UpdateConfig { min_points, max_validators, - } => execute_update_config(deps, info, min_points, max_validators), + scaling, + epoch_reward, + fee_percentage, + auto_unjail, + double_sign_slash_ratio, + distribution_contracts, + verify_validators, + offline_jail_duration, + } => execute_update_config( + deps, + info, + min_points, + max_validators, + scaling, + epoch_reward, + fee_percentage, + auto_unjail, + double_sign_slash_ratio, + distribution_contracts, + verify_validators, + offline_jail_duration, + ), ExecuteMsg::RegisterValidatorKey { pubkey, metadata } => { execute_register_validator_key(deps, env, info, pubkey, metadata) @@ -182,11 +202,20 @@ pub fn execute( } } +#[allow(clippy::too_many_arguments)] fn execute_update_config( deps: DepsMut, info: MessageInfo, min_points: Option, max_validators: Option, + scaling: Option, + epoch_reward: Option, + fee_percentage: Option, + auto_unjail: Option, + double_sign_slash_ratio: Option, + distribution_contracts: Option>, + verify_validators: Option, + offline_jail_duration: Option, ) -> Result { ADMIN.assert_admin(deps.as_ref(), &info.sender)?; @@ -197,6 +226,30 @@ fn execute_update_config( if let Some(max_validators) = max_validators { cfg.max_validators = max_validators; } + if let Some(scaling) = scaling { + cfg.scaling = Option::from(scaling); + } + if let Some(epoch_reward) = epoch_reward { + cfg.epoch_reward = epoch_reward; + } + if let Some(fee_percentage) = fee_percentage { + cfg.fee_percentage = fee_percentage; + } + if let Some(auto_unjail) = auto_unjail { + cfg.auto_unjail = auto_unjail; + } + if let Some(double_sign_slash_ratio) = double_sign_slash_ratio { + cfg.double_sign_slash_ratio = double_sign_slash_ratio; + } + if let Some(distribution_contracts) = distribution_contracts { + cfg.distribution_contracts = distribution_contracts; + } + if let Some(verify_validators) = verify_validators { + cfg.verify_validators = verify_validators; + } + if let Some(offline_jail_duration) = offline_jail_duration { + cfg.offline_jail_duration = offline_jail_duration; + } Ok(cfg) })?; @@ -929,12 +982,11 @@ fn calculate_diff( #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate( - mut deps: DepsMut, + deps: DepsMut, _env: Env, msg: MigrateMsg, ) -> Result { - let original_version = - ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; CONFIG.update::<_, StdError>(deps.storage, |mut cfg| { if let Some(min_points) = msg.min_points { @@ -943,16 +995,15 @@ pub fn migrate( if let Some(max_validators) = msg.max_validators { cfg.max_validators = max_validators; } + if let Some(distribution_contracts) = msg.distribution_contracts { + cfg.distribution_contracts = distribution_contracts; + } if let Some(verify_validators) = msg.verify_validators { cfg.verify_validators = verify_validators; } Ok(cfg) })?; - migrate_jailing_period(deps.branch(), &original_version)?; - - migrate_verify_validators(deps.branch(), &original_version)?; - Ok(Response::new()) } diff --git a/contracts/tgrade-valset/src/lib.rs b/contracts/tgrade-valset/src/lib.rs index f56dc8f..c6b4d74 100644 --- a/contracts/tgrade-valset/src/lib.rs +++ b/contracts/tgrade-valset/src/lib.rs @@ -1,6 +1,5 @@ pub mod contract; pub mod error; -mod migration; pub mod msg; #[cfg(test)] mod multitest; diff --git a/contracts/tgrade-valset/src/migration.rs b/contracts/tgrade-valset/src/migration.rs deleted file mode 100644 index e78a1cb..0000000 --- a/contracts/tgrade-valset/src/migration.rs +++ /dev/null @@ -1,139 +0,0 @@ -use cosmwasm_std::{Addr, CustomQuery, DepsMut, Order, Timestamp}; -use cw_storage_plus::Map; -use schemars::JsonSchema; -use semver::Version; -use serde::{Deserialize, Serialize}; -use tg_utils::Expiration; - -use crate::error::ContractError; -use crate::msg::{JailingEnd, JailingPeriod}; -use crate::state::{CONFIG, JAIL}; - -/// `crate::msg::JailingPeriod` version from v0.6.2 and before -#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] -pub enum JailingPeriodV0_6_2 { - Until(Expiration), - Forever {}, -} - -impl JailingPeriodV0_6_2 { - fn update(self) -> JailingPeriod { - JailingPeriod { - start: Timestamp::from_seconds(0), - end: match self { - JailingPeriodV0_6_2::Until(u) => JailingEnd::Until(u), - JailingPeriodV0_6_2::Forever {} => JailingEnd::Forever {}, - }, - } - } -} - -pub fn migrate_jailing_period( - deps: DepsMut, - version: &Version, -) -> Result<(), ContractError> { - let jailings: Vec<_> = if *version <= "0.6.2".parse::().unwrap() { - let jailings: Map<&Addr, JailingPeriodV0_6_2> = Map::new("jail"); - - jailings - .range(deps.storage, None, None, Order::Ascending) - .map(|record| record.map(|(key, jailing_period)| (key, jailing_period.update()))) - .collect::>()? - } else { - return Ok(()); - }; - - for (addr, jailing_period) in jailings { - JAIL.save(deps.storage, &addr, &jailing_period)?; - } - - Ok(()) -} - -pub fn migrate_verify_validators( - deps: DepsMut, - version: &Version, -) -> Result<(), ContractError> { - let mut config = if *version <= "0.14.0".parse::().unwrap() { - CONFIG.load(deps.storage)? - } else { - return Ok(()); - }; - config.verify_validators = true; - CONFIG.save(deps.storage, &config)?; - - Ok(()) -} - -#[cfg(test)] -mod tests { - //! These are very rudimentary tests that only -mock- old state and perform migrations on it. - //! It's absolutely vital to do more thorough migration testing on some actual old state. - - use cosmwasm_std::{testing::mock_dependencies, StdError, Storage}; - - use super::*; - - fn mock_v_0_6_2_jailing_periods( - store: &mut dyn Storage, - jailings: &[(&str, JailingPeriodV0_6_2)], - ) { - let jail_map: Map<&Addr, JailingPeriodV0_6_2> = Map::new("jail"); - - for (addr, period) in jailings.iter().cloned() { - jail_map - .update(store, &Addr::unchecked(addr), |_| -> Result<_, StdError> { - Ok(period) - }) - .unwrap(); - } - } - - #[test] - fn migrate_jailing_period_v_0_6_2() { - let mut deps = mock_dependencies(); - - mock_v_0_6_2_jailing_periods( - &mut deps.storage, - &[ - ( - "alice", - JailingPeriodV0_6_2::Until(Expiration::at_timestamp(Timestamp::from_seconds( - 123, - ))), - ), - ("bob", JailingPeriodV0_6_2::Forever {}), - ], - ); - - migrate_jailing_period(deps.as_mut(), &Version::parse("0.6.2").unwrap()).unwrap(); - - // verify the data is what we expect - let jailed = JAIL - .range(&deps.storage, None, None, Order::Ascending) - .collect::, _>>() - .unwrap(); - - assert_eq!( - jailed, - [ - ( - Addr::unchecked("alice"), - JailingPeriod { - start: Timestamp::from_seconds(0), - end: JailingEnd::Until(Expiration::at_timestamp(Timestamp::from_seconds( - 123 - ))) - } - ), - ( - Addr::unchecked("bob"), - JailingPeriod { - start: Timestamp::from_seconds(0), - end: JailingEnd::Forever {} - } - ) - ] - ); - } -} diff --git a/contracts/tgrade-valset/src/msg.rs b/contracts/tgrade-valset/src/msg.rs index fcb9190..748e4dc 100644 --- a/contracts/tgrade-valset/src/msg.rs +++ b/contracts/tgrade-valset/src/msg.rs @@ -135,8 +135,47 @@ pub enum ExecuteMsg { }, /// Alter config values UpdateConfig { + /// minimum points needed by an address in `membership` to be considered for the validator set. + /// 0-point members are always filtered out. min_points: Option, + /// The maximum number of validators that can be included in the Tendermint validator set. + /// If there are more validators than slots, we select the top N by membership points + /// descending. max_validators: Option, + /// A scaling factor to multiply tg4-engagement points to produce the tendermint validator power + scaling: Option, + /// Total reward paid out each epoch. This will be split among all validators during the last + /// epoch. + /// (epoch_reward.amount * 86_400 * 30 / epoch_length) is reward tokens to mint each month. + /// Ensure this is sensible in relation to the total token supply. + epoch_reward: Option, + /// Percentage of total accumulated fees which is subtracted from tokens minted as a rewards. + /// 50% as default. To disable this feature just set it to 0 (which effectively means that fees + /// doesn't affect the per epoch reward). + fee_percentage: Option, + /// Flag determining if validators should be automatically unjailed after jailing period, false + /// by default. + auto_unjail: Option, + + /// Validators who are caught double signing are jailed forever and their bonded tokens are + /// slashed based on this value. + double_sign_slash_ratio: Option, + + /// Addresses where part of the reward for non-validators is sent for further distribution. These are + /// required to handle the `Distribute {}` message (eg. tg4-engagement contract) which would + /// distribute the funds sent with this message. + /// The sum of ratios here has to be in the [0, 1] range. The remainder is sent to validators via the + /// rewards contract. + distribution_contracts: Option>, + + /// If this is enabled, signed blocks are watched for, and if a validator fails to sign any blocks + /// in a string of a number of blocks (typically 1000 blocks), they are jailed. + verify_validators: Option, + + /// The duration to jail a validator for in case they don't sign any blocks for a period of time, + /// if `verify_validators` is enabled. + /// After the jailing period, they will be jailed again if not signing blocks, ad infinitum. + offline_jail_duration: Option, }, /// Links info.sender (operator) to this Tendermint consensus key. /// The operator cannot re-register another key. @@ -508,6 +547,7 @@ pub struct InstantiateResponse { pub struct MigrateMsg { pub min_points: Option, pub max_validators: Option, + pub distribution_contracts: Option>, pub verify_validators: Option, } diff --git a/contracts/tgrade-valset/src/multitest/migration.rs b/contracts/tgrade-valset/src/multitest/migration.rs index ce6d0d3..0038817 100644 --- a/contracts/tgrade-valset/src/multitest/migration.rs +++ b/contracts/tgrade-valset/src/multitest/migration.rs @@ -1,5 +1,7 @@ use super::suite::SuiteBuilder; use crate::msg::MigrateMsg; +use crate::state::DistributionContract; +use cosmwasm_std::{Addr, Decimal}; #[test] fn migration_can_alter_cfg() { @@ -19,6 +21,10 @@ fn migration_can_alter_cfg() { &MigrateMsg { min_points: Some(5), max_validators: Some(10), + distribution_contracts: Some(vec![DistributionContract { + contract: Addr::unchecked("engagement1".to_string()), + ratio: Decimal::percent(50), + }]), verify_validators: Some(true), }, ) @@ -27,4 +33,12 @@ fn migration_can_alter_cfg() { let cfg = suite.config().unwrap(); assert_eq!(cfg.max_validators, 10); assert_eq!(cfg.min_points, 5); + assert!(cfg.verify_validators); + assert_eq!( + cfg.distribution_contracts, + vec![DistributionContract { + contract: Addr::unchecked("engagement1".to_string()), + ratio: Decimal::percent(50), + }] + ); } diff --git a/contracts/tgrade-valset/src/multitest/suite.rs b/contracts/tgrade-valset/src/multitest/suite.rs index 4f2083b..96197d0 100644 --- a/contracts/tgrade-valset/src/multitest/suite.rs +++ b/contracts/tgrade-valset/src/multitest/suite.rs @@ -1,5 +1,5 @@ use super::helpers::addr_to_pubkey; -use crate::state::{Config, ValsetState}; +use crate::state::{Config, DistributionContract, ValsetState}; use crate::test_helpers::{mock_metadata, mock_pubkey}; use crate::{msg::*, state::ValidatorInfo}; use anyhow::{bail, Result as AnyResult}; @@ -589,6 +589,7 @@ impl Suite { executor: &str, min_points: impl Into>, max_validators: impl Into>, + distribution_contracts: impl Into>>, ) -> AnyResult { self.app.execute_contract( Addr::unchecked(executor), @@ -596,6 +597,14 @@ impl Suite { &ExecuteMsg::UpdateConfig { min_points: min_points.into(), max_validators: max_validators.into(), + scaling: None, + epoch_reward: None, + fee_percentage: None, + auto_unjail: None, + double_sign_slash_ratio: None, + distribution_contracts: distribution_contracts.into(), + verify_validators: None, + offline_jail_duration: None, }, &[], ) diff --git a/contracts/tgrade-valset/src/multitest/update_config.rs b/contracts/tgrade-valset/src/multitest/update_config.rs index 1fbfc03..82213d4 100644 --- a/contracts/tgrade-valset/src/multitest/update_config.rs +++ b/contracts/tgrade-valset/src/multitest/update_config.rs @@ -1,6 +1,9 @@ +use cosmwasm_std::{Addr, Decimal}; use cw_controllers::AdminError; use crate::error::ContractError; +use crate::multitest::suite::Suite; +use crate::state::DistributionContract; use super::suite::SuiteBuilder; @@ -16,31 +19,63 @@ fn update_cfg() { assert_eq!(cfg.max_validators, 6); assert_eq!(cfg.min_points, 3); - suite.update_config(&admin, Some(5), Some(10)).unwrap(); + suite + .update_config( + &admin, + Some(5), + Some(10), + vec![DistributionContract { + contract: Addr::unchecked("contract1"), + ratio: Decimal::percent(15), + }], + ) + .unwrap(); let cfg = suite.config().unwrap(); assert_eq!(cfg.max_validators, 10); assert_eq!(cfg.min_points, 5); + assert_eq!( + cfg.distribution_contracts, + vec![DistributionContract { + contract: Addr::unchecked("contract1"), + ratio: Decimal::percent(15) + }] + ); } #[test] fn none_values_do_not_alter_cfg() { - let mut suite = SuiteBuilder::new() + let mut suite: Suite = SuiteBuilder::new() .with_max_validators(6) .with_min_points(3) + .with_distribution(Decimal::percent(50), &[("engagement1", 20)], None) .build(); let admin = suite.admin().to_string(); let cfg = suite.config().unwrap(); assert_eq!(cfg.max_validators, 6); assert_eq!(cfg.min_points, 3); + assert_eq!( + cfg.distribution_contracts, + vec![DistributionContract { + contract: Addr::unchecked("contract1"), + ratio: Decimal::percent(50) + }] + ); - suite.update_config(&admin, None, None).unwrap(); + suite.update_config(&admin, None, None, None).unwrap(); // Make sure the values haven't changed. let cfg = suite.config().unwrap(); assert_eq!(cfg.max_validators, 6); assert_eq!(cfg.min_points, 3); + assert_eq!( + cfg.distribution_contracts, + vec![DistributionContract { + contract: Addr::unchecked("contract1"), + ratio: Decimal::percent(50) + }] + ); } #[test] @@ -48,7 +83,7 @@ fn non_admin_cannot_update_cfg() { let mut suite = SuiteBuilder::new().build(); let err = suite - .update_config("random fella", Some(5), Some(10)) + .update_config("random fella", Some(5), Some(10), None) .unwrap_err(); assert_eq!( ContractError::AdminError(AdminError::NotAdmin {}), diff --git a/contracts/tgrade-valset/src/state.rs b/contracts/tgrade-valset/src/state.rs index 3ed7f80..d97eb9b 100644 --- a/contracts/tgrade-valset/src/state.rs +++ b/contracts/tgrade-valset/src/state.rs @@ -23,8 +23,7 @@ pub struct Config { pub min_points: u64, /// The maximum number of validators that can be included in the Tendermint validator set. /// If there are more validators than slots, we select the top N by membership points - /// descending. (In case of ties at the last slot, select by "first" tendermint pubkey - /// lexicographically sorted). + /// descending. In case of ties at the last slot, the first (oldest) validator wins. pub max_validators: u32, /// A scaling factor to multiply tg4-engagement points to produce the tendermint validator power pub scaling: Option, @@ -61,8 +60,8 @@ pub struct Config { /// in a string of a number of blocks (typically 1000 blocks), they are jailed. pub verify_validators: bool, - /// The duration to jail a validator for in case they don't sign their first epoch - /// boundary block. After the period, they have to pass verification again, ad infinitum. + /// The duration to jail a validator for in case they don't sign any blocks for a period of time. + /// After the jailing period, they will be jailed again if not signing, ad infinitum. pub offline_jail_duration: Duration, }