25 releases (12 breaking)

0.17.1 Jan 26, 2023
0.16.0 Dec 16, 2022
0.15.1 Sep 22, 2022
0.13.0 Jul 26, 2022
0.8.1 Mar 29, 2022

#893 in Magic Beans

Download history 13/week @ 2024-02-25 333/week @ 2024-03-31

333 downloads per month

Apache-2.0

305KB
6K SLoC

Tgrade Validator Set

This uses the Tgrade-specific bindings to allow a privileged contract to map a trusted cw4 contract to the Tendermint validator set running the chain. Pointing to a cw4-group contract would implement PoA, pointing to cw4-stake contract would make a pure (undelegated) PoS chain.

(Slashing and reward distributions are future work for other contracts)

Rewards calculation

On the Tgrade::EndBlock sudo message this contract performs rewards calculation and distribution for active validators for the passed epoch.

The cumulative reward value contains:

  • Per epoch reward - newly minted tokens each epoch
  • Fees for transactions in validated blocks

Per epoch reward is configurable in instantiation message, the epoch_reward field. Fees are accumulated on the contract itself.

The epoch reward is not constant - epoch_reward is its base value, but it is modified based on how many fees are accumulated. The final reward formula is:

cumulative_reward = max(0, epoch_rewards - fee_percentage * fees) + fees

The idea is, that on early epochs not so many transactions are expected, so reward is minted to make validation profitable. However, later on when there are more transactions, fees are enough reward for validations, so new tokens doesn't need to be minted, so there is no actual need to introduce tokens inflation.

The reward reduction functionality can be easily disabled by setting fee_percentage to 0 (which effectively makes fee_percentage * fees always 0). Setting it over 1 (or 100%) would cause that cumulative_reward would diminish as fees are growing up to the point, when fees would reach epoch_reward / fee_percentage threshold (as from this point, no new tokens are minted, only fees are split between validators). Setting fee_percentage anywhere in the range (0; 1] causes that cumulative reward grow is reduced - basically up to the time when fees reaches epoch_reward / fee_percentage, all fees are worth (1 - fee_percentage) * fees (they are scalded).

The next step is splitting cumulative_reward into parts. For each distribution contract, an address and a ratio is accepted. distribution_contract_ratio * cumulative_reward is sent to each such contract using a distribute_funds message, whose intention is to split this part of the rewards between non-validators, based on their engagement. The remaining reward tokens are sent as validators_reward to validators of the last epoch. The distribution ratios need to be included in the distribution_contracts vector configured in InstantiateMsg. The sum of these ratios needs to fit in the [0, 1] range. The vector may be empty, in which case the whole reward ends up with the validators.

When validators_reward is calculated, it is split between active validators. Active validators are up to max_validators validators with the highest points, but with at least min_points. scaling is an optional field which allows scaling the points for Tendermint purposes (it should not affect reward splitting). When validators are selected, then cumulative_reward is split between them, proportionally to the validators points. All of max_validators, min_points, and scaling are configurable during instantiation. Splitting of validators_reward is realized by an external contract.

The default value of fee_percentage is 0 (so when it is not specified in the message, the reward reduction is disabled). At Tgrade genesis, fee_percentage is meant to be set to 0.5.

Rewards distribution contract

As stated in previous section, rewards distribution is realized by an external contract managed by tgrade-valset. It is assumed to be the tg4-engagement contract, but in reality it should just support the proper API, which is then used by tgrade-valset (which is just a subset of tg4-engagement).

During valset instantiation, the rewards distribution contract, called validator_group, is instantiated using the message:

{
  "admin": "tgrade_valset_addr",
  "denom": "epoch_reward_denom",
  "members": []
}

The code id of the stored validator group contract is sent to valset in its instantiation message (validator_group_code_id field). The assigned address of the validator group contract would be emitted with a wasm event:

{
  "_contract_addr": "valset_addr",
  "action": "tgrade-valset_instantiation",
  "validator_group": "validator_group_addr"
}

