use serde::{Deserialize, Serialize};
use serde_big_array::BigArray;
use crate::{Address, BlockHeight, Hash, Timestamp};
pub type PolicyAccountId = [u8; 32];
pub type ProposalId = [u8; 32];
pub type PolicyNonce = u64;
pub const POLICY_ACCOUNT_DOMAIN_SEP: &[u8] = b"POLICY-ACCOUNT:";
pub const PROPOSAL_DOMAIN_SEP: &[u8] = b"POLICY-PROPOSAL:";
pub const APPROVAL_DOMAIN_SEP: &[u8] = b"POLICY-APPROVAL:";
pub const MAX_MEMBERS: usize = 100;
pub const MAX_CUSTOM_RULES: usize = 50;
pub const MAX_APPROVALS: usize = 100;
pub const MAX_PROPOSAL_PAYLOAD_SIZE: usize = 100_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum ActionClass {
TransferNative = 0,
TransferTokenOwnership = 1,
AdministerToken = 2,
StakingOperation = 3,
GovernanceAction = 4,
ModifyMembership = 5,
ModifyPolicy = 6,
DeployContract = 7,
CallContract = 8,
Other = 255,
}
impl ActionClass {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(ActionClass::TransferNative),
1 => Some(ActionClass::TransferTokenOwnership),
2 => Some(ActionClass::AdministerToken),
3 => Some(ActionClass::StakingOperation),
4 => Some(ActionClass::GovernanceAction),
5 => Some(ActionClass::ModifyMembership),
6 => Some(ActionClass::ModifyPolicy),
7 => Some(ActionClass::DeployContract),
8 => Some(ActionClass::CallContract),
255 => Some(ActionClass::Other),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
ActionClass::TransferNative => "Transfer Native Balance",
ActionClass::TransferTokenOwnership => "Transfer Token Ownership",
ActionClass::AdministerToken => "Administer Token",
ActionClass::StakingOperation => "Staking Operation",
ActionClass::GovernanceAction => "Governance Action",
ActionClass::ModifyMembership => "Modify Membership",
ActionClass::ModifyPolicy => "Modify Policy",
ActionClass::DeployContract => "Deploy Contract",
ActionClass::CallContract => "Call Contract",
ActionClass::Other => "Other",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ApprovalThreshold {
Unanimous,
Majority,
Percentage(u8),
Absolute(u32),
WeightedPercentage(u8),
Deny,
}
impl ApprovalThreshold {
pub fn is_valid(&self) -> bool {
match self {
ApprovalThreshold::Percentage(p) if *p == 0 || *p > 100 => false,
ApprovalThreshold::WeightedPercentage(p) if *p == 0 || *p > 100 => false,
ApprovalThreshold::Absolute(0) => false,
_ => true,
}
}
pub fn is_met(
&self,
num_approvals: u32,
total_members: u32,
approval_weight: u64,
total_weight: u64,
) -> bool {
match self {
ApprovalThreshold::Unanimous => num_approvals == total_members,
ApprovalThreshold::Majority => num_approvals * 2 > total_members,
ApprovalThreshold::Percentage(p) => {
num_approvals * 100 >= total_members * (*p as u32)
}
ApprovalThreshold::Absolute(n) => num_approvals >= *n,
ApprovalThreshold::WeightedPercentage(p) => {
approval_weight * 100 >= total_weight * (*p as u64)
}
ApprovalThreshold::Deny => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum PolicyProfile {
Conservative = 0,
Company = 1,
DAO = 2,
Personal = 3,
Trust = 4,
Custom = 255,
}
impl PolicyProfile {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(PolicyProfile::Conservative),
1 => Some(PolicyProfile::Company),
2 => Some(PolicyProfile::DAO),
3 => Some(PolicyProfile::Personal),
4 => Some(PolicyProfile::Trust),
255 => Some(PolicyProfile::Custom),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
PolicyProfile::Conservative => "Conservative",
PolicyProfile::Company => "Company",
PolicyProfile::DAO => "DAO",
PolicyProfile::Personal => "Personal",
PolicyProfile::Trust => "Trust",
PolicyProfile::Custom => "Custom",
}
}
pub fn default_threshold(&self, action_class: ActionClass) -> ApprovalThreshold {
match (self, action_class) {
(PolicyProfile::Conservative, ActionClass::TransferNative) => {
ApprovalThreshold::Unanimous
}
(PolicyProfile::Conservative, ActionClass::TransferTokenOwnership) => {
ApprovalThreshold::Unanimous
}
(PolicyProfile::Conservative, ActionClass::ModifyMembership) => {
ApprovalThreshold::Unanimous
}
(PolicyProfile::Conservative, ActionClass::ModifyPolicy) => {
ApprovalThreshold::Unanimous
}
(PolicyProfile::Conservative, _) => ApprovalThreshold::Majority,
(PolicyProfile::Company, ActionClass::TransferNative) => ApprovalThreshold::Unanimous,
(PolicyProfile::Company, ActionClass::TransferTokenOwnership) => {
ApprovalThreshold::Unanimous
}
(PolicyProfile::Company, ActionClass::GovernanceAction) => ApprovalThreshold::Majority,
(PolicyProfile::Company, ActionClass::ModifyMembership) => {
ApprovalThreshold::Percentage(67) }
(PolicyProfile::Company, ActionClass::ModifyPolicy) => {
ApprovalThreshold::Percentage(67)
}
(PolicyProfile::Company, _) => ApprovalThreshold::Majority,
(PolicyProfile::DAO, ActionClass::GovernanceAction) => {
ApprovalThreshold::WeightedPercentage(51)
}
(PolicyProfile::DAO, ActionClass::TransferNative) => ApprovalThreshold::Majority,
(PolicyProfile::DAO, ActionClass::TransferTokenOwnership) => {
ApprovalThreshold::Majority
}
(PolicyProfile::DAO, ActionClass::ModifyMembership) => {
ApprovalThreshold::WeightedPercentage(67)
}
(PolicyProfile::DAO, ActionClass::ModifyPolicy) => {
ApprovalThreshold::WeightedPercentage(67)
}
(PolicyProfile::DAO, _) => ApprovalThreshold::Majority,
(PolicyProfile::Personal, _) => ApprovalThreshold::Majority,
(PolicyProfile::Trust, ActionClass::TransferNative) => ApprovalThreshold::Unanimous,
(PolicyProfile::Trust, ActionClass::TransferTokenOwnership) => {
ApprovalThreshold::Unanimous
}
(PolicyProfile::Trust, ActionClass::ModifyMembership) => ApprovalThreshold::Unanimous,
(PolicyProfile::Trust, ActionClass::ModifyPolicy) => ApprovalThreshold::Unanimous,
(PolicyProfile::Trust, _) => ApprovalThreshold::Percentage(67),
(PolicyProfile::Custom, _) => ApprovalThreshold::Deny,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolicyMember {
pub address: Address,
pub weight: u64,
}
impl PolicyMember {
pub fn new(address: Address) -> Self {
Self { address, weight: 1 }
}
pub fn with_weight(address: Address, weight: u64) -> Self {
Self { address, weight }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolicyRule {
pub action_class: ActionClass,
pub threshold: ApprovalThreshold,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolicyConfig {
pub profile: PolicyProfile,
pub overrides: Vec<PolicyRule>,
}
impl PolicyConfig {
pub fn threshold_for(&self, action_class: ActionClass) -> ApprovalThreshold {
for rule in &self.overrides {
if rule.action_class == action_class {
return rule.threshold;
}
}
self.profile.default_threshold(action_class)
}
pub fn is_valid(&self) -> bool {
if self.overrides.len() > MAX_CUSTOM_RULES {
return false;
}
for rule in &self.overrides {
if !rule.threshold.is_valid() {
return false;
}
}
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum PolicyAccountStatus {
Active = 0,
Frozen = 1,
}
impl PolicyAccountStatus {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(PolicyAccountStatus::Active),
1 => Some(PolicyAccountStatus::Frozen),
_ => None,
}
}
pub fn is_active(&self) -> bool {
matches!(self, PolicyAccountStatus::Active)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolicyAccount {
pub id: PolicyAccountId,
pub address: Address,
pub members: Vec<PolicyMember>,
pub policy: PolicyConfig,
pub nonce: PolicyNonce,
pub status: PolicyAccountStatus,
pub created_at: BlockHeight,
pub created_timestamp: Timestamp,
}
impl PolicyAccount {
pub fn compute_id(members: &[PolicyMember], salt: &[u8]) -> PolicyAccountId {
use blake3::Hasher;
let mut hasher = Hasher::new();
hasher.update(POLICY_ACCOUNT_DOMAIN_SEP);
let mut sorted_members = members.to_vec();
sorted_members.sort_by(|a, b| a.address.cmp(&b.address));
for member in &sorted_members {
hasher.update(member.address.as_bytes());
hasher.update(&member.weight.to_le_bytes());
}
hasher.update(salt);
*hasher.finalize().as_bytes()
}
pub fn id_to_address(id: &PolicyAccountId) -> Address {
let mut addr_bytes = [0u8; 20];
addr_bytes.copy_from_slice(&id[12..32]);
Address::new(addr_bytes)
}
pub fn is_member(&self, addr: &Address) -> bool {
self.members.iter().any(|m| &m.address == addr)
}
pub fn total_weight(&self) -> u64 {
self.members.iter().map(|m| m.weight).sum()
}
pub fn is_valid(&self) -> bool {
if self.members.is_empty() || self.members.len() > MAX_MEMBERS {
return false;
}
for i in 0..self.members.len() {
for j in (i + 1)..self.members.len() {
if self.members[i].address == self.members[j].address {
return false;
}
}
}
if self.members.iter().any(|m| m.weight == 0) {
return false;
}
if !self.policy.is_valid() {
return false;
}
if self.address != Self::id_to_address(&self.id) {
return false;
}
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum ProposalStatus {
Pending = 0,
Executed = 1,
Expired = 2,
Cancelled = 3,
}
impl ProposalStatus {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(ProposalStatus::Pending),
1 => Some(ProposalStatus::Executed),
2 => Some(ProposalStatus::Expired),
3 => Some(ProposalStatus::Cancelled),
_ => None,
}
}
pub fn is_pending(&self) -> bool {
matches!(self, ProposalStatus::Pending)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MemberApproval {
pub approver: Address,
#[serde(with = "BigArray")]
pub signature: [u8; 64],
pub timestamp: Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Proposal {
pub id: ProposalId,
pub policy_account_id: PolicyAccountId,
pub policy_nonce: PolicyNonce,
pub proposer: Address,
pub action_class: ActionClass,
pub action_data: Vec<u8>,
pub action_hash: Hash,
pub approvals: Vec<MemberApproval>,
pub status: ProposalStatus,
pub expires_at: Timestamp,
pub created_at: Timestamp,
pub created_height: BlockHeight,
}
impl Proposal {
pub fn compute_id(
policy_account_id: &PolicyAccountId,
policy_nonce: PolicyNonce,
action_hash: &Hash,
) -> ProposalId {
use blake3::Hasher;
let mut hasher = Hasher::new();
hasher.update(PROPOSAL_DOMAIN_SEP);
hasher.update(policy_account_id);
hasher.update(&policy_nonce.to_le_bytes());
hasher.update(action_hash.as_bytes());
*hasher.finalize().as_bytes()
}
pub fn approval_message(
proposal_id: &ProposalId,
policy_account_id: &PolicyAccountId,
action_hash: &Hash,
policy_nonce: PolicyNonce,
) -> Hash {
use blake3::Hasher;
let mut hasher = Hasher::new();
hasher.update(APPROVAL_DOMAIN_SEP);
hasher.update(proposal_id);
hasher.update(policy_account_id);
hasher.update(action_hash.as_bytes());
hasher.update(&policy_nonce.to_le_bytes());
Hash::new(*hasher.finalize().as_bytes())
}
pub fn has_approval(&self, approver: &Address) -> bool {
self.approvals.iter().any(|a| &a.approver == approver)
}
pub fn is_valid(&self) -> bool {
if self.action_data.len() > MAX_PROPOSAL_PAYLOAD_SIZE {
return false;
}
if self.approvals.len() > MAX_APPROVALS {
return false;
}
let computed_hash = Hash::hash(&self.action_data);
if computed_hash != self.action_hash {
return false;
}
for i in 0..self.approvals.len() {
for j in (i + 1)..self.approvals.len() {
if self.approvals[i].approver == self.approvals[j].approver {
return false;
}
}
}
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum PolicyAccountOperation {
Create = 0,
SubmitProposal = 1,
ExecuteProposal = 2,
CancelProposal = 3,
ModifyMembership = 4,
ModifyPolicy = 5,
Freeze = 6,
Unfreeze = 7,
}
impl PolicyAccountOperation {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(PolicyAccountOperation::Create),
1 => Some(PolicyAccountOperation::SubmitProposal),
2 => Some(PolicyAccountOperation::ExecuteProposal),
3 => Some(PolicyAccountOperation::CancelProposal),
4 => Some(PolicyAccountOperation::ModifyMembership),
5 => Some(PolicyAccountOperation::ModifyPolicy),
6 => Some(PolicyAccountOperation::Freeze),
7 => Some(PolicyAccountOperation::Unfreeze),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolicyAccountTxData {
pub operation: PolicyAccountOperation,
pub data: Vec<u8>,
pub recipient: Address,
}