Skip to main content

sumchain_primitives/
policy_account.rs

1//! Policy Account Module - Consensus-Enforced Multi-Signature Governance
2//!
3//! This module implements group-governed addresses with configurable approval policies.
4//! Key features:
5//! - Group-governed addresses with multiple members
6//! - Action-class-based approval thresholds
7//! - Built-in policy profiles for common use cases
8//! - Replay protection for group-authorized actions
9//! - Deterministic action classification
10//!
11//! Design principles:
12//! - Security-first: fail-closed for unknown actions
13//! - Compatibility: existing single-owner addresses work unchanged
14//! - Flexibility: configurable per-action-class rules
15//! - Transparency: all governance on-chain
16
17use serde::{Deserialize, Serialize};
18use serde_big_array::BigArray;
19
20use crate::{Address, BlockHeight, Hash, Timestamp};
21
22// =============================================================================
23// Type Aliases
24// =============================================================================
25
26/// Policy Account ID (derived from members and creation parameters)
27pub type PolicyAccountId = [u8; 32];
28
29/// Proposal ID for group-authorized actions
30pub type ProposalId = [u8; 32];
31
32/// Nonce for replay protection within a policy account
33pub type PolicyNonce = u64;
34
35// =============================================================================
36// Domain Separation Constants
37// =============================================================================
38
39/// Domain separator for policy account IDs
40pub const POLICY_ACCOUNT_DOMAIN_SEP: &[u8] = b"POLICY-ACCOUNT:";
41
42/// Domain separator for proposal IDs
43pub const PROPOSAL_DOMAIN_SEP: &[u8] = b"POLICY-PROPOSAL:";
44
45/// Domain separator for approval messages
46pub const APPROVAL_DOMAIN_SEP: &[u8] = b"POLICY-APPROVAL:";
47
48// =============================================================================
49// Limits (DoS Prevention)
50// =============================================================================
51
52/// Maximum members per policy account
53pub const MAX_MEMBERS: usize = 100;
54
55/// Maximum custom rules per policy account
56pub const MAX_CUSTOM_RULES: usize = 50;
57
58/// Maximum approvals per proposal
59pub const MAX_APPROVALS: usize = 100;
60
61/// Maximum proposal payload size (bytes)
62pub const MAX_PROPOSAL_PAYLOAD_SIZE: usize = 100_000; // 100 KB
63
64// =============================================================================
65// Action Classification
66// =============================================================================
67
68/// Action classes for policy enforcement
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
70#[repr(u8)]
71pub enum ActionClass {
72    /// Transfer native balance from policy account
73    TransferNative = 0,
74
75    /// Transfer token ownership (SRC-20, NFT, etc.)
76    TransferTokenOwnership = 1,
77
78    /// Administrative token actions (pause, metadata, minter management)
79    AdministerToken = 2,
80
81    /// Stake, unstake, delegate operations
82    StakingOperation = 3,
83
84    /// Governance vote, proposal, parameter change
85    GovernanceAction = 4,
86
87    /// Modify policy account membership
88    ModifyMembership = 5,
89
90    /// Modify policy account rules
91    ModifyPolicy = 6,
92
93    /// Deploy smart contract
94    DeployContract = 7,
95
96    /// Call smart contract
97    CallContract = 8,
98
99    /// Other actions (must be explicitly configured, fail-closed by default)
100    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// =============================================================================
137// Approval Threshold Types
138// =============================================================================
139
140/// Approval threshold for an action class
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
142pub enum ApprovalThreshold {
143    /// Requires unanimous approval from all members
144    Unanimous,
145
146    /// Requires simple majority (>50%)
147    Majority,
148
149    /// Requires specific percentage (1-100)
150    Percentage(u8),
151
152    /// Requires absolute number of approvals
153    Absolute(u32),
154
155    /// Requires weighted threshold (for weighted members)
156    WeightedPercentage(u8),
157
158    /// Deny all (cannot be executed)
159    Deny,
160}
161
162impl ApprovalThreshold {
163    /// Validate threshold is sensible
164    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    /// Check if threshold is met
174    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// =============================================================================
197// Policy Profiles (Safe Defaults)
198// =============================================================================
199
200/// Pre-configured policy profiles for common use cases
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
202#[repr(u8)]
203pub enum PolicyProfile {
204    /// Conservative: unanimous for all asset transfers, majority for admin
205    /// Use case: high-value assets, house, family trust
206    Conservative = 0,
207
208    /// Company: majority for governance, unanimous for ownership transfers
209    /// Use case: traditional corporate governance
210    Company = 1,
211
212    /// DAO: majority for most actions, weighted voting for governance
213    /// Use case: decentralized organizations
214    DAO = 2,
215
216    /// Personal: simple majority for all actions
217    /// Use case: joint personal accounts, shared assets
218    Personal = 3,
219
220    /// Trust: specific thresholds for fiduciary responsibilities
221    /// Use case: legal trusts, estate planning
222    Trust = 4,
223
224    /// Custom: user defines all rules
225    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    /// Get default threshold for an action class under this profile
253    pub fn default_threshold(&self, action_class: ActionClass) -> ApprovalThreshold {
254        match (self, action_class) {
255            // Conservative: unanimous for transfers, majority for admin
256            (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            // Company: majority for governance, unanimous for ownership
271            (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) // Supermajority
278            }
279            (PolicyProfile::Company, ActionClass::ModifyPolicy) => {
280                ApprovalThreshold::Percentage(67)
281            }
282            (PolicyProfile::Company, _) => ApprovalThreshold::Majority,
283
284            // DAO: weighted voting for governance, majority for admin
285            (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            // Personal: simple majority for all
301            (PolicyProfile::Personal, _) => ApprovalThreshold::Majority,
302
303            // Trust: conservative with fiduciary requirements
304            (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            // Custom: deny by default (must be explicitly configured)
313            (PolicyProfile::Custom, _) => ApprovalThreshold::Deny,
314        }
315    }
316}
317
318// =============================================================================
319// Member and Weight
320// =============================================================================
321
322/// Policy account member with optional weight
323#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
324pub struct PolicyMember {
325    /// Member address
326    pub address: Address,
327
328    /// Weight for weighted voting (1 if not weighted)
329    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// =============================================================================
343// Policy Rules
344// =============================================================================
345
346/// Override rule for a specific action class
347#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
348pub struct PolicyRule {
349    /// Action class this rule applies to
350    pub action_class: ActionClass,
351
352    /// Threshold override
353    pub threshold: ApprovalThreshold,
354}
355
356/// Complete policy configuration
357#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
358pub struct PolicyConfig {
359    /// Base profile
360    pub profile: PolicyProfile,
361
362    /// Custom overrides (empty if using profile defaults)
363    pub overrides: Vec<PolicyRule>,
364}
365
366impl PolicyConfig {
367    /// Get effective threshold for an action class
368    pub fn threshold_for(&self, action_class: ActionClass) -> ApprovalThreshold {
369        // Check for override first
370        for rule in &self.overrides {
371            if rule.action_class == action_class {
372                return rule.threshold;
373            }
374        }
375
376        // Fall back to profile default
377        self.profile.default_threshold(action_class)
378    }
379
380    /// Validate policy configuration
381    pub fn is_valid(&self) -> bool {
382        // Check limits
383        if self.overrides.len() > MAX_CUSTOM_RULES {
384            return false;
385        }
386
387        // Check all thresholds are valid
388        for rule in &self.overrides {
389            if !rule.threshold.is_valid() {
390                return false;
391            }
392        }
393
394        true
395    }
396}
397
398// =============================================================================
399// Policy Account
400// =============================================================================
401
402/// Policy account status
403#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
404#[repr(u8)]
405pub enum PolicyAccountStatus {
406    /// Active and operational
407    Active = 0,
408
409    /// Frozen (no operations allowed)
410    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/// Policy-governed account
428#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
429pub struct PolicyAccount {
430    /// Unique policy account ID
431    pub id: PolicyAccountId,
432
433    /// The address this policy controls (derived from ID)
434    pub address: Address,
435
436    /// Members with optional weights
437    pub members: Vec<PolicyMember>,
438
439    /// Policy configuration
440    pub policy: PolicyConfig,
441
442    /// Nonce for replay protection
443    pub nonce: PolicyNonce,
444
445    /// Status
446    pub status: PolicyAccountStatus,
447
448    /// Creation block height
449    pub created_at: BlockHeight,
450
451    /// Creation timestamp
452    pub created_timestamp: Timestamp,
453}
454
455impl PolicyAccount {
456    /// Compute policy account ID
457    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        // Hash all members (sorted by address for determinism)
464        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    /// Derive address from policy account ID
477    pub fn id_to_address(id: &PolicyAccountId) -> Address {
478        // Take last 20 bytes of the ID as the address
479        let mut addr_bytes = [0u8; 20];
480        addr_bytes.copy_from_slice(&id[12..32]);
481        Address::new(addr_bytes)
482    }
483
484    /// Check if address is a member
485    pub fn is_member(&self, addr: &Address) -> bool {
486        self.members.iter().any(|m| &m.address == addr)
487    }
488
489    /// Get total weight of all members
490    pub fn total_weight(&self) -> u64 {
491        self.members.iter().map(|m| m.weight).sum()
492    }
493
494    /// Validate policy account structure
495    pub fn is_valid(&self) -> bool {
496        // Check member count
497        if self.members.is_empty() || self.members.len() > MAX_MEMBERS {
498            return false;
499        }
500
501        // Check for duplicate members
502        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        // Check all weights are positive
511        if self.members.iter().any(|m| m.weight == 0) {
512            return false;
513        }
514
515        // Validate policy
516        if !self.policy.is_valid() {
517            return false;
518        }
519
520        // Verify address matches ID
521        if self.address != Self::id_to_address(&self.id) {
522            return false;
523        }
524
525        true
526    }
527}
528
529// =============================================================================
530// Proposal (Group-Authorized Action)
531// =============================================================================
532
533/// Proposal status
534#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
535#[repr(u8)]
536pub enum ProposalStatus {
537    /// Collecting approvals
538    Pending = 0,
539
540    /// Executed successfully
541    Executed = 1,
542
543    /// Expired before execution
544    Expired = 2,
545
546    /// Cancelled by proposer
547    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/// Member approval for a proposal
567#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
568pub struct MemberApproval {
569    /// Approver address
570    pub approver: Address,
571
572    /// Ed25519 signature over approval message
573    #[serde(with = "BigArray")]
574    pub signature: [u8; 64],
575
576    /// Timestamp of approval
577    pub timestamp: Timestamp,
578}
579
580/// Proposal for group-authorized action
581#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
582pub struct Proposal {
583    /// Unique proposal ID
584    pub id: ProposalId,
585
586    /// Policy account ID this proposal is for
587    pub policy_account_id: PolicyAccountId,
588
589    /// Policy nonce at proposal creation (for replay protection)
590    pub policy_nonce: PolicyNonce,
591
592    /// Proposer address (must be a member)
593    pub proposer: Address,
594
595    /// Action class for this proposal
596    pub action_class: ActionClass,
597
598    /// Serialized action data (transaction payload)
599    pub action_data: Vec<u8>,
600
601    /// Hash of action data (for quick validation)
602    pub action_hash: Hash,
603
604    /// Approvals collected
605    pub approvals: Vec<MemberApproval>,
606
607    /// Status
608    pub status: ProposalStatus,
609
610    /// Expiration timestamp
611    pub expires_at: Timestamp,
612
613    /// Creation timestamp
614    pub created_at: Timestamp,
615
616    /// Creation block height
617    pub created_height: BlockHeight,
618}
619
620impl Proposal {
621    /// Compute proposal ID
622    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    /// Compute approval message to be signed by members
638    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    /// Check if approver already approved
656    pub fn has_approval(&self, approver: &Address) -> bool {
657        self.approvals.iter().any(|a| &a.approver == approver)
658    }
659
660    /// Validate proposal structure
661    pub fn is_valid(&self) -> bool {
662        // Check payload size
663        if self.action_data.len() > MAX_PROPOSAL_PAYLOAD_SIZE {
664            return false;
665        }
666
667        // Check approval count
668        if self.approvals.len() > MAX_APPROVALS {
669            return false;
670        }
671
672        // Verify action hash matches data
673        let computed_hash = Hash::hash(&self.action_data);
674        if computed_hash != self.action_hash {
675            return false;
676        }
677
678        // Check for duplicate approvals
679        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// =============================================================================
692// Transaction Data
693// =============================================================================
694
695/// Policy account operation codes
696#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
697#[repr(u8)]
698pub enum PolicyAccountOperation {
699    /// Create a new policy account
700    Create = 0,
701
702    /// Submit a proposal (or add approvals)
703    SubmitProposal = 1,
704
705    /// Execute a proposal (once threshold met)
706    ExecuteProposal = 2,
707
708    /// Cancel a proposal (proposer only)
709    CancelProposal = 3,
710
711    /// Modify membership (via group approval)
712    ModifyMembership = 4,
713
714    /// Modify policy rules (via group approval)
715    ModifyPolicy = 5,
716
717    /// Freeze policy account
718    Freeze = 6,
719
720    /// Unfreeze policy account
721    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/// Policy account transaction data
741#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
742pub struct PolicyAccountTxData {
743    /// Operation code
744    pub operation: PolicyAccountOperation,
745
746    /// Operation-specific data (serialized)
747    pub data: Vec<u8>,
748
749    /// Recipient address (for fee distribution, etc.)
750    pub recipient: Address,
751}