At every epoch end, rewards would be sent to the validator group contract with the execution message:

{
  "distribute_funds": {}
}

After this, another message would be sent to update validators and their points:

{
  "update_members": {
    "remove": ["validator_to_be_removed"],
    "add": [{
      "addr": "validator_with_points_updated",
      "points": 10
    }]
  }
}

Jailing

Jailing is a mechanism for temporarily disallowing operators to validate blocks.

Only one address is allowed to jail members, and it is configured in InstantiateMsg as an admin. The idea is, that an admin is some voting contract, which would decide about banning by some voting consensus.

Jailing a member disallows him to be a validator for incoming epochs unless he is unjailed. There are three ways to unjail a member:

  • Admin can always unjail a jailed member (unjailing via voting).
  • Any member can unjail himself if the jailing period expired.
  • Members can be unjailed automatically after the jailing period expired (this may be enabled by InstantiateMsg::auto_unjail flag).

The status of jailing can be queried by normal validators queries - if a validator is jailed, the response will contain a jailed_until field with either a single forever field (if this member will never be allowed to unjail himself), or an until field containing a timestamp, indicating since when the member can be unjailed.

Slashing

The contract implements slashing semantics, but doesn't actually implement the full slashing interface. It reacts properly to the slash message:

{
  "slash": {
    "addr": "contract_to_slash",
    "portion": portion_to_slash
  }
}

Slashing is implemented by just forwarding the Slash message to the membership contract (which is set on instantiation - this is preasumed to be a mixer contract, but technically it can be any contract implementing tg4 and Slashing interfaces). Obviously to be able to slash on the membership contracts, tgrade-valset has to register itself as a slasher, so on the instantiation it would send the AddSlasher message to the membership contract which has to succeed for the whole instantiation to succeed. Therefore proper slashing_preauths has to be set on membership contract.

tgrade-valset doesn't react to AddSlasher nor RemoveSlasher messages, as it doesn't support multiple slashers. Only the admin of tgrade-valset can ever slash on this contract (and he also always can do that).

Because only the membership contract is slashed by this implementation of Slash, the membership contract itself is responsible for taking care of aligning points on validators and engagement contracts. However, as the rewards distribution is not recalculated until the next epoch, the slashing would not affect the current epoch.

Init

pub struct InstantiateMsg {
    /// Address allowed to jail, meant to be a OC voting contract. If `None`, then jailing is
    /// impossible in this contract.
    pub admin: Option<String>,
    /// Address of a cw4 contract with the raw membership used to feed the validator set
    pub membership: String,
    /// Minimum points needed by an address in `membership` to be considered for the validator set.
    /// 0-points members are always filtered out.
    /// TODO: if we allow sub-1 scaling factors, determine if this is pre-/post- scaling
    /// (use points for cw4, power for Tendermint)
    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).
    pub max_validators: u32,
    /// Number of seconds in one epoch. We update the Tendermint validator set only once per epoch.
    /// Epoch # is env.block.time/epoch_length (round down). The first block with a new epoch number
    /// will trigger a new validator calculation.
    pub epoch_length: u64,
    /// Total reward paid out at each epoch. This will be split among all validators during the last
    /// epoch.
    /// (epoch_reward.amount * 86_400 * 30 / epoch_length) is the amount of reward tokens to mint
    /// each month.
    /// Ensure this is sensible in relation to the total token supply.
    pub epoch_reward: Coin,

    /// Initial operators and validator keys registered.
    /// If you do not set this, the validators need to register themselves before
    /// making this privileged/calling the EndBlockers, so that we have a non-empty validator set
    pub initial_keys: Vec<OperatorInitInfo>,

    /// A scaling factor to multiply cw4-group points to produce the Tendermint validator power
    /// (TODO: should we allow this to reduce points? Like 1/1000?)
    pub scaling: Option<u32>,

    /// Percentage of total accumulated fees that is subtracted from tokens minted as rewards.
    /// 50% by default. To disable this feature just set it to 0 (which effectively means that fees
    /// don't affect the per-epoch reward).
    #[serde(default = "default_fee_percentage")]
    pub fee_percentage: Decimal,

    /// Flag determining if validators should be automatically unjailed after the jailing period;
    /// false by default.
    #[serde(default)]
    pub auto_unjail: bool,

    /// Validators who are caught double signing are jailed forever and their bonded tokens are
    /// slashed based on this value.
    #[serde(default = "default_double_sign_slash")]
    pub double_sign_slash_ratio: Decimal,

    /// 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.
    ///
    /// Note that the particular algorithm this contract uses calculates token rewards for distribution
    /// contracts by applying decimal division to the pool of reward tokens, and then passes the remainder
    /// to validators via the contract instantiated from `rewards_code_is`. This will cause edge cases where
    /// indivisible tokens end up with the validators. For example if the reward pool for an epoch is 1 token
    /// and there are two distribution contracts with 50% ratio each, that token will end up with the
    /// validators.
    pub distribution_contracts: UnvalidatedDistributionContracts,

    /// Code id of the contract which would be used to distribute the rewards of this token, assuming
    /// `tg4-engagement`. The contract will be initialized with the message:
    /// ```json
    /// {
    ///     "admin": "valset_addr",
    ///     "denom": "reward_denom",
    /// }
    /// ```
    ///
    /// This contract has to support all the `RewardsDistribution` messages
    pub validator_group_id: u64,
}

