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 pub admin: Option<String>,
19 pub membership: String,
21 pub min_points: u64,
25 pub max_validators: u32,
30 pub epoch_length: u64,
34 pub epoch_reward: Coin,
40
41 pub initial_keys: Vec<OperatorInitInfo>,
45
46 pub scaling: Option<u32>,
48
49 #[serde(default = "default_fee_percentage")]
53 pub fee_percentage: Decimal,
54
55 #[serde(default)]
58 pub auto_unjail: bool,
59
60 #[serde(default = "default_double_sign_slash")]
63 pub double_sign_slash_ratio: Decimal,
64
65 pub distribution_contracts: UnvalidatedDistributionContracts,
79
80 pub validator_group_code_id: u64,
91
92 pub verify_validators: bool,
98
99 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 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 UpdateAdmin {
134 admin: Option<String>,
135 },
136 UpdateConfig {
138 min_points: Option<u64>,
141 max_validators: Option<u32>,
145 scaling: Option<u32>,
147 epoch_reward: Option<Coin>,
152 fee_percentage: Option<Decimal>,
156 auto_unjail: Option<bool>,
159
160 double_sign_slash_ratio: Option<Decimal>,
163
164 distribution_contracts: Option<Vec<DistributionContract>>,
170
171 verify_validators: Option<bool>,
174
175 offline_jail_duration: Option<Duration>,
179 },
180 RegisterValidatorKey {
184 pubkey: Pubkey,
185 metadata: ValidatorMetadata,
187 },
188 UpdateMetadata(ValidatorMetadata),
189 Jail {
191 operator: String,
193 duration: JailingDuration,
195 },
196 Unjail {
199 operator: Option<String>,
202 },
203 Slash {
206 addr: String,
207 portion: Decimal,
208 },
209
210 #[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 Configuration {},
223 Epoch {},
225
226 Validator { operator: String },
229 ListValidators {
232 start_after: Option<String>,
233 limit: Option<u32>,
234 },
235
236 ListActiveValidators {
238 start_after: Option<String>,
239 limit: Option<u32>,
240 },
241
242 ListJailedValidators {
244 start_after: Option<String>,
245 limit: Option<u32>,
246 },
247
248 SimulateActiveValidators {},
252
253 ListValidatorSlashing { operator: String },
256
257 Admin {},
259}
260
261#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
262pub struct UnvalidatedDistributionContract {
263 pub contract: String,
265 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 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#[derive(
317 Serialize, Deserialize, Clone, Eq, PartialEq, Ord, PartialOrd, JsonSchema, Debug, Default,
318)]
319pub struct ValidatorMetadata {
320 pub moniker: String,
322
323 pub identity: Option<String>,
325
326 pub website: Option<String>,
328
329 pub security_contact: Option<String>,
331
332 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#[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 pub epoch_length: u64,
410 pub current_epoch: u64,
412 pub last_update_time: u64,
414 pub last_update_height: u64,
415 pub next_update_time: u64,
417}
418
419#[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 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 pub addr: String,
501 pub start_height: u64,
503 pub slashing: Vec<ValidatorSlashing>,
505 pub tombstoned: bool,
508 pub jailed_until: Option<Expiration>,
510}
511
512#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
514#[serde(rename_all = "snake_case")]
515pub enum DistributionMsg {
516 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 let mut with_scaling = proper.clone();
591 with_scaling.scaling = Some(10);
592 with_scaling.validate().unwrap();
593
594 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 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 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 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 let mut no_operators = proper.clone();
620 no_operators.initial_keys = vec![];
621 no_operators.validate().unwrap();
622
623 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 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}