Skip to main content

tgrade_valset/
msg.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use std::convert::TryFrom;
4use std::ops::Add;
5
6use tg4::Member;
7use tg_bindings::{Ed25519Pubkey, Pubkey};
8use tg_utils::{Duration, Expiration, JailingDuration};
9
10use crate::error::ContractError;
11use crate::state::{DistributionContract, OperatorInfo, ValidatorInfo, ValidatorSlashing};
12use cosmwasm_std::{Addr, Api, BlockInfo, Coin, Decimal, Timestamp};
13
14#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
15pub struct InstantiateMsg {
16    /// Address allowed to jail, meant to be a OC voting contract. If `None`, then jailing is
17    /// impossible in this contract.
18    pub admin: Option<String>,
19    /// Address of a cw4 contract with the raw membership used to feed the validator set
20    pub membership: String,
21    /// Minimum points needed by an address in `membership` to be considered for the validator set.
22    /// 0-point members are always filtered out.
23    /// (use points for cw4, power for Tendermint)
24    pub min_points: u64,
25    /// The maximum number of validators that can be included in the Tendermint validator set.
26    /// If there are more validators than slots, we select the top N by membership points
27    /// descending. (In case of ties at the last slot, select by "first" Tendermint pubkey,
28    /// lexicographically sorted).
29    pub max_validators: u32,
30    /// Number of seconds in one epoch. We update the Tendermint validator set only once per epoch.
31    /// Epoch # is env.block.time/epoch_length (round down). The first block with a new epoch number
32    /// will trigger a new validator calculation.
33    pub epoch_length: u64,
34    /// Total reward paid out at each epoch. This will be split among all validators during the last
35    /// epoch.
36    /// (epoch_reward.amount * 86_400 * 30 / epoch_length) is the amount of reward tokens to mint
37    /// each month.
38    /// Ensure this is sensible in relation to the total token supply.
39    pub epoch_reward: Coin,
40
41    /// Initial operators and validator keys registered.
42    /// If you do not set this, the validators need to register themselves before
43    /// making this privileged/calling the EndBlockers, so that we have a non-empty validator set
44    pub initial_keys: Vec<OperatorInitInfo>,
45
46    /// A scaling factor to multiply cw4-group points to produce the Tendermint validator power
47    pub scaling: Option<u32>,
48
49    /// Percentage of total accumulated fees that is subtracted from tokens minted as rewards.
50    /// 50% by default. To disable this feature just set it to 0 (which effectively means that fees
51    /// don't affect the per-epoch reward).
52    #[serde(default = "default_fee_percentage")]
53    pub fee_percentage: Decimal,
54
55    /// Flag determining if validators should be automatically unjailed after the jailing period;
56    /// false by default.
57    #[serde(default)]
58    pub auto_unjail: bool,
59
60    /// Validators who are caught double signing are jailed forever and their bonded tokens are
61    /// slashed based on this value.
62    #[serde(default = "default_double_sign_slash")]
63    pub double_sign_slash_ratio: Decimal,
64
65    /// Addresses where part of the reward for non-validators is sent for further distribution. These are
66    /// required to handle the `Distribute {}` message (eg. tg4-engagement contract) which would
67    /// distribute the funds sent with this message.
68    ///
69    /// The sum of ratios here has to be in the [0, 1] range. The remainder is sent to validators via the
70    /// rewards contract.
71    ///
72    /// Note that the particular algorithm this contract uses calculates token rewards for distribution
73    /// contracts by applying decimal division to the pool of reward tokens, and then passes the remainder
74    /// to validators via the contract instantiated from `rewards_code_is`. This will cause edge cases where
75    /// indivisible tokens end up with the validators. For example if the reward pool for an epoch is 1 token
76    /// and there are two distribution contracts with 50% ratio each, that token will end up with the
77    /// validators.
78    pub distribution_contracts: UnvalidatedDistributionContracts,
79
80    /// Code id of the contract which would be used to distribute the rewards of this token, assuming
81    /// `tg4-engagement`. The contract will be initialized with the message:
82    /// ```json
83    /// {
84    ///     "admin": "valset_addr",
85    ///     "denom": "reward_denom",
86    /// }
87    /// ```
88    ///
89    /// This contract has to support all the `RewardsDistribution` messages
90    pub validator_group_code_id: u64,
91
92    /// When a validator joins the valset, verify they sign the first block since joining
93    /// or jail them for a period otherwise.
94    ///
95    /// The verification happens every time the validator becomes an active validator,
96    /// including when they are unjailed or when they just gain enough power to participate.
97    pub verify_validators: bool,
98
99    /// The duration to jail a validator for in case they don't sign their first epoch
100    /// boundary block. After the period, they have to pass verification again, ad infinitum.
101    pub offline_jail_duration: Duration,
102}
103
104impl InstantiateMsg {
105    pub fn validate(&self) -> Result<(), ContractError> {
106        if self.epoch_length == 0 {
107            return Err(ContractError::InvalidEpoch {});
108        }
109        if self.min_points == 0 {
110            return Err(ContractError::InvalidMinPoints {});
111        }
112        if self.max_validators == 0 {
113            return Err(ContractError::InvalidMaxValidators {});
114        }
115        if self.scaling == Some(0) {
116            return Err(ContractError::InvalidScaling {});
117        }
118        // Current denom regexp in the SDK is [a-zA-Z][a-zA-Z0-9/]{2,127}
119        if self.epoch_reward.denom.len() < 2 || self.epoch_reward.denom.len() > 127 {
120            return Err(ContractError::InvalidRewardDenom {});
121        }
122        for op in self.initial_keys.iter() {
123            op.validate()?
124        }
125        Ok(())
126    }
127}
128
129#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
130#[serde(rename_all = "snake_case")]
131pub enum ExecuteMsg {
132    /// Change the admin
133    UpdateAdmin {
134        admin: Option<String>,
135    },
136    /// Alter config values
137    UpdateConfig {
138        /// minimum points needed by an address in `membership` to be considered for the validator set.
139        /// 0-point members are always filtered out.
140        min_points: Option<u64>,
141        /// The maximum number of validators that can be included in the Tendermint validator set.
142        /// If there are more validators than slots, we select the top N by membership points
143        /// descending.
144        max_validators: Option<u32>,
145        /// A scaling factor to multiply tg4-engagement points to produce the tendermint validator power
146        scaling: Option<u32>,
147        /// Total reward paid out each epoch. This will be split among all validators during the last
148        /// epoch.
149        /// (epoch_reward.amount * 86_400 * 30 / epoch_length) is reward tokens to mint each month.
150        /// Ensure this is sensible in relation to the total token supply.
151        epoch_reward: Option<Coin>,
152        /// Percentage of total accumulated fees which is subtracted from tokens minted as a rewards.
153        /// 50% as default. To disable this feature just set it to 0 (which effectively means that fees
154        /// doesn't affect the per epoch reward).
155        fee_percentage: Option<Decimal>,
156        /// Flag determining if validators should be automatically unjailed after jailing period, false
157        /// by default.
158        auto_unjail: Option<bool>,
159
160        /// Validators who are caught double signing are jailed forever and their bonded tokens are
161        /// slashed based on this value.
162        double_sign_slash_ratio: Option<Decimal>,
163
164        /// Addresses where part of the reward for non-validators is sent for further distribution. These are
165        /// required to handle the `Distribute {}` message (eg. tg4-engagement contract) which would
166        /// distribute the funds sent with this message.
167        /// The sum of ratios here has to be in the [0, 1] range. The remainder is sent to validators via the
168        /// rewards contract.
169        distribution_contracts: Option<Vec<DistributionContract>>,
170
171        /// If this is enabled, signed blocks are watched for, and if a validator fails to sign any blocks
172        /// in a string of a number of blocks (typically 1000 blocks), they are jailed.
173        verify_validators: Option<bool>,
174
175        /// The duration to jail a validator for in case they don't sign any blocks for a period of time,
176        /// if `verify_validators` is enabled.
177        /// After the jailing period, they will be jailed again if not signing blocks, ad infinitum.
178        offline_jail_duration: Option<Duration>,
179    },
180    /// Links info.sender (operator) to this Tendermint consensus key.
181    /// The operator cannot re-register another key.
182    /// No two operators may have the same consensus_key.
183    RegisterValidatorKey {
184        pubkey: Pubkey,
185        /// Additional metadata assigned to this validator
186        metadata: ValidatorMetadata,
187    },
188    UpdateMetadata(ValidatorMetadata),
189    /// Jails validator. Can be executed only by the admin.
190    Jail {
191        /// Operator which should be jailed
192        operator: String,
193        /// Duration for how long validator is jailed
194        duration: JailingDuration,
195    },
196    /// Unjails validator. Admin can unjail anyone anytime, others can unjail only themselves and
197    /// only if the jail period passed.
198    Unjail {
199        /// Address to unjail. Optional, as if not provided it is assumed to be the sender of the
200        /// message (for convenience when unjailing self after the jail period).
201        operator: Option<String>,
202    },
203    /// To be called by admin only. Slashes a given address (by forwarding slash to both rewards
204    /// contract and engagement contract)
205    Slash {
206        addr: String,
207        portion: Decimal,
208    },
209
210    /// This will update the validator set with the passed list.
211    /// Used for testing validators storage.
212    #[cfg(debug_assertions)]
213    SimulateValidatorSet {
214        validators: Vec<ValidatorInfo>,
215    },
216}
217
218#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
219#[serde(rename_all = "snake_case")]
220pub enum QueryMsg {
221    /// Returns configuration
222    Configuration {},
223    /// Returns EpochResponse - get info on current and next epochs
224    Epoch {},
225
226    /// Returns the validator key and associated metadata (if present) for the given operator.
227    /// Returns ValidatorResponse
228    Validator { operator: String },
229    /// Paginate over all operators, using operator address as pagination.
230    /// Returns ListValidatorsResponse
231    ListValidators {
232        start_after: Option<String>,
233        limit: Option<u32>,
234    },
235
236    /// List the current validator set, sorted by power descending
237    ListActiveValidators {
238        start_after: Option<String>,
239        limit: Option<u32>,
240    },
241
242    /// Returns ListValidatorsResponse
243    ListJailedValidators {
244        start_after: Option<String>,
245        limit: Option<u32>,
246    },
247
248    /// This will calculate who the new validators would be if
249    /// we recalculated end block right now.
250    /// Also returns ListActiveValidatorsResponse
251    SimulateActiveValidators {},
252
253    /// Returns a list of validator slashing events.
254    /// Returns ListValidatorSlashingResponse
255    ListValidatorSlashing { operator: String },
256
257    /// Returns cw_controllers::AdminResponse
258    Admin {},
259}
260
261#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
262pub struct UnvalidatedDistributionContract {
263    /// The unvalidated address of the contract to which part of the reward tokens is sent to.
264    pub contract: String,
265    /// The ratio of total reward tokens for an epoch to be sent to that contract for further
266    /// distribution.
267    pub ratio: Decimal,
268}
269
270impl UnvalidatedDistributionContract {
271    fn validate(self, api: &dyn Api) -> Result<DistributionContract, ContractError> {
272        Ok(DistributionContract {
273            contract: api.addr_validate(&self.contract)?,
274            ratio: self.ratio,
275        })
276    }
277}
278
279#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug, Default)]
280#[serde(transparent)]
281pub struct UnvalidatedDistributionContracts {
282    pub inner: Vec<UnvalidatedDistributionContract>,
283}
284
285impl UnvalidatedDistributionContracts {
286    /// Validates the addresses and the sum of ratios.
287    pub fn validate(self, api: &dyn Api) -> Result<Vec<DistributionContract>, ContractError> {
288        if self.sum_ratios() > Decimal::one() {
289            return Err(ContractError::InvalidRewardsRatio {});
290        }
291
292        self.inner.into_iter().map(|c| c.validate(api)).collect()
293    }
294
295    fn sum_ratios(&self) -> Decimal {
296        self.inner
297            .iter()
298            .map(|c| c.ratio)
299            .fold(Decimal::zero(), Decimal::add)
300    }
301}
302
303pub fn default_fee_percentage() -> Decimal {
304    Decimal::zero()
305}
306
307pub fn default_validators_reward_ratio() -> Decimal {
308    Decimal::one()
309}
310
311pub fn default_double_sign_slash() -> Decimal {
312    Decimal::percent(50)
313}
314
315/// Validator Metadata modeled after the Cosmos SDK staking module
316#[derive(
317    Serialize, Deserialize, Clone, Eq, PartialEq, Ord, PartialOrd, JsonSchema, Debug, Default,
318)]
319pub struct ValidatorMetadata {
320    /// The validator's name (required)
321    pub moniker: String,
322
323    /// The optional identity signature (ex. UPort or Keybase)
324    pub identity: Option<String>,
325
326    /// The validator's (optional) website
327    pub website: Option<String>,
328
329    /// The validator's (optional) security contact email
330    pub security_contact: Option<String>,
331
332    /// The validator's (optional) details
333    pub details: Option<String>,
334}
335
336pub const MIN_MONIKER_LENGTH: usize = 3;
337pub const MIN_METADATA_SIZE: usize = 1;
338pub const MAX_METADATA_SIZE: usize = 256;
339
340impl ValidatorMetadata {
341    pub fn validate(&self) -> Result<(), ContractError> {
342        if self.moniker.len() < MIN_MONIKER_LENGTH || self.moniker.len() > MAX_METADATA_SIZE {
343            return Err(ContractError::InvalidMetadata {
344                data: "moniker",
345                min: MIN_MONIKER_LENGTH,
346                max: MAX_METADATA_SIZE,
347            });
348        }
349        if let Some(identity) = &self.identity {
350            if identity.is_empty() || identity.len() > MAX_METADATA_SIZE {
351                return Err(ContractError::InvalidMetadata {
352                    data: "identity",
353                    min: MIN_METADATA_SIZE,
354                    max: MAX_METADATA_SIZE,
355                });
356            }
357        }
358        if let Some(website) = &self.website {
359            if website.is_empty() || website.len() > MAX_METADATA_SIZE {
360                return Err(ContractError::InvalidMetadata {
361                    data: "website",
362                    min: MIN_METADATA_SIZE,
363                    max: MAX_METADATA_SIZE,
364                });
365            } else if !website.starts_with("https://") && !website.starts_with("http://") {
366                return Err(ContractError::InvalidMetadataWebsitePrefix {});
367            }
368        }
369        if let Some(security_contract) = &self.security_contact {
370            if security_contract.is_empty() || security_contract.len() > MAX_METADATA_SIZE {
371                return Err(ContractError::InvalidMetadata {
372                    data: "security_contract",
373                    min: MIN_METADATA_SIZE,
374                    max: MAX_METADATA_SIZE,
375                });
376            }
377        }
378        if let Some(details) = &self.details {
379            if details.is_empty() || details.len() > MAX_METADATA_SIZE {
380                return Err(ContractError::InvalidMetadata {
381                    data: "details",
382                    min: MIN_METADATA_SIZE,
383                    max: MAX_METADATA_SIZE,
384                });
385            }
386        }
387        Ok(())
388    }
389}
390
391/// Maps an sdk address to a Tendermint pubkey.
392#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
393pub struct OperatorInitInfo {
394    pub operator: String,
395    pub validator_pubkey: Pubkey,
396    pub metadata: ValidatorMetadata,
397}
398
399impl OperatorInitInfo {
400    pub fn validate(&self) -> Result<(), ContractError> {
401        Ed25519Pubkey::try_from(&self.validator_pubkey)?;
402        self.metadata.validate()
403    }
404}
405
406#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
407pub struct EpochResponse {
408    /// Number of seconds in one epoch. We update the Tendermint validator set only once per epoch.
409    pub epoch_length: u64,
410    /// The current epoch # (env.block.time/epoch_length, rounding down)
411    pub current_epoch: u64,
412    /// The last time we updated the validator set - block time and height
413    pub last_update_time: u64,
414    pub last_update_height: u64,
415    /// Seconds (UTC UNIX time) of next timestamp that will trigger a validator recalculation
416    pub next_update_time: u64,
417}
418
419// data behind one operator
420#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
421pub struct OperatorResponse {
422    pub operator: String,
423    pub pubkey: Pubkey,
424    pub metadata: ValidatorMetadata,
425    pub jailed_until: Option<JailingPeriod>,
426    pub active_validator: bool,
427}
428
429impl OperatorResponse {
430    pub fn from_info(
431        info: OperatorInfo,
432        operator: String,
433        jailed_until: impl Into<Option<JailingPeriod>>,
434    ) -> Self {
435        OperatorResponse {
436            operator,
437            pubkey: info.pubkey.into(),
438            metadata: info.metadata,
439            jailed_until: jailed_until.into(),
440            active_validator: info.active_validator,
441        }
442    }
443}
444
445#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
446pub struct JailingPeriod {
447    pub start: Timestamp,
448    pub end: JailingEnd,
449}
450
451#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
452#[serde(rename_all = "snake_case")]
453pub enum JailingEnd {
454    Until(Expiration),
455    Forever {},
456}
457
458impl JailingPeriod {
459    pub fn from_duration(duration: JailingDuration, block: &BlockInfo) -> Self {
460        Self {
461            start: block.time,
462            end: match duration {
463                JailingDuration::Duration(duration) => JailingEnd::Until(duration.after(block)),
464                JailingDuration::Forever {} => JailingEnd::Forever {},
465            },
466        }
467    }
468
469    pub fn is_forever(&self) -> bool {
470        matches!(self.end, JailingEnd::Forever {})
471    }
472
473    pub fn is_expired(&self, block: &BlockInfo) -> bool {
474        match self.end {
475            JailingEnd::Forever {} => false,
476            JailingEnd::Until(expires) => expires.is_expired(block),
477        }
478    }
479}
480
481#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
482pub struct ValidatorResponse {
483    /// This is unset if no validator registered
484    pub validator: Option<OperatorResponse>,
485}
486
487#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
488pub struct ListValidatorResponse {
489    pub validators: Vec<OperatorResponse>,
490}
491
492#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
493pub struct ListActiveValidatorsResponse {
494    pub validators: Vec<ValidatorInfo>,
495}
496
497#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
498pub struct ListValidatorSlashingResponse {
499    /// Operator address
500    pub addr: String,
501    /// Block height of first validator addition to validators set
502    pub start_height: u64,
503    /// Slashing events, if any
504    pub slashing: Vec<ValidatorSlashing>,
505    /// Whether or not a validator has been tombstoned (killed out of
506    /// validator set)
507    pub tombstoned: bool,
508    /// If validator is jailed, it will show expiration time
509    pub jailed_until: Option<Expiration>,
510}
511
512/// Messages sent by this contract to an external contract
513#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
514#[serde(rename_all = "snake_case")]
515pub enum DistributionMsg {
516    /// Message sent to `distribution_contract` with funds which are part of the reward to be split
517    /// between engaged operators
518    DistributeRewards {},
519}
520
521#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
522#[serde(rename_all = "snake_case")]
523pub struct RewardsInstantiateMsg {
524    pub admin: Addr,
525    pub denom: String,
526    pub members: Vec<Member>,
527}
528
529#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
530#[serde(rename_all = "snake_case")]
531pub enum RewardsDistribution {
532    UpdateMembers {
533        remove: Vec<String>,
534        add: Vec<Member>,
535    },
536    DistributeRewards {},
537}
538
539#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
540#[serde(rename_all = "snake_case")]
541pub struct InstantiateResponse {
542    pub validator_group: Addr,
543}
544
545#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
546#[serde(rename_all = "snake_case")]
547pub struct MigrateMsg {
548    pub min_points: Option<u64>,
549    pub max_validators: Option<u32>,
550    pub distribution_contracts: Option<Vec<DistributionContract>>,
551    pub verify_validators: Option<bool>,
552}
553
554#[cfg(test)]
555mod test {
556    use super::*;
557    use crate::error::ContractError;
558    use crate::test_helpers::{invalid_operator, valid_operator};
559    use cosmwasm_std::coin;
560
561    #[test]
562    fn validate_operator_key() {
563        valid_operator("foo").validate().unwrap();
564        let err = invalid_operator().validate().unwrap_err();
565        assert_eq!(err, ContractError::InvalidPubkey {});
566    }
567
568    #[test]
569    fn validate_init_msg() {
570        let proper = InstantiateMsg {
571            admin: None,
572            membership: "contract-addr".into(),
573            min_points: 5,
574            max_validators: 20,
575            epoch_length: 5000,
576            epoch_reward: coin(7777, "foobar"),
577            initial_keys: vec![valid_operator("foo"), valid_operator("bar")],
578            scaling: None,
579            fee_percentage: Decimal::zero(),
580            auto_unjail: false,
581            double_sign_slash_ratio: Decimal::percent(50),
582            distribution_contracts: UnvalidatedDistributionContracts::default(),
583            validator_group_code_id: 0,
584            verify_validators: false,
585            offline_jail_duration: Duration::new(0),
586        };
587        proper.validate().unwrap();
588
589        // with scaling also works
590        let mut with_scaling = proper.clone();
591        with_scaling.scaling = Some(10);
592        with_scaling.validate().unwrap();
593
594        // fails on 0 scaling
595        let mut invalid = proper.clone();
596        invalid.scaling = Some(0);
597        let err = invalid.validate().unwrap_err();
598        assert_eq!(err, ContractError::InvalidScaling {});
599
600        // fails on 0 min points
601        let mut invalid = proper.clone();
602        invalid.min_points = 0;
603        let err = invalid.validate().unwrap_err();
604        assert_eq!(err, ContractError::InvalidMinPoints {});
605
606        // fails on 0 max validators
607        let mut invalid = proper.clone();
608        invalid.max_validators = 0;
609        let err = invalid.validate().unwrap_err();
610        assert_eq!(err, ContractError::InvalidMaxValidators {});
611
612        // fails on 0 epoch size
613        let mut invalid = proper.clone();
614        invalid.epoch_length = 0;
615        let err = invalid.validate().unwrap_err();
616        assert_eq!(err, ContractError::InvalidEpoch {});
617
618        // allows no operators
619        let mut no_operators = proper.clone();
620        no_operators.initial_keys = vec![];
621        no_operators.validate().unwrap();
622
623        // fails on invalid operator
624        let mut invalid = proper.clone();
625        invalid.initial_keys = vec![valid_operator("foo"), invalid_operator()];
626        let err = invalid.validate().unwrap_err();
627        assert_eq!(err, ContractError::InvalidPubkey {});
628
629        // fails if no denom set for reward
630        let mut invalid = proper;
631        invalid.epoch_reward.denom = "".into();
632        let err = invalid.validate().unwrap_err();
633        assert_eq!(err, ContractError::InvalidRewardDenom {});
634    }
635
636    #[test]
637    fn validate_metadata() {
638        let meta = ValidatorMetadata {
639            moniker: "example".to_owned(),
640            identity: Some((0..MAX_METADATA_SIZE + 1).map(|_| "X").collect::<String>()),
641            website: Some((0..MAX_METADATA_SIZE + 1).map(|_| "X").collect::<String>()),
642            security_contact: Some((0..MAX_METADATA_SIZE + 1).map(|_| "X").collect::<String>()),
643            details: Some((0..MAX_METADATA_SIZE + 1).map(|_| "X").collect::<String>()),
644        };
645        let resp = meta.validate().unwrap_err();
646        assert_eq!(
647            ContractError::InvalidMetadata {
648                data: "identity",
649                min: MIN_METADATA_SIZE,
650                max: MAX_METADATA_SIZE
651            },
652            resp
653        );
654
655        let meta = ValidatorMetadata {
656            identity: Some("identity".to_owned()),
657            ..meta
658        };
659        let resp = meta.validate().unwrap_err();
660        assert_eq!(
661            ContractError::InvalidMetadata {
662                data: "website",
663                min: MIN_METADATA_SIZE,
664                max: MAX_METADATA_SIZE,
665            },
666            resp
667        );
668
669        let meta = ValidatorMetadata {
670            website: Some("https://website".to_owned()),
671            ..meta
672        };
673        let resp = meta.validate().unwrap_err();
674        assert_eq!(
675            ContractError::InvalidMetadata {
676                data: "security_contract",
677                min: MIN_METADATA_SIZE,
678                max: MAX_METADATA_SIZE,
679            },
680            resp
681        );
682
683        let meta = ValidatorMetadata {
684            security_contact: Some("contract".to_owned()),
685            ..meta
686        };
687        let resp = meta.validate().unwrap_err();
688        assert_eq!(
689            ContractError::InvalidMetadata {
690                data: "details",
691                min: MIN_METADATA_SIZE,
692                max: MAX_METADATA_SIZE,
693            },
694            resp
695        );
696
697        let meta = ValidatorMetadata {
698            identity: Some(String::new()),
699            website: Some(String::new()),
700            security_contact: Some(String::new()),
701            details: Some(String::new()),
702            ..meta
703        };
704        let resp = meta.validate().unwrap_err();
705        assert_eq!(
706            ContractError::InvalidMetadata {
707                data: "identity",
708                min: MIN_METADATA_SIZE,
709                max: MAX_METADATA_SIZE
710            },
711            resp
712        );
713
714        let meta = ValidatorMetadata {
715            identity: Some("identity".to_owned()),
716            ..meta
717        };
718        let resp = meta.validate().unwrap_err();
719        assert_eq!(
720            ContractError::InvalidMetadata {
721                data: "website",
722                min: MIN_METADATA_SIZE,
723                max: MAX_METADATA_SIZE,
724            },
725            resp
726        );
727
728        let meta = ValidatorMetadata {
729            website: Some("http://website".to_owned()),
730            ..meta
731        };
732        let resp = meta.validate().unwrap_err();
733        assert_eq!(
734            ContractError::InvalidMetadata {
735                data: "security_contract",
736                min: MIN_METADATA_SIZE,
737                max: MAX_METADATA_SIZE,
738            },
739            resp
740        );
741
742        let meta = ValidatorMetadata {
743            security_contact: Some("contract".to_owned()),
744            ..meta
745        };
746        let resp = meta.validate().unwrap_err();
747        assert_eq!(
748            ContractError::InvalidMetadata {
749                data: "details",
750                min: MIN_METADATA_SIZE,
751                max: MAX_METADATA_SIZE,
752            },
753            resp
754        );
755
756        let meta = ValidatorMetadata {
757            website: Some("website".to_owned()),
758            ..meta
759        };
760        let resp = meta.validate().unwrap_err();
761        assert_eq!(ContractError::InvalidMetadataWebsitePrefix {}, resp);
762    }
763}