use serde::{Deserialize, Serialize};
use crate::{Address, BlockHeight, Timestamp};
pub type SubjectId = [u8; 32];
pub type PolicyId = [u8; 32];
pub type ProofId = [u8; 32];
pub type ClassId = [u8; 32];
pub type ActionId = [u8; 32];
pub type SnapshotId = [u8; 32];
pub const ENTITY_DOMAIN_SEP: &[u8] = b"SRC831-ENTITY:";
pub const GOVERNANCE_ACTION_DOMAIN_SEP: &[u8] = b"SRC832-ACTION:";
pub const EQUITY_TOKEN_DOMAIN_SEP: &[u8] = b"SRC833-TOKEN:";
pub const CORPORATE_ACTION_DOMAIN_SEP: &[u8] = b"SRC835-CORP-ACTION:";
pub const OWNERSHIP_PROOF_DOMAIN_SEP: &[u8] = b"SRC836-PROOF:";
pub const SNAPSHOT_DOMAIN_SEP: &[u8] = b"SRC835-SNAPSHOT:";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum OrgType {
Corporation = 0,
LLC = 1,
Partnership = 2,
DAO = 3,
Foundation = 4,
Trust = 5,
Cooperative = 6,
Other = 255,
}
impl OrgType {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(OrgType::Corporation),
1 => Some(OrgType::LLC),
2 => Some(OrgType::Partnership),
3 => Some(OrgType::DAO),
4 => Some(OrgType::Foundation),
5 => Some(OrgType::Trust),
6 => Some(OrgType::Cooperative),
255 => Some(OrgType::Other),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
OrgType::Corporation => "Corporation",
OrgType::LLC => "LLC",
OrgType::Partnership => "Partnership",
OrgType::DAO => "DAO",
OrgType::Foundation => "Foundation",
OrgType::Trust => "Trust",
OrgType::Cooperative => "Cooperative",
OrgType::Other => "Other",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum ControllerModel {
SingleSigner = 0,
MultiSig = 1,
BoardMultiSig = 2,
TokenGovernance = 3,
Hybrid = 4,
}
impl ControllerModel {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(ControllerModel::SingleSigner),
1 => Some(ControllerModel::MultiSig),
2 => Some(ControllerModel::BoardMultiSig),
3 => Some(ControllerModel::TokenGovernance),
4 => Some(ControllerModel::Hybrid),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum EntityServiceType {
Mailbox = 0,
InvestorRelations = 1,
TransferAgent = 2,
CapTable = 3,
Governance = 4,
Website = 5,
Other = 255,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EntityService {
pub service_id: String,
pub service_type: EntityServiceType,
pub endpoint: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum EntityStatus {
Active = 0,
Pending = 1,
Suspended = 2,
Dissolved = 3,
}
impl EntityStatus {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(EntityStatus::Active),
1 => Some(EntityStatus::Pending),
2 => Some(EntityStatus::Suspended),
3 => Some(EntityStatus::Dissolved),
_ => None,
}
}
pub fn is_active(&self) -> bool {
matches!(self, EntityStatus::Active)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EntityProfile {
pub subject_id: SubjectId,
pub org_type: OrgType,
pub name_commitment: [u8; 32],
pub jurisdiction: Option<String>,
pub registration_commitment: Option<[u8; 32]>,
pub controller_model: ControllerModel,
pub controllers: Vec<Address>,
pub multisig_threshold: Option<u8>,
pub services: Vec<EntityService>,
pub metadata_hash: [u8; 32],
pub created_at: Timestamp,
pub updated_at: Timestamp,
pub status: EntityStatus,
}
impl EntityProfile {
pub fn generate_subject_id(
org_type: OrgType,
name_commitment: &[u8; 32],
nonce: &[u8; 32],
) -> SubjectId {
let mut hasher = blake3::Hasher::new();
hasher.update(ENTITY_DOMAIN_SEP);
hasher.update(&[org_type as u8]);
hasher.update(b":v1:");
hasher.update(name_commitment);
hasher.update(nonce);
*hasher.finalize().as_bytes()
}
pub fn can_controller_act(&self, controller: &Address) -> bool {
self.status.is_active() && self.controllers.contains(controller)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u16)]
pub enum GovernanceActionType {
BoardResolutionApproved = 100,
BoardMeetingMinutes = 101,
BoardMemberAppointed = 102,
BoardMemberRemoved = 103,
ShareholderVoteApproved = 200,
AnnualMeetingHeld = 201,
SpecialMeetingHeld = 202,
WrittenConsentObtained = 203,
OfficerAppointment = 300,
OfficerRemoval = 301,
OfficerRoleChanged = 302,
SigningAuthorityGrant = 400,
SigningAuthorityRevoke = 401,
AuthorityScopeChanged = 402,
BylawsAmended = 500,
ArticlesAmended = 501,
RegisteredAgentChanged = 502,
}
impl GovernanceActionType {
pub fn from_u16(v: u16) -> Option<Self> {
match v {
100 => Some(GovernanceActionType::BoardResolutionApproved),
101 => Some(GovernanceActionType::BoardMeetingMinutes),
102 => Some(GovernanceActionType::BoardMemberAppointed),
103 => Some(GovernanceActionType::BoardMemberRemoved),
200 => Some(GovernanceActionType::ShareholderVoteApproved),
201 => Some(GovernanceActionType::AnnualMeetingHeld),
202 => Some(GovernanceActionType::SpecialMeetingHeld),
203 => Some(GovernanceActionType::WrittenConsentObtained),
300 => Some(GovernanceActionType::OfficerAppointment),
301 => Some(GovernanceActionType::OfficerRemoval),
302 => Some(GovernanceActionType::OfficerRoleChanged),
400 => Some(GovernanceActionType::SigningAuthorityGrant),
401 => Some(GovernanceActionType::SigningAuthorityRevoke),
402 => Some(GovernanceActionType::AuthorityScopeChanged),
500 => Some(GovernanceActionType::BylawsAmended),
501 => Some(GovernanceActionType::ArticlesAmended),
502 => Some(GovernanceActionType::RegisteredAgentChanged),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
GovernanceActionType::BoardResolutionApproved => "Board Resolution Approved",
GovernanceActionType::BoardMeetingMinutes => "Board Meeting Minutes",
GovernanceActionType::BoardMemberAppointed => "Board Member Appointed",
GovernanceActionType::BoardMemberRemoved => "Board Member Removed",
GovernanceActionType::ShareholderVoteApproved => "Shareholder Vote Approved",
GovernanceActionType::AnnualMeetingHeld => "Annual Meeting Held",
GovernanceActionType::SpecialMeetingHeld => "Special Meeting Held",
GovernanceActionType::WrittenConsentObtained => "Written Consent Obtained",
GovernanceActionType::OfficerAppointment => "Officer Appointment",
GovernanceActionType::OfficerRemoval => "Officer Removal",
GovernanceActionType::OfficerRoleChanged => "Officer Role Changed",
GovernanceActionType::SigningAuthorityGrant => "Signing Authority Grant",
GovernanceActionType::SigningAuthorityRevoke => "Signing Authority Revoke",
GovernanceActionType::AuthorityScopeChanged => "Authority Scope Changed",
GovernanceActionType::BylawsAmended => "Bylaws Amended",
GovernanceActionType::ArticlesAmended => "Articles Amended",
GovernanceActionType::RegisteredAgentChanged => "Registered Agent Changed",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum AttachmentContentType {
Resolution = 0,
Minutes = 1,
Agreement = 2,
Certificate = 3,
Other = 255,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GovernanceAttachment {
pub hash: [u8; 32],
pub size: u64,
pub hint_uri: Option<String>,
pub content_type: AttachmentContentType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum GovernanceActionStatus {
Pending = 0,
Approved = 1,
Executed = 2,
Expired = 3,
Revoked = 4,
}
impl GovernanceActionStatus {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(GovernanceActionStatus::Pending),
1 => Some(GovernanceActionStatus::Approved),
2 => Some(GovernanceActionStatus::Executed),
3 => Some(GovernanceActionStatus::Expired),
4 => Some(GovernanceActionStatus::Revoked),
_ => None,
}
}
pub fn is_effective(&self) -> bool {
matches!(self, GovernanceActionStatus::Approved | GovernanceActionStatus::Executed)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GovernanceAction {
pub action_id: ActionId,
pub org_subject: SubjectId,
pub action_type: GovernanceActionType,
pub policy_id: PolicyId,
pub action_commitment: [u8; 32],
pub effective_at: Timestamp,
pub expires_at: Timestamp,
pub attachments: Option<GovernanceAttachment>,
pub approvers: Vec<Address>,
pub required_threshold: u8,
pub status: GovernanceActionStatus,
pub created_at: Timestamp,
pub recorded_at_height: BlockHeight,
}
impl GovernanceAction {
pub fn generate_action_id(
org_subject: &SubjectId,
action_type: GovernanceActionType,
action_commitment: &[u8; 32],
nonce: &[u8; 32],
) -> ActionId {
let mut hasher = blake3::Hasher::new();
hasher.update(GOVERNANCE_ACTION_DOMAIN_SEP);
hasher.update(&(action_type as u16).to_be_bytes());
hasher.update(b":v1:");
hasher.update(org_subject);
hasher.update(action_commitment);
hasher.update(nonce);
*hasher.finalize().as_bytes()
}
pub fn is_threshold_met(&self) -> bool {
self.approvers.len() >= self.required_threshold as usize
}
pub fn is_expired(&self, current_time: Timestamp) -> bool {
self.expires_at > 0 && current_time > self.expires_at
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum ShareClassType {
Common = 0,
Preferred = 1,
}
impl ShareClassType {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(ShareClassType::Common),
1 => Some(ShareClassType::Preferred),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
ShareClassType::Common => "Common",
ShareClassType::Preferred => "Preferred",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum TokenStatus {
Active = 0,
Paused = 1,
Retired = 2,
}
impl TokenStatus {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(TokenStatus::Active),
1 => Some(TokenStatus::Paused),
2 => Some(TokenStatus::Retired),
_ => None,
}
}
pub fn is_transferable(&self) -> bool {
matches!(self, TokenStatus::Active)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EquityToken {
pub issuer_subject: SubjectId,
pub class_id: ClassId,
pub share_class_type: ShareClassType,
pub name: String,
pub symbol: String,
pub authorized_shares: u128,
pub issued_shares: u128,
pub votes_per_share: u64,
pub economic_rights_hash: [u8; 32],
pub liquidation_preference_hash: Option<[u8; 32]>,
pub dividend_policy_hash: Option<[u8; 32]>,
pub conversion_rules_hash: Option<[u8; 32]>,
pub controller: Address,
pub par_value: Option<u128>,
pub created_at: Timestamp,
pub updated_at: Timestamp,
pub status: TokenStatus,
}
impl EquityToken {
pub fn generate_class_id(
issuer_subject: &SubjectId,
name: &str,
share_class_type: ShareClassType,
) -> ClassId {
let mut hasher = blake3::Hasher::new();
hasher.update(EQUITY_TOKEN_DOMAIN_SEP);
hasher.update(issuer_subject);
hasher.update(b":");
hasher.update(name.as_bytes());
hasher.update(b":");
hasher.update(&[share_class_type as u8]);
*hasher.finalize().as_bytes()
}
pub fn can_mint(&self, amount: u128) -> bool {
self.status.is_transferable()
&& self.issued_shares.saturating_add(amount) <= self.authorized_shares
}
pub fn remaining_authorized(&self) -> u128 {
self.authorized_shares.saturating_sub(self.issued_shares)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum TransferType {
Regular = 0,
CorporateAction = 1,
Conversion = 2,
Redemption = 3,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TransferContext {
pub initiator: Address,
pub governance_action: Option<ActionId>,
pub transfer_type: TransferType,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum IssuanceType {
Initial = 0,
FollowOn = 1,
OptionExercise = 2,
WarrantExercise = 3,
Conversion = 4,
StockSplit = 5,
StockDividend = 6,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IssuanceRef {
pub governance_action_id: ActionId,
pub issuance_type: IssuanceType,
pub price_per_share: Option<u128>,
pub round_id: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum BurnReason {
Redemption = 0,
Buyback = 1,
Cancellation = 2,
ReverseSplit = 3,
Conversion = 4,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u16)]
pub enum ControllerErrorCode {
SenderNotWhitelisted = 1000,
RecipientNotWhitelisted = 1001,
SenderInLockup = 1002,
TradingWindowClosed = 1003,
TransferAmountExceedsLimit = 1004,
InsufficientBalance = 1005,
ExceedsAuthorizedCap = 1100,
UnauthorizedMinter = 1101,
InvalidIssuanceRef = 1102,
UnauthorizedBurner = 1200,
InvalidBurnReason = 1201,
InvalidCorporateAction = 1300,
InsufficientApprovals = 1301,
ActionNotAuthorized = 1302,
PolicyCheckFailed = 9000,
ControllerPaused = 9001,
Unknown = 9999,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LockupInfo {
pub amount: u128,
pub unlock_at: Timestamp,
pub vesting: Option<VestingSchedule>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VestingSchedule {
pub total_amount: u128,
pub vested_amount: u128,
pub start_at: Timestamp,
pub cliff_duration: u64,
pub total_duration: u64,
pub interval: u64,
}
impl VestingSchedule {
pub fn vested_at(&self, current_time: Timestamp) -> u128 {
if current_time < self.start_at {
return 0;
}
let elapsed = current_time.saturating_sub(self.start_at);
if elapsed < self.cliff_duration {
return 0;
}
if elapsed >= self.total_duration {
return self.total_amount;
}
let vesting_elapsed = elapsed.saturating_sub(self.cliff_duration);
let vesting_duration = self.total_duration.saturating_sub(self.cliff_duration);
if vesting_duration == 0 {
return self.total_amount;
}
let intervals_passed = vesting_elapsed / self.interval;
let total_intervals = vesting_duration / self.interval;
if total_intervals == 0 {
return self.total_amount;
}
(self.total_amount * intervals_passed as u128) / total_intervals as u128
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TradingWindow {
pub start_day: u8,
pub end_day: u8,
pub months: u16,
}
impl TradingWindow {
pub fn is_open(&self, day: u8, month: u8) -> bool {
if month < 1 || month > 12 {
return false;
}
let month_allowed = (self.months & (1 << (month - 1))) != 0;
if !month_allowed {
return false;
}
if self.start_day <= self.end_day {
day >= self.start_day && day <= self.end_day
} else {
day >= self.start_day || day <= self.end_day
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EquityControllerConfig {
pub address: Address,
pub whitelist_enabled: bool,
pub trading_windows: Vec<TradingWindow>,
pub transfer_limit: u128,
pub governance_policy_id: PolicyId,
pub paused: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum CorporateActionType {
StockSplit = 0,
ReverseSplit = 1,
DividendDeclare = 2,
DividendDistribute = 3,
StockDividend = 4,
Buyback = 5,
Conversion = 6,
RecordDateSnapshot = 7,
RightsOffering = 8,
}
impl CorporateActionType {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(CorporateActionType::StockSplit),
1 => Some(CorporateActionType::ReverseSplit),
2 => Some(CorporateActionType::DividendDeclare),
3 => Some(CorporateActionType::DividendDistribute),
4 => Some(CorporateActionType::StockDividend),
5 => Some(CorporateActionType::Buyback),
6 => Some(CorporateActionType::Conversion),
7 => Some(CorporateActionType::RecordDateSnapshot),
8 => Some(CorporateActionType::RightsOffering),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
CorporateActionType::StockSplit => "Stock Split",
CorporateActionType::ReverseSplit => "Reverse Split",
CorporateActionType::DividendDeclare => "Dividend Declaration",
CorporateActionType::DividendDistribute => "Dividend Distribution",
CorporateActionType::StockDividend => "Stock Dividend",
CorporateActionType::Buyback => "Buyback",
CorporateActionType::Conversion => "Conversion",
CorporateActionType::RecordDateSnapshot => "Record Date Snapshot",
CorporateActionType::RightsOffering => "Rights Offering",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum RoundingMode {
Down = 0,
Up = 1,
Nearest = 2,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StockSplitParams {
pub ratio_numerator: u64,
pub ratio_denominator: u64,
}
impl StockSplitParams {
pub fn apply(&self, balance: u128) -> u128 {
(balance * self.ratio_numerator as u128) / self.ratio_denominator as u128
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReverseSplitParams {
pub ratio_numerator: u64,
pub ratio_denominator: u64,
pub rounding: RoundingMode,
pub cash_out_fractional: bool,
pub fractional_price: Option<u128>,
}
impl ReverseSplitParams {
pub fn apply(&self, balance: u128) -> (u128, u128) {
let numerator = self.ratio_numerator as u128;
let denominator = self.ratio_denominator as u128;
let new_balance = (balance * numerator) / denominator;
let remainder = balance - (new_balance * denominator) / numerator;
let fractional = match self.rounding {
RoundingMode::Down => 0,
RoundingMode::Up => {
if remainder > 0 {
1
} else {
0
}
}
RoundingMode::Nearest => {
if remainder * 2 >= denominator / numerator {
1
} else {
0
}
}
};
(new_balance + fractional, remainder)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DividendCurrency {
Native,
Token(Address),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DividendDeclareParams {
pub amount_per_share: u128,
pub currency: DividendCurrency,
pub record_date: Timestamp,
pub payment_date: Timestamp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum DistributionMethod {
ProRataSnapshot = 0,
ProRataCurrent = 1,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DividendDistributeParams {
pub declaration_id: ActionId,
pub snapshot_id: SnapshotId,
pub method: DistributionMethod,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConversionParams {
pub from_class_id: ClassId,
pub to_class_id: ClassId,
pub conversion_ratio: u64,
pub holder: Option<Address>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum SnapshotPurpose {
Dividend = 0,
Voting = 1,
Rights = 2,
Other = 255,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecordSnapshotParams {
pub purpose: SnapshotPurpose,
pub reference: Option<[u8; 32]>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum CorporateActionStatus {
Proposed = 0,
Approved = 1,
Executing = 2,
Completed = 3,
Cancelled = 4,
Failed = 5,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum CorporateActionParams {
StockSplit(StockSplitParams),
ReverseSplit(ReverseSplitParams),
DividendDeclare(DividendDeclareParams),
DividendDistribute(DividendDistributeParams),
Conversion(ConversionParams),
RecordSnapshot(RecordSnapshotParams),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CorporateAction {
pub action_id: ActionId,
pub class_id: ClassId,
pub action_type: CorporateActionType,
pub params: CorporateActionParams,
pub record_date: Option<Timestamp>,
pub execution_date: Timestamp,
pub governance_action_id: ActionId,
pub status: CorporateActionStatus,
pub created_at: Timestamp,
pub executed_at: Option<Timestamp>,
}
impl CorporateAction {
pub fn generate_action_id(
class_id: &ClassId,
action_type: CorporateActionType,
governance_action_id: &ActionId,
) -> ActionId {
let mut hasher = blake3::Hasher::new();
hasher.update(CORPORATE_ACTION_DOMAIN_SEP);
hasher.update(&[action_type as u8]);
hasher.update(b":v1:");
hasher.update(class_id);
hasher.update(governance_action_id);
*hasher.finalize().as_bytes()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OwnershipSnapshot {
pub snapshot_id: SnapshotId,
pub class_id: ClassId,
pub timestamp: Timestamp,
pub block_height: BlockHeight,
pub total_supply: u128,
pub holder_count: u64,
pub balances_root: [u8; 32],
pub purpose: SnapshotPurpose,
pub reference: Option<[u8; 32]>,
}
impl OwnershipSnapshot {
pub fn generate_snapshot_id(
class_id: &ClassId,
purpose: SnapshotPurpose,
timestamp: Timestamp,
) -> SnapshotId {
let mut hasher = blake3::Hasher::new();
hasher.update(SNAPSHOT_DOMAIN_SEP);
hasher.update(&[purpose as u8]);
hasher.update(b":v1:");
hasher.update(class_id);
hasher.update(×tamp.to_be_bytes());
*hasher.finalize().as_bytes()
}
}
pub mod proof_profiles {
pub const EQUITY_PROVE_MEMBERSHIP: &str = "equity.prove_membership.v1";
pub const EQUITY_PROVE_OWNERSHIP_THRESHOLD: &str = "equity.prove_ownership_threshold.v1";
pub const EQUITY_PROVE_VOTING_POWER: &str = "equity.prove_voting_power.v1";
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MembershipPublicInputs {
pub org_subject: SubjectId,
pub class_id: ClassId,
pub membership_commitment: [u8; 32],
pub proof_timestamp: Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OwnershipThresholdPublicInputs {
pub org_subject: SubjectId,
pub class_id: ClassId,
pub threshold: u128,
pub ownership_commitment: [u8; 32],
pub proof_timestamp: Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VotingPowerPublicInputs {
pub org_subject: SubjectId,
pub threshold: u128,
pub reference: Option<[u8; 32]>,
pub voting_commitment: [u8; 32],
pub proof_timestamp: Timestamp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum OwnershipProofType {
Mock = 0,
Groth16 = 1,
Plonk = 2,
Signature = 3,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OwnershipProofEnvelope {
pub proof_id: ProofId,
pub profile_id: String,
pub policy_ids: Vec<PolicyId>,
pub public_inputs: Vec<u8>,
pub proof_data: Vec<u8>,
pub proof_type: OwnershipProofType,
pub subject_nullifier: [u8; 32],
pub generated_at: Timestamp,
pub expires_at: Timestamp,
}
impl OwnershipProofEnvelope {
pub fn generate_proof_id(
profile_id: &str,
public_inputs: &[u8],
nonce: &[u8; 32],
) -> ProofId {
let mut hasher = blake3::Hasher::new();
hasher.update(OWNERSHIP_PROOF_DOMAIN_SEP);
hasher.update(profile_id.as_bytes());
hasher.update(b":");
hasher.update(public_inputs);
hasher.update(nonce);
*hasher.finalize().as_bytes()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum EquityOperation {
CreateEntity = 0,
UpdateEntity = 1,
AddController = 2,
RemoveController = 3,
ProposeAction = 10,
ApproveAction = 11,
ExecuteAction = 12,
RevokeAction = 13,
CreateToken = 20,
UpdateToken = 21,
PauseToken = 22,
UnpauseToken = 23,
Transfer = 30,
Approve = 31,
TransferFrom = 32,
Mint = 40,
Burn = 41,
UpdateController = 50,
AddToWhitelist = 51,
RemoveFromWhitelist = 52,
SetLockup = 53,
ExecuteStockSplit = 60,
ExecuteReverseSplit = 61,
DeclareDividend = 62,
DistributeDividend = 63,
ExecuteConversion = 64,
TakeSnapshot = 65,
VerifyOwnershipProof = 70,
}
impl EquityOperation {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(EquityOperation::CreateEntity),
1 => Some(EquityOperation::UpdateEntity),
2 => Some(EquityOperation::AddController),
3 => Some(EquityOperation::RemoveController),
10 => Some(EquityOperation::ProposeAction),
11 => Some(EquityOperation::ApproveAction),
12 => Some(EquityOperation::ExecuteAction),
13 => Some(EquityOperation::RevokeAction),
20 => Some(EquityOperation::CreateToken),
21 => Some(EquityOperation::UpdateToken),
22 => Some(EquityOperation::PauseToken),
23 => Some(EquityOperation::UnpauseToken),
30 => Some(EquityOperation::Transfer),
31 => Some(EquityOperation::Approve),
32 => Some(EquityOperation::TransferFrom),
40 => Some(EquityOperation::Mint),
41 => Some(EquityOperation::Burn),
50 => Some(EquityOperation::UpdateController),
51 => Some(EquityOperation::AddToWhitelist),
52 => Some(EquityOperation::RemoveFromWhitelist),
53 => Some(EquityOperation::SetLockup),
60 => Some(EquityOperation::ExecuteStockSplit),
61 => Some(EquityOperation::ExecuteReverseSplit),
62 => Some(EquityOperation::DeclareDividend),
63 => Some(EquityOperation::DistributeDividend),
64 => Some(EquityOperation::ExecuteConversion),
65 => Some(EquityOperation::TakeSnapshot),
70 => Some(EquityOperation::VerifyOwnershipProof),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EquityTxData {
pub operation: EquityOperation,
pub data: Vec<u8>,
pub recipient: crate::Address,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum EntityProfileEvent {
EntityCreated {
subject_id: SubjectId,
org_type: OrgType,
controller_model: ControllerModel,
},
EntityUpdated {
subject_id: SubjectId,
},
ControllerChanged {
subject_id: SubjectId,
old_controllers: Vec<Address>,
new_controllers: Vec<Address>,
},
EntityStatusChanged {
subject_id: SubjectId,
old_status: EntityStatus,
new_status: EntityStatus,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum GovernanceActionEvent {
ActionProposed {
action_id: ActionId,
org_subject: SubjectId,
action_type: GovernanceActionType,
policy_id: PolicyId,
proposer: Address,
},
ActionApproved {
action_id: ActionId,
approver: Address,
approval_count: u32,
threshold: u32,
},
ActionExecuted {
action_id: ActionId,
executor: Address,
effective_at: Timestamp,
},
ActionRevoked {
action_id: ActionId,
revoker: Address,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum EquityTokenEvent {
TokenCreated {
issuer_subject: SubjectId,
class_id: ClassId,
share_class_type: ShareClassType,
authorized_shares: u128,
},
Transfer {
class_id: ClassId,
from: Address,
to: Address,
amount: u128,
},
Approval {
class_id: ClassId,
owner: Address,
spender: Address,
amount: u128,
},
ControllerUpdated {
class_id: ClassId,
old_controller: Address,
new_controller: Address,
governance_action_id: ActionId,
},
TokenPaused {
class_id: ClassId,
},
TokenUnpaused {
class_id: ClassId,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum CorporateActionEvent {
ActionProposed {
action_id: ActionId,
class_id: ClassId,
action_type: CorporateActionType,
governance_action_id: ActionId,
},
ActionExecuted {
action_id: ActionId,
action_type: CorporateActionType,
affected_holders: u64,
total_shares_affected: u128,
},
DividendDeclared {
declaration_id: ActionId,
class_id: ClassId,
amount_per_share: u128,
record_date: Timestamp,
payment_date: Timestamp,
},
DividendDistributed {
declaration_id: ActionId,
total_distributed: u128,
recipient_count: u64,
},
SnapshotTaken {
snapshot_id: SnapshotId,
class_id: ClassId,
purpose: SnapshotPurpose,
holder_count: u64,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum OwnershipProofEvent {
ProofVerified {
proof_id: ProofId,
profile_id: String,
org_subject: SubjectId,
verifier: Address,
valid: bool,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum EquityEvent {
Entity(EntityProfileEvent),
Governance(GovernanceActionEvent),
Token(EquityTokenEvent),
CorporateAction(CorporateActionEvent),
Proof(OwnershipProofEvent),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_entity_subject_id_generation() {
let name_commitment = [1u8; 32];
let nonce = [2u8; 32];
let id1 = EntityProfile::generate_subject_id(
OrgType::Corporation,
&name_commitment,
&nonce,
);
let id2 = EntityProfile::generate_subject_id(
OrgType::Corporation,
&name_commitment,
&nonce,
);
let id3 = EntityProfile::generate_subject_id(
OrgType::LLC,
&name_commitment,
&nonce,
);
assert_eq!(id1, id2);
assert_ne!(id1, id3);
}
#[test]
fn test_governance_action_threshold() {
let action = GovernanceAction {
action_id: [0u8; 32],
org_subject: [0u8; 32],
action_type: GovernanceActionType::BoardResolutionApproved,
policy_id: [0u8; 32],
action_commitment: [0u8; 32],
effective_at: 0,
expires_at: 0,
attachments: None,
approvers: vec![Address::ZERO, Address::ZERO],
required_threshold: 3,
status: GovernanceActionStatus::Pending,
created_at: 0,
recorded_at_height: 0,
};
assert!(!action.is_threshold_met());
let mut action2 = action.clone();
action2.approvers.push(Address::ZERO);
assert!(action2.is_threshold_met());
}
#[test]
fn test_equity_token_mint_check() {
let token = EquityToken {
issuer_subject: [0u8; 32],
class_id: [0u8; 32],
share_class_type: ShareClassType::Common,
name: "Common Shares".to_string(),
symbol: "ACME".to_string(),
authorized_shares: 1_000_000,
issued_shares: 500_000,
votes_per_share: 1,
economic_rights_hash: [0u8; 32],
liquidation_preference_hash: None,
dividend_policy_hash: None,
conversion_rules_hash: None,
controller: Address::ZERO,
par_value: None,
created_at: 0,
updated_at: 0,
status: TokenStatus::Active,
};
assert!(token.can_mint(100_000));
assert!(token.can_mint(500_000));
assert!(!token.can_mint(500_001));
assert_eq!(token.remaining_authorized(), 500_000);
}
#[test]
fn test_vesting_schedule() {
let schedule = VestingSchedule {
total_amount: 1_000_000,
vested_amount: 0,
start_at: 1000,
cliff_duration: 100,
total_duration: 400,
interval: 30,
};
assert_eq!(schedule.vested_at(500), 0);
assert_eq!(schedule.vested_at(1050), 0);
assert_eq!(schedule.vested_at(1100), 0);
assert!(schedule.vested_at(1130) > 0);
assert_eq!(schedule.vested_at(1400), 1_000_000);
assert_eq!(schedule.vested_at(2000), 1_000_000);
}
#[test]
fn test_trading_window() {
let window = TradingWindow {
start_day: 1,
end_day: 10,
months: 0b000000000101, };
assert!(window.is_open(5, 1));
assert!(window.is_open(1, 3));
assert!(!window.is_open(15, 1));
assert!(!window.is_open(5, 2));
}
#[test]
fn test_stock_split_params() {
let split = StockSplitParams {
ratio_numerator: 2,
ratio_denominator: 1,
};
assert_eq!(split.apply(100), 200);
assert_eq!(split.apply(1), 2);
assert_eq!(split.apply(0), 0);
}
#[test]
fn test_reverse_split_params() {
let reverse = ReverseSplitParams {
ratio_numerator: 1,
ratio_denominator: 10,
rounding: RoundingMode::Down,
cash_out_fractional: false,
fractional_price: None,
};
let (new_balance, remainder) = reverse.apply(100);
assert_eq!(new_balance, 10);
assert_eq!(remainder, 0);
let (new_balance2, remainder2) = reverse.apply(95);
assert_eq!(new_balance2, 9);
assert!(remainder2 > 0);
}
#[test]
fn test_class_id_determinism() {
let subject = [1u8; 32];
let id1 = EquityToken::generate_class_id(&subject, "Series A", ShareClassType::Preferred);
let id2 = EquityToken::generate_class_id(&subject, "Series A", ShareClassType::Preferred);
let id3 = EquityToken::generate_class_id(&subject, "Series B", ShareClassType::Preferred);
assert_eq!(id1, id2);
assert_ne!(id1, id3);
}
#[test]
fn test_snapshot_id_determinism() {
let class_id = [1u8; 32];
let timestamp = 1704067200000u64;
let id1 = OwnershipSnapshot::generate_snapshot_id(&class_id, SnapshotPurpose::Dividend, timestamp);
let id2 = OwnershipSnapshot::generate_snapshot_id(&class_id, SnapshotPurpose::Dividend, timestamp);
let id3 = OwnershipSnapshot::generate_snapshot_id(&class_id, SnapshotPurpose::Voting, timestamp);
assert_eq!(id1, id2);
assert_ne!(id1, id3);
}
}