1use serde::{Deserialize, Serialize};
18use serde_big_array::BigArray;
19
20use crate::{Address, BlockHeight, Hash, Timestamp};
21
22pub type PolicyAccountId = [u8; 32];
28
29pub type ProposalId = [u8; 32];
31
32pub type PolicyNonce = u64;
34
35pub const POLICY_ACCOUNT_DOMAIN_SEP: &[u8] = b"POLICY-ACCOUNT:";
41
42pub const PROPOSAL_DOMAIN_SEP: &[u8] = b"POLICY-PROPOSAL:";
44
45pub const APPROVAL_DOMAIN_SEP: &[u8] = b"POLICY-APPROVAL:";
47
48pub const MAX_MEMBERS: usize = 100;
54
55pub const MAX_CUSTOM_RULES: usize = 50;
57
58pub const MAX_APPROVALS: usize = 100;
60
61pub const MAX_PROPOSAL_PAYLOAD_SIZE: usize = 100_000; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
70#[repr(u8)]
71pub enum ActionClass {
72 TransferNative = 0,
74
75 TransferTokenOwnership = 1,
77
78 AdministerToken = 2,
80
81 StakingOperation = 3,
83
84 GovernanceAction = 4,
86
87 ModifyMembership = 5,
89
90 ModifyPolicy = 6,
92
93 DeployContract = 7,
95
96 CallContract = 8,
98
99 Other = 255,
101}
102
103impl ActionClass {
104 pub fn from_u8(v: u8) -> Option<Self> {
105 match v {
106 0 => Some(ActionClass::TransferNative),
107 1 => Some(ActionClass::TransferTokenOwnership),
108 2 => Some(ActionClass::AdministerToken),
109 3 => Some(ActionClass::StakingOperation),
110 4 => Some(ActionClass::GovernanceAction),
111 5 => Some(ActionClass::ModifyMembership),
112 6 => Some(ActionClass::ModifyPolicy),
113 7 => Some(ActionClass::DeployContract),
114 8 => Some(ActionClass::CallContract),
115 255 => Some(ActionClass::Other),
116 _ => None,
117 }
118 }
119
120 pub fn name(&self) -> &'static str {
121 match self {
122 ActionClass::TransferNative => "Transfer Native Balance",
123 ActionClass::TransferTokenOwnership => "Transfer Token Ownership",
124 ActionClass::AdministerToken => "Administer Token",
125 ActionClass::StakingOperation => "Staking Operation",
126 ActionClass::GovernanceAction => "Governance Action",
127 ActionClass::ModifyMembership => "Modify Membership",
128 ActionClass::ModifyPolicy => "Modify Policy",
129 ActionClass::DeployContract => "Deploy Contract",
130 ActionClass::CallContract => "Call Contract",
131 ActionClass::Other => "Other",
132 }
133 }
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
142pub enum ApprovalThreshold {
143 Unanimous,
145
146 Majority,
148
149 Percentage(u8),
151
152 Absolute(u32),
154
155 WeightedPercentage(u8),
157
158 Deny,
160}
161
162impl ApprovalThreshold {
163 pub fn is_valid(&self) -> bool {
165 match self {
166 ApprovalThreshold::Percentage(p) if *p == 0 || *p > 100 => false,
167 ApprovalThreshold::WeightedPercentage(p) if *p == 0 || *p > 100 => false,
168 ApprovalThreshold::Absolute(0) => false,
169 _ => true,
170 }
171 }
172
173 pub fn is_met(
175 &self,
176 num_approvals: u32,
177 total_members: u32,
178 approval_weight: u64,
179 total_weight: u64,
180 ) -> bool {
181 match self {
182 ApprovalThreshold::Unanimous => num_approvals == total_members,
183 ApprovalThreshold::Majority => num_approvals * 2 > total_members,
184 ApprovalThreshold::Percentage(p) => {
185 num_approvals * 100 >= total_members * (*p as u32)
186 }
187 ApprovalThreshold::Absolute(n) => num_approvals >= *n,
188 ApprovalThreshold::WeightedPercentage(p) => {
189 approval_weight * 100 >= total_weight * (*p as u64)
190 }
191 ApprovalThreshold::Deny => false,
192 }
193 }
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
202#[repr(u8)]
203pub enum PolicyProfile {
204 Conservative = 0,
207
208 Company = 1,
211
212 DAO = 2,
215
216 Personal = 3,
219
220 Trust = 4,
223
224 Custom = 255,
226}
227
228impl PolicyProfile {
229 pub fn from_u8(v: u8) -> Option<Self> {
230 match v {
231 0 => Some(PolicyProfile::Conservative),
232 1 => Some(PolicyProfile::Company),
233 2 => Some(PolicyProfile::DAO),
234 3 => Some(PolicyProfile::Personal),
235 4 => Some(PolicyProfile::Trust),
236 255 => Some(PolicyProfile::Custom),
237 _ => None,
238 }
239 }
240
241 pub fn name(&self) -> &'static str {
242 match self {
243 PolicyProfile::Conservative => "Conservative",
244 PolicyProfile::Company => "Company",
245 PolicyProfile::DAO => "DAO",
246 PolicyProfile::Personal => "Personal",
247 PolicyProfile::Trust => "Trust",
248 PolicyProfile::Custom => "Custom",
249 }
250 }
251
252 pub fn default_threshold(&self, action_class: ActionClass) -> ApprovalThreshold {
254 match (self, action_class) {
255 (PolicyProfile::Conservative, ActionClass::TransferNative) => {
257 ApprovalThreshold::Unanimous
258 }
259 (PolicyProfile::Conservative, ActionClass::TransferTokenOwnership) => {
260 ApprovalThreshold::Unanimous
261 }
262 (PolicyProfile::Conservative, ActionClass::ModifyMembership) => {
263 ApprovalThreshold::Unanimous
264 }
265 (PolicyProfile::Conservative, ActionClass::ModifyPolicy) => {
266 ApprovalThreshold::Unanimous
267 }
268 (PolicyProfile::Conservative, _) => ApprovalThreshold::Majority,
269
270 (PolicyProfile::Company, ActionClass::TransferNative) => ApprovalThreshold::Unanimous,
272 (PolicyProfile::Company, ActionClass::TransferTokenOwnership) => {
273 ApprovalThreshold::Unanimous
274 }
275 (PolicyProfile::Company, ActionClass::GovernanceAction) => ApprovalThreshold::Majority,
276 (PolicyProfile::Company, ActionClass::ModifyMembership) => {
277 ApprovalThreshold::Percentage(67) }
279 (PolicyProfile::Company, ActionClass::ModifyPolicy) => {
280 ApprovalThreshold::Percentage(67)
281 }
282 (PolicyProfile::Company, _) => ApprovalThreshold::Majority,
283
284 (PolicyProfile::DAO, ActionClass::GovernanceAction) => {
286 ApprovalThreshold::WeightedPercentage(51)
287 }
288 (PolicyProfile::DAO, ActionClass::TransferNative) => ApprovalThreshold::Majority,
289 (PolicyProfile::DAO, ActionClass::TransferTokenOwnership) => {
290 ApprovalThreshold::Majority
291 }
292 (PolicyProfile::DAO, ActionClass::ModifyMembership) => {
293 ApprovalThreshold::WeightedPercentage(67)
294 }
295 (PolicyProfile::DAO, ActionClass::ModifyPolicy) => {
296 ApprovalThreshold::WeightedPercentage(67)
297 }
298 (PolicyProfile::DAO, _) => ApprovalThreshold::Majority,
299
300 (PolicyProfile::Personal, _) => ApprovalThreshold::Majority,
302
303 (PolicyProfile::Trust, ActionClass::TransferNative) => ApprovalThreshold::Unanimous,
305 (PolicyProfile::Trust, ActionClass::TransferTokenOwnership) => {
306 ApprovalThreshold::Unanimous
307 }
308 (PolicyProfile::Trust, ActionClass::ModifyMembership) => ApprovalThreshold::Unanimous,
309 (PolicyProfile::Trust, ActionClass::ModifyPolicy) => ApprovalThreshold::Unanimous,
310 (PolicyProfile::Trust, _) => ApprovalThreshold::Percentage(67),
311
312 (PolicyProfile::Custom, _) => ApprovalThreshold::Deny,
314 }
315 }
316}
317
318#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
324pub struct PolicyMember {
325 pub address: Address,
327
328 pub weight: u64,
330}
331
332impl PolicyMember {
333 pub fn new(address: Address) -> Self {
334 Self { address, weight: 1 }
335 }
336
337 pub fn with_weight(address: Address, weight: u64) -> Self {
338 Self { address, weight }
339 }
340}
341
342#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
348pub struct PolicyRule {
349 pub action_class: ActionClass,
351
352 pub threshold: ApprovalThreshold,
354}
355
356#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
358pub struct PolicyConfig {
359 pub profile: PolicyProfile,
361
362 pub overrides: Vec<PolicyRule>,
364}
365
366impl PolicyConfig {
367 pub fn threshold_for(&self, action_class: ActionClass) -> ApprovalThreshold {
369 for rule in &self.overrides {
371 if rule.action_class == action_class {
372 return rule.threshold;
373 }
374 }
375
376 self.profile.default_threshold(action_class)
378 }
379
380 pub fn is_valid(&self) -> bool {
382 if self.overrides.len() > MAX_CUSTOM_RULES {
384 return false;
385 }
386
387 for rule in &self.overrides {
389 if !rule.threshold.is_valid() {
390 return false;
391 }
392 }
393
394 true
395 }
396}
397
398#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
404#[repr(u8)]
405pub enum PolicyAccountStatus {
406 Active = 0,
408
409 Frozen = 1,
411}
412
413impl PolicyAccountStatus {
414 pub fn from_u8(v: u8) -> Option<Self> {
415 match v {
416 0 => Some(PolicyAccountStatus::Active),
417 1 => Some(PolicyAccountStatus::Frozen),
418 _ => None,
419 }
420 }
421
422 pub fn is_active(&self) -> bool {
423 matches!(self, PolicyAccountStatus::Active)
424 }
425}
426
427#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
429pub struct PolicyAccount {
430 pub id: PolicyAccountId,
432
433 pub address: Address,
435
436 pub members: Vec<PolicyMember>,
438
439 pub policy: PolicyConfig,
441
442 pub nonce: PolicyNonce,
444
445 pub status: PolicyAccountStatus,
447
448 pub created_at: BlockHeight,
450
451 pub created_timestamp: Timestamp,
453}
454
455impl PolicyAccount {
456 pub fn compute_id(members: &[PolicyMember], salt: &[u8]) -> PolicyAccountId {
458 use blake3::Hasher;
459
460 let mut hasher = Hasher::new();
461 hasher.update(POLICY_ACCOUNT_DOMAIN_SEP);
462
463 let mut sorted_members = members.to_vec();
465 sorted_members.sort_by(|a, b| a.address.cmp(&b.address));
466
467 for member in &sorted_members {
468 hasher.update(member.address.as_bytes());
469 hasher.update(&member.weight.to_le_bytes());
470 }
471
472 hasher.update(salt);
473 *hasher.finalize().as_bytes()
474 }
475
476 pub fn id_to_address(id: &PolicyAccountId) -> Address {
478 let mut addr_bytes = [0u8; 20];
480 addr_bytes.copy_from_slice(&id[12..32]);
481 Address::new(addr_bytes)
482 }
483
484 pub fn is_member(&self, addr: &Address) -> bool {
486 self.members.iter().any(|m| &m.address == addr)
487 }
488
489 pub fn total_weight(&self) -> u64 {
491 self.members.iter().map(|m| m.weight).sum()
492 }
493
494 pub fn is_valid(&self) -> bool {
496 if self.members.is_empty() || self.members.len() > MAX_MEMBERS {
498 return false;
499 }
500
501 for i in 0..self.members.len() {
503 for j in (i + 1)..self.members.len() {
504 if self.members[i].address == self.members[j].address {
505 return false;
506 }
507 }
508 }
509
510 if self.members.iter().any(|m| m.weight == 0) {
512 return false;
513 }
514
515 if !self.policy.is_valid() {
517 return false;
518 }
519
520 if self.address != Self::id_to_address(&self.id) {
522 return false;
523 }
524
525 true
526 }
527}
528
529#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
535#[repr(u8)]
536pub enum ProposalStatus {
537 Pending = 0,
539
540 Executed = 1,
542
543 Expired = 2,
545
546 Cancelled = 3,
548}
549
550impl ProposalStatus {
551 pub fn from_u8(v: u8) -> Option<Self> {
552 match v {
553 0 => Some(ProposalStatus::Pending),
554 1 => Some(ProposalStatus::Executed),
555 2 => Some(ProposalStatus::Expired),
556 3 => Some(ProposalStatus::Cancelled),
557 _ => None,
558 }
559 }
560
561 pub fn is_pending(&self) -> bool {
562 matches!(self, ProposalStatus::Pending)
563 }
564}
565
566#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
568pub struct MemberApproval {
569 pub approver: Address,
571
572 #[serde(with = "BigArray")]
574 pub signature: [u8; 64],
575
576 pub timestamp: Timestamp,
578}
579
580#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
582pub struct Proposal {
583 pub id: ProposalId,
585
586 pub policy_account_id: PolicyAccountId,
588
589 pub policy_nonce: PolicyNonce,
591
592 pub proposer: Address,
594
595 pub action_class: ActionClass,
597
598 pub action_data: Vec<u8>,
600
601 pub action_hash: Hash,
603
604 pub approvals: Vec<MemberApproval>,
606
607 pub status: ProposalStatus,
609
610 pub expires_at: Timestamp,
612
613 pub created_at: Timestamp,
615
616 pub created_height: BlockHeight,
618}
619
620impl Proposal {
621 pub fn compute_id(
623 policy_account_id: &PolicyAccountId,
624 policy_nonce: PolicyNonce,
625 action_hash: &Hash,
626 ) -> ProposalId {
627 use blake3::Hasher;
628
629 let mut hasher = Hasher::new();
630 hasher.update(PROPOSAL_DOMAIN_SEP);
631 hasher.update(policy_account_id);
632 hasher.update(&policy_nonce.to_le_bytes());
633 hasher.update(action_hash.as_bytes());
634 *hasher.finalize().as_bytes()
635 }
636
637 pub fn approval_message(
639 proposal_id: &ProposalId,
640 policy_account_id: &PolicyAccountId,
641 action_hash: &Hash,
642 policy_nonce: PolicyNonce,
643 ) -> Hash {
644 use blake3::Hasher;
645
646 let mut hasher = Hasher::new();
647 hasher.update(APPROVAL_DOMAIN_SEP);
648 hasher.update(proposal_id);
649 hasher.update(policy_account_id);
650 hasher.update(action_hash.as_bytes());
651 hasher.update(&policy_nonce.to_le_bytes());
652 Hash::new(*hasher.finalize().as_bytes())
653 }
654
655 pub fn has_approval(&self, approver: &Address) -> bool {
657 self.approvals.iter().any(|a| &a.approver == approver)
658 }
659
660 pub fn is_valid(&self) -> bool {
662 if self.action_data.len() > MAX_PROPOSAL_PAYLOAD_SIZE {
664 return false;
665 }
666
667 if self.approvals.len() > MAX_APPROVALS {
669 return false;
670 }
671
672 let computed_hash = Hash::hash(&self.action_data);
674 if computed_hash != self.action_hash {
675 return false;
676 }
677
678 for i in 0..self.approvals.len() {
680 for j in (i + 1)..self.approvals.len() {
681 if self.approvals[i].approver == self.approvals[j].approver {
682 return false;
683 }
684 }
685 }
686
687 true
688 }
689}
690
691#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
697#[repr(u8)]
698pub enum PolicyAccountOperation {
699 Create = 0,
701
702 SubmitProposal = 1,
704
705 ExecuteProposal = 2,
707
708 CancelProposal = 3,
710
711 ModifyMembership = 4,
713
714 ModifyPolicy = 5,
716
717 Freeze = 6,
719
720 Unfreeze = 7,
722}
723
724impl PolicyAccountOperation {
725 pub fn from_u8(v: u8) -> Option<Self> {
726 match v {
727 0 => Some(PolicyAccountOperation::Create),
728 1 => Some(PolicyAccountOperation::SubmitProposal),
729 2 => Some(PolicyAccountOperation::ExecuteProposal),
730 3 => Some(PolicyAccountOperation::CancelProposal),
731 4 => Some(PolicyAccountOperation::ModifyMembership),
732 5 => Some(PolicyAccountOperation::ModifyPolicy),
733 6 => Some(PolicyAccountOperation::Freeze),
734 7 => Some(PolicyAccountOperation::Unfreeze),
735 _ => None,
736 }
737 }
738}
739
740#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
742pub struct PolicyAccountTxData {
743 pub operation: PolicyAccountOperation,
745
746 pub data: Vec<u8>,
748
749 pub recipient: Address,
751}