1use serde::{Deserialize, Serialize};
18
19use crate::{Address, BlockHeight, Timestamp};
20
21pub type SubjectId = [u8; 32];
27
28pub type PolicyId = [u8; 32];
30
31pub type ProofId = [u8; 32];
33
34pub type ClassId = [u8; 32];
36
37pub type ActionId = [u8; 32];
39
40pub type SnapshotId = [u8; 32];
42
43pub const ENTITY_DOMAIN_SEP: &[u8] = b"SRC831-ENTITY:";
49
50pub const GOVERNANCE_ACTION_DOMAIN_SEP: &[u8] = b"SRC832-ACTION:";
52
53pub const EQUITY_TOKEN_DOMAIN_SEP: &[u8] = b"SRC833-TOKEN:";
55
56pub const CORPORATE_ACTION_DOMAIN_SEP: &[u8] = b"SRC835-CORP-ACTION:";
58
59pub const OWNERSHIP_PROOF_DOMAIN_SEP: &[u8] = b"SRC836-PROOF:";
61
62pub const SNAPSHOT_DOMAIN_SEP: &[u8] = b"SRC835-SNAPSHOT:";
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
71#[repr(u8)]
72pub enum OrgType {
73 Corporation = 0,
75 LLC = 1,
77 Partnership = 2,
79 DAO = 3,
81 Foundation = 4,
83 Trust = 5,
85 Cooperative = 6,
87 Other = 255,
89}
90
91impl OrgType {
92 pub fn from_u8(v: u8) -> Option<Self> {
93 match v {
94 0 => Some(OrgType::Corporation),
95 1 => Some(OrgType::LLC),
96 2 => Some(OrgType::Partnership),
97 3 => Some(OrgType::DAO),
98 4 => Some(OrgType::Foundation),
99 5 => Some(OrgType::Trust),
100 6 => Some(OrgType::Cooperative),
101 255 => Some(OrgType::Other),
102 _ => None,
103 }
104 }
105
106 pub fn name(&self) -> &'static str {
107 match self {
108 OrgType::Corporation => "Corporation",
109 OrgType::LLC => "LLC",
110 OrgType::Partnership => "Partnership",
111 OrgType::DAO => "DAO",
112 OrgType::Foundation => "Foundation",
113 OrgType::Trust => "Trust",
114 OrgType::Cooperative => "Cooperative",
115 OrgType::Other => "Other",
116 }
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
122#[repr(u8)]
123pub enum ControllerModel {
124 SingleSigner = 0,
126 MultiSig = 1,
128 BoardMultiSig = 2,
130 TokenGovernance = 3,
132 Hybrid = 4,
134}
135
136impl ControllerModel {
137 pub fn from_u8(v: u8) -> Option<Self> {
138 match v {
139 0 => Some(ControllerModel::SingleSigner),
140 1 => Some(ControllerModel::MultiSig),
141 2 => Some(ControllerModel::BoardMultiSig),
142 3 => Some(ControllerModel::TokenGovernance),
143 4 => Some(ControllerModel::Hybrid),
144 _ => None,
145 }
146 }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
151#[repr(u8)]
152pub enum EntityServiceType {
153 Mailbox = 0,
155 InvestorRelations = 1,
157 TransferAgent = 2,
159 CapTable = 3,
161 Governance = 4,
163 Website = 5,
165 Other = 255,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
171pub struct EntityService {
172 pub service_id: String,
174 pub service_type: EntityServiceType,
176 pub endpoint: String,
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
182#[repr(u8)]
183pub enum EntityStatus {
184 Active = 0,
186 Pending = 1,
188 Suspended = 2,
190 Dissolved = 3,
192}
193
194impl EntityStatus {
195 pub fn from_u8(v: u8) -> Option<Self> {
196 match v {
197 0 => Some(EntityStatus::Active),
198 1 => Some(EntityStatus::Pending),
199 2 => Some(EntityStatus::Suspended),
200 3 => Some(EntityStatus::Dissolved),
201 _ => None,
202 }
203 }
204
205 pub fn is_active(&self) -> bool {
206 matches!(self, EntityStatus::Active)
207 }
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
212pub struct EntityProfile {
213 pub subject_id: SubjectId,
215 pub org_type: OrgType,
217 pub name_commitment: [u8; 32],
219 pub jurisdiction: Option<String>,
221 pub registration_commitment: Option<[u8; 32]>,
223 pub controller_model: ControllerModel,
225 pub controllers: Vec<Address>,
227 pub multisig_threshold: Option<u8>,
229 pub services: Vec<EntityService>,
231 pub metadata_hash: [u8; 32],
233 pub created_at: Timestamp,
235 pub updated_at: Timestamp,
237 pub status: EntityStatus,
239}
240
241impl EntityProfile {
242 pub fn generate_subject_id(
244 org_type: OrgType,
245 name_commitment: &[u8; 32],
246 nonce: &[u8; 32],
247 ) -> SubjectId {
248 let mut hasher = blake3::Hasher::new();
249 hasher.update(ENTITY_DOMAIN_SEP);
250 hasher.update(&[org_type as u8]);
251 hasher.update(b":v1:");
252 hasher.update(name_commitment);
253 hasher.update(nonce);
254 *hasher.finalize().as_bytes()
255 }
256
257 pub fn can_controller_act(&self, controller: &Address) -> bool {
259 self.status.is_active() && self.controllers.contains(controller)
260 }
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
269#[repr(u16)]
270pub enum GovernanceActionType {
271 BoardResolutionApproved = 100,
274 BoardMeetingMinutes = 101,
276 BoardMemberAppointed = 102,
278 BoardMemberRemoved = 103,
280
281 ShareholderVoteApproved = 200,
284 AnnualMeetingHeld = 201,
286 SpecialMeetingHeld = 202,
288 WrittenConsentObtained = 203,
290
291 OfficerAppointment = 300,
294 OfficerRemoval = 301,
296 OfficerRoleChanged = 302,
298
299 SigningAuthorityGrant = 400,
302 SigningAuthorityRevoke = 401,
304 AuthorityScopeChanged = 402,
306
307 BylawsAmended = 500,
310 ArticlesAmended = 501,
312 RegisteredAgentChanged = 502,
314}
315
316impl GovernanceActionType {
317 pub fn from_u16(v: u16) -> Option<Self> {
318 match v {
319 100 => Some(GovernanceActionType::BoardResolutionApproved),
320 101 => Some(GovernanceActionType::BoardMeetingMinutes),
321 102 => Some(GovernanceActionType::BoardMemberAppointed),
322 103 => Some(GovernanceActionType::BoardMemberRemoved),
323 200 => Some(GovernanceActionType::ShareholderVoteApproved),
324 201 => Some(GovernanceActionType::AnnualMeetingHeld),
325 202 => Some(GovernanceActionType::SpecialMeetingHeld),
326 203 => Some(GovernanceActionType::WrittenConsentObtained),
327 300 => Some(GovernanceActionType::OfficerAppointment),
328 301 => Some(GovernanceActionType::OfficerRemoval),
329 302 => Some(GovernanceActionType::OfficerRoleChanged),
330 400 => Some(GovernanceActionType::SigningAuthorityGrant),
331 401 => Some(GovernanceActionType::SigningAuthorityRevoke),
332 402 => Some(GovernanceActionType::AuthorityScopeChanged),
333 500 => Some(GovernanceActionType::BylawsAmended),
334 501 => Some(GovernanceActionType::ArticlesAmended),
335 502 => Some(GovernanceActionType::RegisteredAgentChanged),
336 _ => None,
337 }
338 }
339
340 pub fn name(&self) -> &'static str {
341 match self {
342 GovernanceActionType::BoardResolutionApproved => "Board Resolution Approved",
343 GovernanceActionType::BoardMeetingMinutes => "Board Meeting Minutes",
344 GovernanceActionType::BoardMemberAppointed => "Board Member Appointed",
345 GovernanceActionType::BoardMemberRemoved => "Board Member Removed",
346 GovernanceActionType::ShareholderVoteApproved => "Shareholder Vote Approved",
347 GovernanceActionType::AnnualMeetingHeld => "Annual Meeting Held",
348 GovernanceActionType::SpecialMeetingHeld => "Special Meeting Held",
349 GovernanceActionType::WrittenConsentObtained => "Written Consent Obtained",
350 GovernanceActionType::OfficerAppointment => "Officer Appointment",
351 GovernanceActionType::OfficerRemoval => "Officer Removal",
352 GovernanceActionType::OfficerRoleChanged => "Officer Role Changed",
353 GovernanceActionType::SigningAuthorityGrant => "Signing Authority Grant",
354 GovernanceActionType::SigningAuthorityRevoke => "Signing Authority Revoke",
355 GovernanceActionType::AuthorityScopeChanged => "Authority Scope Changed",
356 GovernanceActionType::BylawsAmended => "Bylaws Amended",
357 GovernanceActionType::ArticlesAmended => "Articles Amended",
358 GovernanceActionType::RegisteredAgentChanged => "Registered Agent Changed",
359 }
360 }
361}
362
363#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
365#[repr(u8)]
366pub enum AttachmentContentType {
367 Resolution = 0,
368 Minutes = 1,
369 Agreement = 2,
370 Certificate = 3,
371 Other = 255,
372}
373
374#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
376pub struct GovernanceAttachment {
377 pub hash: [u8; 32],
379 pub size: u64,
381 pub hint_uri: Option<String>,
383 pub content_type: AttachmentContentType,
385}
386
387#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
389#[repr(u8)]
390pub enum GovernanceActionStatus {
391 Pending = 0,
393 Approved = 1,
395 Executed = 2,
397 Expired = 3,
399 Revoked = 4,
401}
402
403impl GovernanceActionStatus {
404 pub fn from_u8(v: u8) -> Option<Self> {
405 match v {
406 0 => Some(GovernanceActionStatus::Pending),
407 1 => Some(GovernanceActionStatus::Approved),
408 2 => Some(GovernanceActionStatus::Executed),
409 3 => Some(GovernanceActionStatus::Expired),
410 4 => Some(GovernanceActionStatus::Revoked),
411 _ => None,
412 }
413 }
414
415 pub fn is_effective(&self) -> bool {
416 matches!(self, GovernanceActionStatus::Approved | GovernanceActionStatus::Executed)
417 }
418}
419
420#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
422pub struct GovernanceAction {
423 pub action_id: ActionId,
425 pub org_subject: SubjectId,
427 pub action_type: GovernanceActionType,
429 pub policy_id: PolicyId,
431 pub action_commitment: [u8; 32],
433 pub effective_at: Timestamp,
435 pub expires_at: Timestamp,
437 pub attachments: Option<GovernanceAttachment>,
439 pub approvers: Vec<Address>,
441 pub required_threshold: u8,
443 pub status: GovernanceActionStatus,
445 pub created_at: Timestamp,
447 pub recorded_at_height: BlockHeight,
449}
450
451impl GovernanceAction {
452 pub fn generate_action_id(
454 org_subject: &SubjectId,
455 action_type: GovernanceActionType,
456 action_commitment: &[u8; 32],
457 nonce: &[u8; 32],
458 ) -> ActionId {
459 let mut hasher = blake3::Hasher::new();
460 hasher.update(GOVERNANCE_ACTION_DOMAIN_SEP);
461 hasher.update(&(action_type as u16).to_be_bytes());
462 hasher.update(b":v1:");
463 hasher.update(org_subject);
464 hasher.update(action_commitment);
465 hasher.update(nonce);
466 *hasher.finalize().as_bytes()
467 }
468
469 pub fn is_threshold_met(&self) -> bool {
471 self.approvers.len() >= self.required_threshold as usize
472 }
473
474 pub fn is_expired(&self, current_time: Timestamp) -> bool {
476 self.expires_at > 0 && current_time > self.expires_at
477 }
478}
479
480#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
486#[repr(u8)]
487pub enum ShareClassType {
488 Common = 0,
490 Preferred = 1,
492}
493
494impl ShareClassType {
495 pub fn from_u8(v: u8) -> Option<Self> {
496 match v {
497 0 => Some(ShareClassType::Common),
498 1 => Some(ShareClassType::Preferred),
499 _ => None,
500 }
501 }
502
503 pub fn name(&self) -> &'static str {
504 match self {
505 ShareClassType::Common => "Common",
506 ShareClassType::Preferred => "Preferred",
507 }
508 }
509}
510
511#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
513#[repr(u8)]
514pub enum TokenStatus {
515 Active = 0,
516 Paused = 1,
517 Retired = 2,
518}
519
520impl TokenStatus {
521 pub fn from_u8(v: u8) -> Option<Self> {
522 match v {
523 0 => Some(TokenStatus::Active),
524 1 => Some(TokenStatus::Paused),
525 2 => Some(TokenStatus::Retired),
526 _ => None,
527 }
528 }
529
530 pub fn is_transferable(&self) -> bool {
531 matches!(self, TokenStatus::Active)
532 }
533}
534
535#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
537pub struct EquityToken {
538 pub issuer_subject: SubjectId,
540 pub class_id: ClassId,
542 pub share_class_type: ShareClassType,
544 pub name: String,
546 pub symbol: String,
548 pub authorized_shares: u128,
550 pub issued_shares: u128,
552 pub votes_per_share: u64,
554 pub economic_rights_hash: [u8; 32],
556 pub liquidation_preference_hash: Option<[u8; 32]>,
558 pub dividend_policy_hash: Option<[u8; 32]>,
560 pub conversion_rules_hash: Option<[u8; 32]>,
562 pub controller: Address,
564 pub par_value: Option<u128>,
566 pub created_at: Timestamp,
568 pub updated_at: Timestamp,
570 pub status: TokenStatus,
572}
573
574impl EquityToken {
575 pub fn generate_class_id(
577 issuer_subject: &SubjectId,
578 name: &str,
579 share_class_type: ShareClassType,
580 ) -> ClassId {
581 let mut hasher = blake3::Hasher::new();
582 hasher.update(EQUITY_TOKEN_DOMAIN_SEP);
583 hasher.update(issuer_subject);
584 hasher.update(b":");
585 hasher.update(name.as_bytes());
586 hasher.update(b":");
587 hasher.update(&[share_class_type as u8]);
588 *hasher.finalize().as_bytes()
589 }
590
591 pub fn can_mint(&self, amount: u128) -> bool {
593 self.status.is_transferable()
594 && self.issued_shares.saturating_add(amount) <= self.authorized_shares
595 }
596
597 pub fn remaining_authorized(&self) -> u128 {
599 self.authorized_shares.saturating_sub(self.issued_shares)
600 }
601}
602
603#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
609#[repr(u8)]
610pub enum TransferType {
611 Regular = 0,
613 CorporateAction = 1,
615 Conversion = 2,
617 Redemption = 3,
619}
620
621#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
623pub struct TransferContext {
624 pub initiator: Address,
626 pub governance_action: Option<ActionId>,
628 pub transfer_type: TransferType,
630 pub data: Vec<u8>,
632}
633
634#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
636#[repr(u8)]
637pub enum IssuanceType {
638 Initial = 0,
640 FollowOn = 1,
642 OptionExercise = 2,
644 WarrantExercise = 3,
646 Conversion = 4,
648 StockSplit = 5,
650 StockDividend = 6,
652}
653
654#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
656pub struct IssuanceRef {
657 pub governance_action_id: ActionId,
659 pub issuance_type: IssuanceType,
661 pub price_per_share: Option<u128>,
663 pub round_id: Option<String>,
665}
666
667#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
669#[repr(u8)]
670pub enum BurnReason {
671 Redemption = 0,
673 Buyback = 1,
675 Cancellation = 2,
677 ReverseSplit = 3,
679 Conversion = 4,
681}
682
683#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
685#[repr(u16)]
686pub enum ControllerErrorCode {
687 SenderNotWhitelisted = 1000,
689 RecipientNotWhitelisted = 1001,
690 SenderInLockup = 1002,
691 TradingWindowClosed = 1003,
692 TransferAmountExceedsLimit = 1004,
693 InsufficientBalance = 1005,
694
695 ExceedsAuthorizedCap = 1100,
697 UnauthorizedMinter = 1101,
698 InvalidIssuanceRef = 1102,
699
700 UnauthorizedBurner = 1200,
702 InvalidBurnReason = 1201,
703
704 InvalidCorporateAction = 1300,
706 InsufficientApprovals = 1301,
707 ActionNotAuthorized = 1302,
708
709 PolicyCheckFailed = 9000,
711 ControllerPaused = 9001,
712 Unknown = 9999,
713}
714
715#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
717pub struct LockupInfo {
718 pub amount: u128,
720 pub unlock_at: Timestamp,
722 pub vesting: Option<VestingSchedule>,
724}
725
726#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
728pub struct VestingSchedule {
729 pub total_amount: u128,
731 pub vested_amount: u128,
733 pub start_at: Timestamp,
735 pub cliff_duration: u64,
737 pub total_duration: u64,
739 pub interval: u64,
741}
742
743impl VestingSchedule {
744 pub fn vested_at(&self, current_time: Timestamp) -> u128 {
746 if current_time < self.start_at {
747 return 0;
748 }
749
750 let elapsed = current_time.saturating_sub(self.start_at);
751
752 if elapsed < self.cliff_duration {
754 return 0;
755 }
756
757 if elapsed >= self.total_duration {
759 return self.total_amount;
760 }
761
762 let vesting_elapsed = elapsed.saturating_sub(self.cliff_duration);
764 let vesting_duration = self.total_duration.saturating_sub(self.cliff_duration);
765
766 if vesting_duration == 0 {
767 return self.total_amount;
768 }
769
770 let intervals_passed = vesting_elapsed / self.interval;
772 let total_intervals = vesting_duration / self.interval;
773
774 if total_intervals == 0 {
775 return self.total_amount;
776 }
777
778 (self.total_amount * intervals_passed as u128) / total_intervals as u128
779 }
780}
781
782#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
784pub struct TradingWindow {
785 pub start_day: u8,
787 pub end_day: u8,
789 pub months: u16,
791}
792
793impl TradingWindow {
794 pub fn is_open(&self, day: u8, month: u8) -> bool {
796 if month < 1 || month > 12 {
798 return false;
799 }
800 let month_allowed = (self.months & (1 << (month - 1))) != 0;
801 if !month_allowed {
802 return false;
803 }
804
805 if self.start_day <= self.end_day {
807 day >= self.start_day && day <= self.end_day
808 } else {
809 day >= self.start_day || day <= self.end_day
811 }
812 }
813}
814
815#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
817pub struct EquityControllerConfig {
818 pub address: Address,
820 pub whitelist_enabled: bool,
822 pub trading_windows: Vec<TradingWindow>,
824 pub transfer_limit: u128,
826 pub governance_policy_id: PolicyId,
828 pub paused: bool,
830}
831
832#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
838#[repr(u8)]
839pub enum CorporateActionType {
840 StockSplit = 0,
842 ReverseSplit = 1,
844 DividendDeclare = 2,
846 DividendDistribute = 3,
848 StockDividend = 4,
850 Buyback = 5,
852 Conversion = 6,
854 RecordDateSnapshot = 7,
856 RightsOffering = 8,
858}
859
860impl CorporateActionType {
861 pub fn from_u8(v: u8) -> Option<Self> {
862 match v {
863 0 => Some(CorporateActionType::StockSplit),
864 1 => Some(CorporateActionType::ReverseSplit),
865 2 => Some(CorporateActionType::DividendDeclare),
866 3 => Some(CorporateActionType::DividendDistribute),
867 4 => Some(CorporateActionType::StockDividend),
868 5 => Some(CorporateActionType::Buyback),
869 6 => Some(CorporateActionType::Conversion),
870 7 => Some(CorporateActionType::RecordDateSnapshot),
871 8 => Some(CorporateActionType::RightsOffering),
872 _ => None,
873 }
874 }
875
876 pub fn name(&self) -> &'static str {
877 match self {
878 CorporateActionType::StockSplit => "Stock Split",
879 CorporateActionType::ReverseSplit => "Reverse Split",
880 CorporateActionType::DividendDeclare => "Dividend Declaration",
881 CorporateActionType::DividendDistribute => "Dividend Distribution",
882 CorporateActionType::StockDividend => "Stock Dividend",
883 CorporateActionType::Buyback => "Buyback",
884 CorporateActionType::Conversion => "Conversion",
885 CorporateActionType::RecordDateSnapshot => "Record Date Snapshot",
886 CorporateActionType::RightsOffering => "Rights Offering",
887 }
888 }
889}
890
891#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
893#[repr(u8)]
894pub enum RoundingMode {
895 Down = 0,
897 Up = 1,
899 Nearest = 2,
901}
902
903#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
905pub struct StockSplitParams {
906 pub ratio_numerator: u64,
908 pub ratio_denominator: u64,
910}
911
912impl StockSplitParams {
913 pub fn apply(&self, balance: u128) -> u128 {
915 (balance * self.ratio_numerator as u128) / self.ratio_denominator as u128
916 }
917}
918
919#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
921pub struct ReverseSplitParams {
922 pub ratio_numerator: u64,
924 pub ratio_denominator: u64,
926 pub rounding: RoundingMode,
928 pub cash_out_fractional: bool,
930 pub fractional_price: Option<u128>,
932}
933
934impl ReverseSplitParams {
935 pub fn apply(&self, balance: u128) -> (u128, u128) {
937 let numerator = self.ratio_numerator as u128;
938 let denominator = self.ratio_denominator as u128;
939
940 let new_balance = (balance * numerator) / denominator;
941 let remainder = balance - (new_balance * denominator) / numerator;
942
943 let fractional = match self.rounding {
944 RoundingMode::Down => 0,
945 RoundingMode::Up => {
946 if remainder > 0 {
947 1
948 } else {
949 0
950 }
951 }
952 RoundingMode::Nearest => {
953 if remainder * 2 >= denominator / numerator {
954 1
955 } else {
956 0
957 }
958 }
959 };
960
961 (new_balance + fractional, remainder)
962 }
963}
964
965#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
967pub enum DividendCurrency {
968 Native,
970 Token(Address),
972}
973
974#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
976pub struct DividendDeclareParams {
977 pub amount_per_share: u128,
979 pub currency: DividendCurrency,
981 pub record_date: Timestamp,
983 pub payment_date: Timestamp,
985}
986
987#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
989#[repr(u8)]
990pub enum DistributionMethod {
991 ProRataSnapshot = 0,
993 ProRataCurrent = 1,
995}
996
997#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
999pub struct DividendDistributeParams {
1000 pub declaration_id: ActionId,
1002 pub snapshot_id: SnapshotId,
1004 pub method: DistributionMethod,
1006}
1007
1008#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1010pub struct ConversionParams {
1011 pub from_class_id: ClassId,
1013 pub to_class_id: ClassId,
1015 pub conversion_ratio: u64,
1017 pub holder: Option<Address>,
1019}
1020
1021#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1023#[repr(u8)]
1024pub enum SnapshotPurpose {
1025 Dividend = 0,
1027 Voting = 1,
1029 Rights = 2,
1031 Other = 255,
1033}
1034
1035#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1037pub struct RecordSnapshotParams {
1038 pub purpose: SnapshotPurpose,
1040 pub reference: Option<[u8; 32]>,
1042}
1043
1044#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1046#[repr(u8)]
1047pub enum CorporateActionStatus {
1048 Proposed = 0,
1049 Approved = 1,
1050 Executing = 2,
1051 Completed = 3,
1052 Cancelled = 4,
1053 Failed = 5,
1054}
1055
1056#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1058pub enum CorporateActionParams {
1059 StockSplit(StockSplitParams),
1060 ReverseSplit(ReverseSplitParams),
1061 DividendDeclare(DividendDeclareParams),
1062 DividendDistribute(DividendDistributeParams),
1063 Conversion(ConversionParams),
1064 RecordSnapshot(RecordSnapshotParams),
1065}
1066
1067#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1069pub struct CorporateAction {
1070 pub action_id: ActionId,
1072 pub class_id: ClassId,
1074 pub action_type: CorporateActionType,
1076 pub params: CorporateActionParams,
1078 pub record_date: Option<Timestamp>,
1080 pub execution_date: Timestamp,
1082 pub governance_action_id: ActionId,
1084 pub status: CorporateActionStatus,
1086 pub created_at: Timestamp,
1088 pub executed_at: Option<Timestamp>,
1090}
1091
1092impl CorporateAction {
1093 pub fn generate_action_id(
1095 class_id: &ClassId,
1096 action_type: CorporateActionType,
1097 governance_action_id: &ActionId,
1098 ) -> ActionId {
1099 let mut hasher = blake3::Hasher::new();
1100 hasher.update(CORPORATE_ACTION_DOMAIN_SEP);
1101 hasher.update(&[action_type as u8]);
1102 hasher.update(b":v1:");
1103 hasher.update(class_id);
1104 hasher.update(governance_action_id);
1105 *hasher.finalize().as_bytes()
1106 }
1107}
1108
1109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1111pub struct OwnershipSnapshot {
1112 pub snapshot_id: SnapshotId,
1114 pub class_id: ClassId,
1116 pub timestamp: Timestamp,
1118 pub block_height: BlockHeight,
1120 pub total_supply: u128,
1122 pub holder_count: u64,
1124 pub balances_root: [u8; 32],
1126 pub purpose: SnapshotPurpose,
1128 pub reference: Option<[u8; 32]>,
1130}
1131
1132impl OwnershipSnapshot {
1133 pub fn generate_snapshot_id(
1135 class_id: &ClassId,
1136 purpose: SnapshotPurpose,
1137 timestamp: Timestamp,
1138 ) -> SnapshotId {
1139 let mut hasher = blake3::Hasher::new();
1140 hasher.update(SNAPSHOT_DOMAIN_SEP);
1141 hasher.update(&[purpose as u8]);
1142 hasher.update(b":v1:");
1143 hasher.update(class_id);
1144 hasher.update(×tamp.to_be_bytes());
1145 *hasher.finalize().as_bytes()
1146 }
1147}
1148
1149pub mod proof_profiles {
1155 pub const EQUITY_PROVE_MEMBERSHIP: &str = "equity.prove_membership.v1";
1156 pub const EQUITY_PROVE_OWNERSHIP_THRESHOLD: &str = "equity.prove_ownership_threshold.v1";
1157 pub const EQUITY_PROVE_VOTING_POWER: &str = "equity.prove_voting_power.v1";
1158}
1159
1160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1162pub struct MembershipPublicInputs {
1163 pub org_subject: SubjectId,
1165 pub class_id: ClassId,
1167 pub membership_commitment: [u8; 32],
1169 pub proof_timestamp: Timestamp,
1171}
1172
1173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1175pub struct OwnershipThresholdPublicInputs {
1176 pub org_subject: SubjectId,
1178 pub class_id: ClassId,
1180 pub threshold: u128,
1182 pub ownership_commitment: [u8; 32],
1184 pub proof_timestamp: Timestamp,
1186}
1187
1188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1190pub struct VotingPowerPublicInputs {
1191 pub org_subject: SubjectId,
1193 pub threshold: u128,
1195 pub reference: Option<[u8; 32]>,
1197 pub voting_commitment: [u8; 32],
1199 pub proof_timestamp: Timestamp,
1201}
1202
1203#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1205#[repr(u8)]
1206pub enum OwnershipProofType {
1207 Mock = 0,
1209 Groth16 = 1,
1211 Plonk = 2,
1213 Signature = 3,
1215}
1216
1217#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1219pub struct OwnershipProofEnvelope {
1220 pub proof_id: ProofId,
1222 pub profile_id: String,
1224 pub policy_ids: Vec<PolicyId>,
1226 pub public_inputs: Vec<u8>,
1228 pub proof_data: Vec<u8>,
1230 pub proof_type: OwnershipProofType,
1232 pub subject_nullifier: [u8; 32],
1234 pub generated_at: Timestamp,
1236 pub expires_at: Timestamp,
1238}
1239
1240impl OwnershipProofEnvelope {
1241 pub fn generate_proof_id(
1243 profile_id: &str,
1244 public_inputs: &[u8],
1245 nonce: &[u8; 32],
1246 ) -> ProofId {
1247 let mut hasher = blake3::Hasher::new();
1248 hasher.update(OWNERSHIP_PROOF_DOMAIN_SEP);
1249 hasher.update(profile_id.as_bytes());
1250 hasher.update(b":");
1251 hasher.update(public_inputs);
1252 hasher.update(nonce);
1253 *hasher.finalize().as_bytes()
1254 }
1255}
1256
1257#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1263#[repr(u8)]
1264pub enum EquityOperation {
1265 CreateEntity = 0,
1268 UpdateEntity = 1,
1270 AddController = 2,
1272 RemoveController = 3,
1274
1275 ProposeAction = 10,
1278 ApproveAction = 11,
1280 ExecuteAction = 12,
1282 RevokeAction = 13,
1284
1285 CreateToken = 20,
1288 UpdateToken = 21,
1290 PauseToken = 22,
1292 UnpauseToken = 23,
1294
1295 Transfer = 30,
1298 Approve = 31,
1300 TransferFrom = 32,
1302
1303 Mint = 40,
1306 Burn = 41,
1308
1309 UpdateController = 50,
1312 AddToWhitelist = 51,
1314 RemoveFromWhitelist = 52,
1316 SetLockup = 53,
1318
1319 ExecuteStockSplit = 60,
1322 ExecuteReverseSplit = 61,
1324 DeclareDividend = 62,
1326 DistributeDividend = 63,
1328 ExecuteConversion = 64,
1330 TakeSnapshot = 65,
1332
1333 VerifyOwnershipProof = 70,
1336}
1337
1338impl EquityOperation {
1339 pub fn from_u8(v: u8) -> Option<Self> {
1340 match v {
1341 0 => Some(EquityOperation::CreateEntity),
1342 1 => Some(EquityOperation::UpdateEntity),
1343 2 => Some(EquityOperation::AddController),
1344 3 => Some(EquityOperation::RemoveController),
1345 10 => Some(EquityOperation::ProposeAction),
1346 11 => Some(EquityOperation::ApproveAction),
1347 12 => Some(EquityOperation::ExecuteAction),
1348 13 => Some(EquityOperation::RevokeAction),
1349 20 => Some(EquityOperation::CreateToken),
1350 21 => Some(EquityOperation::UpdateToken),
1351 22 => Some(EquityOperation::PauseToken),
1352 23 => Some(EquityOperation::UnpauseToken),
1353 30 => Some(EquityOperation::Transfer),
1354 31 => Some(EquityOperation::Approve),
1355 32 => Some(EquityOperation::TransferFrom),
1356 40 => Some(EquityOperation::Mint),
1357 41 => Some(EquityOperation::Burn),
1358 50 => Some(EquityOperation::UpdateController),
1359 51 => Some(EquityOperation::AddToWhitelist),
1360 52 => Some(EquityOperation::RemoveFromWhitelist),
1361 53 => Some(EquityOperation::SetLockup),
1362 60 => Some(EquityOperation::ExecuteStockSplit),
1363 61 => Some(EquityOperation::ExecuteReverseSplit),
1364 62 => Some(EquityOperation::DeclareDividend),
1365 63 => Some(EquityOperation::DistributeDividend),
1366 64 => Some(EquityOperation::ExecuteConversion),
1367 65 => Some(EquityOperation::TakeSnapshot),
1368 70 => Some(EquityOperation::VerifyOwnershipProof),
1369 _ => None,
1370 }
1371 }
1372}
1373
1374#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1376pub struct EquityTxData {
1377 pub operation: EquityOperation,
1379 pub data: Vec<u8>,
1381 pub recipient: crate::Address,
1383}
1384
1385#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1391pub enum EntityProfileEvent {
1392 EntityCreated {
1393 subject_id: SubjectId,
1394 org_type: OrgType,
1395 controller_model: ControllerModel,
1396 },
1397 EntityUpdated {
1398 subject_id: SubjectId,
1399 },
1400 ControllerChanged {
1401 subject_id: SubjectId,
1402 old_controllers: Vec<Address>,
1403 new_controllers: Vec<Address>,
1404 },
1405 EntityStatusChanged {
1406 subject_id: SubjectId,
1407 old_status: EntityStatus,
1408 new_status: EntityStatus,
1409 },
1410}
1411
1412#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1414pub enum GovernanceActionEvent {
1415 ActionProposed {
1416 action_id: ActionId,
1417 org_subject: SubjectId,
1418 action_type: GovernanceActionType,
1419 policy_id: PolicyId,
1420 proposer: Address,
1421 },
1422 ActionApproved {
1423 action_id: ActionId,
1424 approver: Address,
1425 approval_count: u32,
1426 threshold: u32,
1427 },
1428 ActionExecuted {
1429 action_id: ActionId,
1430 executor: Address,
1431 effective_at: Timestamp,
1432 },
1433 ActionRevoked {
1434 action_id: ActionId,
1435 revoker: Address,
1436 },
1437}
1438
1439#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1441pub enum EquityTokenEvent {
1442 TokenCreated {
1443 issuer_subject: SubjectId,
1444 class_id: ClassId,
1445 share_class_type: ShareClassType,
1446 authorized_shares: u128,
1447 },
1448 Transfer {
1449 class_id: ClassId,
1450 from: Address,
1451 to: Address,
1452 amount: u128,
1453 },
1454 Approval {
1455 class_id: ClassId,
1456 owner: Address,
1457 spender: Address,
1458 amount: u128,
1459 },
1460 ControllerUpdated {
1461 class_id: ClassId,
1462 old_controller: Address,
1463 new_controller: Address,
1464 governance_action_id: ActionId,
1465 },
1466 TokenPaused {
1467 class_id: ClassId,
1468 },
1469 TokenUnpaused {
1470 class_id: ClassId,
1471 },
1472}
1473
1474#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1476pub enum CorporateActionEvent {
1477 ActionProposed {
1478 action_id: ActionId,
1479 class_id: ClassId,
1480 action_type: CorporateActionType,
1481 governance_action_id: ActionId,
1482 },
1483 ActionExecuted {
1484 action_id: ActionId,
1485 action_type: CorporateActionType,
1486 affected_holders: u64,
1487 total_shares_affected: u128,
1488 },
1489 DividendDeclared {
1490 declaration_id: ActionId,
1491 class_id: ClassId,
1492 amount_per_share: u128,
1493 record_date: Timestamp,
1494 payment_date: Timestamp,
1495 },
1496 DividendDistributed {
1497 declaration_id: ActionId,
1498 total_distributed: u128,
1499 recipient_count: u64,
1500 },
1501 SnapshotTaken {
1502 snapshot_id: SnapshotId,
1503 class_id: ClassId,
1504 purpose: SnapshotPurpose,
1505 holder_count: u64,
1506 },
1507}
1508
1509#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1511pub enum OwnershipProofEvent {
1512 ProofVerified {
1513 proof_id: ProofId,
1514 profile_id: String,
1515 org_subject: SubjectId,
1516 verifier: Address,
1517 valid: bool,
1518 },
1519}
1520
1521#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1523pub enum EquityEvent {
1524 Entity(EntityProfileEvent),
1525 Governance(GovernanceActionEvent),
1526 Token(EquityTokenEvent),
1527 CorporateAction(CorporateActionEvent),
1528 Proof(OwnershipProofEvent),
1529}
1530
1531#[cfg(test)]
1536mod tests {
1537 use super::*;
1538
1539 #[test]
1540 fn test_entity_subject_id_generation() {
1541 let name_commitment = [1u8; 32];
1542 let nonce = [2u8; 32];
1543
1544 let id1 = EntityProfile::generate_subject_id(
1545 OrgType::Corporation,
1546 &name_commitment,
1547 &nonce,
1548 );
1549 let id2 = EntityProfile::generate_subject_id(
1550 OrgType::Corporation,
1551 &name_commitment,
1552 &nonce,
1553 );
1554 let id3 = EntityProfile::generate_subject_id(
1555 OrgType::LLC,
1556 &name_commitment,
1557 &nonce,
1558 );
1559
1560 assert_eq!(id1, id2);
1561 assert_ne!(id1, id3);
1562 }
1563
1564 #[test]
1565 fn test_governance_action_threshold() {
1566 let action = GovernanceAction {
1567 action_id: [0u8; 32],
1568 org_subject: [0u8; 32],
1569 action_type: GovernanceActionType::BoardResolutionApproved,
1570 policy_id: [0u8; 32],
1571 action_commitment: [0u8; 32],
1572 effective_at: 0,
1573 expires_at: 0,
1574 attachments: None,
1575 approvers: vec![Address::ZERO, Address::ZERO],
1576 required_threshold: 3,
1577 status: GovernanceActionStatus::Pending,
1578 created_at: 0,
1579 recorded_at_height: 0,
1580 };
1581
1582 assert!(!action.is_threshold_met());
1583
1584 let mut action2 = action.clone();
1585 action2.approvers.push(Address::ZERO);
1586 assert!(action2.is_threshold_met());
1587 }
1588
1589 #[test]
1590 fn test_equity_token_mint_check() {
1591 let token = EquityToken {
1592 issuer_subject: [0u8; 32],
1593 class_id: [0u8; 32],
1594 share_class_type: ShareClassType::Common,
1595 name: "Common Shares".to_string(),
1596 symbol: "ACME".to_string(),
1597 authorized_shares: 1_000_000,
1598 issued_shares: 500_000,
1599 votes_per_share: 1,
1600 economic_rights_hash: [0u8; 32],
1601 liquidation_preference_hash: None,
1602 dividend_policy_hash: None,
1603 conversion_rules_hash: None,
1604 controller: Address::ZERO,
1605 par_value: None,
1606 created_at: 0,
1607 updated_at: 0,
1608 status: TokenStatus::Active,
1609 };
1610
1611 assert!(token.can_mint(100_000));
1612 assert!(token.can_mint(500_000));
1613 assert!(!token.can_mint(500_001));
1614 assert_eq!(token.remaining_authorized(), 500_000);
1615 }
1616
1617 #[test]
1618 fn test_vesting_schedule() {
1619 let schedule = VestingSchedule {
1620 total_amount: 1_000_000,
1621 vested_amount: 0,
1622 start_at: 1000,
1623 cliff_duration: 100,
1624 total_duration: 400,
1625 interval: 30,
1626 };
1627
1628 assert_eq!(schedule.vested_at(500), 0);
1630
1631 assert_eq!(schedule.vested_at(1050), 0);
1633
1634 assert_eq!(schedule.vested_at(1100), 0);
1636 assert!(schedule.vested_at(1130) > 0);
1638
1639 assert_eq!(schedule.vested_at(1400), 1_000_000);
1641
1642 assert_eq!(schedule.vested_at(2000), 1_000_000);
1644 }
1645
1646 #[test]
1647 fn test_trading_window() {
1648 let window = TradingWindow {
1649 start_day: 1,
1650 end_day: 10,
1651 months: 0b000000000101, };
1653
1654 assert!(window.is_open(5, 1));
1656
1657 assert!(window.is_open(1, 3));
1659
1660 assert!(!window.is_open(15, 1));
1662
1663 assert!(!window.is_open(5, 2));
1665 }
1666
1667 #[test]
1668 fn test_stock_split_params() {
1669 let split = StockSplitParams {
1670 ratio_numerator: 2,
1671 ratio_denominator: 1,
1672 };
1673
1674 assert_eq!(split.apply(100), 200);
1675 assert_eq!(split.apply(1), 2);
1676 assert_eq!(split.apply(0), 0);
1677 }
1678
1679 #[test]
1680 fn test_reverse_split_params() {
1681 let reverse = ReverseSplitParams {
1682 ratio_numerator: 1,
1683 ratio_denominator: 10,
1684 rounding: RoundingMode::Down,
1685 cash_out_fractional: false,
1686 fractional_price: None,
1687 };
1688
1689 let (new_balance, remainder) = reverse.apply(100);
1690 assert_eq!(new_balance, 10);
1691 assert_eq!(remainder, 0);
1692
1693 let (new_balance2, remainder2) = reverse.apply(95);
1694 assert_eq!(new_balance2, 9);
1695 assert!(remainder2 > 0);
1696 }
1697
1698 #[test]
1699 fn test_class_id_determinism() {
1700 let subject = [1u8; 32];
1701
1702 let id1 = EquityToken::generate_class_id(&subject, "Series A", ShareClassType::Preferred);
1703 let id2 = EquityToken::generate_class_id(&subject, "Series A", ShareClassType::Preferred);
1704 let id3 = EquityToken::generate_class_id(&subject, "Series B", ShareClassType::Preferred);
1705
1706 assert_eq!(id1, id2);
1707 assert_ne!(id1, id3);
1708 }
1709
1710 #[test]
1711 fn test_snapshot_id_determinism() {
1712 let class_id = [1u8; 32];
1713 let timestamp = 1704067200000u64;
1714
1715 let id1 = OwnershipSnapshot::generate_snapshot_id(&class_id, SnapshotPurpose::Dividend, timestamp);
1716 let id2 = OwnershipSnapshot::generate_snapshot_id(&class_id, SnapshotPurpose::Dividend, timestamp);
1717 let id3 = OwnershipSnapshot::generate_snapshot_id(&class_id, SnapshotPurpose::Voting, timestamp);
1718
1719 assert_eq!(id1, id2);
1720 assert_ne!(id1, id3);
1721 }
1722}