Messages

pub enum ExecuteMsg {
    /// Change the admin
    UpdateAdmin {
        admin: Option<String>,
    },
    /// Links info.sender (operator) to this Tendermint consensus key.
    /// The operator cannot re-register another key.
    /// No two operators may have the same consensus_key.
    RegisterValidatorKey {
        pubkey: Pubkey,
        /// Additional metadata assigned to this validator
        metadata: ValidatorMetadata,
    },
    UpdateMetadata(ValidatorMetadata),
    /// Jails validator. Can be executed only by the admin.
    Jail {
        /// Operator which should be jailed
        operator: String,
        /// Duration for how long validator is jailed, `None` for jailing forever
        duration: Option<Duration>,
    },
    /// Unjails validator. Admin can unjail anyone anytime, others can unjail only themselves and
    /// only if the jail period passed.
    Unjail {
        /// Address to unjail. Optional, as if not provided it is assumed to be the sender of the
        /// message (for convenience when unjailing self after the jail period).
        operator: Option<String>,
    },
    /// To be called by admin only. Slashes a given address (by forwarding slash to both rewards
    /// contract and engagement contract)
    Slash {
        addr: String,
        portion: Decimal,
    },
}

pub struct ValidatorMetadata {
    /// The validator's name (required)
    pub moniker: String,

    /// The optional identity signature (ex. UPort or Keybase)
    pub identity: Option<String>,

    /// The validator's (optional) website
    pub website: Option<String>,

    /// The validator's (optional) security contact email
    pub security_contact: Option<String>,

    /// The validator's (optional) details
    pub details: Option<String>,
}

Queries

pub enum QueryMsg {
    /// Returns ConfigResponse - static contract data
    Config {},
    /// Returns EpochResponse - get info on current and next epochs
    Epoch {},

    /// Returns the validator key and associated metadata (if present) for the given operator.
    /// Returns ValidatorResponse
    Validator { operator: String },
    /// Paginate over all operators, using operator address as pagination.
    /// Returns ListValidatorsResponse
    ListValidators {
        start_after: Option<String>,
        limit: Option<u32>,
    },

    /// List the current validator set, sorted by power descending
    /// (no pagination - reasonable limit from max_validators)
    ListActiveValidators {},

    /// This will calculate who the new validators would be if
    /// we recalculated end block right now.
    /// Also returns ListActiveValidatorsResponse
    SimulateActiveValidators {},

    /// Returns a list of validator slashing events.
    /// Returns ListValidatorSlashingResponse
    ListValidatorSlashing { operator: String },
}

Dependencies

~4–17MB
~236K SLoC