From 4e07ce3cfedc08890b0b0ae866d0e43cdf79a045 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 20:20:25 -0400 Subject: [PATCH 01/27] feat: enforce OCF reference integrity --- OpenCapTable-v34/daml.yaml | 2 +- Test/daml.yaml | 2 +- .../TestConversionMechanisms.daml | 72 +++---- ...tConvertibleConversionRightReferences.daml | 107 ++++++++++ Test/daml/OpenCapTable/TestDocument.daml | 57 +++++- .../TestEquityCompensationAcceptance.daml | 4 +- .../TestEquityCompensationCancellation.daml | 4 +- .../TestEquityCompensationIssuance.daml | 56 +++--- ...tEquityCompensationIssuanceReferences.daml | 141 ++++++++++++++ .../TestEquityCompensationRetraction.daml | 4 +- .../TestEquityCompensationTransfer.daml | 4 +- Test/daml/OpenCapTable/TestHelpers.daml | 45 ++++- .../TestIssuerAuthorizedSharesAdjustment.daml | 39 +++- .../OpenCapTable/TestStockAcceptance.daml | 2 +- .../OpenCapTable/TestStockCancellation.daml | 2 +- Test/daml/OpenCapTable/TestStockClass.daml | 140 ++++++++------ ...stStockClassConversionRightReferences.daml | 91 +++++++++ Test/daml/OpenCapTable/TestStockIssuance.daml | 12 +- .../TestStockIssuanceReferences.daml | 75 +++++++ .../OpenCapTable/TestStockPlanReferences.daml | 45 +++++ .../TestStockPlanReturnToPool.daml | 7 +- .../OpenCapTable/TestStockRetraction.daml | 2 +- .../OpenCapTable/TestValuationReferences.daml | 76 ++++++++ .../OpenCapTable/TestVestingAcceleration.daml | 6 +- Test/daml/OpenCapTable/TestVestingEvent.daml | 6 +- Test/daml/OpenCapTable/TestVestingStart.daml | 12 +- .../TestWarrantConversionRightReferences.daml | 183 ++++++++++++++++++ .../OpenCapTable/TestWarrantIssuance.daml | 43 ++-- .../TestWarrantIssuanceReferences.daml | 108 +++++++++++ .../0.0.3/OpenCapTable-v34.dar | 3 + dars/dars.lock | 7 + scripts/codegen/captable-config.yaml | 27 ++- scripts/codegen/generate-captable.ts | 49 ++++- .../codegen/templates/CapTable.daml.template | 134 ++++++++++++- .../codegen/templates/loops/create-case.daml | 15 ++ .../codegen/templates/loops/edit-case.daml | 15 ++ 36 files changed, 1400 insertions(+), 197 deletions(-) create mode 100644 Test/daml/OpenCapTable/TestConvertibleConversionRightReferences.daml create mode 100644 Test/daml/OpenCapTable/TestEquityCompensationIssuanceReferences.daml create mode 100644 Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml create mode 100644 Test/daml/OpenCapTable/TestStockIssuanceReferences.daml create mode 100644 Test/daml/OpenCapTable/TestStockPlanReferences.daml create mode 100644 Test/daml/OpenCapTable/TestValuationReferences.daml create mode 100644 Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml create mode 100644 Test/daml/OpenCapTable/TestWarrantIssuanceReferences.daml create mode 100644 dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar diff --git a/OpenCapTable-v34/daml.yaml b/OpenCapTable-v34/daml.yaml index 6d6d6797..2403aff1 100644 --- a/OpenCapTable-v34/daml.yaml +++ b/OpenCapTable-v34/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.4.10 name: OpenCapTable-v34 source: daml -version: 0.0.2 +version: 0.0.3 build-options: - -Wno-crypto-text-is-alpha - --typecheck-upgrades=no diff --git a/Test/daml.yaml b/Test/daml.yaml index cdf173c0..151e762a 100644 --- a/Test/daml.yaml +++ b/Test/daml.yaml @@ -7,6 +7,6 @@ dependencies: - daml-stdlib - daml-script data-dependencies: - - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.2.dar + - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.3.dar build-options: - -Wno-template-interface-depends-on-daml-script diff --git a/Test/daml/OpenCapTable/TestConversionMechanisms.daml b/Test/daml/OpenCapTable/TestConversionMechanisms.daml index f0d24123..a1ea8057 100644 --- a/Test/daml/OpenCapTable/TestConversionMechanisms.daml +++ b/Test/daml/OpenCapTable/TestConversionMechanisms.daml @@ -13,7 +13,7 @@ import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Stock (OcfInitialSharesAuthorized(..), OcfQuantitySourceType(..), OcfRatio(..), OcfStockClassType(..)) import OpenCapTable.HelpersTriggers (ocfTriggerElectiveAtWill) import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addDefaultStakeholder) +import OpenCapTable.TestHelpers (addDefaultStakeholder, defaultStockClass) testTime : Time testTime = DT.time (DA.date 2024 Jan 1) 0 0 0 @@ -392,22 +392,25 @@ testConversionMechanismRights_AllValidVariantsCreate = script do capTableCid <- addDefaultStakeholder issuer cap_table result1 <- submit issuer do exerciseCmd capTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockClass StockClassOcfData with - id = "SC_PREFERRED_ALL_CONVERSION_MECHANISMS" - name = "Preferred All Conversion Mechanisms" - class_type = OcfStockClassTypePreferred - default_id_prefix = "PAM-" - initial_shares_authorized = OcfInitialSharesNumeric 1_000_000.0 - votes_per_share = 1.0 - seniority = 2.0 - board_approval_date = None - stockholder_approval_date = None - par_value = None - price_per_share = None - liquidation_preference_multiple = None - participation_cap_multiple = None - conversion_rights = validStockClassRights - comments = []] + creates = + [ CT.OcfCreateStockClass (defaultStockClass "SC_COMMON") + , CT.OcfCreateStockClass (StockClassOcfData with + id = "SC_PREFERRED_ALL_CONVERSION_MECHANISMS" + name = "Preferred All Conversion Mechanisms" + class_type = OcfStockClassTypePreferred + default_id_prefix = "PAM-" + initial_shares_authorized = OcfInitialSharesNumeric 1_000_000.0 + votes_per_share = 1.0 + seniority = 2.0 + board_approval_date = None + stockholder_approval_date = None + par_value = None + price_per_share = None + liquidation_preference_multiple = None + participation_cap_multiple = None + conversion_rights = validStockClassRights + comments = []) + ] edits = [] deletes = [] result2 <- submit issuer do @@ -460,22 +463,25 @@ testConversionMechanismRights_InvalidVariantsReject = script do capTableCid <- addDefaultStakeholder issuer cap_table stockControl <- submit issuer do exerciseCmd capTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockClass StockClassOcfData with - id = "SC_VALID_CONVERSION_MECHANISM_CONTROL" - name = "Valid Conversion Mechanism Control" - class_type = OcfStockClassTypePreferred - default_id_prefix = "VCM-" - initial_shares_authorized = OcfInitialSharesNumeric 1_000_000.0 - votes_per_share = 1.0 - seniority = 2.0 - board_approval_date = None - stockholder_approval_date = None - par_value = None - price_per_share = None - liquidation_preference_multiple = None - participation_cap_multiple = None - conversion_rights = [stockClassRightWith OcfConversionMechanismRatioConversion None None (Some validRatio) None None None None None] - comments = []] + creates = + [ CT.OcfCreateStockClass (defaultStockClass "SC_COMMON") + , CT.OcfCreateStockClass (StockClassOcfData with + id = "SC_VALID_CONVERSION_MECHANISM_CONTROL" + name = "Valid Conversion Mechanism Control" + class_type = OcfStockClassTypePreferred + default_id_prefix = "VCM-" + initial_shares_authorized = OcfInitialSharesNumeric 1_000_000.0 + votes_per_share = 1.0 + seniority = 2.0 + board_approval_date = None + stockholder_approval_date = None + par_value = None + price_per_share = None + liquidation_preference_multiple = None + participation_cap_multiple = None + conversion_rights = [stockClassRightWith OcfConversionMechanismRatioConversion None None (Some validRatio) None None None None None] + comments = []) + ] edits = [] deletes = [] submitMustFail issuer do diff --git a/Test/daml/OpenCapTable/TestConvertibleConversionRightReferences.daml b/Test/daml/OpenCapTable/TestConvertibleConversionRightReferences.daml new file mode 100644 index 00000000..8c665951 --- /dev/null +++ b/Test/daml/OpenCapTable/TestConvertibleConversionRightReferences.daml @@ -0,0 +1,107 @@ +module OpenCapTable.TestConvertibleConversionRightReferences where + +import Fairmint.OpenCapTable.OCF.ConvertibleIssuance +import Fairmint.OpenCapTable.Types.Conversion +import Fairmint.OpenCapTable.Types.Monetary +import qualified Fairmint.OpenCapTable.CapTable as CT +import OpenCapTable.Setup +import OpenCapTable.TestHelpers +import Daml.Script + +convertibleConversionTriggerTo : Optional Text -> OcfConvertibleConversionTrigger +convertibleConversionTriggerTo stockClassId = OcfConvertibleConversionTrigger with + conversion_right = OcfConvertibleConversionRight with + conversion_mechanism = OcfConvMechSAFE (OcfSAFEConversionMechanism with + capitalization_definition = None + capitalization_definition_rules = None + conversion_discount = Some 0.2 + conversion_mfn = False + conversion_timing = None + conversion_valuation_cap = None + exit_multiple = None) + converts_to_future_round = None + converts_to_stock_class_id = stockClassId + type_ = "CONVERTIBLE_CONVERSION_RIGHT" + end_date = None + nickname = None + start_date = None + trigger_condition = None + trigger_date = None + trigger_description = None + trigger_id = "TRIGGER-1" + type_ = OcfTriggerTypeTypeUnspecified + +convertibleIssuanceWithConversionTarget : Text -> Text -> Optional Text -> ConvertibleIssuanceOcfData +convertibleIssuanceWithConversionTarget issuanceId securityId stockClassId = ConvertibleIssuanceOcfData with + id = issuanceId + date = defaultTestDate + security_id = securityId + custom_id = "CN-" <> issuanceId + stakeholder_id = defaultStakeholderId + board_approval_date = None + stockholder_approval_date = None + consideration_text = None + security_law_exemptions = [] + investment_amount = OcfMonetary with amount = 100_000.0, currency = "USD" + convertible_type = OcfConvertibleSafe + conversion_triggers = [convertibleConversionTriggerTo stockClassId] + pro_rata = None + seniority = 1 + comments = [] + +testConvertibleConversionRightTargetCreatedInSameBatchSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableWithStakeholder <- addDefaultStakeholder issuer cap_table + + _ <- submit issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass (defaultStockClass "SC_CONVERTIBLE_TARGET") + , CT.OcfCreateConvertibleIssuance (convertibleIssuanceWithConversionTarget "TX_CONVERTIBLE_TARGET_REF" "CONV-TARGET-REF" (Some "SC_CONVERTIBLE_TARGET")) + ] + edits = [] + deletes = [] + pure () + +testConvertibleConversionRightMissingTargetFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableWithStakeholder <- addDefaultStakeholder issuer cap_table + + submitMustFail issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance (convertibleIssuanceWithConversionTarget "TX_CONVERTIBLE_MISSING_TARGET" "CONV-MISSING-TARGET" (Some "SC_MISSING_CONVERTIBLE_TARGET"))] + edits = [] + deletes = [] + +testConvertibleConversionRightEditMissingTargetFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableWithStakeholder <- addDefaultStakeholder issuer cap_table + result <- submit issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance (convertibleIssuanceWithConversionTarget "TX_CONVERTIBLE_EDIT_TARGET" "CONV-EDIT-TARGET" None)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditConvertibleIssuance (convertibleIssuanceWithConversionTarget "TX_CONVERTIBLE_EDIT_TARGET" "CONV-EDIT-TARGET" (Some "SC_MISSING_CONVERTIBLE_EDIT_TARGET"))] + deletes = [] + +testConvertibleConversionRightTargetDeleteCannotLeaveDanglingReference = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableWithStakeholder <- addDefaultStakeholder issuer cap_table + result <- submit issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass (defaultStockClass "SC_CONVERTIBLE_DELETE_TARGET") + , CT.OcfCreateConvertibleIssuance (convertibleIssuanceWithConversionTarget "TX_CONVERTIBLE_DANGLING_TARGET" "CONV-DANGLING-TARGET" (Some "SC_CONVERTIBLE_DELETE_TARGET")) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockClass "SC_CONVERTIBLE_DELETE_TARGET"] diff --git a/Test/daml/OpenCapTable/TestDocument.daml b/Test/daml/OpenCapTable/TestDocument.daml index 52f97506..04625212 100644 --- a/Test/daml/OpenCapTable/TestDocument.daml +++ b/Test/daml/OpenCapTable/TestDocument.daml @@ -2,8 +2,18 @@ module OpenCapTable.TestDocument where import Fairmint.OpenCapTable.OCF.Document import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup +import OpenCapTable.TestHelpers import Daml.Script +documentData : Text -> [OcfObjectReference] -> DocumentOcfData +documentData documentId relatedObjects = DocumentOcfData with + id = documentId + path = Some ("/docs/" <> documentId <> ".pdf") + uri = None + md5 = "0123456789abcdef0123456789abcdef" + related_objects = relatedObjects + comments = [] + testCreateDocument = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp -- Add document to CapTable via batch @@ -52,7 +62,7 @@ testCreateDocumentWithOptionalsSome = script do path = None uri = Some "https://cdn.example.com/docs/acme/charter.pdf" md5 = "0123456789abcdef0123456789abcdef" - related_objects = [ OcfObjectReference with object_type = OcfObjIssuer, object_id = "ISS_FOR_DOC" ] + related_objects = [ OcfObjectReference with object_type = OcfObjIssuer, object_id = "ISSUER_1" ] comments = ["External link"]] edits = [] deletes = [] @@ -85,3 +95,48 @@ testEditDocument = script do comments = ["Updated version"]] deletes = [] pure () + +testDocumentRelatedIssuerReferenceMustExist = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateDocument (documentData "DOC_BAD_ISSUER_REF" [OcfObjectReference with object_type = OcfObjIssuer, object_id = "ISSUER_MISSING"])] + edits = [] + deletes = [] + +testDocumentRelatedObjectCreatedInSameBatchSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + _ <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateDocument (documentData "DOC_STOCK_CLASS_REF" [OcfObjectReference with object_type = OcfObjStockClass, object_id = "SC_DOC_REF"]) + , CT.OcfCreateStockClass (defaultStockClass "SC_DOC_REF") + ] + edits = [] + deletes = [] + pure () + +testDocumentEditMissingRelatedObjectFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + result <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateDocument (documentData "DOC_EDIT_REF" [])] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditDocument (documentData "DOC_EDIT_REF" [OcfObjectReference with object_type = OcfObjStockClass, object_id = "SC_MISSING_FOR_DOC"])] + deletes = [] + +testDocumentUnsupportedRelatedObjectTypeFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateDocument (documentData "DOC_UNSUPPORTED_REF" [OcfObjectReference with object_type = OcfObjFinancing, object_id = "FINANCING_1"])] + edits = [] + deletes = [] diff --git a/Test/daml/OpenCapTable/TestEquityCompensationAcceptance.daml b/Test/daml/OpenCapTable/TestEquityCompensationAcceptance.daml index 93196948..778a0765 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationAcceptance.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationAcceptance.daml @@ -25,8 +25,8 @@ createEquityCompensationIssuance issuer capTableCid = stockholder_approval_date = None consideration_text = Some "Services rendered" security_law_exemptions = [] - stock_plan_id = Some "SP_2024" - stock_class_id = Some "SC_COMMON" + stock_plan_id = None + stock_class_id = None compensation_type = OcfCompensationTypeOptionISO quantity = 10000.0 exercise_price = Some (OcfMonetary with amount = 0.50, currency = "USD") diff --git a/Test/daml/OpenCapTable/TestEquityCompensationCancellation.daml b/Test/daml/OpenCapTable/TestEquityCompensationCancellation.daml index 7e0aaf6e..e1995dd5 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationCancellation.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationCancellation.daml @@ -25,8 +25,8 @@ createEquityCompensationIssuance issuer capTableCid securityId quantity = stockholder_approval_date = None consideration_text = None security_law_exemptions = [] - stock_plan_id = Some "PLAN-2020" - stock_class_id = Some "COMMON" + stock_plan_id = None + stock_class_id = None compensation_type = OcfCompensationTypeOptionNSO quantity = quantity exercise_price = Some (OcfMonetary with amount = 0.10, currency = "USD") diff --git a/Test/daml/OpenCapTable/TestEquityCompensationIssuance.daml b/Test/daml/OpenCapTable/TestEquityCompensationIssuance.daml index 197b697f..98e63c55 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationIssuance.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationIssuance.daml @@ -7,6 +7,7 @@ import Fairmint.OpenCapTable.Types.Stakeholder (OcfStakeholderRelationshipType(. import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..), OcfVesting(..), OcfTerminationWindow(..), OcfTerminationWindowType(..), OcfPeriodType(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup +import OpenCapTable.TestHelpers (defaultStockClass, defaultStockPlan, defaultVestingTerms) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -109,31 +110,36 @@ testEquityCompensationIssuance_OptionalsSome = script do let capTableCid = result.updatedCapTableCid _ <- submit issuer do exerciseCmd capTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationIssuance EquityCompensationIssuanceOcfData with - id = "TX_EC_ISS_2" - date = DT.time (DA.date 2024 Feb 01) 0 0 0 - security_id = "EC-2" - custom_id = "EC-CERT-2" - stakeholder_id = "SH-1" - base_price = Some (OcfMonetary with amount = 2.0, currency = "USD") - board_approval_date = Some (DT.time (DA.date 2024 Jan 15) 0 0 0) - consideration_text = Some "Employee incentive" - early_exercisable = Some True - exercise_price = Some (OcfMonetary with amount = 2.5, currency = "USD") - expiration_date = Some (DT.time (DA.date 2034 Feb 01) 0 0 0) - stock_class_id = Some "SC-1" - stock_plan_id = Some "PLAN-1" - stockholder_approval_date = Some (DT.time (DA.date 2024 Jan 20) 0 0 0) - vesting_terms_id = Some "VT-1" - security_law_exemptions = [OcfSecurityExemption with description = "ISO", jurisdiction = "US"] - vestings = [OcfVesting with date = DT.time (DA.date 2025 Jan 01) 0 0 0, amount = 125.0] - termination_exercise_windows = [OcfTerminationWindow with - reason = OcfTermVoluntaryGoodCause - period = 90 - period_type = OcfPeriodDays] - compensation_type = OcfCompensationTypeOptionISO - quantity = 500.0 - comments = ["Initial grant for new hire"]] + creates = + [ CT.OcfCreateStockClass (defaultStockClass "SC-1") + , CT.OcfCreateStockPlan (defaultStockPlan "PLAN-1" "SC-1") + , CT.OcfCreateVestingTerms (defaultVestingTerms "VT-1") + , CT.OcfCreateEquityCompensationIssuance EquityCompensationIssuanceOcfData with + id = "TX_EC_ISS_2" + date = DT.time (DA.date 2024 Feb 01) 0 0 0 + security_id = "EC-2" + custom_id = "EC-CERT-2" + stakeholder_id = "SH-1" + base_price = Some (OcfMonetary with amount = 2.0, currency = "USD") + board_approval_date = Some (DT.time (DA.date 2024 Jan 15) 0 0 0) + consideration_text = Some "Employee incentive" + early_exercisable = Some True + exercise_price = Some (OcfMonetary with amount = 2.5, currency = "USD") + expiration_date = Some (DT.time (DA.date 2034 Feb 01) 0 0 0) + stock_class_id = Some "SC-1" + stock_plan_id = Some "PLAN-1" + stockholder_approval_date = Some (DT.time (DA.date 2024 Jan 20) 0 0 0) + vesting_terms_id = Some "VT-1" + security_law_exemptions = [OcfSecurityExemption with description = "ISO", jurisdiction = "US"] + vestings = [OcfVesting with date = DT.time (DA.date 2025 Jan 01) 0 0 0, amount = 125.0] + termination_exercise_windows = [OcfTerminationWindow with + reason = OcfTermVoluntaryGoodCause + period = 90 + period_type = OcfPeriodDays] + compensation_type = OcfCompensationTypeOptionISO + quantity = 500.0 + comments = ["Initial grant for new hire"] + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestEquityCompensationIssuanceReferences.daml b/Test/daml/OpenCapTable/TestEquityCompensationIssuanceReferences.daml new file mode 100644 index 00000000..3c924486 --- /dev/null +++ b/Test/daml/OpenCapTable/TestEquityCompensationIssuanceReferences.daml @@ -0,0 +1,141 @@ +module OpenCapTable.TestEquityCompensationIssuanceReferences where + +import qualified Fairmint.OpenCapTable.CapTable as CT +import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance (EquityCompensationIssuanceOcfData(..)) +import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) +import OpenCapTable.Setup +import OpenCapTable.TestHelpers +import Daml.Script + +equityCompensationIssuance : Text -> Text -> Text -> EquityCompensationIssuanceOcfData +equityCompensationIssuance issuanceId securityId stakeholderId = EquityCompensationIssuanceOcfData with + id = issuanceId + date = defaultTestDate + security_id = securityId + custom_id = "EC-" <> issuanceId + stakeholder_id = stakeholderId + base_price = None + board_approval_date = None + consideration_text = None + early_exercisable = Some False + exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + expiration_date = None + stock_class_id = None + stock_plan_id = None + stockholder_approval_date = None + vesting_terms_id = None + security_law_exemptions = [] + vestings = [] + termination_exercise_windows = [] + compensation_type = OcfCompensationTypeOptionNSO + quantity = 100.0 + comments = [] + +createReferencedEquityCompensationIssuance : + Party -> + ContractId CT.CapTable -> + Script (ContractId CT.CapTable, EquityCompensationIssuanceOcfData, Text, Text, Text) +createReferencedEquityCompensationIssuance issuer capTableCid = do + (capTableWithStakeholder, stakeholderId) <- setupStakeholderWithId issuer capTableCid "SH_EC_REF" "Equity Holder" + let stockClassId = "SC_EC_REF" + let stockPlanId = "PLAN_EC_REF" + let vestingTermsId = "VT_EC_REF" + let issuance = (equityCompensationIssuance "TX_EC_REF" "SEC_EC_REF" stakeholderId) with + stock_class_id = Some stockClassId + stock_plan_id = Some stockPlanId + vesting_terms_id = Some vestingTermsId + + result <- submit issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass (defaultStockClass stockClassId) + , CT.OcfCreateStockPlan (defaultStockPlan stockPlanId stockClassId) + , CT.OcfCreateVestingTerms (defaultVestingTerms vestingTermsId) + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + pure (result.updatedCapTableCid, issuance, stockClassId, stockPlanId, vestingTermsId) + +testEquityCompensationIssuance_MissingStockClassReferenceFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, stakeholderId) <- setupStakeholderWithId issuer cap_table "SH_EC_MISSING_CLASS" "Equity Holder" + let issuance = (equityCompensationIssuance "TX_EC_MISSING_CLASS" "SEC_EC_MISSING_CLASS" stakeholderId) with + stock_class_id = Some "SC_MISSING" + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance issuance] + edits = [] + deletes = [] + +testEquityCompensationIssuance_MissingStockPlanReferenceFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, stakeholderId) <- setupStakeholderWithId issuer cap_table "SH_EC_MISSING_PLAN" "Equity Holder" + let issuance = (equityCompensationIssuance "TX_EC_MISSING_PLAN" "SEC_EC_MISSING_PLAN" stakeholderId) with + stock_plan_id = Some "PLAN_MISSING" + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance issuance] + edits = [] + deletes = [] + +testEquityCompensationIssuance_MissingVestingTermsReferenceFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, stakeholderId) <- setupStakeholderWithId issuer cap_table "SH_EC_MISSING_VESTING" "Equity Holder" + let issuance = (equityCompensationIssuance "TX_EC_MISSING_VESTING" "SEC_EC_MISSING_VESTING" stakeholderId) with + vesting_terms_id = Some "VT_MISSING" + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance issuance] + edits = [] + deletes = [] + +testEquityCompensationIssuance_ReferencesCreatedInSameBatchSucceed = script do + TestOcp{issuer, cap_table} <- setupTestOcp + _ <- createReferencedEquityCompensationIssuance issuer cap_table + pure () + +testEquityCompensationIssuance_EditMissingReferenceFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, issuance, _, _, _) <- createReferencedEquityCompensationIssuance issuer cap_table + let editedIssuance = issuance with stock_plan_id = Some "PLAN_MISSING_ON_EDIT" + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditEquityCompensationIssuance editedIssuance] + deletes = [] + +testEquityCompensationIssuance_DeleteReferencedStockClassFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, _, stockClassId, _, _) <- createReferencedEquityCompensationIssuance issuer cap_table + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockClass stockClassId] + +testEquityCompensationIssuance_DeleteReferencedStockPlanFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, _, _, stockPlanId, _) <- createReferencedEquityCompensationIssuance issuer cap_table + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockPlan stockPlanId] + +testEquityCompensationIssuance_DeleteReferencedVestingTermsFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, _, _, _, vestingTermsId) <- createReferencedEquityCompensationIssuance issuer cap_table + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteVestingTerms vestingTermsId] diff --git a/Test/daml/OpenCapTable/TestEquityCompensationRetraction.daml b/Test/daml/OpenCapTable/TestEquityCompensationRetraction.daml index 2103da62..7b639000 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationRetraction.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationRetraction.daml @@ -25,8 +25,8 @@ createEquityCompensationIssuance issuer capTableCid = stockholder_approval_date = None consideration_text = Some "Services rendered" security_law_exemptions = [] - stock_plan_id = Some "SP_2024" - stock_class_id = Some "SC_COMMON" + stock_plan_id = None + stock_class_id = None compensation_type = OcfCompensationTypeOptionISO quantity = 10000.0 exercise_price = Some (OcfMonetary with amount = 0.50, currency = "USD") diff --git a/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml b/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml index a3055eac..b6ba7cf1 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml @@ -34,8 +34,8 @@ createEquityCompensationIssuance issuer capTableCid securityId quantity = exercise_price = Some (OcfMonetary with amount = 0.50, currency = "USD") early_exercisable = None expiration_date = Some (DT.time (DA.date 2032 Jan 15) 0 0 0) - stock_class_id = Some "COMMON" - stock_plan_id = Some "PLAN-2020" + stock_class_id = None + stock_plan_id = None vesting_terms_id = None stockholder_approval_date = None] edits = [] diff --git a/Test/daml/OpenCapTable/TestHelpers.daml b/Test/daml/OpenCapTable/TestHelpers.daml index 1e4db3a0..d8d1bf34 100644 --- a/Test/daml/OpenCapTable/TestHelpers.daml +++ b/Test/daml/OpenCapTable/TestHelpers.daml @@ -4,6 +4,9 @@ import DA.Assert (assertMsg) import Fairmint.OpenCapTable.OCF.Stakeholder import Fairmint.OpenCapTable.OCF.StockClass import Fairmint.OpenCapTable.OCF.StockIssuance (StockIssuanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockLegendTemplate (StockLegendTemplateOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockPlan (StockPlanOcfData(..)) +import Fairmint.OpenCapTable.OCF.VestingTerms (VestingTermsOcfData(..), OcfAllocationType(..), OcfVestingCondition(..), OcfVestingTrigger(..)) import Fairmint.OpenCapTable.OCF.WarrantIssuance (WarrantIssuanceOcfData(..)) import Fairmint.OpenCapTable.OCF.ConvertibleIssuance (ConvertibleIssuanceOcfData(..), OcfConvertibleConversionTrigger(..)) import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance (EquityCompensationIssuanceOcfData(..)) @@ -79,9 +82,9 @@ defaultTestDate : Time defaultTestDate = DT.time (DA.date 2024 Jan 01) 0 0 0 -- | Default stock legend IDs for test issuances. --- Using "L1" as a placeholder - tests should override if specific legends matter. +-- Defaults to no legends. Tests that reference legends should create matching StockLegendTemplate records. defaultStockLegendIds : [Text] -defaultStockLegendIds = ["L1"] +defaultStockLegendIds = [] -- | Returns valid StockIssuanceOcfData with sensible defaults. -- Provide IDs for the issuance, security, stakeholder, and stock class. @@ -112,6 +115,44 @@ defaultStockIssuance issuanceId securityId stakeholderId stockClassId = StockIss issuance_type = None comments = [] +-- | Returns valid StockLegendTemplateOcfData with sensible defaults. +defaultStockLegendTemplate : Text -> StockLegendTemplateOcfData +defaultStockLegendTemplate legendId = StockLegendTemplateOcfData with + id = legendId + name = "Restricted Stock Legend" + text = "Shares represented by this certificate are subject to transfer restrictions." + comments = [] + +-- | Returns valid StockPlanOcfData with sensible defaults for a stock class. +defaultStockPlan : Text -> Text -> StockPlanOcfData +defaultStockPlan planId stockClassId = StockPlanOcfData with + id = planId + initial_shares_reserved = 1000.0 + plan_name = "Equity Incentive Plan" + comments = [] + stock_class_ids = [stockClassId] + board_approval_date = None + default_cancellation_behavior = None + stockholder_approval_date = None + +-- | Returns valid VestingTermsOcfData with a single start condition. +defaultVestingTerms : Text -> VestingTermsOcfData +defaultVestingTerms vestingTermsId = + let startCondition = OcfVestingCondition with + id = "start" + trigger = OcfVestingStartTrigger + next_condition_ids = [] + description = Some "All at start" + portion = None + quantity = Some 100.0 + in VestingTermsOcfData with + id = vestingTermsId + allocation_type = OcfAllocationCumulativeRounding + description = "All shares vest at start" + name = "Immediate vesting" + comments = [] + vesting_conditions = [startCondition] + -- ============================================================================= -- SETUP COMBINATORS -- ============================================================================= diff --git a/Test/daml/OpenCapTable/TestIssuerAuthorizedSharesAdjustment.daml b/Test/daml/OpenCapTable/TestIssuerAuthorizedSharesAdjustment.daml index f9628510..c7f4356f 100644 --- a/Test/daml/OpenCapTable/TestIssuerAuthorizedSharesAdjustment.daml +++ b/Test/daml/OpenCapTable/TestIssuerAuthorizedSharesAdjustment.daml @@ -28,17 +28,21 @@ mkIssuer issuer system_operator ctx = initial_shares_authorized = Some (OcfInitialSharesNumeric 1000.0) comments = [] +issuerAuthorizedSharesAdjustment : Text -> Text -> Decimal -> IssuerAuthorizedSharesAdjustmentOcfData +issuerAuthorizedSharesAdjustment adjustmentId issuerId sharesAuthorized = + IssuerAuthorizedSharesAdjustmentOcfData with + id = adjustmentId + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + issuer_id = issuerId + new_shares_authorized = sharesAuthorized + board_approval_date = None + stockholder_approval_date = None + comments = [] + createIssuerAuthorizedSharesAdjustment issuer capTableCid = submit issuer do exerciseCmd capTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateIssuerAuthorizedSharesAdjustment IssuerAuthorizedSharesAdjustmentOcfData with - id = "TX_ISSUER_AUTH_SHARES_ADJ_1" - date = DT.time (DA.date 2024 Jan 01) 0 0 0 - issuer_id = "ISSUER_1" - new_shares_authorized = 2000.0 - board_approval_date = None - stockholder_approval_date = None - comments = []] + creates = [CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_AUTH_SHARES_ADJ_1" "ISSUER_1" 2000.0)] edits = [] deletes = [] @@ -68,3 +72,22 @@ testIssuerAuthorizedSharesAdjustment_OptionalsSome = script do edits = [] deletes = [] pure () + +testIssuerAuthorizedSharesAdjustment_WrongIssuerIdCreateFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_BAD_ISSUER_ID" "OTHER_ISSUER" 2000.0)] + edits = [] + deletes = [] + +testIssuerAuthorizedSharesAdjustment_WrongIssuerIdEditFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + result <- createIssuerAuthorizedSharesAdjustment issuer cap_table + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_AUTH_SHARES_ADJ_1" "OTHER_ISSUER" 2000.0)] + deletes = [] diff --git a/Test/daml/OpenCapTable/TestStockAcceptance.daml b/Test/daml/OpenCapTable/TestStockAcceptance.daml index 88d7c17d..bbf45706 100644 --- a/Test/daml/OpenCapTable/TestStockAcceptance.daml +++ b/Test/daml/OpenCapTable/TestStockAcceptance.daml @@ -33,7 +33,7 @@ createStockIssuance issuer capTableCid = vesting_terms_id = None vestings = [] cost_basis = None - stock_legend_ids = ["L1"] + stock_legend_ids = [] issuance_type = None comments = []] edits = [] diff --git a/Test/daml/OpenCapTable/TestStockCancellation.daml b/Test/daml/OpenCapTable/TestStockCancellation.daml index e10b5cf1..3853f15e 100644 --- a/Test/daml/OpenCapTable/TestStockCancellation.daml +++ b/Test/daml/OpenCapTable/TestStockCancellation.daml @@ -56,7 +56,7 @@ createStockIssuance issuer capTableCid = vesting_terms_id = None vestings = [] cost_basis = None - stock_legend_ids = ["L1"] + stock_legend_ids = [] issuance_type = None comments = []] edits = [] diff --git a/Test/daml/OpenCapTable/TestStockClass.daml b/Test/daml/OpenCapTable/TestStockClass.daml index fbed6ef5..677643ef 100644 --- a/Test/daml/OpenCapTable/TestStockClass.daml +++ b/Test/daml/OpenCapTable/TestStockClass.daml @@ -11,25 +11,28 @@ import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT +commonStockClassData : StockClassOcfData +commonStockClassData = StockClassOcfData with + id = "SC_COMMON" + name = "Common" + class_type = OcfStockClassTypeCommon + default_id_prefix = "CS-" + initial_shares_authorized = OcfInitialSharesNumeric 1_000_000.0 + votes_per_share = 1.0 + seniority = 1.0 + board_approval_date = None + stockholder_approval_date = None + par_value = None + price_per_share = None + liquidation_preference_multiple = None + participation_cap_multiple = None + conversion_rights = [] + comments = [] + createStockClass issuer capTableCid = submit issuer do exerciseCmd capTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockClass StockClassOcfData with - id = "SC_COMMON" - name = "Common" - class_type = OcfStockClassTypeCommon - default_id_prefix = "CS-" - initial_shares_authorized = OcfInitialSharesNumeric 1_000_000.0 - votes_per_share = 1.0 - seniority = 1.0 - board_approval_date = None - stockholder_approval_date = None - par_value = None - price_per_share = None - liquidation_preference_multiple = None - participation_cap_multiple = None - conversion_rights = [] - comments = []] + creates = [CT.OcfCreateStockClass commonStockClassData] edits = [] deletes = [] @@ -65,22 +68,25 @@ testCreateStockClass_OptionalsSome = script do valuation_cap = None _ <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with - creates = [CT.OcfCreateStockClass StockClassOcfData with - id = "SC_PREFERRED" - name = "Preferred" - class_type = OcfStockClassTypePreferred - default_id_prefix = "PS-" - initial_shares_authorized = OcfInitialSharesNumeric 2_000_000.0 - votes_per_share = 1.0 - seniority = 2.0 - board_approval_date = Some (DT.time (DA.date 2024 Jan 1) 0 0 0) - stockholder_approval_date = Some (DT.time (DA.date 2024 Jan 15) 0 0 0) - par_value = Some (OcfMonetary with amount = 0.0001, currency = "USD") - price_per_share = Some (OcfMonetary with amount = 1.0, currency = "USD") - liquidation_preference_multiple = Some 1.0 - participation_cap_multiple = None - conversion_rights = [ convRight ] - comments = ["Preferred terms"]] + creates = + [ CT.OcfCreateStockClass commonStockClassData + , CT.OcfCreateStockClass (StockClassOcfData with + id = "SC_PREFERRED" + name = "Preferred" + class_type = OcfStockClassTypePreferred + default_id_prefix = "PS-" + initial_shares_authorized = OcfInitialSharesNumeric 2_000_000.0 + votes_per_share = 1.0 + seniority = 2.0 + board_approval_date = Some (DT.time (DA.date 2024 Jan 1) 0 0 0) + stockholder_approval_date = Some (DT.time (DA.date 2024 Jan 15) 0 0 0) + par_value = Some (OcfMonetary with amount = 0.0001, currency = "USD") + price_per_share = Some (OcfMonetary with amount = 1.0, currency = "USD") + liquidation_preference_multiple = Some 1.0 + participation_cap_multiple = None + conversion_rights = [ convRight ] + comments = ["Preferred terms"]) + ] edits = [] deletes = [] pure () @@ -107,22 +113,25 @@ testCreateStockClass_PpsBasedConversion = script do valuation_cap = None _ <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with - creates = [CT.OcfCreateStockClass StockClassOcfData with - id = "SC_PREFERRED_PPS" - name = "Preferred PPS" - class_type = OcfStockClassTypePreferred - default_id_prefix = "PP-" - initial_shares_authorized = OcfInitialSharesNumeric 1_000_000.0 - votes_per_share = 1.0 - seniority = 2.0 - board_approval_date = None - stockholder_approval_date = None - par_value = None - price_per_share = None - liquidation_preference_multiple = None - participation_cap_multiple = None - conversion_rights = [ convRight ] - comments = []] + creates = + [ CT.OcfCreateStockClass commonStockClassData + , CT.OcfCreateStockClass (StockClassOcfData with + id = "SC_PREFERRED_PPS" + name = "Preferred PPS" + class_type = OcfStockClassTypePreferred + default_id_prefix = "PP-" + initial_shares_authorized = OcfInitialSharesNumeric 1_000_000.0 + votes_per_share = 1.0 + seniority = 2.0 + board_approval_date = None + stockholder_approval_date = None + par_value = None + price_per_share = None + liquidation_preference_multiple = None + participation_cap_multiple = None + conversion_rights = [ convRight ] + comments = []) + ] edits = [] deletes = [] pure () @@ -148,22 +157,25 @@ testCreateStockClass_PpsBasedConversionFailsWithoutReferencePrice = script do valuation_cap = None submitMustFail issuer do exerciseCmd cap_table CT.UpdateCapTable with - creates = [CT.OcfCreateStockClass StockClassOcfData with - id = "SC_PREFERRED_PPS_FAIL" - name = "Preferred PPS Fail" - class_type = OcfStockClassTypePreferred - default_id_prefix = "PF-" - initial_shares_authorized = OcfInitialSharesNumeric 1_000_000.0 - votes_per_share = 1.0 - seniority = 2.0 - board_approval_date = None - stockholder_approval_date = None - par_value = None - price_per_share = None - liquidation_preference_multiple = None - participation_cap_multiple = None - conversion_rights = [ convRight ] - comments = []] + creates = + [ CT.OcfCreateStockClass commonStockClassData + , CT.OcfCreateStockClass (StockClassOcfData with + id = "SC_PREFERRED_PPS_FAIL" + name = "Preferred PPS Fail" + class_type = OcfStockClassTypePreferred + default_id_prefix = "PF-" + initial_shares_authorized = OcfInitialSharesNumeric 1_000_000.0 + votes_per_share = 1.0 + seniority = 2.0 + board_approval_date = None + stockholder_approval_date = None + par_value = None + price_per_share = None + liquidation_preference_multiple = None + participation_cap_multiple = None + conversion_rights = [ convRight ] + comments = []) + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml b/Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml new file mode 100644 index 00000000..b6e0186f --- /dev/null +++ b/Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml @@ -0,0 +1,91 @@ +module OpenCapTable.TestStockClassConversionRightReferences where + +import Fairmint.OpenCapTable.OCF.StockClass +import Fairmint.OpenCapTable.Types.Conversion +import Fairmint.OpenCapTable.Types.Stock +import qualified Fairmint.OpenCapTable.CapTable as CT +import OpenCapTable.HelpersTriggers (ocfTriggerElectiveAtWill) +import OpenCapTable.Setup +import OpenCapTable.TestHelpers +import Daml.Script + +stockClassConversionRightTo : Text -> OcfStockClassConversionRight +stockClassConversionRightTo stockClassId = OcfStockClassConversionRight with + ceiling_price_per_share = None + conversion_mechanism = OcfConversionMechanismRatioConversion + conversion_price = None + conversion_trigger = ocfTriggerElectiveAtWill + converts_to_future_round = None + converts_to_stock_class_id = stockClassId + custom_description = None + discount_rate = None + expires_at = None + floor_price_per_share = None + percent_of_capitalization = None + ratio = Some (OcfRatio with numerator = 1.0, denominator = 1.0) + reference_share_price = None + reference_valuation_price_per_share = None + type_ = "STOCK_CLASS_CONVERSION_RIGHT" + valuation_cap = None + +preferredStockClassWithConversionRight : Text -> Text -> StockClassOcfData +preferredStockClassWithConversionRight stockClassId targetStockClassId = + (defaultStockClass stockClassId) with + name = "Preferred Stock" + class_type = OcfStockClassTypePreferred + default_id_prefix = "PS-" + seniority = 2.0 + conversion_rights = [stockClassConversionRightTo targetStockClassId] + +testStockClassConversionRightTargetCreatedInSameBatchSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + _ <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass (preferredStockClassWithConversionRight "SC_PREFERRED_CONVERTS" "SC_COMMON_TARGET") + , CT.OcfCreateStockClass (defaultStockClass "SC_COMMON_TARGET") + ] + edits = [] + deletes = [] + pure () + +testStockClassConversionRightMissingTargetFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateStockClass (preferredStockClassWithConversionRight "SC_PREFERRED_MISSING_TARGET" "SC_MISSING_TARGET")] + edits = [] + deletes = [] + +testStockClassConversionRightEditMissingTargetFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + result <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateStockClass (defaultStockClass "SC_PREFERRED_EDIT_TARGET")] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockClass (preferredStockClassWithConversionRight "SC_PREFERRED_EDIT_TARGET" "SC_MISSING_EDIT_TARGET")] + deletes = [] + +testStockClassConversionRightTargetDeleteCannotLeaveDanglingReference = script do + TestOcp{issuer, cap_table} <- setupTestOcp + result <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass (preferredStockClassWithConversionRight "SC_PREFERRED_DANGLING_TARGET" "SC_COMMON_DELETE_TARGET") + , CT.OcfCreateStockClass (defaultStockClass "SC_COMMON_DELETE_TARGET") + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockClass "SC_COMMON_DELETE_TARGET"] diff --git a/Test/daml/OpenCapTable/TestStockIssuance.daml b/Test/daml/OpenCapTable/TestStockIssuance.daml index cfa5f86a..bcaec868 100644 --- a/Test/daml/OpenCapTable/TestStockIssuance.daml +++ b/Test/daml/OpenCapTable/TestStockIssuance.daml @@ -49,6 +49,16 @@ testStockIssuance_OptionalsSome = script do -- Setup prerequisites with specific stakeholder ID (cap_table', classId) <- setupStockClass issuer cap_table (cap_table'', stakeholderId) <- setupStakeholderWithId issuer cap_table' "SH-3" "Test Holder" + referenceResult <- submit issuer do + exerciseCmd cap_table'' CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockLegendTemplate (defaultStockLegendTemplate "L1") + , CT.OcfCreateStockLegendTemplate (defaultStockLegendTemplate "L2") + , CT.OcfCreateStockPlan (defaultStockPlan "PLAN_1" classId) + , CT.OcfCreateVestingTerms (defaultVestingTerms "VT_1") + ] + edits = [] + deletes = [] -- Create Stock Issuance with all optional fields populated let issuance = (defaultStockIssuance "TX_STOCK_ISS_SOME" "SEC-3" stakeholderId classId) with @@ -69,5 +79,5 @@ testStockIssuance_OptionalsSome = script do issuance_type = Some OcfStockIssuanceRSA comments = ["Initial issuance"] - _ <- createStockIssuanceWith issuer cap_table'' issuance + _ <- createStockIssuanceWith issuer referenceResult.updatedCapTableCid issuance pure () diff --git a/Test/daml/OpenCapTable/TestStockIssuanceReferences.daml b/Test/daml/OpenCapTable/TestStockIssuanceReferences.daml new file mode 100644 index 00000000..b9dd3c5e --- /dev/null +++ b/Test/daml/OpenCapTable/TestStockIssuanceReferences.daml @@ -0,0 +1,75 @@ +module OpenCapTable.TestStockIssuanceReferences where + +import qualified Fairmint.OpenCapTable.CapTable as CT +import OpenCapTable.Setup +import OpenCapTable.TestHelpers +import Daml.Script + +testStockIssuance_MissingLegendReferenceFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, classId, stakeholderId) <- setupForStockIssuance issuer cap_table + let issuance = (defaultStockIssuance "TX_MISSING_LEGEND" "SEC-MISSING-LEGEND" stakeholderId classId) with + stock_legend_ids = ["LEGEND_MISSING"] + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance issuance] + edits = [] + deletes = [] + +testStockIssuance_MissingStockPlanReferenceFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, classId, stakeholderId) <- setupForStockIssuance issuer cap_table + let issuance = (defaultStockIssuance "TX_MISSING_PLAN" "SEC-MISSING-PLAN" stakeholderId classId) with + stock_plan_id = Some "PLAN_MISSING" + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance issuance] + edits = [] + deletes = [] + +testStockIssuance_MissingVestingTermsReferenceFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, classId, stakeholderId) <- setupForStockIssuance issuer cap_table + let issuance = (defaultStockIssuance "TX_MISSING_VESTING" "SEC-MISSING-VESTING" stakeholderId classId) with + vesting_terms_id = Some "VT_MISSING" + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance issuance] + edits = [] + deletes = [] + +testStockIssuance_ReferencesCreatedInSameBatchSucceed = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, classId, stakeholderId) <- setupForStockIssuance issuer cap_table + let issuance = (defaultStockIssuance "TX_REFERENCES_VALID" "SEC-REFERENCES-VALID" stakeholderId classId) with + stock_legend_ids = ["LEGEND_VALID"] + stock_plan_id = Some "PLAN_VALID" + vesting_terms_id = Some "VT_VALID" + + _ <- submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockLegendTemplate (defaultStockLegendTemplate "LEGEND_VALID") + , CT.OcfCreateStockPlan (defaultStockPlan "PLAN_VALID" classId) + , CT.OcfCreateVestingTerms (defaultVestingTerms "VT_VALID") + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + pure () + +testStockIssuance_EditMissingReferenceFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, classId, stakeholderId) <- setupForStockIssuance issuer cap_table + let issuance = defaultStockIssuance "TX_EDIT_REFERENCE" "SEC-EDIT-REFERENCE" stakeholderId classId + capTableCid' <- createStockIssuanceWith issuer capTableCid issuance + let editedIssuance = issuance with stock_plan_id = Some "PLAN_MISSING_ON_EDIT" + + submitMustFail issuer do + exerciseCmd capTableCid' CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockIssuance editedIssuance] + deletes = [] diff --git a/Test/daml/OpenCapTable/TestStockPlanReferences.daml b/Test/daml/OpenCapTable/TestStockPlanReferences.daml new file mode 100644 index 00000000..db8f6f4d --- /dev/null +++ b/Test/daml/OpenCapTable/TestStockPlanReferences.daml @@ -0,0 +1,45 @@ +module OpenCapTable.TestStockPlanReferences where + +import qualified Fairmint.OpenCapTable.CapTable as CT +import OpenCapTable.Setup +import OpenCapTable.TestHelpers +import Daml.Script + +testStockPlan_MissingStockClassReferenceFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateStockPlan (defaultStockPlan "PLAN_MISSING_CLASS" "SC_MISSING")] + edits = [] + deletes = [] + +testStockPlan_StockClassCreatedInSameBatchSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + _ <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan (defaultStockPlan "PLAN_VALID" "SC_VALID") + , CT.OcfCreateStockClass (defaultStockClass "SC_VALID") + ] + edits = [] + deletes = [] + pure () + +testStockPlan_EditMissingStockClassReferenceFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, classId) <- setupStockClass issuer cap_table + let plan = defaultStockPlan "PLAN_EDIT_CLASS" classId + createResult <- submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockPlan plan] + edits = [] + deletes = [] + let editedPlan = plan with stock_class_ids = ["SC_MISSING_ON_EDIT"] + + submitMustFail issuer do + exerciseCmd createResult.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockPlan editedPlan] + deletes = [] diff --git a/Test/daml/OpenCapTable/TestStockPlanReturnToPool.daml b/Test/daml/OpenCapTable/TestStockPlanReturnToPool.daml index 3dc9004d..53da24bb 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReturnToPool.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReturnToPool.daml @@ -4,6 +4,7 @@ import Fairmint.OpenCapTable.OCF.StockPlan import Fairmint.OpenCapTable.OCF.StockPlanReturnToPool import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup +import OpenCapTable.TestHelpers (addDefaultStockClass) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -13,8 +14,9 @@ import Daml.Script -- Test: Create and archive return to pool (termination scenario) testCreateAndArchiveStockPlanReturnToPool = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp + capTableCid <- addDefaultStockClass issuer cap_table result <- submit issuer do - exerciseCmd cap_table CT.UpdateCapTable with + exerciseCmd capTableCid CT.UpdateCapTable with creates = [CT.OcfCreateStockPlan StockPlanOcfData with id = "PLAN_2021" plan_name = "2021 Equity Incentive Plan" @@ -45,8 +47,9 @@ testCreateAndArchiveStockPlanReturnToPool = script do -- Test: Create and archive with Archive choice (expiration scenario) testCreateAndArchiveStockPlanReturnToPoolExpiration = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp + capTableCid <- addDefaultStockClass issuer cap_table result <- submit issuer do - exerciseCmd cap_table CT.UpdateCapTable with + exerciseCmd capTableCid CT.UpdateCapTable with creates = [CT.OcfCreateStockPlan StockPlanOcfData with id = "PLAN_2015" plan_name = "2015 Stock Option Plan" diff --git a/Test/daml/OpenCapTable/TestStockRetraction.daml b/Test/daml/OpenCapTable/TestStockRetraction.daml index 0eb26a78..78be2584 100644 --- a/Test/daml/OpenCapTable/TestStockRetraction.daml +++ b/Test/daml/OpenCapTable/TestStockRetraction.daml @@ -57,7 +57,7 @@ createStockIssuance issuer capTableCid = vesting_terms_id = None vestings = [] cost_basis = None - stock_legend_ids = ["L1"] + stock_legend_ids = [] issuance_type = None comments = []] edits = [] diff --git a/Test/daml/OpenCapTable/TestValuationReferences.daml b/Test/daml/OpenCapTable/TestValuationReferences.daml new file mode 100644 index 00000000..d7ff6f17 --- /dev/null +++ b/Test/daml/OpenCapTable/TestValuationReferences.daml @@ -0,0 +1,76 @@ +module OpenCapTable.TestValuationReferences where + +import Fairmint.OpenCapTable.OCF.Valuation +import Fairmint.OpenCapTable.Types.Monetary +import qualified Fairmint.OpenCapTable.CapTable as CT +import OpenCapTable.Setup +import OpenCapTable.TestHelpers +import Daml.Script + +valuationForStockClass : Text -> Text -> ValuationOcfData +valuationForStockClass valuationId stockClassId = ValuationOcfData with + id = valuationId + effective_date = defaultTestDate + price_per_share = OcfMonetary with amount = 1.0, currency = "USD" + stock_class_id = stockClassId + valuation_type = OcfValuationType409A + comments = [] + board_approval_date = None + provider = None + stockholder_approval_date = None + +testValuationStockClassCreatedInSameBatchSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + _ <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass (defaultStockClass "SC_VALUATION_TARGET") + , CT.OcfCreateValuation (valuationForStockClass "VAL_TARGET_REF" "SC_VALUATION_TARGET") + ] + edits = [] + deletes = [] + pure () + +testValuationMissingStockClassFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateValuation (valuationForStockClass "VAL_MISSING_TARGET" "SC_MISSING_VALUATION_TARGET")] + edits = [] + deletes = [] + +testValuationEditMissingStockClassFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + result <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass (defaultStockClass "SC_VALUATION_EDIT_TARGET") + , CT.OcfCreateValuation (valuationForStockClass "VAL_EDIT_TARGET" "SC_VALUATION_EDIT_TARGET") + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditValuation (valuationForStockClass "VAL_EDIT_TARGET" "SC_MISSING_VALUATION_EDIT_TARGET")] + deletes = [] + +testValuationStockClassDeleteCannotLeaveDanglingReference = script do + TestOcp{issuer, cap_table} <- setupTestOcp + result <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass (defaultStockClass "SC_VALUATION_DELETE_TARGET") + , CT.OcfCreateValuation (valuationForStockClass "VAL_DANGLING_TARGET" "SC_VALUATION_DELETE_TARGET") + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockClass "SC_VALUATION_DELETE_TARGET"] diff --git a/Test/daml/OpenCapTable/TestVestingAcceleration.daml b/Test/daml/OpenCapTable/TestVestingAcceleration.daml index 75248cc7..99aa5f77 100644 --- a/Test/daml/OpenCapTable/TestVestingAcceleration.daml +++ b/Test/daml/OpenCapTable/TestVestingAcceleration.daml @@ -26,8 +26,8 @@ createEquityCompensationIssuance issuer capTableCid securityId quantity = stockholder_approval_date = None consideration_text = None security_law_exemptions = [] - stock_plan_id = Some "ESOP-2024" - stock_class_id = Some "COMMON" + stock_plan_id = None + stock_class_id = None compensation_type = OcfCompensationTypeOptionISO quantity = quantity base_price = None @@ -36,7 +36,7 @@ createEquityCompensationIssuance issuer capTableCid securityId quantity = expiration_date = Some (DT.time (DA.date 2034 Jan 01) 0 0 0) termination_exercise_windows = [] vestings = [] - vesting_terms_id = Some "standard-4yr-1yr-cliff" + vesting_terms_id = None comments = []] edits = [] deletes = [] diff --git a/Test/daml/OpenCapTable/TestVestingEvent.daml b/Test/daml/OpenCapTable/TestVestingEvent.daml index 77acc903..b1f77c51 100644 --- a/Test/daml/OpenCapTable/TestVestingEvent.daml +++ b/Test/daml/OpenCapTable/TestVestingEvent.daml @@ -26,8 +26,8 @@ createEquityCompensationIssuance issuer capTableCid securityId quantity = stockholder_approval_date = None consideration_text = None security_law_exemptions = [] - stock_plan_id = Some "ESOP-2024" - stock_class_id = Some "COMMON" + stock_plan_id = None + stock_class_id = None compensation_type = OcfCompensationTypeOptionISO quantity = quantity base_price = None @@ -36,7 +36,7 @@ createEquityCompensationIssuance issuer capTableCid securityId quantity = expiration_date = Some (DT.time (DA.date 2035 Jan 01) 0 0 0) termination_exercise_windows = [] vestings = [] - vesting_terms_id = Some "milestone-based" + vesting_terms_id = None comments = []] edits = [] deletes = [] diff --git a/Test/daml/OpenCapTable/TestVestingStart.daml b/Test/daml/OpenCapTable/TestVestingStart.daml index 20f23760..a9f1ade0 100644 --- a/Test/daml/OpenCapTable/TestVestingStart.daml +++ b/Test/daml/OpenCapTable/TestVestingStart.daml @@ -26,8 +26,8 @@ createEquityCompensationIssuance issuer capTableCid securityId quantity = stockholder_approval_date = None consideration_text = None security_law_exemptions = [] - stock_plan_id = Some "ESOP-2024" - stock_class_id = Some "COMMON" + stock_plan_id = None + stock_class_id = None compensation_type = OcfCompensationTypeOptionISO quantity = quantity base_price = None @@ -36,7 +36,7 @@ createEquityCompensationIssuance issuer capTableCid securityId quantity = expiration_date = Some (DT.time (DA.date 2035 Jul 01) 0 0 0) termination_exercise_windows = [] vestings = [] - vesting_terms_id = Some "standard-4yr-1yr-cliff" + vesting_terms_id = None comments = []] edits = [] deletes = [] @@ -95,8 +95,8 @@ testVestingStart_RSUGrant = script do stockholder_approval_date = None consideration_text = None security_law_exemptions = [] - stock_plan_id = Some "ESOP-2024" - stock_class_id = Some "COMMON" + stock_plan_id = None + stock_class_id = None compensation_type = OcfCompensationTypeRSU quantity = 25000.0 base_price = None @@ -105,7 +105,7 @@ testVestingStart_RSUGrant = script do expiration_date = None termination_exercise_windows = [] vestings = [] - vesting_terms_id = Some "quarterly-4yr" + vesting_terms_id = None comments = []] edits = [] deletes = [] diff --git a/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml b/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml new file mode 100644 index 00000000..61ee7158 --- /dev/null +++ b/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml @@ -0,0 +1,183 @@ +module OpenCapTable.TestWarrantConversionRightReferences where + +import Fairmint.OpenCapTable.OCF.WarrantIssuance +import Fairmint.OpenCapTable.Types.Conversion +import Fairmint.OpenCapTable.Types.Monetary +import qualified Fairmint.OpenCapTable.CapTable as CT +import OpenCapTable.Setup +import OpenCapTable.TestHelpers +import Daml.Script + +warrantConversionTriggerTo : Optional Text -> OcfConversionTrigger +warrantConversionTriggerTo stockClassId = OcfConversionTrigger with + conversion_right = OcfRightWarrant (OcfWarrantConversionRight with + conversion_mechanism = OcfWarrantMechanismFixedAmount with converts_to_quantity = 100.0 + converts_to_future_round = None + converts_to_stock_class_id = stockClassId + type_ = "WARRANT_CONVERSION_RIGHT") + end_date = None + nickname = None + start_date = None + trigger_condition = None + trigger_date = None + trigger_description = None + trigger_id = "TRIGGER-1" + type_ = OcfTriggerTypeTypeElectiveAtWill + +convertibleConversionTriggerTo : Optional Text -> OcfConversionTrigger +convertibleConversionTriggerTo stockClassId = OcfConversionTrigger with + conversion_right = OcfRightConvertible (OcfConvertibleConversionRight with + conversion_mechanism = OcfConvMechCustom (OcfCustomConversionMechanism with custom_conversion_description = "Custom") + converts_to_future_round = None + converts_to_stock_class_id = stockClassId + type_ = "CONVERTIBLE_CONVERSION_RIGHT") + end_date = None + nickname = None + start_date = None + trigger_condition = None + trigger_date = None + trigger_description = None + trigger_id = "TRIGGER-CONVERTIBLE" + type_ = OcfTriggerTypeTypeElectiveAtWill + +stockClassConversionTriggerTo : Text -> OcfConversionTrigger +stockClassConversionTriggerTo stockClassId = OcfConversionTrigger with + conversion_right = OcfRightStockClass (OcfStockClassConversionRight with + ceiling_price_per_share = None + conversion_mechanism = OcfConversionMechanismCustomConversion + conversion_price = None + conversion_trigger = warrantConversionTriggerTo None + converts_to_future_round = None + converts_to_stock_class_id = stockClassId + custom_description = Some "Custom" + discount_rate = None + expires_at = None + floor_price_per_share = None + percent_of_capitalization = None + ratio = None + reference_share_price = None + reference_valuation_price_per_share = None + type_ = "STOCK_CLASS_CONVERSION_RIGHT" + valuation_cap = None) + end_date = None + nickname = None + start_date = None + trigger_condition = None + trigger_date = None + trigger_description = None + trigger_id = "TRIGGER-STOCK-CLASS" + type_ = OcfTriggerTypeTypeElectiveAtWill + +warrantIssuanceWithExerciseTriggers : Text -> Text -> [OcfConversionTrigger] -> WarrantIssuanceOcfData +warrantIssuanceWithExerciseTriggers issuanceId securityId exerciseTriggers = WarrantIssuanceOcfData with + id = issuanceId + date = defaultTestDate + security_id = securityId + custom_id = "W-" <> issuanceId + stakeholder_id = defaultStakeholderId + board_approval_date = None + stockholder_approval_date = None + consideration_text = None + security_law_exemptions = [] + quantity = Some 100.0 + quantity_source = None + exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + purchase_price = OcfMonetary with amount = 0.0, currency = "USD" + exercise_triggers = exerciseTriggers + warrant_expiration_date = None + vesting_terms_id = None + vestings = [] + comments = [] + +warrantIssuanceWithConversionTarget : Text -> Text -> Optional Text -> WarrantIssuanceOcfData +warrantIssuanceWithConversionTarget issuanceId securityId stockClassId = + warrantIssuanceWithExerciseTriggers issuanceId securityId [warrantConversionTriggerTo stockClassId] + +testWarrantConversionRightTargetCreatedInSameBatchSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableWithStakeholder <- addDefaultStakeholder issuer cap_table + + _ <- submit issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass (defaultStockClass "SC_WARRANT_TARGET") + , CT.OcfCreateWarrantIssuance (warrantIssuanceWithConversionTarget "TX_WARRANT_TARGET_REF" "WARRANT-TARGET-REF" (Some "SC_WARRANT_TARGET")) + ] + edits = [] + deletes = [] + pure () + +testWarrantConversionRightMissingTargetFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableWithStakeholder <- addDefaultStakeholder issuer cap_table + + submitMustFail issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (warrantIssuanceWithConversionTarget "TX_WARRANT_MISSING_TARGET" "WARRANT-MISSING-TARGET" (Some "SC_MISSING_WARRANT_TARGET"))] + edits = [] + deletes = [] + +testWarrantConvertibleConversionRightMissingTargetFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableWithStakeholder <- addDefaultStakeholder issuer cap_table + + submitMustFail issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance + (warrantIssuanceWithExerciseTriggers + "TX_WARRANT_CONVERTIBLE_MISSING_TARGET" + "WARRANT-CONVERTIBLE-MISSING-TARGET" + [convertibleConversionTriggerTo (Some "SC_MISSING_WARRANT_CONVERTIBLE_TARGET")]) + ] + edits = [] + deletes = [] + +testWarrantStockClassConversionRightMissingTargetFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableWithStakeholder <- addDefaultStakeholder issuer cap_table + + submitMustFail issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance + (warrantIssuanceWithExerciseTriggers + "TX_WARRANT_STOCK_CLASS_MISSING_TARGET" + "WARRANT-STOCK-CLASS-MISSING-TARGET" + [stockClassConversionTriggerTo "SC_MISSING_WARRANT_STOCK_CLASS_TARGET"]) + ] + edits = [] + deletes = [] + +testWarrantConversionRightEditMissingTargetFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableWithStakeholder <- addDefaultStakeholder issuer cap_table + result <- submit issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (warrantIssuanceWithConversionTarget "TX_WARRANT_EDIT_TARGET" "WARRANT-EDIT-TARGET" None)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditWarrantIssuance (warrantIssuanceWithConversionTarget "TX_WARRANT_EDIT_TARGET" "WARRANT-EDIT-TARGET" (Some "SC_MISSING_WARRANT_EDIT_TARGET"))] + deletes = [] + +testWarrantConversionRightTargetDeleteCannotLeaveDanglingReference = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableWithStakeholder <- addDefaultStakeholder issuer cap_table + result <- submit issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass (defaultStockClass "SC_WARRANT_DELETE_TARGET") + , CT.OcfCreateWarrantIssuance (warrantIssuanceWithConversionTarget "TX_WARRANT_DANGLING_TARGET" "WARRANT-DANGLING-TARGET" (Some "SC_WARRANT_DELETE_TARGET")) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockClass "SC_WARRANT_DELETE_TARGET"] diff --git a/Test/daml/OpenCapTable/TestWarrantIssuance.daml b/Test/daml/OpenCapTable/TestWarrantIssuance.daml index fea17c9d..19d9a8a3 100644 --- a/Test/daml/OpenCapTable/TestWarrantIssuance.daml +++ b/Test/daml/OpenCapTable/TestWarrantIssuance.daml @@ -6,7 +6,7 @@ import Fairmint.OpenCapTable.Types.Stock (OcfQuantitySourceType(..), OcfSecurity import Fairmint.OpenCapTable.Types.Vesting (OcfVesting(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addDefaultStakeholder) +import OpenCapTable.TestHelpers (addDefaultStakeholder, defaultVestingTerms) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -57,25 +57,28 @@ testWarrantIssuance_OptionalsSome_ArraysNonEmpty = script do capTableCid <- addDefaultStakeholder issuer cap_table _ <- submit issuer do exerciseCmd capTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with - id = "TX_WARRANT_SOME" - date = DT.time (DA.date 2024 Feb 01) 0 0 0 - security_id = "WSEC-3" - custom_id = "W-3" - stakeholder_id = "SH-1" - board_approval_date = Some (DT.time (DA.date 2024 Jan 15) 0 0 0) - stockholder_approval_date = Some (DT.time (DA.date 2024 Jan 20) 0 0 0) - consideration_text = Some "Cash" - security_law_exemptions = [ OcfSecurityExemption with description = "Reg D", jurisdiction = "US-DE" ] - quantity = Some 100.0 - quantity_source = Some OcfQuantityInstrumentFixed - exercise_price = Some (OcfMonetary with amount = 1.5, currency = "USD") - purchase_price = OcfMonetary with amount = 0.1, currency = "USD" - exercise_triggers = [ ocfTriggerElectiveAtWill ] - warrant_expiration_date = Some (DT.time (DA.date 2025 Jan 01) 0 0 0) - vesting_terms_id = Some "VT_W" - vestings = [ OcfVesting with date = DT.time (DA.date 2024 Mar 01) 0 0 0, amount = 10.0 ] - comments = ["Issued"]] + creates = + [ CT.OcfCreateVestingTerms (defaultVestingTerms "VT_W") + , CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with + id = "TX_WARRANT_SOME" + date = DT.time (DA.date 2024 Feb 01) 0 0 0 + security_id = "WSEC-3" + custom_id = "W-3" + stakeholder_id = "SH-1" + board_approval_date = Some (DT.time (DA.date 2024 Jan 15) 0 0 0) + stockholder_approval_date = Some (DT.time (DA.date 2024 Jan 20) 0 0 0) + consideration_text = Some "Cash" + security_law_exemptions = [ OcfSecurityExemption with description = "Reg D", jurisdiction = "US-DE" ] + quantity = Some 100.0 + quantity_source = Some OcfQuantityInstrumentFixed + exercise_price = Some (OcfMonetary with amount = 1.5, currency = "USD") + purchase_price = OcfMonetary with amount = 0.1, currency = "USD" + exercise_triggers = [ ocfTriggerElectiveAtWill ] + warrant_expiration_date = Some (DT.time (DA.date 2025 Jan 01) 0 0 0) + vesting_terms_id = Some "VT_W" + vestings = [ OcfVesting with date = DT.time (DA.date 2024 Mar 01) 0 0 0, amount = 10.0 ] + comments = ["Issued"] + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestWarrantIssuanceReferences.daml b/Test/daml/OpenCapTable/TestWarrantIssuanceReferences.daml new file mode 100644 index 00000000..fb32b411 --- /dev/null +++ b/Test/daml/OpenCapTable/TestWarrantIssuanceReferences.daml @@ -0,0 +1,108 @@ +module OpenCapTable.TestWarrantIssuanceReferences where + +import qualified Fairmint.OpenCapTable.CapTable as CT +import Fairmint.OpenCapTable.OCF.WarrantIssuance (WarrantIssuanceOcfData(..)) +import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import Fairmint.OpenCapTable.Types.Stock (OcfQuantitySourceType(..)) +import OpenCapTable.HelpersTriggers (ocfTriggerElectiveAtWill) +import OpenCapTable.Setup +import OpenCapTable.TestHelpers +import Daml.Script + +warrantIssuance : Text -> Text -> Text -> WarrantIssuanceOcfData +warrantIssuance issuanceId securityId stakeholderId = WarrantIssuanceOcfData with + id = issuanceId + date = defaultTestDate + security_id = securityId + custom_id = "W-" <> issuanceId + stakeholder_id = stakeholderId + board_approval_date = None + stockholder_approval_date = None + consideration_text = None + security_law_exemptions = [] + quantity = Some 100.0 + quantity_source = Some OcfQuantityUnspecified + exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + purchase_price = OcfMonetary with amount = 0.0, currency = "USD" + exercise_triggers = [ocfTriggerElectiveAtWill] + warrant_expiration_date = None + vesting_terms_id = None + vestings = [] + comments = [] + +createReferencedWarrantIssuance : + Party -> + ContractId CT.CapTable -> + Script (ContractId CT.CapTable, WarrantIssuanceOcfData, Text) +createReferencedWarrantIssuance issuer capTableCid = do + (capTableWithStakeholder, stakeholderId) <- setupStakeholderWithId issuer capTableCid "SH_WARRANT_REF" "Warrant Holder" + let vestingTermsId = "VT_WARRANT_REF" + let issuance = (warrantIssuance "TX_WARRANT_REF" "WARRANT-REF" stakeholderId) with + vesting_terms_id = Some vestingTermsId + + result <- submit issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = + [ CT.OcfCreateVestingTerms (defaultVestingTerms vestingTermsId) + , CT.OcfCreateWarrantIssuance issuance + ] + edits = [] + deletes = [] + pure (result.updatedCapTableCid, issuance, vestingTermsId) + +testWarrantIssuance_MissingVestingTermsReferenceFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, stakeholderId) <- setupStakeholderWithId issuer cap_table "SH_WARRANT_MISSING_VESTING" "Warrant Holder" + let issuance = (warrantIssuance "TX_WARRANT_MISSING_VESTING" "WARRANT-MISSING-VESTING" stakeholderId) with + vesting_terms_id = Some "VT_MISSING_WARRANT" + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + +testWarrantIssuance_VestingTermsCreatedInSameBatchSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + _ <- createReferencedWarrantIssuance issuer cap_table + pure () + +testWarrantIssuance_EditMissingVestingTermsReferenceFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, issuance, _) <- createReferencedWarrantIssuance issuer cap_table + let editedIssuance = issuance with vesting_terms_id = Some "VT_MISSING_WARRANT_ON_EDIT" + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditWarrantIssuance editedIssuance] + deletes = [] + +testWarrantIssuance_EditVestingTermsCreatedInSameBatchSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, stakeholderId) <- setupStakeholderWithId issuer cap_table "SH_WARRANT_EDIT_BATCH" "Warrant Holder" + let issuance = warrantIssuance "TX_WARRANT_EDIT_BATCH" "WARRANT-EDIT-BATCH" stakeholderId + createResult <- submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + + let vestingTermsId = "VT_WARRANT_EDIT_BATCH" + let editedIssuance = issuance with vesting_terms_id = Some vestingTermsId + _ <- submit issuer do + exerciseCmd createResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingTerms (defaultVestingTerms vestingTermsId)] + edits = [CT.OcfEditWarrantIssuance editedIssuance] + deletes = [] + pure () + +testWarrantIssuance_DeleteReferencedVestingTermsFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, _, vestingTermsId) <- createReferencedWarrantIssuance issuer cap_table + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteVestingTerms vestingTermsId] diff --git a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar new file mode 100644 index 00000000..a2d58011 --- /dev/null +++ b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01150cc5fc53f4db275b32012bb0a759de740323663dd13bce9fce59f346bc05 +size 2503150 diff --git a/dars/dars.lock b/dars/dars.lock index 9dfea42f..16aa1a3d 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -17,6 +17,13 @@ "sdkVersion": "3.4.10", "uploadedAt": "2026-07-07T21:50:59.634Z", "networks": [] + }, + "OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar": { + "sha256": "01150cc5fc53f4db275b32012bb0a759de740323663dd13bce9fce59f346bc05", + "size": 2503150, + "sdkVersion": "3.4.10", + "uploadedAt": "2026-07-09T00:16:37.252Z", + "networks": [] } } } diff --git a/scripts/codegen/captable-config.yaml b/scripts/codegen/captable-config.yaml index 9ad67e1a..ed9a8f44 100644 --- a/scripts/codegen/captable-config.yaml +++ b/scripts/codegen/captable-config.yaml @@ -6,8 +6,8 @@ # (e.g., create Stakeholder then StockIssuance referencing that stakeholder in same batch) # # Tier 1: Base objects with no OCF dependencies -# Tier 2: Objects that depend on Tier 1 (stock class adjustments, issuer adjustments, stakeholder events) -# Tier 3: Issuances (reference stakeholders and/or stock classes) +# Tier 2: Objects that depend on Tier 1 objects +# Tier 3: Issuances and stock plan adjustments # Tier 4: All other transactions (depend on issuances via security_id) tiers: 1: @@ -15,7 +15,6 @@ tiers: - Stakeholder - StockClass - StockLegendTemplate - - StockPlan - VestingTerms 2: - IssuerAuthorizedSharesAdjustment @@ -24,13 +23,14 @@ tiers: - StockClassAuthorizedSharesAdjustment - StockClassConversionRatioAdjustment - StockClassSplit - - StockPlanPoolAdjustment - - StockPlanReturnToPool + - StockPlan - Valuation 3: - ConvertibleIssuance - EquityCompensationIssuance - StockIssuance + - StockPlanPoolAdjustment + - StockPlanReturnToPool - WarrantIssuance 4: - ConvertibleAcceptance @@ -68,14 +68,27 @@ tiers: # Validation syntax: # - Simple: "field_id" - looks up field_id in auto-derived map (e.g., stakeholder_id -> stakeholders) # - Explicit: "field:map_name" - looks up field in specified map (e.g., security_id:stock_issuances_by_security_id) +# - Array: "field_ids[]:map_name" - validates every ID in the list, if any +# - Optional: "field_id?:map_name" - validates the referenced ID only when present # # Security ID validations use the type-specific index maps (*_by_security_id) to validate # that the security exists in the correct issuance type. This ensures type safety - a # StockTransfer can only reference a stock security, not a warrant security. validations: + # Stock plans - validate referenced stock classes exist + StockPlan: ['stock_class_ids[]:stock_classes'] + # Issuances - validate stakeholder exists - StockIssuance: [stakeholder_id, stock_class_id] - EquityCompensationIssuance: [stakeholder_id] + StockIssuance: + [ + stakeholder_id, + stock_class_id, + 'stock_legend_ids[]:stock_legend_templates', + 'stock_plan_id?:stock_plans', + 'vesting_terms_id?:vesting_terms', + ] + EquityCompensationIssuance: + [stakeholder_id, 'stock_class_id?:stock_classes', 'stock_plan_id?:stock_plans', 'vesting_terms_id?:vesting_terms'] ConvertibleIssuance: [stakeholder_id] WarrantIssuance: [stakeholder_id] # Stakeholder events - validate stakeholder exists diff --git a/scripts/codegen/generate-captable.ts b/scripts/codegen/generate-captable.ts index 21c5f8ed..a03e93c4 100644 --- a/scripts/codegen/generate-captable.ts +++ b/scripts/codegen/generate-captable.ts @@ -33,6 +33,8 @@ interface Validation { thirdMap?: string; // For array field validation (validates each element in the array) is_array?: boolean; + // For Optional Text reference fields + is_optional?: boolean; } // Maps issuance type names to their security_id index map names @@ -43,6 +45,11 @@ const SECURITY_ID_INDEX_MAPS: Record = { WarrantIssuance: 'warrant_issuances_by_security_id', }; +const OBJECT_TYPE_CONSTRUCTOR_OVERRIDES: Record = { + StakeholderRelationshipChangeEvent: 'OcfObjCeStakeholderRelationship', + StakeholderStatusChangeEvent: 'OcfObjCeStakeholderStatus', +}; + interface TypeDef { name: string; module: string; @@ -54,6 +61,9 @@ interface TypeDef { validates_stock_class_initial_authorized_shares: boolean; validates_stock_class_authorized_adjustment: boolean; validates_stock_issuance_authorized_shares: boolean; + validates_issuer_id_reference: boolean; + is_document: boolean; + object_type_constructor: string; // For issuance types, the name of the security_id index map (e.g., 'stock_issuances_by_security_id') security_id_index_map: string | null; } @@ -99,6 +109,27 @@ function pluralize(str: string): string { return `${str}s`; } +function objectTypeConstructor(name: string): string { + if (OBJECT_TYPE_CONSTRUCTOR_OVERRIDES[name]) return OBJECT_TYPE_CONSTRUCTOR_OVERRIDES[name]; + if ( + /^(Convertible|EquityCompensation|Stock|Warrant).*(Acceptance|Cancellation|Conversion|Exercise|Issuance|Release|Repricing|Retraction|Transfer|Consolidation|Repurchase|Reissuance)$/.test( + name + ) || + name.startsWith('IssuerAuthorizedSharesAdjustment') || + name.startsWith('StockClassAuthorizedSharesAdjustment') || + name.startsWith('StockClassConversionRatioAdjustment') || + name.startsWith('StockClassSplit') || + name.startsWith('StockPlanPoolAdjustment') || + name.startsWith('StockPlanReturnToPool') || + name === 'VestingAcceleration' || + name === 'VestingEvent' || + name === 'VestingStart' + ) { + return `OcfObjTx${name}`; + } + return `OcfObj${name}`; +} + /** Parse a DAML file to find its main data type and field name */ function parseTypeInfo(filePath: string, typeName: string): { dataType: string; fieldName: string } | null { const content = fs.readFileSync(filePath, 'utf-8'); @@ -175,20 +206,27 @@ function discoverTypes(config: Config): TypeDef[] { map_field: pluralize(snakeName), tier, validations: validationFields.map((fieldSpec) => { - // Support four formats: + // Support five formats: // - Simple: "stakeholder_id" -> field: stakeholder_id, map: stakeholders (auto-derived) // - Explicit: "security_id:stock_issuances_by_security_id" -> field: security_id, map: stock_issuances_by_security_id // - OR-based: "security_id:map1|map2|map3" -> field: security_id, checks all three maps with OR logic // - Array: "security_ids[]:stock_issuances_by_security_id" -> field: security_ids, validates each element in array + // - Optional: "stock_plan_id?:stock_plans" -> field: stock_plan_id, validates only when Some if (fieldSpec.includes(':')) { const parts = fieldSpec.split(':'); let field = parts[0]; const mapSpec = parts[1]; + // Check for optional syntax (e.g., "stock_plan_id?") + const is_optional = field.endsWith('?'); + if (is_optional) { + field = field.slice(0, -1); // Remove ? suffix + } // Check for array syntax (e.g., "security_ids[]") const is_array = field.endsWith('[]'); if (is_array) { field = field.slice(0, -2); // Remove [] suffix } + const error = field.includes('security_id') ? 'Security not found' : `${toTitleCase(field)} not found`; // Check for OR-based multiple maps (e.g., "map1|map2|map3") if (mapSpec.includes('|')) { const maps = mapSpec.split('|'); @@ -201,19 +239,21 @@ function discoverTypes(config: Config): TypeDef[] { return { field, map: maps[0], // Default map (not used when or_maps is true) - error: `Security not found`, + error, or_maps: true, firstMap: maps[0], secondMap: maps[1], thirdMap: maps[2], is_array, + is_optional, }; } return { field, map: mapSpec, - error: `Security not found`, + error, is_array, + is_optional, }; } return { @@ -225,6 +265,9 @@ function discoverTypes(config: Config): TypeDef[] { validates_stock_class_initial_authorized_shares: name === 'StockClass', validates_stock_class_authorized_adjustment: name === 'StockClassAuthorizedSharesAdjustment', validates_stock_issuance_authorized_shares: name === 'StockIssuance', + validates_issuer_id_reference: name === 'IssuerAuthorizedSharesAdjustment', + is_document: name === 'Document', + object_type_constructor: objectTypeConstructor(name), security_id_index_map: SECURITY_ID_INDEX_MAPS[name] ?? null, }; diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 3619a830..6bea5c7f 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -18,12 +18,16 @@ import DA.Foldable (mapA_) import DA.Optional (isNone, isSome) import Fairmint.OpenCapTable.Types.Core (Context) +import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..)) import Fairmint.OpenCapTable.Types.Stock (OcfInitialSharesAuthorized(..)) import Fairmint.OpenCapTable.OCF.Issuer (Issuer(..), IssuerOcfData) +import Fairmint.OpenCapTable.OCF.Document (Document(..), DocumentOcfData, OcfObjectReference(..), OcfObjectType(..)) -- OCF Types {{#each types_alpha}} +{{#unless is_document}} import {{module}} ({{name}}(..), {{data_type}}) +{{/unless}} {{/each}} @@ -204,6 +208,116 @@ validateCurrentStockClassAuthorizedShares maps stockClassId excludedAdjustmentId limitOpt <- currentStockClassAuthorizedLimit maps stockClassId excludedAdjustmentId stockClassOverride additionalAdjustment assertWithinStockClassAuthorizedShares stockClassId currentIssued limitOpt +validateIssuerReference : ContractId Issuer -> Text -> Update () +validateIssuerReference issuerCid issuerId = do + issuerContract <- fetch issuerCid + assertMsg + ("Issuer ID mismatch: " <> issuerId <> " does not match cap table issuer " <> issuerContract.issuer_data.id) + (issuerId == issuerContract.issuer_data.id) + +validateObjectReference : CapTableMaps -> ContractId Issuer -> OcfObjectReference -> Update () +validateObjectReference maps issuerCid ref = case ref.object_type of + OcfObjIssuer -> do + validateIssuerReference issuerCid ref.object_id +{{#each types_alpha}} + {{object_type_constructor}} -> + assertMsg ("{{name}} not found: " <> ref.object_id) (isSome (Map.lookup ref.object_id maps.{{map_field}})) +{{/each}} + _ -> assertMsg ("Unsupported related object type " <> show ref.object_type <> " for object id " <> ref.object_id) False + +validateDocumentRelatedObjects : CapTableMaps -> ContractId Issuer -> Update () +validateDocumentRelatedObjects maps issuerCid = + mapA_ + (\documentCid -> do + document <- fetch documentCid + mapA_ (validateObjectReference maps issuerCid) document.document_data.related_objects) + (Map.values maps.documents) + +validateStockClassReference : Text -> CapTableMaps -> Text -> Update () +validateStockClassReference label maps stockClassId = + validateReference label maps.stock_classes stockClassId + +validateReference : Text -> Map Text a -> Text -> Update () +validateReference label refMap refId = + assertMsg + (label <> " not found: " <> refId) + (isSome (Map.lookup refId refMap)) + +validateOptionalReference : Text -> Map Text a -> Optional Text -> Update () +validateOptionalReference label refMap refId = case refId of + Some id -> validateReference label refMap id + None -> pure () + +validateOptionalStockClassReference : Text -> CapTableMaps -> Optional Text -> Update () +validateOptionalStockClassReference label maps stockClassId = case stockClassId of + Some id -> validateStockClassReference label maps id + None -> pure () + +validateStockClassConversionRightReferences : CapTableMaps -> Update () +validateStockClassConversionRightReferences maps = + mapA_ + (\stockClassCid -> do + stockClass <- fetch stockClassCid + mapA_ + (\conversionRight -> validateStockClassReference ("Stock class conversion target for " <> stockClass.stock_class_data.id) maps conversionRight.converts_to_stock_class_id) + stockClass.stock_class_data.conversion_rights) + (Map.values maps.stock_classes) + +validateAnyConversionRightStockClassReferences : Text -> CapTableMaps -> OcfAnyConversionRight -> Update () +validateAnyConversionRightStockClassReferences label maps conversionRight = case conversionRight of + OcfRightConvertible convertibleRight -> + validateOptionalStockClassReference (label <> " convertible conversion target") maps convertibleRight.converts_to_stock_class_id + OcfRightWarrant warrantRight -> + validateOptionalStockClassReference (label <> " warrant conversion target") maps warrantRight.converts_to_stock_class_id + OcfRightStockClass stockClassRight -> + validateStockClassReference (label <> " stock class conversion target") maps stockClassRight.converts_to_stock_class_id + +validateConvertibleConversionRightReferences : CapTableMaps -> Update () +validateConvertibleConversionRightReferences maps = + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + mapA_ + (\trigger -> validateOptionalStockClassReference "Convertible conversion target" maps trigger.conversion_right.converts_to_stock_class_id) + issuance.issuance_data.conversion_triggers) + (Map.values maps.convertible_issuances) + +validateWarrantConversionRightReferences : CapTableMaps -> Update () +validateWarrantConversionRightReferences maps = + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + mapA_ + (\trigger -> validateAnyConversionRightStockClassReferences ("Warrant exercise trigger for " <> issuance.issuance_data.id) maps trigger.conversion_right) + issuance.issuance_data.exercise_triggers) + (Map.values maps.warrant_issuances) + +validateValuationStockClassReferences : CapTableMaps -> Update () +validateValuationStockClassReferences maps = + mapA_ + (\valuationCid -> do + valuation <- fetch valuationCid + validateStockClassReference "Valuation stock class" maps valuation.valuation_data.stock_class_id) + (Map.values maps.valuations) + +validateEquityCompensationIssuanceReferences : CapTableMaps -> Update () +validateEquityCompensationIssuanceReferences maps = + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + validateOptionalStockClassReference "Equity compensation stock class" maps issuance.issuance_data.stock_class_id + validateOptionalReference "Equity compensation stock plan" maps.stock_plans issuance.issuance_data.stock_plan_id + validateOptionalReference "Equity compensation vesting terms" maps.vesting_terms issuance.issuance_data.vesting_terms_id) + (Map.values maps.equity_compensation_issuances) + +validateWarrantIssuanceReferences : CapTableMaps -> Update () +validateWarrantIssuanceReferences maps = + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + validateOptionalReference "Warrant vesting terms" maps.vesting_terms issuance.issuance_data.vesting_terms_id) + (Map.values maps.warrant_issuances) + -- | Get the processing tier for a create operation getCreateTier : OcfCreateData -> Int getCreateTier createData = case createData of @@ -212,16 +326,16 @@ getCreateTier createData = case createData of {{/each}} -- | Process a single create operation, returning updated maps and the created ContractId -processCreate : Context -> CapTableMaps -> OcfCreateData -> Update (CapTableMaps, OcfContractId) -processCreate ctx maps createData = case createData of +processCreate : Context -> ContractId Issuer -> CapTableMaps -> OcfCreateData -> Update (CapTableMaps, OcfContractId) +processCreate ctx issuerCid maps createData = case createData of {{#each types_tier}} {{> create-case}} {{/each}} -- | Process a single edit operation, returning updated maps and the edited ContractId -- Note: OcfEditIssuer is handled separately in UpdateCapTable since issuer is not a map -processEdit : Context -> CapTableMaps -> OcfEditData -> Update (CapTableMaps, OcfContractId) -processEdit ctx maps editData = case editData of +processEdit : Context -> ContractId Issuer -> CapTableMaps -> OcfEditData -> Update (CapTableMaps, OcfContractId) +processEdit ctx issuerCid maps editData = case editData of OcfEditIssuer _ -> error "OcfEditIssuer should be handled separately in UpdateCapTable" {{#each types_alpha}} {{> edit-case}} @@ -305,7 +419,7 @@ template CapTable -- Process regular edits (map-based entities) (mapsAfterEdits, regularEditedCids) <- foldlA (\(maps, cids) editData -> do - (newMaps, cid) <- processEdit context maps editData + (newMaps, cid) <- processEdit context issuer maps editData pure (newMaps, cids ++ [cid])) (mapsAfterDeletes, []) regularEdits @@ -332,11 +446,19 @@ template CapTable -- Process creates last (after deletes freed constraints) (finalMaps, createdCids) <- foldlA (\(maps, cids) createData -> do - (newMaps, cid) <- processCreate context maps createData + (newMaps, cid) <- processCreate context newIssuerCid maps createData pure (newMaps, cids ++ [cid])) (mapsAfterEdits, []) sortedCreates + validateDocumentRelatedObjects finalMaps newIssuerCid + validateStockClassConversionRightReferences finalMaps + validateConvertibleConversionRightReferences finalMaps + validateWarrantConversionRightReferences finalMaps + validateValuationStockClassReferences finalMaps + validateEquityCompensationIssuanceReferences finalMaps + validateWarrantIssuanceReferences finalMaps + -- Create new CapTable with updated maps and potentially new issuer newCapTableCid <- create this with issuer = newIssuerCid diff --git a/scripts/codegen/templates/loops/create-case.daml b/scripts/codegen/templates/loops/create-case.daml index 2af254ac..69e78119 100644 --- a/scripts/codegen/templates/loops/create-case.daml +++ b/scripts/codegen/templates/loops/create-case.daml @@ -6,6 +6,17 @@ OcfCreate{{name}} d -> do assertMsg "{{name}} security_id already exists" (isNone (Map.lookup d.security_id maps.{{security_id_index_map}})) {{/if}} {{#each validations}} +{{#if is_optional}} +{{#if or_maps}} + case d.{{field}} of + Some refId -> assertMsg ("{{error}}: " <> refId) (isSome (Map.lookup refId maps.{{firstMap}}) || isSome (Map.lookup refId maps.{{secondMap}}) || isSome (Map.lookup refId maps.{{thirdMap}})) + None -> pure () +{{else}} + case d.{{field}} of + Some refId -> assertMsg ("{{error}}: " <> refId) (isSome (Map.lookup refId maps.{{map}})) + None -> pure () +{{/if}} +{{else}} {{#if is_array}} -- Validate each element in array field {{field}} mapA_ (\secId -> assertMsg ("{{error}}: " <> secId) (isSome (Map.lookup secId maps.{{map}}))) d.{{field}} @@ -16,12 +27,16 @@ OcfCreate{{name}} d -> do assertMsg ("{{error}}: " <> d.{{field}}) (isSome (Map.lookup d.{{field}} maps.{{map}})) {{/if}} {{/if}} +{{/if}} {{/each}} {{#if validates_stock_issuance_authorized_shares}} validateStockIssuanceAuthorizedShares maps d None {{/if}} {{#if validates_stock_class_authorized_adjustment}} validateCurrentStockClassAuthorizedShares maps d.stock_class_id None None (Some d) +{{/if}} +{{#if validates_issuer_id_reference}} + validateIssuerReference issuerCid d.issuer_id {{/if}} cid <- create {{name}} with context = ctx, {{data_param}} = d {{#if security_id_index_map}} diff --git a/scripts/codegen/templates/loops/edit-case.daml b/scripts/codegen/templates/loops/edit-case.daml index 895b4cf1..cb52d612 100644 --- a/scripts/codegen/templates/loops/edit-case.daml +++ b/scripts/codegen/templates/loops/edit-case.daml @@ -8,6 +8,17 @@ OcfEdit{{name}} d -> do oldAdjustment <- fetch oldCid {{/if}} {{#each validations}} +{{#if is_optional}} +{{#if or_maps}} + case d.{{field}} of + Some refId -> assertMsg ("{{error}}: " <> refId) (isSome (Map.lookup refId maps.{{firstMap}}) || isSome (Map.lookup refId maps.{{secondMap}}) || isSome (Map.lookup refId maps.{{thirdMap}})) + None -> pure () +{{else}} + case d.{{field}} of + Some refId -> assertMsg ("{{error}}: " <> refId) (isSome (Map.lookup refId maps.{{map}})) + None -> pure () +{{/if}} +{{else}} {{#if is_array}} -- Validate each element in array field {{field}} mapA_ (\secId -> assertMsg ("{{error}}: " <> secId) (isSome (Map.lookup secId maps.{{map}}))) d.{{field}} @@ -18,6 +29,7 @@ OcfEdit{{name}} d -> do assertMsg ("{{error}}: " <> d.{{field}}) (isSome (Map.lookup d.{{field}} maps.{{map}})) {{/if}} {{/if}} +{{/if}} {{/each}} {{#if validates_stock_class_initial_authorized_shares}} validateCurrentStockClassAuthorizedShares maps d.id None (Some d) None @@ -26,6 +38,9 @@ OcfEdit{{name}} d -> do validateCurrentStockClassAuthorizedShares maps oldAdjustment.{{data_param}}.stock_class_id (Some oldAdjustment.{{data_param}}.id) None (Some d) validateCurrentStockClassAuthorizedShares maps d.stock_class_id (Some oldAdjustment.{{data_param}}.id) None (Some d) {{/if}} +{{#if validates_issuer_id_reference}} + validateIssuerReference issuerCid d.issuer_id +{{/if}} {{#if security_id_index_map}} -- Fetch old contract to get old security_id for index map cleanup oldContract <- fetch oldCid From ece751176b061915414b680ab4b8313d51618f16 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 23:17:21 -0400 Subject: [PATCH 02/27] Restack reference integrity on share ceiling --- dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar index c5627a61..d497befd 100644 --- a/dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:61e7cc2e7ff50f421b0cc9761d30c888e36bef7051dfda2a07c859cb9d7629e1 -size 2537476 +oid sha256:f9c1734d34a32f379cd7876199771feb807d4ca20e767507f5bd12d046050d56 +size 2538785 diff --git a/dars/dars.lock b/dars/dars.lock index f3bd5f33..6d7ba641 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,8 +26,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar": { - "sha256": "61e7cc2e7ff50f421b0cc9761d30c888e36bef7051dfda2a07c859cb9d7629e1", - "size": 2537476, + "sha256": "f9c1734d34a32f379cd7876199771feb807d4ca20e767507f5bd12d046050d56", + "size": 2538785, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T02:25:32.342Z", "networks": [] From 45c6352cf9b6a600046b9ee66f8630b32df23379 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 01:19:36 -0400 Subject: [PATCH 03/27] Address reference integrity review feedback --- Test/daml/OpenCapTable/TestDocument.daml | 17 +++ .../TestStockIssuanceReferences.daml | 106 ++++++++++++++++++ .../OpenCapTable/TestStockPlanReferences.daml | 35 ++++++ .../TestWarrantConversionRightReferences.daml | 45 ++++++++ .../0.0.4/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- scripts/codegen/captable-config.yaml | 16 +-- .../codegen/templates/CapTable.daml.template | 47 +++++++- 8 files changed, 259 insertions(+), 17 deletions(-) diff --git a/Test/daml/OpenCapTable/TestDocument.daml b/Test/daml/OpenCapTable/TestDocument.daml index 04625212..15fb202e 100644 --- a/Test/daml/OpenCapTable/TestDocument.daml +++ b/Test/daml/OpenCapTable/TestDocument.daml @@ -118,6 +118,23 @@ testDocumentRelatedObjectCreatedInSameBatchSucceeds = script do deletes = [] pure () +testDocumentPlanSecurityAliasReferencesEquityCompensationIssuance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithStakeholder, stakeholderId) <- setupStakeholderWithId issuer cap_table "SH_DOC_PLAN_SECURITY" "Plan Security Holder" + capTableWithIssuance <- addEquityCompensationIssuance issuer capTableWithStakeholder "EC_DOC_PLAN_SECURITY" "SEC_DOC_PLAN_SECURITY" stakeholderId + + _ <- submit issuer do + exerciseCmd capTableWithIssuance CT.UpdateCapTable with + creates = + [ CT.OcfCreateDocument + (documentData + "DOC_PLAN_SECURITY_REF" + [OcfObjectReference with object_type = OcfObjTxPlanSecurityIssuance, object_id = "EC_DOC_PLAN_SECURITY"]) + ] + edits = [] + deletes = [] + pure () + testDocumentEditMissingRelatedObjectFails = script do TestOcp{issuer, cap_table} <- setupTestOcp result <- submit issuer do diff --git a/Test/daml/OpenCapTable/TestStockIssuanceReferences.daml b/Test/daml/OpenCapTable/TestStockIssuanceReferences.daml index b9dd3c5e..228148d5 100644 --- a/Test/daml/OpenCapTable/TestStockIssuanceReferences.daml +++ b/Test/daml/OpenCapTable/TestStockIssuanceReferences.daml @@ -1,10 +1,51 @@ module OpenCapTable.TestStockIssuanceReferences where import qualified Fairmint.OpenCapTable.CapTable as CT +import Fairmint.OpenCapTable.OCF.StockIssuance (StockIssuanceOcfData) import OpenCapTable.Setup import OpenCapTable.TestHelpers import Daml.Script +data ReferencedStockIssuance = ReferencedStockIssuance with + cap_table_cid: ContractId CT.CapTable + issuance: StockIssuanceOcfData + stock_class_id: Text + stock_legend_id: Text + stock_plan_id: Text + vesting_terms_id: Text + +createReferencedStockIssuance : + Party -> + ContractId CT.CapTable -> + Script ReferencedStockIssuance +createReferencedStockIssuance issuer capTableCid = do + (capTableWithPrereqs, classId, stakeholderId) <- setupForStockIssuance issuer capTableCid + let legendId = "LEGEND_STOCK_REF" + let planId = "PLAN_STOCK_REF" + let vestingTermsId = "VT_STOCK_REF" + let issuance = (defaultStockIssuance "TX_STOCK_REF" "SEC-STOCK-REF" stakeholderId classId) with + stock_legend_ids = [legendId] + stock_plan_id = Some planId + vesting_terms_id = Some vestingTermsId + + result <- submit issuer do + exerciseCmd capTableWithPrereqs CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockLegendTemplate (defaultStockLegendTemplate legendId) + , CT.OcfCreateStockPlan (defaultStockPlan planId classId) + , CT.OcfCreateVestingTerms (defaultVestingTerms vestingTermsId) + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + pure ReferencedStockIssuance with + cap_table_cid = result.updatedCapTableCid + issuance = issuance + stock_class_id = classId + stock_legend_id = legendId + stock_plan_id = planId + vesting_terms_id = vestingTermsId + testStockIssuance_MissingLegendReferenceFails = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableCid, classId, stakeholderId) <- setupForStockIssuance issuer cap_table @@ -73,3 +114,68 @@ testStockIssuance_EditMissingReferenceFails = script do creates = [] edits = [CT.OcfEditStockIssuance editedIssuance] deletes = [] + +testStockIssuance_EditReferencesCreatedInSameBatchSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, classId, stakeholderId) <- setupForStockIssuance issuer cap_table + let issuance = defaultStockIssuance "TX_EDIT_BATCH_REFERENCE" "SEC-EDIT-BATCH-REFERENCE" stakeholderId classId + capTableWithIssuance <- createStockIssuanceWith issuer capTableCid issuance + + let legendId = "LEGEND_EDIT_BATCH" + let planId = "PLAN_EDIT_BATCH" + let vestingTermsId = "VT_EDIT_BATCH" + let editedIssuance = issuance with + stock_legend_ids = [legendId] + stock_plan_id = Some planId + vesting_terms_id = Some vestingTermsId + + _ <- submit issuer do + exerciseCmd capTableWithIssuance CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockLegendTemplate (defaultStockLegendTemplate legendId) + , CT.OcfCreateStockPlan (defaultStockPlan planId classId) + , CT.OcfCreateVestingTerms (defaultVestingTerms vestingTermsId) + ] + edits = [CT.OcfEditStockIssuance editedIssuance] + deletes = [] + pure () + +testStockIssuance_DeleteReferencedStockClassFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + ref <- createReferencedStockIssuance issuer cap_table + + submitMustFail issuer do + exerciseCmd ref.cap_table_cid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockClass ref.stock_class_id] + +testStockIssuance_DeleteReferencedStockLegendFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + ref <- createReferencedStockIssuance issuer cap_table + + submitMustFail issuer do + exerciseCmd ref.cap_table_cid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockLegendTemplate ref.stock_legend_id] + +testStockIssuance_DeleteReferencedStockPlanFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + ref <- createReferencedStockIssuance issuer cap_table + + submitMustFail issuer do + exerciseCmd ref.cap_table_cid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockPlan ref.stock_plan_id] + +testStockIssuance_DeleteReferencedVestingTermsFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + ref <- createReferencedStockIssuance issuer cap_table + + submitMustFail issuer do + exerciseCmd ref.cap_table_cid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteVestingTerms ref.vesting_terms_id] diff --git a/Test/daml/OpenCapTable/TestStockPlanReferences.daml b/Test/daml/OpenCapTable/TestStockPlanReferences.daml index db8f6f4d..177bf21e 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReferences.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReferences.daml @@ -43,3 +43,38 @@ testStockPlan_EditMissingStockClassReferenceFails = script do creates = [] edits = [CT.OcfEditStockPlan editedPlan] deletes = [] + +testStockPlan_EditStockClassCreatedInSameBatchSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, classId) <- setupStockClass issuer cap_table + let plan = defaultStockPlan "PLAN_EDIT_BATCH_CLASS" classId + createResult <- submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockPlan plan] + edits = [] + deletes = [] + + let newClassId = "SC_EDIT_BATCH" + let editedPlan = plan with stock_class_ids = [newClassId] + _ <- submit issuer do + exerciseCmd createResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockClass (defaultStockClass newClassId)] + edits = [CT.OcfEditStockPlan editedPlan] + deletes = [] + pure () + +testStockPlan_DeleteReferencedStockClassFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, classId) <- setupStockClass issuer cap_table + let plan = defaultStockPlan "PLAN_DELETE_CLASS" classId + createResult <- submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockPlan plan] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd createResult.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockClass classId] diff --git a/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml b/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml index 61ee7158..b6454029 100644 --- a/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml +++ b/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml @@ -68,6 +68,34 @@ stockClassConversionTriggerTo stockClassId = OcfConversionTrigger with trigger_id = "TRIGGER-STOCK-CLASS" type_ = OcfTriggerTypeTypeElectiveAtWill +stockClassConversionTriggerWithNestedWarrantTarget : Text -> Optional Text -> OcfConversionTrigger +stockClassConversionTriggerWithNestedWarrantTarget stockClassId nestedStockClassId = OcfConversionTrigger with + conversion_right = OcfRightStockClass (OcfStockClassConversionRight with + ceiling_price_per_share = None + conversion_mechanism = OcfConversionMechanismCustomConversion + conversion_price = None + conversion_trigger = warrantConversionTriggerTo nestedStockClassId + converts_to_future_round = None + converts_to_stock_class_id = stockClassId + custom_description = Some "Custom" + discount_rate = None + expires_at = None + floor_price_per_share = None + percent_of_capitalization = None + ratio = None + reference_share_price = None + reference_valuation_price_per_share = None + type_ = "STOCK_CLASS_CONVERSION_RIGHT" + valuation_cap = None) + end_date = None + nickname = None + start_date = None + trigger_condition = None + trigger_date = None + trigger_description = None + trigger_id = "TRIGGER-STOCK-CLASS-NESTED" + type_ = OcfTriggerTypeTypeElectiveAtWill + warrantIssuanceWithExerciseTriggers : Text -> Text -> [OcfConversionTrigger] -> WarrantIssuanceOcfData warrantIssuanceWithExerciseTriggers issuanceId securityId exerciseTriggers = WarrantIssuanceOcfData with id = issuanceId @@ -149,6 +177,23 @@ testWarrantStockClassConversionRightMissingTargetFails = script do edits = [] deletes = [] +testWarrantStockClassConversionRightNestedMissingTargetFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableWithStakeholder <- addDefaultStakeholder issuer cap_table + + submitMustFail issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass (defaultStockClass "SC_WARRANT_OUTER_TARGET") + , CT.OcfCreateWarrantIssuance + (warrantIssuanceWithExerciseTriggers + "TX_WARRANT_STOCK_CLASS_NESTED_MISSING_TARGET" + "WARRANT-STOCK-CLASS-NESTED-MISSING-TARGET" + [stockClassConversionTriggerWithNestedWarrantTarget "SC_WARRANT_OUTER_TARGET" (Some "SC_MISSING_WARRANT_NESTED_TARGET")]) + ] + edits = [] + deletes = [] + testWarrantConversionRightEditMissingTargetFails = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableWithStakeholder <- addDefaultStakeholder issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar index 02637cb6..fe09198d 100644 --- a/dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c583453aa3e21adba4ee499ada6ae18e10847fdf1e98e5ad35a4dc2afaeaac2e -size 2543833 +oid sha256:ab44477bcc7583b19345b04fc1e970bf18a270e4b2efe4cf93253efd01b38d57 +size 2545199 diff --git a/dars/dars.lock b/dars/dars.lock index 85d78145..a9170843 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,10 +26,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar": { - "sha256": "c583453aa3e21adba4ee499ada6ae18e10847fdf1e98e5ad35a4dc2afaeaac2e", - "size": 2543833, + "sha256": "ab44477bcc7583b19345b04fc1e970bf18a270e4b2efe4cf93253efd01b38d57", + "size": 2545199, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T02:25:32.342Z", + "uploadedAt": "2026-07-09T05:19:01.000Z", "networks": [] } } diff --git a/scripts/codegen/captable-config.yaml b/scripts/codegen/captable-config.yaml index ed9a8f44..72ab59a8 100644 --- a/scripts/codegen/captable-config.yaml +++ b/scripts/codegen/captable-config.yaml @@ -75,18 +75,12 @@ tiers: # that the security exists in the correct issuance type. This ensures type safety - a # StockTransfer can only reference a stock security, not a warrant security. validations: - # Stock plans - validate referenced stock classes exist - StockPlan: ['stock_class_ids[]:stock_classes'] + # Stock plans and stock issuances are validated against final CapTable maps so + # same-batch edits can point at objects created later in the batch. + StockPlan: [] - # Issuances - validate stakeholder exists - StockIssuance: - [ - stakeholder_id, - stock_class_id, - 'stock_legend_ids[]:stock_legend_templates', - 'stock_plan_id?:stock_plans', - 'vesting_terms_id?:vesting_terms', - ] + # Other issuance validations that can safely run before creates are processed. + StockIssuance: [] EquityCompensationIssuance: [stakeholder_id, 'stock_class_id?:stock_classes', 'stock_plan_id?:stock_plans', 'vesting_terms_id?:vesting_terms'] ConvertibleIssuance: [stakeholder_id] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 11e60c74..b52ca01f 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -447,6 +447,20 @@ validateObjectReference : CapTableMaps -> ContractId Issuer -> OcfObjectReferenc validateObjectReference maps issuerCid ref = case ref.object_type of OcfObjIssuer -> do validateIssuerReference issuerCid ref.object_id + OcfObjTxPlanSecurityAcceptance -> + assertMsg ("EquityCompensationAcceptance not found: " <> ref.object_id) (isSome (Map.lookup ref.object_id maps.equity_compensation_acceptances)) + OcfObjTxPlanSecurityCancellation -> + assertMsg ("EquityCompensationCancellation not found: " <> ref.object_id) (isSome (Map.lookup ref.object_id maps.equity_compensation_cancellations)) + OcfObjTxPlanSecurityExercise -> + assertMsg ("EquityCompensationExercise not found: " <> ref.object_id) (isSome (Map.lookup ref.object_id maps.equity_compensation_exercises)) + OcfObjTxPlanSecurityIssuance -> + assertMsg ("EquityCompensationIssuance not found: " <> ref.object_id) (isSome (Map.lookup ref.object_id maps.equity_compensation_issuances)) + OcfObjTxPlanSecurityRelease -> + assertMsg ("EquityCompensationRelease not found: " <> ref.object_id) (isSome (Map.lookup ref.object_id maps.equity_compensation_releases)) + OcfObjTxPlanSecurityRetraction -> + assertMsg ("EquityCompensationRetraction not found: " <> ref.object_id) (isSome (Map.lookup ref.object_id maps.equity_compensation_retractions)) + OcfObjTxPlanSecurityTransfer -> + assertMsg ("EquityCompensationTransfer not found: " <> ref.object_id) (isSome (Map.lookup ref.object_id maps.equity_compensation_transfers)) {{#each types_alpha}} {{object_type_constructor}} -> assertMsg ("{{name}} not found: " <> ref.object_id) (isSome (Map.lookup ref.object_id maps.{{map_field}})) @@ -471,6 +485,10 @@ validateReference label refMap refId = (label <> " not found: " <> refId) (isSome (Map.lookup refId refMap)) +validateStakeholderReference : Text -> CapTableMaps -> Text -> Update () +validateStakeholderReference label maps stakeholderId = + validateReference label maps.stakeholders stakeholderId + validateOptionalReference : Text -> Map Text a -> Optional Text -> Update () validateOptionalReference label refMap refId = case refId of Some id -> validateReference label refMap id @@ -497,8 +515,9 @@ validateAnyConversionRightStockClassReferences label maps conversionRight = case validateOptionalStockClassReference (label <> " convertible conversion target") maps convertibleRight.converts_to_stock_class_id OcfRightWarrant warrantRight -> validateOptionalStockClassReference (label <> " warrant conversion target") maps warrantRight.converts_to_stock_class_id - OcfRightStockClass stockClassRight -> + OcfRightStockClass stockClassRight -> do validateStockClassReference (label <> " stock class conversion target") maps stockClassRight.converts_to_stock_class_id + validateAnyConversionRightStockClassReferences (label <> " stock class nested trigger") maps stockClassRight.conversion_trigger.conversion_right validateConvertibleConversionRightReferences : CapTableMaps -> Update () validateConvertibleConversionRightReferences maps = @@ -528,6 +547,30 @@ validateValuationStockClassReferences maps = validateStockClassReference "Valuation stock class" maps valuation.valuation_data.stock_class_id) (Map.values maps.valuations) +validateStockPlanReferences : CapTableMaps -> Update () +validateStockPlanReferences maps = + mapA_ + (\stockPlanCid -> do + stockPlan <- fetch stockPlanCid + mapA_ + (validateStockClassReference ("Stock plan stock class for " <> stockPlan.plan_data.id) maps) + stockPlan.plan_data.stock_class_ids) + (Map.values maps.stock_plans) + +validateStockIssuanceReferences : CapTableMaps -> Update () +validateStockIssuanceReferences maps = + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + validateStakeholderReference "Stock issuance stakeholder" maps issuance.issuance_data.stakeholder_id + validateStockClassReference "Stock issuance stock class" maps issuance.issuance_data.stock_class_id + mapA_ + (validateReference "Stock issuance stock legend" maps.stock_legend_templates) + issuance.issuance_data.stock_legend_ids + validateOptionalReference "Stock issuance stock plan" maps.stock_plans issuance.issuance_data.stock_plan_id + validateOptionalReference "Stock issuance vesting terms" maps.vesting_terms issuance.issuance_data.vesting_terms_id) + (Map.values maps.stock_issuances) + validateEquityCompensationIssuanceReferences : CapTableMaps -> Update () validateEquityCompensationIssuanceReferences maps = mapA_ @@ -702,6 +745,8 @@ template CapTable validateConvertibleConversionRightReferences finalMaps validateWarrantConversionRightReferences finalMaps validateValuationStockClassReferences finalMaps + validateStockPlanReferences finalMaps + validateStockIssuanceReferences finalMaps validateEquityCompensationIssuanceReferences finalMaps validateWarrantIssuanceReferences finalMaps authorizedShareAdjustmentIndex <- stockClassAuthorizedAdjustmentIndex finalMaps From 37674c1371dc1cfeb56fed229be947d5cee6f85c Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 01:25:47 -0400 Subject: [PATCH 04/27] Allow equity compensation same-batch reference edits --- ...tEquityCompensationIssuanceReferences.daml | 29 +++++++++++++++++++ .../0.0.4/OpenCapTable-v34.dar | 4 +-- dars/dars.lock | 6 ++-- scripts/codegen/captable-config.yaml | 7 ++--- .../codegen/templates/CapTable.daml.template | 1 + 5 files changed, 38 insertions(+), 9 deletions(-) diff --git a/Test/daml/OpenCapTable/TestEquityCompensationIssuanceReferences.daml b/Test/daml/OpenCapTable/TestEquityCompensationIssuanceReferences.daml index 3c924486..86e56f63 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationIssuanceReferences.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationIssuanceReferences.daml @@ -110,6 +110,35 @@ testEquityCompensationIssuance_EditMissingReferenceFails = script do edits = [CT.OcfEditEquityCompensationIssuance editedIssuance] deletes = [] +testEquityCompensationIssuance_EditReferencesCreatedInSameBatchSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithStakeholder, stakeholderId) <- setupStakeholderWithId issuer cap_table "SH_EC_EDIT_BATCH" "Equity Holder" + let issuance = equityCompensationIssuance "TX_EC_EDIT_BATCH" "SEC_EC_EDIT_BATCH" stakeholderId + createResult <- submit issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance issuance] + edits = [] + deletes = [] + + let stockClassId = "SC_EC_EDIT_BATCH" + let stockPlanId = "PLAN_EC_EDIT_BATCH" + let vestingTermsId = "VT_EC_EDIT_BATCH" + let editedIssuance = issuance with + stock_class_id = Some stockClassId + stock_plan_id = Some stockPlanId + vesting_terms_id = Some vestingTermsId + + _ <- submit issuer do + exerciseCmd createResult.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass (defaultStockClass stockClassId) + , CT.OcfCreateStockPlan (defaultStockPlan stockPlanId stockClassId) + , CT.OcfCreateVestingTerms (defaultVestingTerms vestingTermsId) + ] + edits = [CT.OcfEditEquityCompensationIssuance editedIssuance] + deletes = [] + pure () + testEquityCompensationIssuance_DeleteReferencedStockClassFails = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableCid, _, stockClassId, _, _) <- createReferencedEquityCompensationIssuance issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar index fe09198d..5ade0c04 100644 --- a/dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab44477bcc7583b19345b04fc1e970bf18a270e4b2efe4cf93253efd01b38d57 -size 2545199 +oid sha256:a470cf1ab9c726ab53b8cac488eddec4f5f6e62c75f00d46f0f0005415e64e8f +size 2541785 diff --git a/dars/dars.lock b/dars/dars.lock index a9170843..a1b11654 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,10 +26,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar": { - "sha256": "ab44477bcc7583b19345b04fc1e970bf18a270e4b2efe4cf93253efd01b38d57", - "size": 2545199, + "sha256": "a470cf1ab9c726ab53b8cac488eddec4f5f6e62c75f00d46f0f0005415e64e8f", + "size": 2541785, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T05:19:01.000Z", + "uploadedAt": "2026-07-09T05:25:00.000Z", "networks": [] } } diff --git a/scripts/codegen/captable-config.yaml b/scripts/codegen/captable-config.yaml index 72ab59a8..91aa3d76 100644 --- a/scripts/codegen/captable-config.yaml +++ b/scripts/codegen/captable-config.yaml @@ -75,14 +75,13 @@ tiers: # that the security exists in the correct issuance type. This ensures type safety - a # StockTransfer can only reference a stock security, not a warrant security. validations: - # Stock plans and stock issuances are validated against final CapTable maps so + # These reference-rich objects are validated against final CapTable maps so # same-batch edits can point at objects created later in the batch. StockPlan: [] + StockIssuance: [] + EquityCompensationIssuance: [] # Other issuance validations that can safely run before creates are processed. - StockIssuance: [] - EquityCompensationIssuance: - [stakeholder_id, 'stock_class_id?:stock_classes', 'stock_plan_id?:stock_plans', 'vesting_terms_id?:vesting_terms'] ConvertibleIssuance: [stakeholder_id] WarrantIssuance: [stakeholder_id] # Stakeholder events - validate stakeholder exists diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index b52ca01f..9bc8af28 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -576,6 +576,7 @@ validateEquityCompensationIssuanceReferences maps = mapA_ (\issuanceCid -> do issuance <- fetch issuanceCid + validateStakeholderReference "Equity compensation stakeholder" maps issuance.issuance_data.stakeholder_id validateOptionalStockClassReference "Equity compensation stock class" maps issuance.issuance_data.stock_class_id validateOptionalReference "Equity compensation stock plan" maps.stock_plans issuance.issuance_data.stock_plan_id validateOptionalReference "Equity compensation vesting terms" maps.vesting_terms issuance.issuance_data.vesting_terms_id) From b2fcf46f49e60aa316ac024a63b8185d6c44f9b4 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 09:43:36 -0400 Subject: [PATCH 05/27] Address reference integrity review comments --- ...tConvertibleConversionRightReferences.daml | 15 +++++++ ...stStockClassConversionRightReferences.daml | 39 +++++++++++++++++++ .../TestWarrantIssuanceReferences.daml | 16 ++++++++ .../0.0.6/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +-- .../codegen/templates/CapTable.daml.template | 4 +- 6 files changed, 78 insertions(+), 6 deletions(-) diff --git a/Test/daml/OpenCapTable/TestConvertibleConversionRightReferences.daml b/Test/daml/OpenCapTable/TestConvertibleConversionRightReferences.daml index 8c665951..15a2fb73 100644 --- a/Test/daml/OpenCapTable/TestConvertibleConversionRightReferences.daml +++ b/Test/daml/OpenCapTable/TestConvertibleConversionRightReferences.daml @@ -105,3 +105,18 @@ testConvertibleConversionRightTargetDeleteCannotLeaveDanglingReference = script creates = [] edits = [] deletes = [CT.OcfDeleteStockClass "SC_CONVERTIBLE_DELETE_TARGET"] + +testConvertibleIssuanceStakeholderDeleteCannotLeaveDanglingReference = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableWithStakeholder <- addDefaultStakeholder issuer cap_table + result <- submit issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance (convertibleIssuanceWithConversionTarget "TX_CONVERTIBLE_DANGLING_STAKEHOLDER" "CONV-DANGLING-STAKEHOLDER" None)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStakeholder defaultStakeholderId] diff --git a/Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml b/Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml index b6e0186f..b735d713 100644 --- a/Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml +++ b/Test/daml/OpenCapTable/TestStockClassConversionRightReferences.daml @@ -28,6 +28,24 @@ stockClassConversionRightTo stockClassId = OcfStockClassConversionRight with type_ = "STOCK_CLASS_CONVERSION_RIGHT" valuation_cap = None +stockClassConversionRightToWithNestedWarrantTarget : Text -> Optional Text -> OcfStockClassConversionRight +stockClassConversionRightToWithNestedWarrantTarget stockClassId nestedStockClassId = + (stockClassConversionRightTo stockClassId) with + conversion_trigger = OcfConversionTrigger with + conversion_right = OcfRightWarrant (OcfWarrantConversionRight with + conversion_mechanism = OcfWarrantMechanismFixedAmount with converts_to_quantity = 100.0 + converts_to_future_round = None + converts_to_stock_class_id = nestedStockClassId + type_ = "WARRANT_CONVERSION_RIGHT") + end_date = None + nickname = None + start_date = None + trigger_condition = None + trigger_date = None + trigger_description = None + trigger_id = "TRIGGER-NESTED-WARRANT" + type_ = OcfTriggerTypeTypeElectiveAtWill + preferredStockClassWithConversionRight : Text -> Text -> StockClassOcfData preferredStockClassWithConversionRight stockClassId targetStockClassId = (defaultStockClass stockClassId) with @@ -37,6 +55,15 @@ preferredStockClassWithConversionRight stockClassId targetStockClassId = seniority = 2.0 conversion_rights = [stockClassConversionRightTo targetStockClassId] +preferredStockClassWithNestedConversionRight : Text -> Text -> Optional Text -> StockClassOcfData +preferredStockClassWithNestedConversionRight stockClassId targetStockClassId nestedTargetStockClassId = + (defaultStockClass stockClassId) with + name = "Preferred Stock" + class_type = OcfStockClassTypePreferred + default_id_prefix = "PS-" + seniority = 2.0 + conversion_rights = [stockClassConversionRightToWithNestedWarrantTarget targetStockClassId nestedTargetStockClassId] + testStockClassConversionRightTargetCreatedInSameBatchSucceeds = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -59,6 +86,18 @@ testStockClassConversionRightMissingTargetFails = script do edits = [] deletes = [] +testStockClassConversionRightNestedMissingTargetFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass (preferredStockClassWithNestedConversionRight "SC_PREFERRED_NESTED_MISSING" "SC_COMMON_OUTER_TARGET" (Some "SC_MISSING_NESTED_TARGET")) + , CT.OcfCreateStockClass (defaultStockClass "SC_COMMON_OUTER_TARGET") + ] + edits = [] + deletes = [] + testStockClassConversionRightEditMissingTargetFails = script do TestOcp{issuer, cap_table} <- setupTestOcp result <- submit issuer do diff --git a/Test/daml/OpenCapTable/TestWarrantIssuanceReferences.daml b/Test/daml/OpenCapTable/TestWarrantIssuanceReferences.daml index fb32b411..c70c321d 100644 --- a/Test/daml/OpenCapTable/TestWarrantIssuanceReferences.daml +++ b/Test/daml/OpenCapTable/TestWarrantIssuanceReferences.daml @@ -106,3 +106,19 @@ testWarrantIssuance_DeleteReferencedVestingTermsFails = script do creates = [] edits = [] deletes = [CT.OcfDeleteVestingTerms vestingTermsId] + +testWarrantIssuance_DeleteReferencedStakeholderFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithStakeholder, stakeholderId) <- setupStakeholderWithId issuer cap_table "SH_WARRANT_DELETE_STAKEHOLDER" "Warrant Holder" + let issuance = warrantIssuance "TX_WARRANT_DELETE_STAKEHOLDER" "WARRANT-DELETE-STAKEHOLDER" stakeholderId + result <- submit issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStakeholder stakeholderId] diff --git a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar index 68240e58..076a6788 100644 --- a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8f7341fc72c68ced7f234333c85dd2324a184b5e6ad0f2c9acd741b2d6e2c60 -size 2626593 +oid sha256:8cf8ca466103744bffcfd1990748ea01c6832d5daa88f1a0fc73d7e2d937132d +size 2658138 diff --git a/dars/dars.lock b/dars/dars.lock index ebbd8386..66efccf4 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,10 +40,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar": { - "sha256": "b8f7341fc72c68ced7f234333c85dd2324a184b5e6ad0f2c9acd741b2d6e2c60", - "size": 2626593, + "sha256": "8cf8ca466103744bffcfd1990748ea01c6832d5daa88f1a0fc73d7e2d937132d", + "size": 2658138, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T12:51:31.980Z", + "uploadedAt": "2026-07-09T13:43:07.082Z", "networks": [] } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index efbcc2ce..49cb1502 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -1163,7 +1163,7 @@ validateStockClassConversionRightReferences maps = (\stockClassCid -> do stockClass <- fetch stockClassCid mapA_ - (\conversionRight -> validateStockClassReference ("Stock class conversion target for " <> stockClass.stock_class_data.id) maps conversionRight.converts_to_stock_class_id) + (\conversionRight -> validateAnyConversionRightStockClassReferences ("Stock class conversion right for " <> stockClass.stock_class_data.id) maps (OcfRightStockClass conversionRight)) stockClass.stock_class_data.conversion_rights) (Map.values maps.stock_classes) @@ -1182,6 +1182,7 @@ validateConvertibleConversionRightReferences maps = mapA_ (\issuanceCid -> do issuance <- fetch issuanceCid + validateStakeholderReference "Convertible issuance stakeholder" maps issuance.issuance_data.stakeholder_id mapA_ (\trigger -> validateOptionalStockClassReference "Convertible conversion target" maps trigger.conversion_right.converts_to_stock_class_id) issuance.issuance_data.conversion_triggers) @@ -1192,6 +1193,7 @@ validateWarrantConversionRightReferences maps = mapA_ (\issuanceCid -> do issuance <- fetch issuanceCid + validateStakeholderReference "Warrant issuance stakeholder" maps issuance.issuance_data.stakeholder_id mapA_ (\trigger -> validateAnyConversionRightStockClassReferences ("Warrant exercise trigger for " <> issuance.issuance_data.id) maps trigger.conversion_right) issuance.issuance_data.exercise_triggers) From ead5716679c8b85ccfa69956b2427e87c0098ad9 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 10:40:48 -0400 Subject: [PATCH 06/27] Clarify final issuance reference validation --- .../codegen/templates/CapTable.daml.template | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index cf55075c..8a31b7f0 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -1190,18 +1190,24 @@ validateConvertibleConversionRightReferences maps = mapA_ (\issuanceCid -> do issuance <- fetch issuanceCid - validateStakeholderReference "Convertible issuance stakeholder" maps issuance.issuance_data.stakeholder_id mapA_ (\trigger -> validateOptionalStockClassReference "Convertible conversion target" maps trigger.conversion_right.converts_to_stock_class_id) issuance.issuance_data.conversion_triggers) (Map.values maps.convertible_issuances) +validateConvertibleIssuanceReferences : CapTableMaps -> Update () +validateConvertibleIssuanceReferences maps = + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + validateStakeholderReference "Convertible issuance stakeholder" maps issuance.issuance_data.stakeholder_id) + (Map.values maps.convertible_issuances) + validateWarrantConversionRightReferences : CapTableMaps -> Update () validateWarrantConversionRightReferences maps = mapA_ (\issuanceCid -> do issuance <- fetch issuanceCid - validateStakeholderReference "Warrant issuance stakeholder" maps issuance.issuance_data.stakeholder_id mapA_ (\trigger -> validateAnyConversionRightStockClassReferences ("Warrant exercise trigger for " <> issuance.issuance_data.id) maps trigger.conversion_right) issuance.issuance_data.exercise_triggers) @@ -1255,6 +1261,7 @@ validateWarrantIssuanceReferences maps = mapA_ (\issuanceCid -> do issuance <- fetch issuanceCid + validateStakeholderReference "Warrant issuance stakeholder" maps issuance.issuance_data.stakeholder_id validateOptionalReference "Warrant vesting terms" maps.vesting_terms issuance.issuance_data.vesting_terms_id) (Map.values maps.warrant_issuances) @@ -1788,11 +1795,11 @@ template CapTable assertMsg "Cannot edit issuer more than once per batch" (length issuerEdits <= 1) -- Process regular edits (map-based entities) - (mapsAfterEdits, regularEditedCids, regularInitialAuthorizedShareEdits) <- foldlA - (\(maps, cids, initialAuthorizedShareEdits) editData -> do - (newMaps, cid, editInitialAuthorizedShareEdits) <- processEdit context issuer maps editData - pure (newMaps, cids ++ [cid], initialAuthorizedShareEdits ++ editInitialAuthorizedShareEdits)) - (mapsAfterDeletes, [], []) + (mapsAfterEdits, regularEditedCids) <- foldlA + (\(maps, cids) editData -> do + (newMaps, cid, _) <- processEdit context issuer maps editData + pure (newMaps, cids ++ [cid])) + (mapsAfterDeletes, []) regularEdits -- Process issuer edit (if any) - special handling since it's a single reference @@ -1824,6 +1831,7 @@ template CapTable validateDocumentRelatedObjects finalMaps newIssuerCid validateStockClassConversionRightReferences finalMaps + validateConvertibleIssuanceReferences finalMaps validateConvertibleConversionRightReferences finalMaps validateWarrantConversionRightReferences finalMaps validateValuationStockClassReferences finalMaps From 3dc1d9c0069adf13cae3a2ff6a7a3950bc155189 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 10:58:31 -0400 Subject: [PATCH 07/27] Update reference integrity DAR backup --- dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar index 076a6788..9a34093b 100644 --- a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8cf8ca466103744bffcfd1990748ea01c6832d5daa88f1a0fc73d7e2d937132d -size 2658138 +oid sha256:b41dbe62fd45fd436c09567f2b27ce9bab3a0fb3bbfd15331f986df704471dd5 +size 2660801 diff --git a/dars/dars.lock b/dars/dars.lock index 01531ff7..aab462c2 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,8 +40,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar": { - "sha256": "8cf8ca466103744bffcfd1990748ea01c6832d5daa88f1a0fc73d7e2d937132d", - "size": 2658138, + "sha256": "b41dbe62fd45fd436c09567f2b27ce9bab3a0fb3bbfd15331f986df704471dd5", + "size": 2660801, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T13:43:07.082Z", "networks": [] From 3273664463383afd8e7081f26948ad6f9d0721be Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 11:23:07 -0400 Subject: [PATCH 08/27] Refresh reference integrity DAR after base update --- dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar index 9a34093b..4fe8b38c 100644 --- a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b41dbe62fd45fd436c09567f2b27ce9bab3a0fb3bbfd15331f986df704471dd5 -size 2660801 +oid sha256:c697f89a773d16dae60616997a13ed0f0169939496f233d4480c3109b9c25827 +size 2665949 diff --git a/dars/dars.lock b/dars/dars.lock index 5eae2af4..8857923a 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,8 +40,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar": { - "sha256": "b41dbe62fd45fd436c09567f2b27ce9bab3a0fb3bbfd15331f986df704471dd5", - "size": 2660801, + "sha256": "c697f89a773d16dae60616997a13ed0f0169939496f233d4480c3109b9c25827", + "size": 2665949, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T13:43:07.082Z", "networks": [] From e5f08cce854442719d091e12f9c63972ca1addc4 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 11:41:23 -0400 Subject: [PATCH 09/27] Refresh reference integrity DAR after ceiling update --- dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar index 4fe8b38c..d370f0e7 100644 --- a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c697f89a773d16dae60616997a13ed0f0169939496f233d4480c3109b9c25827 -size 2665949 +oid sha256:d4e6e1ef790acb1b319f3cfc4ee7dbb2948acb07568cdddc54414cb4b5bc33fc +size 2666478 diff --git a/dars/dars.lock b/dars/dars.lock index 0a12e663..eef04764 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,8 +40,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar": { - "sha256": "c697f89a773d16dae60616997a13ed0f0169939496f233d4480c3109b9c25827", - "size": 2665949, + "sha256": "d4e6e1ef790acb1b319f3cfc4ee7dbb2948acb07568cdddc54414cb4b5bc33fc", + "size": 2666478, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T13:43:07.082Z", "networks": [] From e862d6eb73536701023f0175b22d2f759eea9fa0 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 12:20:48 -0400 Subject: [PATCH 10/27] Revalidate configured references after deletes --- .../TestFinalReferenceValidation.daml | 134 ++++++++++++++++++ .../OpenCapTable/TestStockReissuance.daml | 9 +- .../0.0.6/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- scripts/codegen/generate-captable.ts | 2 + .../codegen/templates/CapTable.daml.template | 82 +++++++++++ 6 files changed, 230 insertions(+), 7 deletions(-) create mode 100644 Test/daml/OpenCapTable/TestFinalReferenceValidation.daml diff --git a/Test/daml/OpenCapTable/TestFinalReferenceValidation.daml b/Test/daml/OpenCapTable/TestFinalReferenceValidation.daml new file mode 100644 index 00000000..7e8f8d16 --- /dev/null +++ b/Test/daml/OpenCapTable/TestFinalReferenceValidation.daml @@ -0,0 +1,134 @@ +module OpenCapTable.TestFinalReferenceValidation where + +import qualified Fairmint.OpenCapTable.CapTable as CT +import Fairmint.OpenCapTable.OCF.ConvertibleConversion (ConvertibleConversionOcfData(..)) +import Fairmint.OpenCapTable.OCF.Stakeholder (OcfStakeholderStatusType(..)) +import Fairmint.OpenCapTable.OCF.StakeholderStatusChangeEvent (StakeholderStatusChangeEventOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockReissuance (StockReissuanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockTransfer (StockTransferOcfData(..)) +import Fairmint.OpenCapTable.OCF.WarrantExercise (WarrantExerciseOcfData(..)) +import OpenCapTable.Setup +import OpenCapTable.TestHelpers + ( addDefaultConvertibleIssuance + , addDefaultStakeholder + , addDefaultWarrantIssuance + , addPrerequisites + , addStockIssuance + ) +import qualified DA.Date as DA +import DA.Date (Month(..)) +import qualified DA.Time as DT +import Daml.Script + +defaultReferenceTestTime : Time +defaultReferenceTestTime = DT.time (DA.date 2024 Jan 01) 0 0 0 + +stockTransferData : Text -> Text -> StockTransferOcfData +stockTransferData transferId securityId = StockTransferOcfData with + id = transferId + date = defaultReferenceTestTime + security_id = securityId + quantity = 10.0 + resulting_security_ids = ["SEC-TRANSFER-RESULT"] + balance_security_id = Some "SEC-TRANSFER-BALANCE" + consideration_text = None + comments = [] + +testFinalReferenceValidation_DeleteReferencedStockIssuanceFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + capTable <- addStockIssuance issuer capTable "TX-STOCK-FINAL-REF" "SEC-FINAL-REF" "SH-1" "SC_COMMON" + result <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateStockTransfer (stockTransferData "TX-STOCK-TRANSFER-FINAL-REF" "SEC-FINAL-REF")] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockIssuance "TX-STOCK-FINAL-REF"] + pure () + +testFinalReferenceValidation_DeleteReferencedStakeholderFromStatusEventFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addDefaultStakeholder issuer cap_table + result <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateStakeholderStatusChangeEvent StakeholderStatusChangeEventOcfData with + id = "CE-STATUS-FINAL-REF" + date = defaultReferenceTestTime + stakeholder_id = "SH-1" + new_status = OcfStakeholderStatusActive + comments = []] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStakeholder "SH-1"] + pure () + +testFinalReferenceValidation_ConvertibleConversionTriggerMustExist = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addDefaultStakeholder issuer cap_table + capTable <- addDefaultConvertibleIssuance issuer capTable + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with + id = "TX-CONV-MISSING-TRIGGER" + date = defaultReferenceTestTime + security_id = "SEC-CV-1" + trigger_id = "TRIGGER-MISSING" + reason_text = "Missing trigger reference" + resulting_security_ids = ["SEC-CONV-RESULT"] + balance_security_id = None + capitalization_definition = None + quantity_converted = None + comments = []] + edits = [] + deletes = [] + pure () + +testFinalReferenceValidation_WarrantExerciseTriggerMustExist = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + capTable <- addDefaultWarrantIssuance issuer capTable + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantExercise WarrantExerciseOcfData with + id = "TX-WARRANT-MISSING-TRIGGER" + date = defaultReferenceTestTime + security_id = "SEC-WR-1" + trigger_id = "TRIGGER-MISSING" + quantity = Some 10.0 + resulting_security_ids = ["SEC-WARRANT-RESULT"] + consideration_text = None + comments = []] + edits = [] + deletes = [] + pure () + +testFinalReferenceValidation_StockReissuanceSplitMustExist = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + capTable <- addStockIssuance issuer capTable "TX-STOCK-SPLIT-REF" "SEC-SPLIT-REF" "SH-1" "SC_COMMON" + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateStockReissuance StockReissuanceOcfData with + id = "TX-REISSUANCE-MISSING-SPLIT" + date = defaultReferenceTestTime + security_id = "SEC-SPLIT-REF" + resulting_security_ids = ["SEC-SPLIT-REF-NEW"] + split_transaction_id = Some "TX-MISSING-SPLIT" + reason_text = Some "Split certificate replacement" + comments = []] + edits = [] + deletes = [] + pure () diff --git a/Test/daml/OpenCapTable/TestStockReissuance.daml b/Test/daml/OpenCapTable/TestStockReissuance.daml index 50304f51..11ab6716 100644 --- a/Test/daml/OpenCapTable/TestStockReissuance.daml +++ b/Test/daml/OpenCapTable/TestStockReissuance.daml @@ -5,7 +5,7 @@ import Fairmint.OpenCapTable.OCF.StockReissuance import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addPrerequisites) +import OpenCapTable.TestHelpers (addPrerequisites, stockClassSplit) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -65,8 +65,13 @@ testStockReissuance_StockSplit = script do capTableCid <- addPrerequisites issuer cap_table let securityId = "SSEC-002" result <- createStockIssuance issuer capTableCid securityId 25000.0 - _ <- submit issuer do + splitResult <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassSplit (stockClassSplit "TX_STOCK_CLASS_SPLIT_1" "SC_COMMON" 2.0 1.0)] + edits = [] + deletes = [] + _ <- submit issuer do + exerciseCmd splitResult.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateStockReissuance StockReissuanceOcfData with id = "TX_STOCK_REISSUANCE_2" date = DT.time (DA.date 2025 Jul 01) 0 0 0 diff --git a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar index d370f0e7..38f1270d 100644 --- a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4e6e1ef790acb1b319f3cfc4ee7dbb2948acb07568cdddc54414cb4b5bc33fc -size 2666478 +oid sha256:69c1062989be8348fd7a06463a4e8a3d7b3424cdd1faefbceb8bf950484984ac +size 2709879 diff --git a/dars/dars.lock b/dars/dars.lock index eef04764..b2e4aae8 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,10 +40,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar": { - "sha256": "d4e6e1ef790acb1b319f3cfc4ee7dbb2948acb07568cdddc54414cb4b5bc33fc", - "size": 2666478, + "sha256": "69c1062989be8348fd7a06463a4e8a3d7b3424cdd1faefbceb8bf950484984ac", + "size": 2709879, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T13:43:07.082Z", + "uploadedAt": "2026-07-09T16:19:24Z", "networks": [] } } diff --git a/scripts/codegen/generate-captable.ts b/scripts/codegen/generate-captable.ts index 5079acec..dc67074b 100644 --- a/scripts/codegen/generate-captable.ts +++ b/scripts/codegen/generate-captable.ts @@ -58,6 +58,7 @@ interface TypeDef { map_field: string; tier: number; validations: Validation[]; + has_validations: boolean; validates_stock_class_initial_authorized_shares: boolean; validates_issuer_id_reference: boolean; is_document: boolean; @@ -260,6 +261,7 @@ function discoverTypes(config: Config): TypeDef[] { error: `${toTitleCase(fieldSpec)} not found`, }; }), + has_validations: validationFields.length > 0, validates_stock_class_initial_authorized_shares: name === 'StockClass', validates_issuer_id_reference: name === 'IssuerAuthorizedSharesAdjustment', is_document: name === 'Document', diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index a5f31ea8..b93c8c77 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -1265,6 +1265,84 @@ validateWarrantIssuanceReferences maps = validateOptionalReference "Warrant vesting terms" maps.vesting_terms issuance.issuance_data.vesting_terms_id) (Map.values maps.warrant_issuances) +validateConvertibleConversionTriggerReferences : CapTableMaps -> Update () +validateConvertibleConversionTriggerReferences maps = + mapA_ + (\conversionCid -> do + conversion <- fetch conversionCid + case Map.lookup conversion.conversion_data.security_id maps.convertible_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertMsg + ("Convertible conversion trigger not found: " <> conversion.conversion_data.trigger_id) + (conversion.conversion_data.trigger_id `elem` map (\trigger -> trigger.trigger_id) issuance.issuance_data.conversion_triggers) + None -> assertMsg ("Security not found: " <> conversion.conversion_data.security_id) False) + (Map.values maps.convertible_conversions) + +validateWarrantExerciseTriggerReferences : CapTableMaps -> Update () +validateWarrantExerciseTriggerReferences maps = + mapA_ + (\exerciseCid -> do + exercise <- fetch exerciseCid + case Map.lookup exercise.exercise_data.security_id maps.warrant_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertMsg + ("Warrant exercise trigger not found: " <> exercise.exercise_data.trigger_id) + (exercise.exercise_data.trigger_id `elem` map (\trigger -> trigger.trigger_id) issuance.issuance_data.exercise_triggers) + None -> assertMsg ("Security not found: " <> exercise.exercise_data.security_id) False) + (Map.values maps.warrant_exercises) + +validateStockReissuanceSplitReferences : CapTableMaps -> Update () +validateStockReissuanceSplitReferences maps = + mapA_ + (\reissuanceCid -> do + reissuance <- fetch reissuanceCid + validateOptionalReference "Stock reissuance split transaction" maps.stock_class_splits reissuance.reissuance_data.split_transaction_id) + (Map.values maps.stock_reissuances) + +{{#each types_alpha}} +{{#if has_validations}} +validate{{name}}ConfiguredReferences : CapTableMaps -> Update () +validate{{name}}ConfiguredReferences maps = + mapA_ + (\contractCid -> do + contract <- fetch contractCid +{{#each validations}} +{{#if is_optional}} +{{#if or_maps}} + case contract.{{../data_param}}.{{field}} of + Some refId -> assertMsg ("{{error}}: " <> refId) (isSome (Map.lookup refId maps.{{firstMap}}) || isSome (Map.lookup refId maps.{{secondMap}}) || isSome (Map.lookup refId maps.{{thirdMap}})) + None -> pure () +{{else}} + case contract.{{../data_param}}.{{field}} of + Some refId -> assertMsg ("{{error}}: " <> refId) (isSome (Map.lookup refId maps.{{map}})) + None -> pure () +{{/if}} +{{else}} +{{#if is_array}} + mapA_ (\refId -> assertMsg ("{{error}}: " <> refId) (isSome (Map.lookup refId maps.{{map}}))) contract.{{../data_param}}.{{field}} +{{else}} +{{#if or_maps}} + assertMsg ("{{error}}: " <> contract.{{../data_param}}.{{field}}) (isSome (Map.lookup contract.{{../data_param}}.{{field}} maps.{{firstMap}}) || isSome (Map.lookup contract.{{../data_param}}.{{field}} maps.{{secondMap}}) || isSome (Map.lookup contract.{{../data_param}}.{{field}} maps.{{thirdMap}})) +{{else}} + assertMsg ("{{error}}: " <> contract.{{../data_param}}.{{field}}) (isSome (Map.lookup contract.{{../data_param}}.{{field}} maps.{{map}})) +{{/if}} +{{/if}} +{{/if}} +{{/each}} ) + (Map.values maps.{{map_field}}) + +{{/if}} +{{/each}} +validateConfiguredReferences : CapTableMaps -> Update () +validateConfiguredReferences maps = do +{{#each types_alpha}} +{{#if has_validations}} + validate{{name}}ConfiguredReferences maps +{{/if}} +{{/each}} + addQuantityById : Text -> Decimal -> Map Text Decimal -> Map Text Decimal addQuantityById key quantity totals = let current = fromOptional 0.0 (Map.lookup key totals) @@ -1895,6 +1973,10 @@ template CapTable validateStockIssuanceReferences finalMaps validateEquityCompensationIssuanceReferences finalMaps validateWarrantIssuanceReferences finalMaps + validateConvertibleConversionTriggerReferences finalMaps + validateWarrantExerciseTriggerReferences finalMaps + validateStockReissuanceSplitReferences finalMaps + validateConfiguredReferences finalMaps authorizedShareAdjustmentIndex <- stockClassAuthorizedAdjustmentIndex finalMaps validateAuthorizedShareCeilings finalMaps From 25df21ea8ad3b0e69c7ad78f065ee8d2a7ac8355 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 13:44:50 -0400 Subject: [PATCH 11/27] Refresh reference integrity after ceiling update --- dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar index 38f1270d..9ca9142b 100644 --- a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69c1062989be8348fd7a06463a4e8a3d7b3424cdd1faefbceb8bf950484984ac -size 2709879 +oid sha256:1761c9218b26072a19f61e9cd80acbe714f67bd7c4f2865f43259d519788d209 +size 2732290 diff --git a/dars/dars.lock b/dars/dars.lock index d2f4a563..f0ffd26c 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,10 +40,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar": { - "sha256": "69c1062989be8348fd7a06463a4e8a3d7b3424cdd1faefbceb8bf950484984ac", - "size": 2709879, + "sha256": "1761c9218b26072a19f61e9cd80acbe714f67bd7c4f2865f43259d519788d209", + "size": 2732290, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T16:19:24Z", + "uploadedAt": "2026-07-09T17:43:51Z", "networks": [] } } From ef5a898c0348c8b25bd5b8a6d43a62add3576ac9 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 14:06:44 -0400 Subject: [PATCH 12/27] Validate stock plan class membership --- .../TestStockIssuanceReferences.daml | 36 +++++++++++++++++++ .../0.0.6/OpenCapTable-v34.dar | 4 +-- dars/dars.lock | 6 ++-- .../codegen/templates/CapTable.daml.template | 13 ++++++- 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockIssuanceReferences.daml b/Test/daml/OpenCapTable/TestStockIssuanceReferences.daml index 228148d5..81799a02 100644 --- a/Test/daml/OpenCapTable/TestStockIssuanceReferences.daml +++ b/Test/daml/OpenCapTable/TestStockIssuanceReferences.daml @@ -70,6 +70,24 @@ testStockIssuance_MissingStockPlanReferenceFails = script do edits = [] deletes = [] +testStockIssuance_StockPlanMustIncludeIssuanceClassFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, classId, stakeholderId) <- setupForStockIssuance issuer cap_table + let otherClassId = "SC_OTHER_PLAN_CLASS" + let planId = "PLAN_OTHER_CLASS" + let issuance = (defaultStockIssuance "TX_PLAN_WRONG_CLASS" "SEC-PLAN-WRONG-CLASS" stakeholderId classId) with + stock_plan_id = Some planId + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass (defaultStockClass otherClassId) + , CT.OcfCreateStockPlan (defaultStockPlan planId otherClassId) + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + testStockIssuance_MissingVestingTermsReferenceFails = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableCid, classId, stakeholderId) <- setupForStockIssuance issuer cap_table @@ -115,6 +133,24 @@ testStockIssuance_EditMissingReferenceFails = script do edits = [CT.OcfEditStockIssuance editedIssuance] deletes = [] +testStockIssuance_EditStockPlanMustIncludeIssuanceClassFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, classId, stakeholderId) <- setupForStockIssuance issuer cap_table + let issuance = defaultStockIssuance "TX_EDIT_PLAN_WRONG_CLASS" "SEC-EDIT-PLAN-WRONG-CLASS" stakeholderId classId + capTableWithIssuance <- createStockIssuanceWith issuer capTableCid issuance + let otherClassId = "SC_EDIT_OTHER_PLAN_CLASS" + let planId = "PLAN_EDIT_OTHER_CLASS" + let editedIssuance = issuance with stock_plan_id = Some planId + + submitMustFail issuer do + exerciseCmd capTableWithIssuance CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass (defaultStockClass otherClassId) + , CT.OcfCreateStockPlan (defaultStockPlan planId otherClassId) + ] + edits = [CT.OcfEditStockIssuance editedIssuance] + deletes = [] + testStockIssuance_EditReferencesCreatedInSameBatchSucceeds = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableCid, classId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar index 9ca9142b..4c6142f8 100644 --- a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1761c9218b26072a19f61e9cd80acbe714f67bd7c4f2865f43259d519788d209 -size 2732290 +oid sha256:2d44294f02e4ee8dc5f12d9c7c9a881b0a831d306c96670bd889febe1096846d +size 2733833 diff --git a/dars/dars.lock b/dars/dars.lock index f0ffd26c..e6e918c7 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,10 +40,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar": { - "sha256": "1761c9218b26072a19f61e9cd80acbe714f67bd7c4f2865f43259d519788d209", - "size": 2732290, + "sha256": "2d44294f02e4ee8dc5f12d9c7c9a881b0a831d306c96670bd889febe1096846d", + "size": 2733833, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T17:43:51Z", + "uploadedAt": "2026-07-09T18:05:44Z", "networks": [] } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index f8376266..5161991c 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -1238,6 +1238,17 @@ validateOptionalStockClassReference label maps stockClassId = case stockClassId Some id -> validateStockClassReference label maps id None -> pure () +validateOptionalStockPlanClassReference : Text -> CapTableMaps -> Optional Text -> Text -> Update () +validateOptionalStockPlanClassReference label maps stockPlanIdOpt stockClassId = case stockPlanIdOpt of + Some stockPlanId -> case Map.lookup stockPlanId maps.stock_plans of + Some stockPlanCid -> do + stockPlan <- fetch stockPlanCid + assertMsg + (label <> " " <> stockPlanId <> " does not include stock class " <> stockClassId) + (stockClassId `elem` stockPlan.plan_data.stock_class_ids) + None -> assertMsg (label <> " not found: " <> stockPlanId) False + None -> pure () + validateStockClassConversionRightReferences : CapTableMaps -> Update () validateStockClassConversionRightReferences maps = mapA_ @@ -1314,7 +1325,7 @@ validateStockIssuanceReferences maps = mapA_ (validateReference "Stock issuance stock legend" maps.stock_legend_templates) issuance.issuance_data.stock_legend_ids - validateOptionalReference "Stock issuance stock plan" maps.stock_plans issuance.issuance_data.stock_plan_id + validateOptionalStockPlanClassReference "Stock issuance stock plan" maps issuance.issuance_data.stock_plan_id issuance.issuance_data.stock_class_id validateOptionalReference "Stock issuance vesting terms" maps.vesting_terms issuance.issuance_data.vesting_terms_id) (Map.values maps.stock_issuances) From 9e49a9ad4d635e4f977c538e520b61acb11af2e9 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 14:41:56 -0400 Subject: [PATCH 13/27] Enforce vesting transaction condition references --- Test/daml/OpenCapTable/TestHelpers.daml | 24 ++ Test/daml/OpenCapTable/TestVestingEvent.daml | 59 ++--- Test/daml/OpenCapTable/TestVestingStart.daml | 105 +++++---- ...VestingTransactionConditionReferences.daml | 219 ++++++++++++++++++ .../0.0.6/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- .../codegen/templates/CapTable.daml.template | 112 +++++++++ 7 files changed, 449 insertions(+), 80 deletions(-) create mode 100644 Test/daml/OpenCapTable/TestVestingTransactionConditionReferences.daml diff --git a/Test/daml/OpenCapTable/TestHelpers.daml b/Test/daml/OpenCapTable/TestHelpers.daml index 9928dda0..605dc840 100644 --- a/Test/daml/OpenCapTable/TestHelpers.daml +++ b/Test/daml/OpenCapTable/TestHelpers.daml @@ -231,6 +231,30 @@ defaultVestingTerms vestingTermsId = comments = [] vesting_conditions = [startCondition] +defaultVestingCondition : Text -> OcfVestingTrigger -> [Text] -> OcfVestingCondition +defaultVestingCondition conditionId trigger nextConditionIds = OcfVestingCondition with + id = conditionId + trigger = trigger + next_condition_ids = nextConditionIds + description = Some conditionId + portion = None + quantity = Some 100.0 + +defaultVestingTermsWithConditions : Text -> [OcfVestingCondition] -> VestingTermsOcfData +defaultVestingTermsWithConditions vestingTermsId conditions = VestingTermsOcfData with + id = vestingTermsId + allocation_type = OcfAllocationCumulativeRounding + description = "Vesting terms " <> vestingTermsId + name = "Vesting terms " <> vestingTermsId + comments = [] + vesting_conditions = conditions + +defaultVestingTermsWithCondition : Text -> Text -> OcfVestingTrigger -> VestingTermsOcfData +defaultVestingTermsWithCondition vestingTermsId conditionId trigger = + defaultVestingTermsWithConditions + vestingTermsId + [defaultVestingCondition conditionId trigger []] + -- ============================================================================= -- SETUP COMBINATORS -- ============================================================================= diff --git a/Test/daml/OpenCapTable/TestVestingEvent.daml b/Test/daml/OpenCapTable/TestVestingEvent.daml index b1f77c51..00462c43 100644 --- a/Test/daml/OpenCapTable/TestVestingEvent.daml +++ b/Test/daml/OpenCapTable/TestVestingEvent.daml @@ -2,42 +2,47 @@ module OpenCapTable.TestVestingEvent where import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance import Fairmint.OpenCapTable.OCF.VestingEvent +import Fairmint.OpenCapTable.OCF.VestingTerms (OcfVestingTrigger(..)) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addDefaultStakeholder) +import OpenCapTable.TestHelpers (addDefaultStakeholder, defaultVestingTermsWithCondition) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT import Daml.Script -- Create an equity compensation issuance with milestone-based vesting -createEquityCompensationIssuance issuer capTableCid securityId quantity = +createEquityCompensationIssuance issuer capTableCid securityId quantity vestingConditionId = + let vestingTermsId = "VT-" <> securityId in submit issuer do exerciseCmd capTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationIssuance EquityCompensationIssuanceOcfData with - id = "TX_EC_ISS_MILESTONE" - date = DT.time (DA.date 2025 Jan 01) 0 0 0 - security_id = securityId - custom_id = "OPT-MILESTONE-001" - stakeholder_id = "SH-1" - board_approval_date = None - stockholder_approval_date = None - consideration_text = None - security_law_exemptions = [] - stock_plan_id = None - stock_class_id = None - compensation_type = OcfCompensationTypeOptionISO - quantity = quantity - base_price = None - early_exercisable = Some False - exercise_price = Some (OcfMonetary with amount = 2.50, currency = "USD") - expiration_date = Some (DT.time (DA.date 2035 Jan 01) 0 0 0) - termination_exercise_windows = [] - vestings = [] - vesting_terms_id = None - comments = []] + creates = + [ CT.OcfCreateVestingTerms (defaultVestingTermsWithCondition vestingTermsId vestingConditionId OcfVestingEventTrigger) + , CT.OcfCreateEquityCompensationIssuance EquityCompensationIssuanceOcfData with + id = "TX_EC_ISS_MILESTONE" + date = DT.time (DA.date 2025 Jan 01) 0 0 0 + security_id = securityId + custom_id = "OPT-MILESTONE-001" + stakeholder_id = "SH-1" + board_approval_date = None + stockholder_approval_date = None + consideration_text = None + security_law_exemptions = [] + stock_plan_id = None + stock_class_id = None + compensation_type = OcfCompensationTypeOptionISO + quantity = quantity + base_price = None + early_exercisable = Some False + exercise_price = Some (OcfMonetary with amount = 2.50, currency = "USD") + expiration_date = Some (DT.time (DA.date 2035 Jan 01) 0 0 0) + termination_exercise_windows = [] + vestings = [] + vesting_terms_id = Some vestingTermsId + comments = [] + ] edits = [] deletes = [] @@ -46,7 +51,7 @@ testVestingEvent_MilestoneAchieved = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "ECSEC-MILESTONE-001" - result <- createEquityCompensationIssuance issuer capTableCid securityId 100000.0 + result <- createEquityCompensationIssuance issuer capTableCid securityId 100000.0 "revenue-milestone-1m" _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateVestingEvent VestingEventOcfData with @@ -64,7 +69,7 @@ testVestingEvent_ProductLaunchMilestone = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "ECSEC-MILESTONE-002" - result <- createEquityCompensationIssuance issuer capTableCid securityId 50000.0 + result <- createEquityCompensationIssuance issuer capTableCid securityId 50000.0 "product-launch-v2" _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateVestingEvent VestingEventOcfData with @@ -82,7 +87,7 @@ testVestingEvent_IPOTrigger = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "ECSEC-MILESTONE-003" - result <- createEquityCompensationIssuance issuer capTableCid securityId 200000.0 + result <- createEquityCompensationIssuance issuer capTableCid securityId 200000.0 "ipo-trigger" _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateVestingEvent VestingEventOcfData with diff --git a/Test/daml/OpenCapTable/TestVestingStart.daml b/Test/daml/OpenCapTable/TestVestingStart.daml index a9f1ade0..88b84de5 100644 --- a/Test/daml/OpenCapTable/TestVestingStart.daml +++ b/Test/daml/OpenCapTable/TestVestingStart.daml @@ -2,42 +2,47 @@ module OpenCapTable.TestVestingStart where import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance import Fairmint.OpenCapTable.OCF.VestingStart +import Fairmint.OpenCapTable.OCF.VestingTerms (OcfVestingTrigger(..)) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addDefaultStakeholder) +import OpenCapTable.TestHelpers (addDefaultStakeholder, defaultVestingTermsWithCondition) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT import Daml.Script -- Create an equity compensation issuance with vesting -createEquityCompensationIssuance issuer capTableCid securityId quantity = +createEquityCompensationIssuance issuer capTableCid securityId quantity vestingConditionId = + let vestingTermsId = "VT-" <> securityId in submit issuer do exerciseCmd capTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationIssuance EquityCompensationIssuanceOcfData with - id = "TX_EC_ISS_1" - date = DT.time (DA.date 2025 Jul 01) 0 0 0 - security_id = securityId - custom_id = "OPT-001" - stakeholder_id = "SH-1" - board_approval_date = None - stockholder_approval_date = None - consideration_text = None - security_law_exemptions = [] - stock_plan_id = None - stock_class_id = None - compensation_type = OcfCompensationTypeOptionISO - quantity = quantity - base_price = None - early_exercisable = Some False - exercise_price = Some (OcfMonetary with amount = 1.25, currency = "USD") - expiration_date = Some (DT.time (DA.date 2035 Jul 01) 0 0 0) - termination_exercise_windows = [] - vestings = [] - vesting_terms_id = None - comments = []] + creates = + [ CT.OcfCreateVestingTerms (defaultVestingTermsWithCondition vestingTermsId vestingConditionId OcfVestingStartTrigger) + , CT.OcfCreateEquityCompensationIssuance EquityCompensationIssuanceOcfData with + id = "TX_EC_ISS_1" + date = DT.time (DA.date 2025 Jul 01) 0 0 0 + security_id = securityId + custom_id = "OPT-001" + stakeholder_id = "SH-1" + board_approval_date = None + stockholder_approval_date = None + consideration_text = None + security_law_exemptions = [] + stock_plan_id = None + stock_class_id = None + compensation_type = OcfCompensationTypeOptionISO + quantity = quantity + base_price = None + early_exercisable = Some False + exercise_price = Some (OcfMonetary with amount = 1.25, currency = "USD") + expiration_date = Some (DT.time (DA.date 2035 Jul 01) 0 0 0) + termination_exercise_windows = [] + vestings = [] + vesting_terms_id = Some vestingTermsId + comments = [] + ] edits = [] deletes = [] @@ -47,7 +52,7 @@ testVestingStart_BasicStart = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "ec7d8e9f-0a1b-2c3d-4e5f-6a7b8c9d0e1f" - result <- createEquityCompensationIssuance issuer capTableCid securityId 50000.0 + result <- createEquityCompensationIssuance issuer capTableCid securityId 50000.0 "vesting-start" _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateVestingStart VestingStartOcfData with @@ -65,7 +70,7 @@ testVestingStart_WithComments = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "ECSEC-002" - result <- createEquityCompensationIssuance issuer capTableCid securityId 100000.0 + result <- createEquityCompensationIssuance issuer capTableCid securityId 100000.0 "vesting-commencement" _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateVestingStart VestingStartOcfData with @@ -83,30 +88,34 @@ testVestingStart_RSUGrant = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "ECSEC-RSU-001" + let vestingTermsId = "VT-" <> securityId result <- submit issuer do exerciseCmd capTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationIssuance EquityCompensationIssuanceOcfData with - id = "TX_EC_ISS_RSU" - date = DT.time (DA.date 2025 Sep 01) 0 0 0 - security_id = securityId - custom_id = "RSU-001" - stakeholder_id = "SH-1" - board_approval_date = None - stockholder_approval_date = None - consideration_text = None - security_law_exemptions = [] - stock_plan_id = None - stock_class_id = None - compensation_type = OcfCompensationTypeRSU - quantity = 25000.0 - base_price = None - early_exercisable = None - exercise_price = None - expiration_date = None - termination_exercise_windows = [] - vestings = [] - vesting_terms_id = None - comments = []] + creates = + [ CT.OcfCreateVestingTerms (defaultVestingTermsWithCondition vestingTermsId "rsu-vesting-start" OcfVestingStartTrigger) + , CT.OcfCreateEquityCompensationIssuance EquityCompensationIssuanceOcfData with + id = "TX_EC_ISS_RSU" + date = DT.time (DA.date 2025 Sep 01) 0 0 0 + security_id = securityId + custom_id = "RSU-001" + stakeholder_id = "SH-1" + board_approval_date = None + stockholder_approval_date = None + consideration_text = None + security_law_exemptions = [] + stock_plan_id = None + stock_class_id = None + compensation_type = OcfCompensationTypeRSU + quantity = 25000.0 + base_price = None + early_exercisable = None + exercise_price = None + expiration_date = None + termination_exercise_windows = [] + vestings = [] + vesting_terms_id = Some vestingTermsId + comments = [] + ] edits = [] deletes = [] _ <- submit issuer do diff --git a/Test/daml/OpenCapTable/TestVestingTransactionConditionReferences.daml b/Test/daml/OpenCapTable/TestVestingTransactionConditionReferences.daml new file mode 100644 index 00000000..73983ebd --- /dev/null +++ b/Test/daml/OpenCapTable/TestVestingTransactionConditionReferences.daml @@ -0,0 +1,219 @@ +module OpenCapTable.TestVestingTransactionConditionReferences where + +import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance +import Fairmint.OpenCapTable.OCF.VestingEvent +import Fairmint.OpenCapTable.OCF.VestingStart +import Fairmint.OpenCapTable.OCF.VestingTerms +import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) +import qualified Fairmint.OpenCapTable.CapTable as CT +import OpenCapTable.Setup +import OpenCapTable.TestHelpers + ( addDefaultStakeholder + , defaultTestDate + , defaultVestingCondition + , defaultVestingTermsWithCondition + , defaultVestingTermsWithConditions + ) +import qualified DA.Date as DA +import DA.Date (Month(..)) +import qualified DA.Time as DT +import Daml.Script + +equityCompensationIssuance : Text -> Text -> Optional Text -> EquityCompensationIssuanceOcfData +equityCompensationIssuance issuanceId securityId vestingTermsId = + EquityCompensationIssuanceOcfData with + id = issuanceId + date = defaultTestDate + security_id = securityId + custom_id = "EC-" <> issuanceId + stakeholder_id = "SH-1" + board_approval_date = None + stockholder_approval_date = None + consideration_text = None + security_law_exemptions = [] + stock_plan_id = None + stock_class_id = None + compensation_type = OcfCompensationTypeOptionISO + quantity = 100.0 + base_price = None + early_exercisable = Some False + exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + expiration_date = Some (DT.time (DA.date 2034 Jan 01) 0 0 0) + termination_exercise_windows = [] + vestings = [] + vesting_terms_id = vestingTermsId + comments = [] + +vestingStart : Text -> Text -> Text -> VestingStartOcfData +vestingStart txId securityId conditionId = + VestingStartOcfData with + id = txId + date = defaultTestDate + security_id = securityId + vesting_condition_id = conditionId + comments = [] + +vestingEvent : Text -> Text -> Text -> VestingEventOcfData +vestingEvent txId securityId conditionId = + VestingEventOcfData with + id = txId + date = defaultTestDate + security_id = securityId + vesting_condition_id = conditionId + comments = [] + +createSecurityWithVestingTerms : + Party -> + ContractId CT.CapTable -> + Text -> + Text -> + VestingTermsOcfData -> + Script (ContractId CT.CapTable) +createSecurityWithVestingTerms issuer capTable issuanceId securityId vestingTermsData = do + result <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = + [ CT.OcfCreateVestingTerms vestingTermsData + , CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance issuanceId securityId (Some vestingTermsData.id)) + ] + edits = [] + deletes = [] + pure result.updatedCapTableCid + +testVestingStartConditionReferenceSameBatchSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addDefaultStakeholder issuer cap_table + let securityId = "SEC-VESTING-START-BATCH" + _ <- submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateVestingTerms (defaultVestingTermsWithCondition "VT_START_BATCH" "start" OcfVestingStartTrigger) + , CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "EC_START_BATCH" securityId (Some "VT_START_BATCH")) + , CT.OcfCreateVestingStart (vestingStart "VS_START_BATCH" securityId "start") + ] + edits = [] + deletes = [] + pure () + +testVestingEventConditionReferenceSameBatchSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addDefaultStakeholder issuer cap_table + let securityId = "SEC-VESTING-EVENT-BATCH" + _ <- submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateVestingTerms (defaultVestingTermsWithCondition "VT_EVENT_BATCH" "event" OcfVestingEventTrigger) + , CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "EC_EVENT_BATCH" securityId (Some "VT_EVENT_BATCH")) + , CT.OcfCreateVestingEvent (vestingEvent "VE_EVENT_BATCH" securityId "event") + ] + edits = [] + deletes = [] + pure () + +testVestingStartMissingConditionFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addDefaultStakeholder issuer cap_table + let securityId = "SEC-VESTING-START-MISSING" + capTableWithSecurity <- createSecurityWithVestingTerms + issuer + capTableCid + "EC_START_MISSING" + securityId + (defaultVestingTermsWithCondition "VT_START_MISSING" "start" OcfVestingStartTrigger) + submitMustFail issuer do + exerciseCmd capTableWithSecurity CT.UpdateCapTable with + creates = [CT.OcfCreateVestingStart (vestingStart "VS_START_MISSING" securityId "missing")] + edits = [] + deletes = [] + pure () + +testVestingEventMissingConditionFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addDefaultStakeholder issuer cap_table + let securityId = "SEC-VESTING-EVENT-MISSING" + capTableWithSecurity <- createSecurityWithVestingTerms + issuer + capTableCid + "EC_EVENT_MISSING" + securityId + (defaultVestingTermsWithCondition "VT_EVENT_MISSING" "event" OcfVestingEventTrigger) + submitMustFail issuer do + exerciseCmd capTableWithSecurity CT.UpdateCapTable with + creates = [CT.OcfCreateVestingEvent (vestingEvent "VE_EVENT_MISSING" securityId "missing")] + edits = [] + deletes = [] + pure () + +testVestingStartWrongTriggerFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addDefaultStakeholder issuer cap_table + let securityId = "SEC-VESTING-START-WRONG-TRIGGER" + capTableWithSecurity <- createSecurityWithVestingTerms + issuer + capTableCid + "EC_START_WRONG_TRIGGER" + securityId + (defaultVestingTermsWithCondition "VT_START_WRONG_TRIGGER" "event" OcfVestingEventTrigger) + submitMustFail issuer do + exerciseCmd capTableWithSecurity CT.UpdateCapTable with + creates = [CT.OcfCreateVestingStart (vestingStart "VS_START_WRONG_TRIGGER" securityId "event")] + edits = [] + deletes = [] + pure () + +testVestingEventWrongTriggerFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addDefaultStakeholder issuer cap_table + let securityId = "SEC-VESTING-EVENT-WRONG-TRIGGER" + capTableWithSecurity <- createSecurityWithVestingTerms + issuer + capTableCid + "EC_EVENT_WRONG_TRIGGER" + securityId + (defaultVestingTermsWithCondition "VT_EVENT_WRONG_TRIGGER" "start" OcfVestingStartTrigger) + submitMustFail issuer do + exerciseCmd capTableWithSecurity CT.UpdateCapTable with + creates = [CT.OcfCreateVestingEvent (vestingEvent "VE_EVENT_WRONG_TRIGGER" securityId "start")] + edits = [] + deletes = [] + pure () + +testVestingTransactionSecurityWithoutVestingTermsFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addDefaultStakeholder issuer cap_table + let securityId = "SEC-VESTING-NO-TERMS" + result <- submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "EC_NO_TERMS" securityId None)] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingStart (vestingStart "VS_NO_TERMS" securityId "start")] + edits = [] + deletes = [] + pure () + +testVestingTermsEditCannotRemoveReferencedCondition = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addDefaultStakeholder issuer cap_table + let securityId = "SEC-VESTING-EDIT-REMOVES-CONDITION" + result <- submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateVestingTerms (defaultVestingTermsWithCondition "VT_EDIT_REFERENCED" "start" OcfVestingStartTrigger) + , CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "EC_EDIT_REFERENCED" securityId (Some "VT_EDIT_REFERENCED")) + , CT.OcfCreateVestingStart (vestingStart "VS_EDIT_REFERENCED" securityId "start") + ] + edits = [] + deletes = [] + let editedTerms = defaultVestingTermsWithConditions + "VT_EDIT_REFERENCED" + [defaultVestingCondition "replacement-start" OcfVestingStartTrigger []] + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditVestingTerms editedTerms] + deletes = [] + pure () diff --git a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar index 4c6142f8..54ba974c 100644 --- a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d44294f02e4ee8dc5f12d9c7c9a881b0a831d306c96670bd889febe1096846d -size 2733833 +oid sha256:a4a2fe4e5deacd493e0ce1e29b82014b824e068c0c44f6278592d188e3e1806d +size 2747254 diff --git a/dars/dars.lock b/dars/dars.lock index e6e918c7..6d5e4577 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,10 +40,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar": { - "sha256": "2d44294f02e4ee8dc5f12d9c7c9a881b0a831d306c96670bd889febe1096846d", - "size": 2733833, + "sha256": "a4a2fe4e5deacd493e0ce1e29b82014b824e068c0c44f6278592d188e3e1806d", + "size": 2747254, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T18:05:44Z", + "uploadedAt": "2026-07-09T18:39:26.997Z", "networks": [] } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 5161991c..b2061954 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -22,6 +22,7 @@ import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfCon import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..)) import Fairmint.OpenCapTable.OCF.Issuer (Issuer(..), IssuerOcfData) import Fairmint.OpenCapTable.OCF.Document (Document(..), DocumentOcfData, OcfObjectReference(..), OcfObjectType(..)) +import qualified Fairmint.OpenCapTable.OCF.VestingTerms as VT -- OCF Types {{#each types_alpha}} @@ -1349,6 +1350,116 @@ validateWarrantIssuanceReferences maps = validateOptionalReference "Warrant vesting terms" maps.vesting_terms issuance.issuance_data.vesting_terms_id) (Map.values maps.warrant_issuances) +vestingTermsIdForSecurity : CapTableMaps -> Text -> Update (Optional Text) +vestingTermsIdForSecurity maps securityId = do + let stockCidOpt = Map.lookup securityId maps.stock_issuances_by_security_id + let warrantCidOpt = Map.lookup securityId maps.warrant_issuances_by_security_id + let equityCompensationCidOpt = Map.lookup securityId maps.equity_compensation_issuances_by_security_id + let stockMatch = if isSome stockCidOpt then 1 else 0 + let warrantMatch = if isSome warrantCidOpt then 1 else 0 + let equityCompensationMatch = if isSome equityCompensationCidOpt then 1 else 0 + assertMsg + ("Vesting transaction security id is ambiguous: " <> securityId) + (stockMatch + warrantMatch + equityCompensationMatch <= 1) + case (stockCidOpt, warrantCidOpt, equityCompensationCidOpt) of + (Some stockCid, None, None) -> do + issuance <- fetch stockCid + pure issuance.issuance_data.vesting_terms_id + (None, Some warrantCid, None) -> do + issuance <- fetch warrantCid + pure issuance.issuance_data.vesting_terms_id + (None, None, Some equityCompensationCid) -> do + issuance <- fetch equityCompensationCid + pure issuance.issuance_data.vesting_terms_id + (None, None, None) -> do + assertMsg ("Vesting transaction security not found: " <> securityId) False + pure None + _ -> do + assertMsg ("Vesting transaction security id is ambiguous: " <> securityId) False + pure None + +vestingTermsDataForSecurity : CapTableMaps -> Text -> Update (Optional VestingTermsOcfData) +vestingTermsDataForSecurity maps securityId = do + vestingTermsIdOpt <- vestingTermsIdForSecurity maps securityId + case vestingTermsIdOpt of + Some vestingTermsId -> case Map.lookup vestingTermsId maps.vesting_terms of + Some vestingTermsCid -> do + vestingTerms <- fetch vestingTermsCid + pure (Some vestingTerms.vesting_terms_data) + None -> do + assertMsg ("Vesting transaction references missing VestingTerms " <> vestingTermsId <> " for security " <> securityId) False + pure None + None -> do + assertMsg ("Vesting transaction security has no VestingTerms: " <> securityId) False + pure None + +vestingConditionMatchesExpectedTrigger : VT.OcfVestingTrigger -> VT.OcfVestingCondition -> Bool +vestingConditionMatchesExpectedTrigger expectedTrigger condition = + case (expectedTrigger, condition.trigger) of + (VT.OcfVestingStartTrigger, VT.OcfVestingStartTrigger) -> True + (VT.OcfVestingEventTrigger, VT.OcfVestingEventTrigger) -> True + _ -> False + +vestingConditionById : [VT.OcfVestingCondition] -> Text -> Optional VT.OcfVestingCondition +vestingConditionById conditions conditionId = case conditions of + [] -> None + condition :: rest -> + if condition.id == conditionId + then Some condition + else vestingConditionById rest conditionId + +validateVestingTransactionCondition : + Text -> + CapTableMaps -> + Text -> + Text -> + VT.OcfVestingTrigger -> + Update () +validateVestingTransactionCondition label maps securityId vestingConditionId expectedTrigger = do + vestingTermsDataOpt <- vestingTermsDataForSecurity maps securityId + case vestingTermsDataOpt of + Some vestingTermsData -> do + let conditionOpt = vestingConditionById vestingTermsData.vesting_conditions vestingConditionId + assertMsg + (label <> " references missing VestingCondition " <> vestingConditionId <> " for security " <> securityId) + (isSome conditionOpt) + let Some condition = conditionOpt + assertMsg + (label <> " references VestingCondition " <> vestingConditionId <> " with the wrong trigger type") + (vestingConditionMatchesExpectedTrigger expectedTrigger condition) + None -> pure () + +validateVestingStartConditionReferences : CapTableMaps -> Update () +validateVestingStartConditionReferences maps = + mapA_ + (\vestingStartCid -> do + vestingStart <- fetch vestingStartCid + validateVestingTransactionCondition + "VestingStart" + maps + vestingStart.vesting_data.security_id + vestingStart.vesting_data.vesting_condition_id + VT.OcfVestingStartTrigger) + (Map.values maps.vesting_starts) + +validateVestingEventConditionReferences : CapTableMaps -> Update () +validateVestingEventConditionReferences maps = + mapA_ + (\vestingEventCid -> do + vestingEvent <- fetch vestingEventCid + validateVestingTransactionCondition + "VestingEvent" + maps + vestingEvent.vesting_data.security_id + vestingEvent.vesting_data.vesting_condition_id + VT.OcfVestingEventTrigger) + (Map.values maps.vesting_events) + +validateVestingTransactionConditionReferences : CapTableMaps -> Update () +validateVestingTransactionConditionReferences maps = do + validateVestingStartConditionReferences maps + validateVestingEventConditionReferences maps + validateConvertibleConversionTriggerReferences : CapTableMaps -> Update () validateConvertibleConversionTriggerReferences maps = mapA_ @@ -2186,6 +2297,7 @@ template CapTable validateStockIssuanceReferences finalMaps validateEquityCompensationIssuanceReferences finalMaps validateWarrantIssuanceReferences finalMaps + validateVestingTransactionConditionReferences finalMaps validateConvertibleConversionTriggerReferences finalMaps validateWarrantExerciseTriggerReferences finalMaps validateStockReissuanceSplitReferences finalMaps From a6b5f6c3f18140b68c57ccceb510375ca6737eae Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 15:40:33 -0400 Subject: [PATCH 14/27] Validate equity plan stock class references --- ...tEquityCompensationIssuanceReferences.daml | 48 +++++++++++++++++++ .../0.0.6/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- scripts/codegen/generate-captable.ts | 2 - .../codegen/templates/CapTable.daml.template | 8 +++- .../codegen/templates/loops/edit-case.daml | 11 +---- 6 files changed, 59 insertions(+), 18 deletions(-) diff --git a/Test/daml/OpenCapTable/TestEquityCompensationIssuanceReferences.daml b/Test/daml/OpenCapTable/TestEquityCompensationIssuanceReferences.daml index 86e56f63..1a7d121a 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationIssuanceReferences.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationIssuanceReferences.daml @@ -82,6 +82,27 @@ testEquityCompensationIssuance_MissingStockPlanReferenceFails = script do edits = [] deletes = [] +testEquityCompensationIssuance_StockPlanMustIncludeIssuanceClassFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, stakeholderId) <- setupStakeholderWithId issuer cap_table "SH_EC_PLAN_CLASS" "Equity Holder" + let stockClassId = "SC_EC_ISSUANCE_CLASS" + let otherStockClassId = "SC_EC_PLAN_CLASS" + let stockPlanId = "PLAN_EC_OTHER_CLASS" + let issuance = (equityCompensationIssuance "TX_EC_PLAN_WRONG_CLASS" "SEC_EC_PLAN_WRONG_CLASS" stakeholderId) with + stock_class_id = Some stockClassId + stock_plan_id = Some stockPlanId + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass (defaultStockClass stockClassId) + , CT.OcfCreateStockClass (defaultStockClass otherStockClassId) + , CT.OcfCreateStockPlan (defaultStockPlan stockPlanId otherStockClassId) + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + testEquityCompensationIssuance_MissingVestingTermsReferenceFails = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableCid, stakeholderId) <- setupStakeholderWithId issuer cap_table "SH_EC_MISSING_VESTING" "Equity Holder" @@ -110,6 +131,33 @@ testEquityCompensationIssuance_EditMissingReferenceFails = script do edits = [CT.OcfEditEquityCompensationIssuance editedIssuance] deletes = [] +testEquityCompensationIssuance_EditStockPlanMustIncludeIssuanceClassFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableCid, stakeholderId) <- setupStakeholderWithId issuer cap_table "SH_EC_EDIT_PLAN_CLASS" "Equity Holder" + let issuance = equityCompensationIssuance "TX_EC_EDIT_PLAN_CLASS" "SEC_EC_EDIT_PLAN_CLASS" stakeholderId + createResult <- submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance issuance] + edits = [] + deletes = [] + + let stockClassId = "SC_EC_EDIT_ISSUANCE_CLASS" + let otherStockClassId = "SC_EC_EDIT_PLAN_CLASS" + let stockPlanId = "PLAN_EC_EDIT_OTHER_CLASS" + let editedIssuance = issuance with + stock_class_id = Some stockClassId + stock_plan_id = Some stockPlanId + + submitMustFail issuer do + exerciseCmd createResult.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass (defaultStockClass stockClassId) + , CT.OcfCreateStockClass (defaultStockClass otherStockClassId) + , CT.OcfCreateStockPlan (defaultStockPlan stockPlanId otherStockClassId) + ] + edits = [CT.OcfEditEquityCompensationIssuance editedIssuance] + deletes = [] + testEquityCompensationIssuance_EditReferencesCreatedInSameBatchSucceeds = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithStakeholder, stakeholderId) <- setupStakeholderWithId issuer cap_table "SH_EC_EDIT_BATCH" "Equity Holder" diff --git a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar index 54ba974c..2602d756 100644 --- a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4a2fe4e5deacd493e0ce1e29b82014b824e068c0c44f6278592d188e3e1806d -size 2747254 +oid sha256:f36a47377cc2749da6755879c97cf7f0ff105344c5ebafabfafe20ef0f05f0f7 +size 2741265 diff --git a/dars/dars.lock b/dars/dars.lock index 6d5e4577..26983a77 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,8 +40,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar": { - "sha256": "a4a2fe4e5deacd493e0ce1e29b82014b824e068c0c44f6278592d188e3e1806d", - "size": 2747254, + "sha256": "f36a47377cc2749da6755879c97cf7f0ff105344c5ebafabfafe20ef0f05f0f7", + "size": 2741265, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T18:39:26.997Z", "networks": [] diff --git a/scripts/codegen/generate-captable.ts b/scripts/codegen/generate-captable.ts index dc67074b..d807a8ef 100644 --- a/scripts/codegen/generate-captable.ts +++ b/scripts/codegen/generate-captable.ts @@ -59,7 +59,6 @@ interface TypeDef { tier: number; validations: Validation[]; has_validations: boolean; - validates_stock_class_initial_authorized_shares: boolean; validates_issuer_id_reference: boolean; is_document: boolean; object_type_constructor: string; @@ -262,7 +261,6 @@ function discoverTypes(config: Config): TypeDef[] { }; }), has_validations: validationFields.length > 0, - validates_stock_class_initial_authorized_shares: name === 'StockClass', validates_issuer_id_reference: name === 'IssuerAuthorizedSharesAdjustment', is_document: name === 'Document', object_type_constructor: objectTypeConstructor(name), diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index b2061954..a533452b 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -1337,6 +1337,10 @@ validateEquityCompensationIssuanceReferences maps = issuance <- fetch issuanceCid validateStakeholderReference "Equity compensation stakeholder" maps issuance.issuance_data.stakeholder_id validateOptionalStockClassReference "Equity compensation stock class" maps issuance.issuance_data.stock_class_id + case (issuance.issuance_data.stock_plan_id, issuance.issuance_data.stock_class_id) of + (Some _, Some stockClassId) -> + validateOptionalStockPlanClassReference "Equity compensation stock plan" maps issuance.issuance_data.stock_plan_id stockClassId + _ -> pure () validateOptionalReference "Equity compensation stock plan" maps.stock_plans issuance.issuance_data.stock_plan_id validateOptionalReference "Equity compensation vesting terms" maps.vesting_terms issuance.issuance_data.vesting_terms_id) (Map.values maps.equity_compensation_issuances) @@ -2170,7 +2174,7 @@ processCreate ctx issuerCid maps createData = case createData of -- | Process a single edit operation, returning updated maps and the edited ContractId. -- Note: OcfEditIssuer is handled separately in UpdateCapTable since issuer is not a map -processEdit : Context -> ContractId Issuer -> CapTableMaps -> OcfEditData -> Update (CapTableMaps, OcfContractId, [OcfEditData]) +processEdit : Context -> ContractId Issuer -> CapTableMaps -> OcfEditData -> Update (CapTableMaps, OcfContractId) processEdit ctx issuerCid maps editData = case editData of OcfEditIssuer _ -> error "OcfEditIssuer should be handled separately in UpdateCapTable" {{#each types_alpha}} @@ -2255,7 +2259,7 @@ template CapTable -- Process regular edits (map-based entities) (mapsAfterEdits, regularEditedCids) <- foldlA (\(maps, cids) editData -> do - (newMaps, cid, _) <- processEdit context issuer maps editData + (newMaps, cid) <- processEdit context issuer maps editData pure (newMaps, cids ++ [cid])) (mapsAfterDeletes, []) regularEdits diff --git a/scripts/codegen/templates/loops/edit-case.daml b/scripts/codegen/templates/loops/edit-case.daml index 69745ee7..975309fa 100644 --- a/scripts/codegen/templates/loops/edit-case.daml +++ b/scripts/codegen/templates/loops/edit-case.daml @@ -31,15 +31,6 @@ OcfEdit{{name}} d -> do {{#if validates_issuer_id_reference}} validateIssuerReference issuerCid d.issuer_id {{/if}} -{{#if validates_stock_class_initial_authorized_shares}} - oldContract <- fetch oldCid - let authorizedShareInitialEdits = - if d.initial_shares_authorized /= oldContract.{{data_param}}.initial_shares_authorized - then [OcfEdit{{name}} d] - else [] -{{else}} - let authorizedShareInitialEdits = [] -{{/if}} {{#if security_id_index_map}} -- Fetch old contract to get old security_id for index map cleanup oldContract <- fetch oldCid @@ -57,4 +48,4 @@ OcfEdit{{name}} d -> do newCid <- create {{name}} with context = ctx, {{data_param}} = d let newMaps = maps with {{map_field}} = Map.insert d.id newCid maps.{{map_field}} {{/if}} - pure (newMaps, Cid{{name}} newCid, authorizedShareInitialEdits) + pure (newMaps, Cid{{name}} newCid) From 091e046c75b00c6d64f7fe825c46e75acfaa82f9 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 22:12:31 -0400 Subject: [PATCH 15/27] Test orphaned stock issuance references --- .../TestFinalReferenceValidation.daml | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/Test/daml/OpenCapTable/TestFinalReferenceValidation.daml b/Test/daml/OpenCapTable/TestFinalReferenceValidation.daml index 7e8f8d16..04500f10 100644 --- a/Test/daml/OpenCapTable/TestFinalReferenceValidation.daml +++ b/Test/daml/OpenCapTable/TestFinalReferenceValidation.daml @@ -34,6 +34,49 @@ stockTransferData transferId securityId = StockTransferOcfData with consideration_text = None comments = [] +setupStockIssuanceStakeholderReference : Script (Party, ContractId CT.CapTable) +setupStockIssuanceStakeholderReference = do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + capTable <- addStockIssuance issuer capTable "TX-STOCK-STAKEHOLDER-REF" "SEC-STOCK-STAKEHOLDER-REF" "SH-1" "SC_COMMON" + pure (issuer, capTable) + +testFinalReferenceValidation_DeleteStakeholderReferencedByStockIssuanceFails = script do + (issuer, capTable) <- setupStockIssuanceStakeholderReference + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStakeholder "SH-1"] + pure () + +testFinalReferenceValidation_DeleteStockIssuanceThenStakeholderSucceeds = script do + (issuer, capTable) <- setupStockIssuanceStakeholderReference + + _ <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [] + edits = [] + deletes = + [ CT.OcfDeleteStockIssuance "TX-STOCK-STAKEHOLDER-REF" + , CT.OcfDeleteStakeholder "SH-1" + ] + pure () + +testFinalReferenceValidation_DeleteStakeholderThenStockIssuanceSucceeds = script do + (issuer, capTable) <- setupStockIssuanceStakeholderReference + + _ <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [] + edits = [] + deletes = + [ CT.OcfDeleteStakeholder "SH-1" + , CT.OcfDeleteStockIssuance "TX-STOCK-STAKEHOLDER-REF" + ] + pure () + testFinalReferenceValidation_DeleteReferencedStockIssuanceFails = script do TestOcp{issuer, cap_table} <- setupTestOcp capTable <- addPrerequisites issuer cap_table From a7b1197a5aad7f6813cf3b097dcb3d5662d0a4ba Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 06:53:35 -0400 Subject: [PATCH 16/27] fix: avoid partial vesting condition match --- dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- scripts/codegen/templates/CapTable.daml.template | 16 +++++++++------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar index 2602d756..b8499fd9 100644 --- a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f36a47377cc2749da6755879c97cf7f0ff105344c5ebafabfafe20ef0f05f0f7 -size 2741265 +oid sha256:f1fe456db2ce1c9060f5dbe3a0dff2ecdca5feca12d5a215d43e3bddd6ea3d7b +size 2741018 diff --git a/dars/dars.lock b/dars/dars.lock index 26983a77..25152a7f 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,10 +40,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar": { - "sha256": "f36a47377cc2749da6755879c97cf7f0ff105344c5ebafabfafe20ef0f05f0f7", - "size": 2741265, + "sha256": "f1fe456db2ce1c9060f5dbe3a0dff2ecdca5feca12d5a215d43e3bddd6ea3d7b", + "size": 2741018, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T18:39:26.997Z", + "uploadedAt": "2026-07-10T10:45:50.000Z", "networks": [] } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index a533452b..9971d78b 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -1424,13 +1424,15 @@ validateVestingTransactionCondition label maps securityId vestingConditionId exp case vestingTermsDataOpt of Some vestingTermsData -> do let conditionOpt = vestingConditionById vestingTermsData.vesting_conditions vestingConditionId - assertMsg - (label <> " references missing VestingCondition " <> vestingConditionId <> " for security " <> securityId) - (isSome conditionOpt) - let Some condition = conditionOpt - assertMsg - (label <> " references VestingCondition " <> vestingConditionId <> " with the wrong trigger type") - (vestingConditionMatchesExpectedTrigger expectedTrigger condition) + case conditionOpt of + Some condition -> + assertMsg + (label <> " references VestingCondition " <> vestingConditionId <> " with the wrong trigger type") + (vestingConditionMatchesExpectedTrigger expectedTrigger condition) + None -> + assertMsg + (label <> " references missing VestingCondition " <> vestingConditionId <> " for security " <> securityId) + False None -> pure () validateVestingStartConditionReferences : CapTableMaps -> Update () From 40f99b430eca97290eb46c2ef24553a50b04d53a Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 07:44:58 -0400 Subject: [PATCH 17/27] test: cover reference review edge cases --- .../TestFinalReferenceValidation.daml | 21 +++++++++++++++++++ Test/daml/OpenCapTable/TestStockIssuance.daml | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Test/daml/OpenCapTable/TestFinalReferenceValidation.daml b/Test/daml/OpenCapTable/TestFinalReferenceValidation.daml index 04500f10..64c48265 100644 --- a/Test/daml/OpenCapTable/TestFinalReferenceValidation.daml +++ b/Test/daml/OpenCapTable/TestFinalReferenceValidation.daml @@ -161,6 +161,7 @@ testFinalReferenceValidation_StockReissuanceSplitMustExist = script do TestOcp{issuer, cap_table} <- setupTestOcp capTable <- addPrerequisites issuer cap_table capTable <- addStockIssuance issuer capTable "TX-STOCK-SPLIT-REF" "SEC-SPLIT-REF" "SH-1" "SC_COMMON" + capTable <- addStockIssuance issuer capTable "TX-STOCK-SPLIT-RESULT" "SEC-SPLIT-REF-NEW" "SH-1" "SC_COMMON" submitMustFail issuer do exerciseCmd capTable CT.UpdateCapTable with @@ -175,3 +176,23 @@ testFinalReferenceValidation_StockReissuanceSplitMustExist = script do edits = [] deletes = [] pure () + +testFinalReferenceValidation_StockReissuanceSplitMustBeStockClassSplit = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + capTable <- addStockIssuance issuer capTable "TX-STOCK-WRONG-SPLIT-TYPE" "SEC-WRONG-SPLIT-TYPE" "SH-1" "SC_COMMON" + capTable <- addStockIssuance issuer capTable "TX-STOCK-WRONG-SPLIT-RESULT" "SEC-WRONG-SPLIT-RESULT" "SH-1" "SC_COMMON" + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateStockReissuance StockReissuanceOcfData with + id = "TX-REISSUANCE-WRONG-SPLIT-TYPE" + date = defaultReferenceTestTime + security_id = "SEC-WRONG-SPLIT-TYPE" + resulting_security_ids = ["SEC-WRONG-SPLIT-RESULT"] + split_transaction_id = Some "TX-STOCK-WRONG-SPLIT-TYPE" + reason_text = Some "References a stock issuance instead of a stock-class split" + comments = []] + edits = [] + deletes = [] + pure () diff --git a/Test/daml/OpenCapTable/TestStockIssuance.daml b/Test/daml/OpenCapTable/TestStockIssuance.daml index eaa0a22d..bcaec868 100644 --- a/Test/daml/OpenCapTable/TestStockIssuance.daml +++ b/Test/daml/OpenCapTable/TestStockIssuance.daml @@ -68,7 +68,7 @@ testStockIssuance_OptionalsSome = script do stockholder_approval_date = Some (DT.time (DA.date 2024 Jan 16) 0 0 0) consideration_text = Some "Cash consideration" security_law_exemptions = [OcfSecurityExemption with description = "Rule 506(b)", jurisdiction = "US-CA"] - stock_plan_id = None + stock_plan_id = Some "PLAN_1" share_numbers_issued = [OcfShareNumberRange with starting_share_number = 1.0, ending_share_number = 10.0] share_price = OcfMonetary with amount = 2.5, currency = "USD" quantity = 10.0 From cda94f86d17bd8c2a89720f43be994e17891e3b1 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 12:35:00 -0400 Subject: [PATCH 18/27] fix: align reference checks with ceiling parent --- .../daml/OpenCapTable/TestStockClassAuthorizedShares.daml | 8 +++++--- dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 420ed0dd..0de8cd24 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -685,10 +685,12 @@ testStockConversionCreditsDestinationClassForCeiling = script do testStockConversionMissingDestinationClassFailsForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp + let destinationClassId = "SC_CONVERSION_MISSING_DEST" + (capTableWithDestination, _) <- setupStockClassWith issuer cap_table (limitedStockClass destinationClassId 100.0) let preferredClassId = "SC_CONVERSION_MISSING_DEST_SOURCE" let preferredClass = (limitedStockClass preferredClassId 200.0) with - conversion_rights = [stockClassConversionRightTo "SC_CONVERSION_MISSING_DEST"] - (capTableWithPreferred, _) <- setupStockClassWith issuer cap_table preferredClass + conversion_rights = [stockClassConversionRightTo destinationClassId] + (capTableWithPreferred, _) <- setupStockClassWith issuer capTableWithDestination preferredClass (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_MISSING_DEST" "Conversion Missing Destination Holder" capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_MISSING_DEST_SOURCE" "SEC_CONVERSION_MISSING_DEST_SOURCE" stakeholderId preferredClassId) with quantity = 50.0) @@ -696,7 +698,7 @@ testStockConversionMissingDestinationClassFailsForCeiling = script do exerciseCmd capTableIssued CT.UpdateCapTable with creates = [CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_MISSING_DEST" "SEC_CONVERSION_MISSING_DEST_SOURCE" 50.0)] edits = [] - deletes = [] + deletes = [CT.OcfDeleteStockClass destinationClassId] testStockConversionMissingSourceIssuanceFailsForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar index b8499fd9..79ab0471 100644 --- a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1fe456db2ce1c9060f5dbe3a0dff2ecdca5feca12d5a215d43e3bddd6ea3d7b -size 2741018 +oid sha256:1e36e044ef32e4de169fe4569710067717f86eeb38d6eec080589c6dc4d74de9 +size 2787401 diff --git a/dars/dars.lock b/dars/dars.lock index 7937dfec..b3736f5c 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,8 +40,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar": { - "sha256": "f1fe456db2ce1c9060f5dbe3a0dff2ecdca5feca12d5a215d43e3bddd6ea3d7b", - "size": 2741018, + "sha256": "1e36e044ef32e4de169fe4569710067717f86eeb38d6eec080589c6dc4d74de9", + "size": 2787401, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-10T10:45:50.000Z", "networks": [] From 0dff01ed876e6215d5aca997f3f9fd02e2f1c4c7 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 12:51:46 -0400 Subject: [PATCH 19/27] fix: close reference integrity review gaps --- .../TestConvertibleConversion.daml | 39 +++++++- ...tConvertibleConversionRightReferences.daml | 33 +++++++ ...VestingTransactionConditionReferences.daml | 40 ++++++++ .../TestWarrantConversionRightReferences.daml | 17 ++++ .../TestWarrantIssuanceReferences.daml | 17 ++++ .../0.0.6/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- scripts/codegen/captable-config.yaml | 7 +- .../codegen/templates/CapTable.daml.template | 94 ++++++++++++++++++- 9 files changed, 245 insertions(+), 10 deletions(-) diff --git a/Test/daml/OpenCapTable/TestConvertibleConversion.daml b/Test/daml/OpenCapTable/TestConvertibleConversion.daml index 83452d47..18fc99fd 100644 --- a/Test/daml/OpenCapTable/TestConvertibleConversion.daml +++ b/Test/daml/OpenCapTable/TestConvertibleConversion.daml @@ -6,7 +6,7 @@ import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Conversion (OcfConversionTriggerType(..), OcfConvertibleConversionRight(..), OcfConvertibleConversionMechanism(..), OcfSAFEConversionMechanism(..), OcfConvertibleType(..), OcfCapitalizationDefinition(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addDefaultStakeholder) +import OpenCapTable.TestHelpers (addDefaultStakeholder, defaultStockClass, defaultStockPlan) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -85,8 +85,17 @@ testConvertibleConversion_WithCapDefinition = script do capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "CONV-SEC-002" result <- createConvertibleIssuance issuer capTableCid securityId 250000.0 - _ <- submit issuer do + capTableWithCapDefinitionRefs <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass (defaultStockClass "COMMON") + , CT.OcfCreateStockClass (defaultStockClass "PREFERRED") + , CT.OcfCreateStockPlan (defaultStockPlan "ESOP-2024" "COMMON") + ] + edits = [] + deletes = [] + _ <- submit issuer do + exerciseCmd capTableWithCapDefinitionRefs.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with id = "TX_CONV_CONVERSION_2" date = DT.time (DA.date 2025 Dec 01) 0 0 0 @@ -106,6 +115,32 @@ testConvertibleConversion_WithCapDefinition = script do deletes = [] pure () +testConvertibleConversion_CapDefinitionMissingRefsFails = script do + TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp + capTableCid <- addDefaultStakeholder issuer cap_table + let securityId = "CONV-SEC-MISSING-CAP-REF" + result <- createConvertibleIssuance issuer capTableCid securityId 250000.0 + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with + id = "TX_CONV_CONVERSION_MISSING_CAP_REF" + date = DT.time (DA.date 2025 Dec 01) 0 0 0 + security_id = securityId + trigger_id = "equity-financing-trigger" + reason_text = "Conversion with an invalid capitalization definition" + resulting_security_ids = ["SSEC-PREFERRED-MISSING-CAP-REF"] + comments = [] + balance_security_id = None + capitalization_definition = Some (OcfCapitalizationDefinition with + exclude_security_ids = [] + include_security_ids = ["SEC-MISSING-CAP-REF"] + include_stock_class_ids = ["SC-MISSING-CAP-REF"] + include_stock_plans_ids = ["SP-MISSING-CAP-REF"]) + quantity_converted = Some 15625.0] + edits = [] + deletes = [] + pure () + -- Test 3: Partial conversion with balance security testConvertibleConversion_PartialConversion = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestConvertibleConversionRightReferences.daml b/Test/daml/OpenCapTable/TestConvertibleConversionRightReferences.daml index 15a2fb73..b9f8b00f 100644 --- a/Test/daml/OpenCapTable/TestConvertibleConversionRightReferences.daml +++ b/Test/daml/OpenCapTable/TestConvertibleConversionRightReferences.daml @@ -49,6 +49,19 @@ convertibleIssuanceWithConversionTarget issuanceId securityId stockClassId = Con seniority = 1 comments = [] +testConvertibleConversionRightDuplicateTriggerIdsFail = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableWithStakeholder <- addDefaultStakeholder issuer cap_table + let trigger = convertibleConversionTriggerTo None + let issuance = (convertibleIssuanceWithConversionTarget "TX_CONVERTIBLE_DUPLICATE_TRIGGER" "CONV-DUPLICATE-TRIGGER" None) with + conversion_triggers = [trigger, trigger] + + submitMustFail issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance issuance] + edits = [] + deletes = [] + testConvertibleConversionRightTargetCreatedInSameBatchSucceeds = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableWithStakeholder <- addDefaultStakeholder issuer cap_table @@ -120,3 +133,23 @@ testConvertibleIssuanceStakeholderDeleteCannotLeaveDanglingReference = script do creates = [] edits = [] deletes = [CT.OcfDeleteStakeholder defaultStakeholderId] + +testConvertibleIssuanceStakeholderEditCreatedInSameBatchSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableWithStakeholder <- addDefaultStakeholder issuer cap_table + result <- submit issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance (convertibleIssuanceWithConversionTarget "TX_CONVERTIBLE_EDIT_STAKEHOLDER_BATCH" "CONV-EDIT-STAKEHOLDER-BATCH" None)] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStakeholder (defaultStakeholder "SH_CONVERTIBLE_EDIT_BATCH" "Convertible Holder")] + edits = + [ CT.OcfEditConvertibleIssuance + ((convertibleIssuanceWithConversionTarget "TX_CONVERTIBLE_EDIT_STAKEHOLDER_BATCH" "CONV-EDIT-STAKEHOLDER-BATCH" None) with + stakeholder_id = "SH_CONVERTIBLE_EDIT_BATCH") + ] + deletes = [] + pure () diff --git a/Test/daml/OpenCapTable/TestVestingTransactionConditionReferences.daml b/Test/daml/OpenCapTable/TestVestingTransactionConditionReferences.daml index 73983ebd..7cd8e18d 100644 --- a/Test/daml/OpenCapTable/TestVestingTransactionConditionReferences.daml +++ b/Test/daml/OpenCapTable/TestVestingTransactionConditionReferences.daml @@ -179,6 +179,46 @@ testVestingEventWrongTriggerFails = script do deletes = [] pure () +testVestingTermsMissingNextConditionFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addDefaultStakeholder issuer cap_table + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateVestingTerms + (defaultVestingTermsWithConditions + "VT_MISSING_NEXT_CONDITION" + [defaultVestingCondition "start" OcfVestingStartTrigger ["missing-next"]]) + ] + edits = [] + deletes = [] + pure () + +testVestingTermsMissingRelativeConditionFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addDefaultStakeholder issuer cap_table + let relativeCondition = defaultVestingCondition + "relative" + (OcfVestingScheduleRelativeTrigger with + period = OcfVestingPeriodMonths with + length_ = 1 + occurrences = 4 + day_of_month = OcfVestingDay15 + cliff_installment = Some 1 + relative_to_condition_id = "missing-relative") + [] + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateVestingTerms + (defaultVestingTermsWithConditions + "VT_MISSING_RELATIVE_CONDITION" + [relativeCondition]) + ] + edits = [] + deletes = [] + pure () + testVestingTransactionSecurityWithoutVestingTermsFails = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table diff --git a/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml b/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml index b6454029..94f5c499 100644 --- a/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml +++ b/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml @@ -135,6 +135,23 @@ testWarrantConversionRightTargetCreatedInSameBatchSucceeds = script do deletes = [] pure () +testWarrantConversionRightDuplicateTriggerIdsFail = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableWithStakeholder <- addDefaultStakeholder issuer cap_table + let trigger = warrantConversionTriggerTo None + + submitMustFail issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance + (warrantIssuanceWithExerciseTriggers + "TX_WARRANT_DUPLICATE_TRIGGER" + "WARRANT-DUPLICATE-TRIGGER" + [trigger, trigger]) + ] + edits = [] + deletes = [] + testWarrantConversionRightMissingTargetFails = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableWithStakeholder <- addDefaultStakeholder issuer cap_table diff --git a/Test/daml/OpenCapTable/TestWarrantIssuanceReferences.daml b/Test/daml/OpenCapTable/TestWarrantIssuanceReferences.daml index c70c321d..597686e2 100644 --- a/Test/daml/OpenCapTable/TestWarrantIssuanceReferences.daml +++ b/Test/daml/OpenCapTable/TestWarrantIssuanceReferences.daml @@ -122,3 +122,20 @@ testWarrantIssuance_DeleteReferencedStakeholderFails = script do creates = [] edits = [] deletes = [CT.OcfDeleteStakeholder stakeholderId] + +testWarrantIssuance_EditStakeholderCreatedInSameBatchSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithStakeholder, stakeholderId) <- setupStakeholderWithId issuer cap_table "SH_WARRANT_EDIT_OLD" "Warrant Holder" + let issuance = warrantIssuance "TX_WARRANT_EDIT_STAKEHOLDER_BATCH" "WARRANT-EDIT-STAKEHOLDER-BATCH" stakeholderId + result <- submit issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStakeholder (defaultStakeholder "SH_WARRANT_EDIT_NEW" "New Warrant Holder")] + edits = [CT.OcfEditWarrantIssuance (issuance with stakeholder_id = "SH_WARRANT_EDIT_NEW")] + deletes = [] + pure () diff --git a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar index 79ab0471..380bd15c 100644 --- a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1e36e044ef32e4de169fe4569710067717f86eeb38d6eec080589c6dc4d74de9 -size 2787401 +oid sha256:9c91ddfae19c966142b599294b393926f3546d40905d6e0fc6f44e6e968180d2 +size 2794874 diff --git a/dars/dars.lock b/dars/dars.lock index b3736f5c..26e8c6dd 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,8 +40,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar": { - "sha256": "1e36e044ef32e4de169fe4569710067717f86eeb38d6eec080589c6dc4d74de9", - "size": 2787401, + "sha256": "9c91ddfae19c966142b599294b393926f3546d40905d6e0fc6f44e6e968180d2", + "size": 2794874, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-10T10:45:50.000Z", "networks": [] diff --git a/scripts/codegen/captable-config.yaml b/scripts/codegen/captable-config.yaml index 8e29dcfe..86b43dff 100644 --- a/scripts/codegen/captable-config.yaml +++ b/scripts/codegen/captable-config.yaml @@ -82,9 +82,10 @@ validations: EquityCompensationIssuance: [] Valuation: [] - # Other issuance validations that can safely run before creates are processed. - ConvertibleIssuance: [stakeholder_id] - WarrantIssuance: [stakeholder_id] + # These issuance types are validated against final CapTable maps so same-batch + # edits can point at stakeholders or other objects created later in the batch. + ConvertibleIssuance: [] + WarrantIssuance: [] # Stakeholder events - validate stakeholder exists StakeholderRelationshipChangeEvent: [stakeholder_id] StakeholderStatusChangeEvent: [stakeholder_id] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 8ea6e5e1..03690557 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -18,7 +18,7 @@ import DA.Foldable (mapA_) import DA.Optional (fromOptional, isNone, isSome) import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfConversionMechanism(..), OcfConversionTrigger(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) +import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfCapitalizationDefinition, OcfConversionMechanism(..), OcfConversionTrigger(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..)) import Fairmint.OpenCapTable.OCF.Issuer (Issuer(..), IssuerOcfData) import Fairmint.OpenCapTable.OCF.Document (Document(..), DocumentOcfData, OcfObjectReference(..), OcfObjectType(..)) @@ -1276,6 +1276,17 @@ validateReference label refMap refId = (label <> " not found: " <> refId) (isSome (Map.lookup refId refMap)) +hasDuplicateText : [Text] -> Bool +hasDuplicateText ids = case ids of + [] -> False + id :: rest -> id `elem` rest || hasDuplicateText rest + +validateUniqueTextList : Text -> [Text] -> Update () +validateUniqueTextList label ids = + assertMsg + (label <> " contains duplicate id") + (not (hasDuplicateText ids)) + validateStakeholderReference : Text -> CapTableMaps -> Text -> Update () validateStakeholderReference label maps stakeholderId = validateReference label maps.stakeholders stakeholderId @@ -1326,6 +1337,9 @@ validateConvertibleConversionRightReferences maps = mapA_ (\issuanceCid -> do issuance <- fetch issuanceCid + validateUniqueTextList + ("Convertible conversion triggers for " <> issuance.issuance_data.id) + (map (\trigger -> trigger.trigger_id) issuance.issuance_data.conversion_triggers) mapA_ (\trigger -> validateOptionalStockClassReference "Convertible conversion target" maps trigger.conversion_right.converts_to_stock_class_id) issuance.issuance_data.conversion_triggers) @@ -1344,6 +1358,9 @@ validateWarrantConversionRightReferences maps = mapA_ (\issuanceCid -> do issuance <- fetch issuanceCid + validateUniqueTextList + ("Warrant exercise triggers for " <> issuance.issuance_data.id) + (map (\trigger -> trigger.trigger_id) issuance.issuance_data.exercise_triggers) mapA_ (\trigger -> validateAnyConversionRightStockClassReferences ("Warrant exercise trigger for " <> issuance.issuance_data.id) maps trigger.conversion_right) issuance.issuance_data.exercise_triggers) @@ -1517,6 +1534,79 @@ validateVestingTransactionConditionReferences maps = do validateVestingStartConditionReferences maps validateVestingEventConditionReferences maps +vestingConditionReferenceExists : Text -> [Text] -> Text -> Update () +vestingConditionReferenceExists label conditionIds conditionId = + assertMsg + (label <> " references missing VestingCondition: " <> conditionId) + (conditionId `elem` conditionIds) + +validateVestingConditionTriggerReferences : Text -> [Text] -> VT.OcfVestingCondition -> Update () +validateVestingConditionTriggerReferences label conditionIds condition = + case condition.trigger of + VT.OcfVestingScheduleRelativeTrigger with relative_to_condition_id -> + vestingConditionReferenceExists label conditionIds relative_to_condition_id + _ -> pure () + +validateVestingTermsConditionGraphReferences : CapTableMaps -> Update () +validateVestingTermsConditionGraphReferences maps = + mapA_ + (\vestingTermsCid -> do + vestingTerms <- fetch vestingTermsCid + let conditions = vestingTerms.vesting_terms_data.vesting_conditions + let conditionIds = map (\condition -> condition.id) conditions + validateUniqueTextList + ("VestingTerms conditions for " <> vestingTerms.vesting_terms_data.id) + conditionIds + mapA_ + (\condition -> do + validateVestingConditionTriggerReferences + ("VestingTerms " <> vestingTerms.vesting_terms_data.id <> " condition " <> condition.id) + conditionIds + condition + mapA_ + (vestingConditionReferenceExists ("VestingTerms " <> vestingTerms.vesting_terms_data.id <> " condition " <> condition.id) conditionIds) + condition.next_condition_ids) + conditions) + (Map.values maps.vesting_terms) + +validateSecurityReference : Text -> CapTableMaps -> Text -> Update () +validateSecurityReference label maps securityId = + assertMsg + (label <> " security not found: " <> securityId) + (isSome (Map.lookup securityId maps.convertible_issuances_by_security_id) + || isSome (Map.lookup securityId maps.equity_compensation_issuances_by_security_id) + || isSome (Map.lookup securityId maps.stock_issuances_by_security_id) + || isSome (Map.lookup securityId maps.warrant_issuances_by_security_id)) + +validateCapitalizationDefinitionReferences : Text -> CapTableMaps -> OcfCapitalizationDefinition -> Update () +validateCapitalizationDefinitionReferences label maps capitalizationDefinition = do + mapA_ + (validateSecurityReference (label <> " included") maps) + capitalizationDefinition.include_security_ids + mapA_ + (validateSecurityReference (label <> " excluded") maps) + capitalizationDefinition.exclude_security_ids + mapA_ + (validateStockClassReference (label <> " stock class") maps) + capitalizationDefinition.include_stock_class_ids + mapA_ + (validateReference (label <> " stock plan") maps.stock_plans) + capitalizationDefinition.include_stock_plans_ids + +validateConvertibleConversionCapitalizationReferences : CapTableMaps -> Update () +validateConvertibleConversionCapitalizationReferences maps = + mapA_ + (\conversionCid -> do + conversion <- fetch conversionCid + case conversion.conversion_data.capitalization_definition of + Some capitalizationDefinition -> + validateCapitalizationDefinitionReferences + ("Convertible conversion capitalization definition for " <> conversion.conversion_data.id) + maps + capitalizationDefinition + None -> pure ()) + (Map.values maps.convertible_conversions) + validateConvertibleConversionTriggerReferences : CapTableMaps -> Update () validateConvertibleConversionTriggerReferences maps = mapA_ @@ -2659,6 +2749,8 @@ template CapTable validateEquityCompensationIssuanceReferences finalMaps validateWarrantIssuanceReferences finalMaps validateVestingTransactionConditionReferences finalMaps + validateVestingTermsConditionGraphReferences finalMaps + validateConvertibleConversionCapitalizationReferences finalMaps validateConvertibleConversionTriggerReferences finalMaps validateWarrantExerciseTriggerReferences finalMaps validateStockReissuanceSplitReferences finalMaps From 9d00073f0acfcb452c4c01eba3875361423c1462 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 13:40:47 -0400 Subject: [PATCH 20/27] chore: refresh reference integrity DAR --- dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar index 380bd15c..a2e4737d 100644 --- a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c91ddfae19c966142b599294b393926f3546d40905d6e0fc6f44e6e968180d2 -size 2794874 +oid sha256:5e77628ca47d11e1cd12c62a45a98355b73538970d78922ff4a0f0bbe6d5989c +size 2862394 diff --git a/dars/dars.lock b/dars/dars.lock index 02ff7db9..0eec2358 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,8 +40,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar": { - "sha256": "9c91ddfae19c966142b599294b393926f3546d40905d6e0fc6f44e6e968180d2", - "size": 2794874, + "sha256": "5e77628ca47d11e1cd12c62a45a98355b73538970d78922ff4a0f0bbe6d5989c", + "size": 2862394, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-10T10:45:50.000Z", "networks": [] From 69f1755f4628f5f0e913b19c383c6ca2db96b344 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 15:42:27 -0400 Subject: [PATCH 21/27] test: align reference fixture with ceiling base --- Test/daml/OpenCapTable/TestDocument.daml | 3 ++- dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Test/daml/OpenCapTable/TestDocument.daml b/Test/daml/OpenCapTable/TestDocument.daml index 15fb202e..88a29844 100644 --- a/Test/daml/OpenCapTable/TestDocument.daml +++ b/Test/daml/OpenCapTable/TestDocument.daml @@ -121,7 +121,8 @@ testDocumentRelatedObjectCreatedInSameBatchSucceeds = script do testDocumentPlanSecurityAliasReferencesEquityCompensationIssuance = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithStakeholder, stakeholderId) <- setupStakeholderWithId issuer cap_table "SH_DOC_PLAN_SECURITY" "Plan Security Holder" - capTableWithIssuance <- addEquityCompensationIssuance issuer capTableWithStakeholder "EC_DOC_PLAN_SECURITY" "SEC_DOC_PLAN_SECURITY" stakeholderId + capTableWithStockClass <- addDefaultStockClass issuer capTableWithStakeholder + capTableWithIssuance <- addEquityCompensationIssuance issuer capTableWithStockClass "EC_DOC_PLAN_SECURITY" "SEC_DOC_PLAN_SECURITY" stakeholderId _ <- submit issuer do exerciseCmd capTableWithIssuance CT.UpdateCapTable with diff --git a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar index a2e4737d..8a4ca8f4 100644 --- a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e77628ca47d11e1cd12c62a45a98355b73538970d78922ff4a0f0bbe6d5989c -size 2862394 +oid sha256:b821f280f50aafad68e7a808de841eee1b6dd7028544ed3a6db84f578ae3165e +size 2920527 diff --git a/dars/dars.lock b/dars/dars.lock index 41bc9246..4c2a36de 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,8 +40,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar": { - "sha256": "5e77628ca47d11e1cd12c62a45a98355b73538970d78922ff4a0f0bbe6d5989c", - "size": 2862394, + "sha256": "b821f280f50aafad68e7a808de841eee1b6dd7028544ed3a6db84f578ae3165e", + "size": 2920527, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-10T10:45:50.000Z", "networks": [] From 67fe05aba6cccaf690b51e60cbbe9a7e77d855c7 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 18:34:24 -0400 Subject: [PATCH 22/27] test: cover duplicate trigger IDs on edits --- ...tConvertibleConversionRightReferences.daml | 21 +++++++++++++++++++ .../TestWarrantConversionRightReferences.daml | 21 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/Test/daml/OpenCapTable/TestConvertibleConversionRightReferences.daml b/Test/daml/OpenCapTable/TestConvertibleConversionRightReferences.daml index b9f8b00f..79e01f1a 100644 --- a/Test/daml/OpenCapTable/TestConvertibleConversionRightReferences.daml +++ b/Test/daml/OpenCapTable/TestConvertibleConversionRightReferences.daml @@ -62,6 +62,27 @@ testConvertibleConversionRightDuplicateTriggerIdsFail = script do edits = [] deletes = [] +testConvertibleConversionRightDuplicateTriggerIdsEditFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableWithStakeholder <- addDefaultStakeholder issuer cap_table + let trigger = convertibleConversionTriggerTo None + let issuance = convertibleIssuanceWithConversionTarget + "TX_CONVERTIBLE_DUPLICATE_TRIGGER_EDIT" + "CONV-DUPLICATE-TRIGGER-EDIT" + None + + result <- submit issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleIssuance issuance] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditConvertibleIssuance (issuance with conversion_triggers = [trigger, trigger])] + deletes = [] + testConvertibleConversionRightTargetCreatedInSameBatchSucceeds = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableWithStakeholder <- addDefaultStakeholder issuer cap_table diff --git a/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml b/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml index 94f5c499..0c7fcd5c 100644 --- a/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml +++ b/Test/daml/OpenCapTable/TestWarrantConversionRightReferences.daml @@ -152,6 +152,27 @@ testWarrantConversionRightDuplicateTriggerIdsFail = script do edits = [] deletes = [] +testWarrantConversionRightDuplicateTriggerIdsEditFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableWithStakeholder <- addDefaultStakeholder issuer cap_table + let trigger = warrantConversionTriggerTo None + let issuance = warrantIssuanceWithExerciseTriggers + "TX_WARRANT_DUPLICATE_TRIGGER_EDIT" + "WARRANT-DUPLICATE-TRIGGER-EDIT" + [trigger] + + result <- submit issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditWarrantIssuance (issuance with exercise_triggers = [trigger, trigger])] + deletes = [] + testWarrantConversionRightMissingTargetFails = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableWithStakeholder <- addDefaultStakeholder issuer cap_table From 89a1c48f442f7a7ff2f0a6ff8dbf065ee3c1916c Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 19:12:03 -0400 Subject: [PATCH 23/27] feat: enforce lifecycle successor references --- .../TestBalanceSecurityReferences.daml | 152 ++++++++ .../TestConvertibleCancellation.daml | 11 +- .../TestConvertibleConversion.daml | 100 +++-- .../OpenCapTable/TestConvertibleTransfer.daml | 24 +- .../TestEquityCompensationExercise.daml | 12 +- .../TestEquityCompensationRelease.daml | 9 +- .../TestEquityCompensationTransfer.daml | 29 +- .../TestFinalReferenceValidation.daml | 4 + Test/daml/OpenCapTable/TestHelpers.daml | 92 +++++ .../TestResultingSecurityReferences.daml | 360 ++++++++++++++++++ .../TestSecurityIdValidation.daml | 52 ++- .../TestStockClassAuthorizedShares.daml | 80 +++- .../OpenCapTable/TestStockConsolidation.daml | 11 +- .../OpenCapTable/TestStockConversion.daml | 42 +- .../OpenCapTable/TestStockReissuance.daml | 12 +- Test/daml/OpenCapTable/TestStockTransfer.daml | 46 ++- .../OpenCapTable/TestWarrantCancellation.daml | 15 +- .../OpenCapTable/TestWarrantExercise.daml | 11 +- .../OpenCapTable/TestWarrantTransfer.daml | 26 +- .../0.0.6/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- scripts/codegen/captable-config.yaml | 66 +++- .../codegen/templates/loops/edit-case.daml | 25 +- 23 files changed, 964 insertions(+), 223 deletions(-) create mode 100644 Test/daml/OpenCapTable/TestBalanceSecurityReferences.daml create mode 100644 Test/daml/OpenCapTable/TestResultingSecurityReferences.daml diff --git a/Test/daml/OpenCapTable/TestBalanceSecurityReferences.daml b/Test/daml/OpenCapTable/TestBalanceSecurityReferences.daml new file mode 100644 index 00000000..200af723 --- /dev/null +++ b/Test/daml/OpenCapTable/TestBalanceSecurityReferences.daml @@ -0,0 +1,152 @@ +module OpenCapTable.TestBalanceSecurityReferences where + +import qualified Fairmint.OpenCapTable.CapTable as CT +import Fairmint.OpenCapTable.OCF.ConvertibleCancellation (ConvertibleCancellationOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationCancellation (EquityCompensationCancellationOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockCancellation (StockCancellationOcfData(..)) +import Fairmint.OpenCapTable.OCF.WarrantCancellation (WarrantCancellationOcfData(..)) +import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import OpenCapTable.Setup +import OpenCapTable.TestHelpers + ( addDefaultConvertibleIssuance + , addDefaultEquityCompensationIssuance + , addDefaultStakeholder + , addDefaultStockIssuance + , addDefaultWarrantIssuance + , addPrerequisites + , defaultStockIssuance + , defaultTestDate + ) +import Daml.Script + +testBalanceSecurityReferences_MissingStockSuccessorFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + capTableCid <- addDefaultStockIssuance issuer capTableCid + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockCancellation StockCancellationOcfData with + id = "TX-STOCK-MISSING-BALANCE" + date = defaultTestDate + quantity = 10.0 + reason_text = "Partial cancellation" + security_id = "SEC-STOCK-1" + comments = [] + balance_security_id = Some "SEC-STOCK-MISSING-BALANCE"] + edits = [] + deletes = [] + +testBalanceSecurityReferences_MissingWarrantSuccessorFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addDefaultStakeholder issuer cap_table + capTableCid <- addDefaultWarrantIssuance issuer capTableCid + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantCancellation WarrantCancellationOcfData with + id = "TX-WARRANT-MISSING-BALANCE" + date = defaultTestDate + quantity = 10.0 + reason_text = "Partial cancellation" + security_id = "SEC-WR-1" + comments = [] + balance_security_id = Some "SEC-WARRANT-MISSING-BALANCE"] + edits = [] + deletes = [] + +testBalanceSecurityReferences_MissingConvertibleSuccessorFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addDefaultStakeholder issuer cap_table + capTableCid <- addDefaultConvertibleIssuance issuer capTableCid + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleCancellation ConvertibleCancellationOcfData with + id = "TX-CONVERTIBLE-MISSING-BALANCE" + amount = OcfMonetary with amount = 10.0, currency = "USD" + date = defaultTestDate + reason_text = "Partial cancellation" + security_id = "SEC-CV-1" + comments = [] + balance_security_id = Some "SEC-CONVERTIBLE-MISSING-BALANCE"] + edits = [] + deletes = [] + +testBalanceSecurityReferences_MissingEquityCompensationSuccessorFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + capTableCid <- addDefaultEquityCompensationIssuance issuer capTableCid + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationCancellation EquityCompensationCancellationOcfData with + id = "TX-EC-MISSING-BALANCE" + date = defaultTestDate + quantity = 10.0 + reason_text = "Partial cancellation" + security_id = "SEC-EC-1" + comments = [] + balance_security_id = Some "SEC-EC-MISSING-BALANCE"] + edits = [] + deletes = [] + +testBalanceSecurityReferences_StockSuccessorCreatedInSameBatchSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + capTableCid <- addDefaultStockIssuance issuer capTableCid + let balanceIssuance = (defaultStockIssuance + "TX-STOCK-BALANCE-SUCCESSOR" + "SEC-STOCK-BALANCE-SUCCESSOR" + "SH-1" + "SC_COMMON") with quantity = 90.0 + + _ <- submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance balanceIssuance + , CT.OcfCreateStockCancellation StockCancellationOcfData with + id = "TX-STOCK-CANCELLATION-BALANCE-SUCCESSOR" + date = defaultTestDate + quantity = 10.0 + reason_text = "Partial cancellation" + security_id = "SEC-STOCK-1" + comments = [] + balance_security_id = Some "SEC-STOCK-BALANCE-SUCCESSOR" + ] + edits = [] + deletes = [] + pure () + +testBalanceSecurityReferences_EditCanCreateStockSuccessorInSameBatch = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + capTableCid <- addDefaultStockIssuance issuer capTableCid + let cancellation = StockCancellationOcfData with + id = "TX-STOCK-EDIT-BALANCE" + date = defaultTestDate + quantity = 10.0 + reason_text = "Partial cancellation" + security_id = "SEC-STOCK-1" + comments = [] + balance_security_id = None + + created <- submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockCancellation cancellation] + edits = [] + deletes = [] + + let balanceIssuance = (defaultStockIssuance + "TX-STOCK-EDIT-BALANCE-SUCCESSOR" + "SEC-STOCK-EDIT-BALANCE-SUCCESSOR" + "SH-1" + "SC_COMMON") with quantity = 90.0 + + _ <- submit issuer do + exerciseCmd created.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance balanceIssuance] + edits = [CT.OcfEditStockCancellation (cancellation with + balance_security_id = Some "SEC-STOCK-EDIT-BALANCE-SUCCESSOR")] + deletes = [] + pure () diff --git a/Test/daml/OpenCapTable/TestConvertibleCancellation.daml b/Test/daml/OpenCapTable/TestConvertibleCancellation.daml index 8cef2086..ba3c7466 100644 --- a/Test/daml/OpenCapTable/TestConvertibleCancellation.daml +++ b/Test/daml/OpenCapTable/TestConvertibleCancellation.daml @@ -13,12 +13,12 @@ import qualified DA.Time as DT import Daml.Script -- Create a convertible issuance to cancel -createConvertibleIssuance issuer capTableCid securityId amount = +createConvertibleIssuance issuer capTableCid securityId amount issuanceDate = submit issuer do exerciseCmd capTableCid CT.UpdateCapTable with creates = [CT.OcfCreateConvertibleIssuance ConvertibleIssuanceOcfData with id = "TX_CONV_ISS_" <> securityId - date = DT.time (DA.date 2022 Jun 01) 0 0 0 + date = issuanceDate security_id = securityId custom_id = "SAFE-1" stakeholder_id = "SH-1" @@ -38,7 +38,7 @@ createConvertibleIssuance issuer capTableCid securityId amount = testConvertibleCancellation_Create = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table - result <- createConvertibleIssuance issuer capTableCid "SEC-1" 25000.0 + result <- createConvertibleIssuance issuer capTableCid "SEC-1" 25000.0 (DT.time (DA.date 2022 Jun 01) 0 0 0) _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateConvertibleCancellation ConvertibleCancellationOcfData with @@ -56,9 +56,10 @@ testConvertibleCancellation_Create = script do testConvertibleCancellation_PartialWithOptionals = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table - result <- createConvertibleIssuance issuer capTableCid "SEC-2" 100000.0 + result <- createConvertibleIssuance issuer capTableCid "SEC-2" 100000.0 (DT.time (DA.date 2022 Jun 01) 0 0 0) + balanceResult <- createConvertibleIssuance issuer result.updatedCapTableCid "SEC-CONV-PARTIAL-BAL" 75000.0 (DT.time (DA.date 2024 Jun 15) 0 0 0) _ <- submit issuer do - exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + exerciseCmd balanceResult.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateConvertibleCancellation ConvertibleCancellationOcfData with id = "TX_CONV_CANCEL_PARTIAL" date = DT.time (DA.date 2024 Jun 15) 0 0 0 diff --git a/Test/daml/OpenCapTable/TestConvertibleConversion.daml b/Test/daml/OpenCapTable/TestConvertibleConversion.daml index 18fc99fd..0016cba0 100644 --- a/Test/daml/OpenCapTable/TestConvertibleConversion.daml +++ b/Test/daml/OpenCapTable/TestConvertibleConversion.daml @@ -6,7 +6,14 @@ import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Conversion (OcfConversionTriggerType(..), OcfConvertibleConversionRight(..), OcfConvertibleConversionMechanism(..), OcfSAFEConversionMechanism(..), OcfConvertibleType(..), OcfCapitalizationDefinition(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addDefaultStakeholder, defaultStockClass, defaultStockPlan) +import OpenCapTable.TestHelpers + ( addDefaultStakeholder + , addPrerequisites + , convertibleSuccessorIssuance + , defaultStockClass + , defaultStockPlan + , stockSuccessorIssuance + ) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -59,22 +66,26 @@ createConvertibleIssuance issuer capTableCid securityId amount = -- Based on prod: SAFE conversion to Series Seed-2 Preferred testConvertibleConversion_SAFEConversion = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp - capTableCid <- addDefaultStakeholder issuer cap_table + capTableCid <- addPrerequisites issuer cap_table let securityId = "7b8c9d0e-1f2a-3b4c-5d6e-7f8a9b0c1d2e" result <- createConvertibleIssuance issuer capTableCid securityId 100000.0 + let conversionDate = DT.time (DA.date 2025 Nov 10) 0 0 0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with - id = "25d377f8-a8de-4d8d-853c-2d75927e7927" - date = DT.time (DA.date 2025 Nov 10) 0 0 0 - security_id = securityId - trigger_id = "equity-financing-trigger" - reason_text = "Automatic conversion upon Series Seed-2 equity financing" - resulting_security_ids = ["5f4a6b7c-9d0e-2f3a-6b7c-8d9e0f1a2b3c"] - comments = ["SAFE conversion to Series Seed-2 Preferred for Investor A"] - balance_security_id = None - capitalization_definition = None - quantity_converted = None] + creates = + [ CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with + id = "25d377f8-a8de-4d8d-853c-2d75927e7927" + date = conversionDate + security_id = securityId + trigger_id = "equity-financing-trigger" + reason_text = "Automatic conversion upon Series Seed-2 equity financing" + resulting_security_ids = ["5f4a6b7c-9d0e-2f3a-6b7c-8d9e0f1a2b3c"] + comments = ["SAFE conversion to Series Seed-2 Preferred for Investor A"] + balance_security_id = None + capitalization_definition = None + quantity_converted = None + , stockSuccessorIssuance "TX-CONV-SAFE-RESULT" "5f4a6b7c-9d0e-2f3a-6b7c-8d9e0f1a2b3c" "SH-1" "SC_COMMON" conversionDate 6250.0 + ] edits = [] deletes = [] pure () @@ -94,23 +105,27 @@ testConvertibleConversion_WithCapDefinition = script do ] edits = [] deletes = [] + let conversionDate = DT.time (DA.date 2025 Dec 01) 0 0 0 _ <- submit issuer do exerciseCmd capTableWithCapDefinitionRefs.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with - id = "TX_CONV_CONVERSION_2" - date = DT.time (DA.date 2025 Dec 01) 0 0 0 - security_id = securityId - trigger_id = "equity-financing-trigger" - reason_text = "Conversion based on fully diluted capitalization at $16M valuation cap" - resulting_security_ids = ["SSEC-PREFERRED-001"] - comments = ["Conversion of $250,000 SAFE at $16M post-money valuation cap"] - balance_security_id = None - capitalization_definition = Some (OcfCapitalizationDefinition with - exclude_security_ids = [] - include_security_ids = [] - include_stock_class_ids = ["COMMON", "PREFERRED"] - include_stock_plans_ids = ["ESOP-2024"]) - quantity_converted = Some 15625.0] -- $250K / $16 per share + creates = + [ CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with + id = "TX_CONV_CONVERSION_2" + date = conversionDate + security_id = securityId + trigger_id = "equity-financing-trigger" + reason_text = "Conversion based on fully diluted capitalization at $16M valuation cap" + resulting_security_ids = ["SSEC-PREFERRED-001"] + comments = ["Conversion of $250,000 SAFE at $16M post-money valuation cap"] + balance_security_id = None + capitalization_definition = Some (OcfCapitalizationDefinition with + exclude_security_ids = [] + include_security_ids = [] + include_stock_class_ids = ["COMMON", "PREFERRED"] + include_stock_plans_ids = ["ESOP-2024"]) + quantity_converted = Some 15625.0 -- $250K / $16 per share + , stockSuccessorIssuance "TX-CONV-CAP-RESULT" "SSEC-PREFERRED-001" "SH-1" "PREFERRED" conversionDate 15625.0 + ] edits = [] deletes = [] pure () @@ -144,22 +159,27 @@ testConvertibleConversion_CapDefinitionMissingRefsFails = script do -- Test 3: Partial conversion with balance security testConvertibleConversion_PartialConversion = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp - capTableCid <- addDefaultStakeholder issuer cap_table + capTableCid <- addPrerequisites issuer cap_table let securityId = "CONV-SEC-003" result <- createConvertibleIssuance issuer capTableCid securityId 500000.0 + let conversionDate = DT.time (DA.date 2025 Nov 15) 0 0 0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with - id = "TX_CONV_CONVERSION_3" - date = DT.time (DA.date 2025 Nov 15) 0 0 0 - security_id = securityId - trigger_id = "equity-financing-trigger" - reason_text = "Partial conversion - investor electing to convert $300,000 of $500,000 SAFE" - resulting_security_ids = ["SSEC-PREFERRED-002"] - comments = [] - balance_security_id = Some "CONV-SEC-003-REMAINDER" - capitalization_definition = None - quantity_converted = Some 300000.0] + creates = + [ CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with + id = "TX_CONV_CONVERSION_3" + date = conversionDate + security_id = securityId + trigger_id = "equity-financing-trigger" + reason_text = "Partial conversion - investor electing to convert $300,000 of $500,000 SAFE" + resulting_security_ids = ["SSEC-PREFERRED-002"] + comments = [] + balance_security_id = Some "CONV-SEC-003-REMAINDER" + capitalization_definition = None + quantity_converted = Some 300000.0 + , stockSuccessorIssuance "TX-CONV-PARTIAL-RESULT" "SSEC-PREFERRED-002" "SH-1" "SC_COMMON" conversionDate 18750.0 + , convertibleSuccessorIssuance "TX-CONV-PARTIAL-BALANCE" "CONV-SEC-003-REMAINDER" "SH-1" conversionDate 200000.0 + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestConvertibleTransfer.daml b/Test/daml/OpenCapTable/TestConvertibleTransfer.daml index fa70907f..51f8dae2 100644 --- a/Test/daml/OpenCapTable/TestConvertibleTransfer.daml +++ b/Test/daml/OpenCapTable/TestConvertibleTransfer.daml @@ -7,19 +7,19 @@ import Fairmint.OpenCapTable.Types.Conversion (OcfConvertibleType(..)) import OpenCapTable.HelpersTriggers (ocfConvTriggerAutomaticOnDate) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addDefaultStakeholder) +import OpenCapTable.TestHelpers (addDefaultStakeholder, convertibleSuccessorIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT import Daml.Script -- Create a convertible issuance to transfer (follows real workflow) -createConvertibleIssuance issuer capTableCid securityId amount = +createConvertibleIssuance issuer capTableCid securityId amount issuanceDate = submit issuer do exerciseCmd capTableCid CT.UpdateCapTable with creates = [CT.OcfCreateConvertibleIssuance ConvertibleIssuanceOcfData with - id = "TX_CONVERTIBLE_ISS_1" - date = DT.time (DA.date 2024 Mar 01) 0 0 0 + id = "TX_CONVERTIBLE_ISS_" <> securityId + date = issuanceDate security_id = securityId custom_id = "SAFE-1" stakeholder_id = "SH-1" @@ -42,10 +42,11 @@ testConvertibleTransfer_SAFEAssignment = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "CSEC-SAFE-001" - result <- createConvertibleIssuance issuer capTableCid securityId 100000.0 + result <- createConvertibleIssuance issuer capTableCid securityId 100000.0 (DT.time (DA.date 2024 Mar 01) 0 0 0) _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateConvertibleTransfer ConvertibleTransferOcfData with + creates = [convertibleSuccessorIssuance "CV-ISS-SAFE-ASSIGNED-001" "CSEC-SAFE-ASSIGNED-001" "SH-1" (DT.time (DA.date 2025 Sep 08) 0 0 0) 100000.0 + , CT.OcfCreateConvertibleTransfer ConvertibleTransferOcfData with id = "TX_CONVERTIBLE_TRANSFER_1" date = DT.time (DA.date 2025 Sep 08) 0 0 0 amount = OcfMonetary with amount = 100000.0, currency = "USD" @@ -63,10 +64,12 @@ testConvertibleTransfer_PartialSAFE = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "CSEC-SAFE-PARTIAL" - result <- createConvertibleIssuance issuer capTableCid securityId 500000.0 + result <- createConvertibleIssuance issuer capTableCid securityId 500000.0 (DT.time (DA.date 2024 Mar 01) 0 0 0) + balanceResult <- createConvertibleIssuance issuer result.updatedCapTableCid "CSEC-SAFE-RETAINED" 300000.0 (DT.time (DA.date 2025 Oct 15) 0 0 0) _ <- submit issuer do - exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateConvertibleTransfer ConvertibleTransferOcfData with + exerciseCmd balanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = [convertibleSuccessorIssuance "CV-ISS-SAFE-SPLIT-001" "CSEC-SAFE-SPLIT-001" "SH-1" (DT.time (DA.date 2025 Oct 15) 0 0 0) 200000.0 + , CT.OcfCreateConvertibleTransfer ConvertibleTransferOcfData with id = "TX_CONVERTIBLE_TRANSFER_PARTIAL" date = DT.time (DA.date 2025 Oct 15) 0 0 0 amount = OcfMonetary with amount = 200000.0, currency = "USD" @@ -107,7 +110,8 @@ testConvertibleTransfer_NoteFullAssignment = script do deletes = [] _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateConvertibleTransfer ConvertibleTransferOcfData with + creates = [convertibleSuccessorIssuance "CV-ISS-NOTE-ASSIGNED" "CSEC-NOTE-ASSIGNED" "SH-1" (DT.time (DA.date 2025 Dec 01) 0 0 0) 250000.0 + , CT.OcfCreateConvertibleTransfer ConvertibleTransferOcfData with id = "TX_NOTE_TRANSFER_1" date = DT.time (DA.date 2025 Dec 01) 0 0 0 amount = OcfMonetary with amount = 250000.0, currency = "USD" diff --git a/Test/daml/OpenCapTable/TestEquityCompensationExercise.daml b/Test/daml/OpenCapTable/TestEquityCompensationExercise.daml index 81cec73d..acb53da3 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationExercise.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationExercise.daml @@ -2,7 +2,7 @@ module OpenCapTable.TestEquityCompensationExercise where import Fairmint.OpenCapTable.OCF.EquityCompensationExercise import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addPrerequisites, addEquityCompensationIssuance) +import OpenCapTable.TestHelpers (addPrerequisites, addEquityCompensationIssuance, stockSuccessorIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -15,7 +15,8 @@ testCreateAndArchiveEquityCompensationExercise = script do cap_table <- addEquityCompensationIssuance issuer cap_table "EC-ISS-1" "EC-1" "SH-1" _ <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationExercise EquityCompensationExerciseOcfData with + creates = [stockSuccessorIssuance "STOCK-ISS-EC-EX-1" "SEC-RES-1" "SH-1" "SC_COMMON" (DT.time (DA.date 2024 Jan 01) 0 0 0) 10.0 + , CT.OcfCreateEquityCompensationExercise EquityCompensationExerciseOcfData with id = "TX_EC_EX_1" date = DT.time (DA.date 2024 Jan 01) 0 0 0 security_id = "EC-1" @@ -34,7 +35,8 @@ testCreateAndArchiveEquityCompensationExerciseBuiltIn = script do cap_table <- addEquityCompensationIssuance issuer cap_table "EC-ISS-1" "EC-1" "SH-1" _ <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationExercise EquityCompensationExerciseOcfData with + creates = [stockSuccessorIssuance "STOCK-ISS-EC-EX-BUILTIN" "SEC-RES-1" "SH-1" "SC_COMMON" (DT.time (DA.date 2024 Jan 01) 0 0 0) 10.0 + , CT.OcfCreateEquityCompensationExercise EquityCompensationExerciseOcfData with id = "TX_EC_EX_1" date = DT.time (DA.date 2024 Jan 01) 0 0 0 security_id = "EC-1" @@ -54,7 +56,9 @@ testEquityCompensationExercise_OptionalsSome = script do cap_table <- addEquityCompensationIssuance issuer cap_table "EC-ISS-2" "EC-2" "SH-1" _ <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationExercise EquityCompensationExerciseOcfData with + creates = [stockSuccessorIssuance "STOCK-ISS-EC-EX-2A" "SEC-RES-2" "SH-1" "SC_COMMON" (DT.time (DA.date 2024 Feb 01) 0 0 0) 2.0 + , stockSuccessorIssuance "STOCK-ISS-EC-EX-2B" "SEC-RES-3" "SH-1" "SC_COMMON" (DT.time (DA.date 2024 Feb 01) 0 0 0) 3.0 + , CT.OcfCreateEquityCompensationExercise EquityCompensationExerciseOcfData with id = "TX_EC_EX_2" date = DT.time (DA.date 2024 Feb 01) 0 0 0 security_id = "EC-2" diff --git a/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml b/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml index 36f71b7d..eed5fb80 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml @@ -3,7 +3,7 @@ module OpenCapTable.TestEquityCompensationRelease where import Fairmint.OpenCapTable.OCF.EquityCompensationRelease import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addPrerequisites, addEquityCompensationIssuance) +import OpenCapTable.TestHelpers (addPrerequisites, addEquityCompensationIssuance, stockSuccessorIssuance) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import qualified DA.Date as DA import DA.Date (Month(..)) @@ -20,7 +20,8 @@ testCreateAndArchiveEquityCompensationRelease = script do -- RSU vesting and release _ <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationRelease EquityCompensationReleaseOcfData with + creates = [stockSuccessorIssuance "STOCK-ISS-EC-RELEASE-1" "SEC_STOCK_101" "SH-1" "SC_COMMON" (DT.time (DA.date 2024 Apr 03) 0 0 0) 5000.0 + , CT.OcfCreateEquityCompensationRelease EquityCompensationReleaseOcfData with id = "TX_EC_RELEASE_1" date = DT.time (DA.date 2024 Apr 01) 0 0 0 security_id = "SEC_RSU_001" @@ -44,7 +45,9 @@ testCreateAndArchiveEquityCompensationReleaseQuarterly = script do -- Quarterly RSU vest with tax withholding _ <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationRelease EquityCompensationReleaseOcfData with + creates = [stockSuccessorIssuance "STOCK-ISS-EC-RELEASE-2A" "SEC_STOCK_102" "SH-1" "SC_COMMON" (DT.time (DA.date 2024 Jul 05) 0 0 0) 625.0 + , stockSuccessorIssuance "STOCK-ISS-EC-RELEASE-2B" "SEC_STOCK_103" "SH-1" "SC_COMMON" (DT.time (DA.date 2024 Jul 05) 0 0 0) 625.0 + , CT.OcfCreateEquityCompensationRelease EquityCompensationReleaseOcfData with id = "TX_EC_RELEASE_2" date = DT.time (DA.date 2024 Jul 01) 0 0 0 security_id = "SEC_RSU_002" diff --git a/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml b/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml index b6ba7cf1..af7a5f13 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml @@ -6,19 +6,19 @@ import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addDefaultStakeholder) +import OpenCapTable.TestHelpers (addDefaultStakeholder, equityCompensationSuccessorIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT import Daml.Script -- Create an equity compensation issuance to transfer (follows real workflow) -createEquityCompensationIssuance issuer capTableCid securityId quantity = +createEquityCompensationIssuance issuer capTableCid securityId quantity issuanceDate = submit issuer do exerciseCmd capTableCid CT.UpdateCapTable with creates = [CT.OcfCreateEquityCompensationIssuance EquityCompensationIssuanceOcfData with - id = "TX_EQUITY_COMP_ISS_1" - date = DT.time (DA.date 2022 Jan 15) 0 0 0 + id = "TX_EQUITY_COMP_ISS_" <> securityId + date = issuanceDate security_id = securityId custom_id = "OPT-1" stakeholder_id = "SH-1" @@ -48,10 +48,12 @@ testEquityCompensationTransfer_NSOToFamilyTrust = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "ESEC-NSO-001" - result <- createEquityCompensationIssuance issuer capTableCid securityId 10000.0 + result <- createEquityCompensationIssuance issuer capTableCid securityId 10000.0 (DT.time (DA.date 2022 Jan 15) 0 0 0) + balanceResult <- createEquityCompensationIssuance issuer result.updatedCapTableCid "ESEC-NSO-REMAIN" 5000.0 (DT.time (DA.date 2025 Jun 01) 0 0 0) _ <- submit issuer do - exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationTransfer EquityCompensationTransferOcfData with + exerciseCmd balanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = [equityCompensationSuccessorIssuance "EC-ISS-TRANSFERRED-001" "ESEC-NSO-TRANSFERRED-001" "SH-1" (DT.time (DA.date 2025 Jun 01) 0 0 0) 5000.0 + , CT.OcfCreateEquityCompensationTransfer EquityCompensationTransferOcfData with id = "TX_EQUITY_COMP_TRANSFER_1" date = DT.time (DA.date 2025 Jun 01) 0 0 0 quantity = 5000.0 @@ -69,10 +71,11 @@ testEquityCompensationTransfer_FullTransfer = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "ESEC-NSO-FULL" - result <- createEquityCompensationIssuance issuer capTableCid securityId 2500.0 + result <- createEquityCompensationIssuance issuer capTableCid securityId 2500.0 (DT.time (DA.date 2022 Jan 15) 0 0 0) _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationTransfer EquityCompensationTransferOcfData with + creates = [equityCompensationSuccessorIssuance "EC-ISS-NEWHOLDER" "ESEC-NSO-NEWHOLDER" "SH-1" (DT.time (DA.date 2025 Sep 15) 0 0 0) 2500.0 + , CT.OcfCreateEquityCompensationTransfer EquityCompensationTransferOcfData with id = "TX_EQUITY_COMP_TRANSFER_FULL" date = DT.time (DA.date 2025 Sep 15) 0 0 0 quantity = 2500.0 @@ -90,10 +93,12 @@ testEquityCompensationTransfer_WithApprovalComments = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "ESEC-NSO-APPROVAL" - result <- createEquityCompensationIssuance issuer capTableCid securityId 15000.0 + result <- createEquityCompensationIssuance issuer capTableCid securityId 15000.0 (DT.time (DA.date 2022 Jan 15) 0 0 0) + balanceResult <- createEquityCompensationIssuance issuer result.updatedCapTableCid "ESEC-NSO-REMAINING" 7000.0 (DT.time (DA.date 2025 Nov 01) 0 0 0) _ <- submit issuer do - exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationTransfer EquityCompensationTransferOcfData with + exerciseCmd balanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = [equityCompensationSuccessorIssuance "EC-ISS-TRUST" "ESEC-NSO-TRUST" "SH-1" (DT.time (DA.date 2025 Nov 01) 0 0 0) 8000.0 + , CT.OcfCreateEquityCompensationTransfer EquityCompensationTransferOcfData with id = "TX_EQUITY_COMP_TRANSFER_APPROVED" date = DT.time (DA.date 2025 Nov 01) 0 0 0 quantity = 8000.0 diff --git a/Test/daml/OpenCapTable/TestFinalReferenceValidation.daml b/Test/daml/OpenCapTable/TestFinalReferenceValidation.daml index cbf1c4b2..cd959b04 100644 --- a/Test/daml/OpenCapTable/TestFinalReferenceValidation.daml +++ b/Test/daml/OpenCapTable/TestFinalReferenceValidation.daml @@ -85,11 +85,15 @@ testFinalReferenceValidation_DeleteReferencedStockIssuanceFails = script do let balanceIssuance = (defaultStockIssuance "TX-STOCK-TRANSFER-BALANCE" "SEC-TRANSFER-BALANCE" "SH-1" "SC_COMMON") with date = defaultReferenceTestTime quantity = 90.0 + let resultIssuance = (defaultStockIssuance "TX-STOCK-TRANSFER-RESULT" "SEC-TRANSFER-RESULT" "SH-1" "SC_COMMON") with + date = defaultReferenceTestTime + quantity = 10.0 result <- submit issuer do exerciseCmd capTable CT.UpdateCapTable with creates = [ CT.OcfCreateStockTransfer (stockTransferData "TX-STOCK-TRANSFER-FINAL-REF" "SEC-FINAL-REF") , CT.OcfCreateStockIssuance balanceIssuance + , CT.OcfCreateStockIssuance resultIssuance ] edits = [] deletes = [] diff --git a/Test/daml/OpenCapTable/TestHelpers.daml b/Test/daml/OpenCapTable/TestHelpers.daml index 876e2529..0fe8d939 100644 --- a/Test/daml/OpenCapTable/TestHelpers.daml +++ b/Test/daml/OpenCapTable/TestHelpers.daml @@ -193,6 +193,98 @@ defaultStockIssuance issuanceId securityId stakeholderId stockClassId = StockIss issuance_type = None comments = [] +stockSuccessorIssuance : Text -> Text -> Text -> Text -> Time -> Decimal -> CT.OcfCreateData +stockSuccessorIssuance issuanceId securityId stakeholderId stockClassId date quantity = + CT.OcfCreateStockIssuance ((defaultStockIssuance issuanceId securityId stakeholderId stockClassId) with + date = date + quantity = quantity) + +warrantSuccessorIssuance : Text -> Text -> Text -> Time -> Decimal -> CT.OcfCreateData +warrantSuccessorIssuance issuanceId securityId stakeholderId date quantity = + CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with + id = issuanceId + custom_id = "WR-" <> issuanceId + date = date + purchase_price = OcfMonetary with amount = 1.0, currency = "USD" + security_id = securityId + stakeholder_id = stakeholderId + comments = [] + exercise_triggers = [] + security_law_exemptions = [] + vestings = [] + board_approval_date = None + consideration_text = None + exercise_price = None + quantity = Some quantity + quantity_source = None + stockholder_approval_date = None + vesting_terms_id = None + warrant_expiration_date = None + +convertibleSuccessorIssuance : Text -> Text -> Text -> Time -> Decimal -> CT.OcfCreateData +convertibleSuccessorIssuance issuanceId securityId stakeholderId date investmentAmount = + CT.OcfCreateConvertibleIssuance ConvertibleIssuanceOcfData with + id = issuanceId + convertible_type = OcfConvertibleSafe + custom_id = "CV-" <> issuanceId + date = date + investment_amount = OcfMonetary with amount = investmentAmount, currency = "USD" + security_id = securityId + seniority = 1 + stakeholder_id = stakeholderId + comments = [] + conversion_triggers = [OcfConvertibleConversionTrigger with + conversion_right = OcfConvertibleConversionRight with + conversion_mechanism = OcfConvMechSAFE (OcfSAFEConversionMechanism with + capitalization_definition = None + capitalization_definition_rules = None + conversion_discount = Some 0.2 + conversion_mfn = False + conversion_timing = None + conversion_valuation_cap = None + exit_multiple = None) + converts_to_future_round = None + converts_to_stock_class_id = None + type_ = "convertible_conversion_right" + end_date = None + nickname = None + start_date = None + trigger_condition = None + trigger_date = None + trigger_description = None + trigger_id = "TRIGGER-" <> issuanceId + type_ = OcfTriggerTypeTypeUnspecified] + security_law_exemptions = [] + board_approval_date = None + consideration_text = None + pro_rata = None + stockholder_approval_date = None + +equityCompensationSuccessorIssuance : Text -> Text -> Text -> Time -> Decimal -> CT.OcfCreateData +equityCompensationSuccessorIssuance issuanceId securityId stakeholderId date quantity = + CT.OcfCreateEquityCompensationIssuance EquityCompensationIssuanceOcfData with + id = issuanceId + custom_id = "EC-" <> issuanceId + date = date + compensation_type = OcfCompensationTypeOptionNSO + quantity = quantity + security_id = securityId + stakeholder_id = stakeholderId + comments = [] + security_law_exemptions = [] + vestings = [] + termination_exercise_windows = [] + base_price = None + board_approval_date = None + consideration_text = None + early_exercisable = None + exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + expiration_date = None + stock_class_id = None + stock_plan_id = None + stockholder_approval_date = None + vesting_terms_id = None + -- | Returns valid StockLegendTemplateOcfData with sensible defaults. defaultStockLegendTemplate : Text -> StockLegendTemplateOcfData defaultStockLegendTemplate legendId = StockLegendTemplateOcfData with diff --git a/Test/daml/OpenCapTable/TestResultingSecurityReferences.daml b/Test/daml/OpenCapTable/TestResultingSecurityReferences.daml new file mode 100644 index 00000000..6f2beaca --- /dev/null +++ b/Test/daml/OpenCapTable/TestResultingSecurityReferences.daml @@ -0,0 +1,360 @@ +module OpenCapTable.TestResultingSecurityReferences where + +import qualified Fairmint.OpenCapTable.CapTable as CT +import Fairmint.OpenCapTable.OCF.ConvertibleConversion (ConvertibleConversionOcfData(..)) +import Fairmint.OpenCapTable.OCF.ConvertibleTransfer (ConvertibleTransferOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationExercise (EquityCompensationExerciseOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationRelease (EquityCompensationReleaseOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationTransfer (EquityCompensationTransferOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockConsolidation (StockConsolidationOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockReissuance (StockReissuanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockTransfer (StockTransferOcfData(..)) +import Fairmint.OpenCapTable.OCF.WarrantExercise (WarrantExerciseOcfData(..)) +import Fairmint.OpenCapTable.OCF.WarrantIssuance (WarrantIssuanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.WarrantTransfer (WarrantTransferOcfData(..)) +import Fairmint.OpenCapTable.Types.Conversion +import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import OpenCapTable.Setup +import OpenCapTable.TestHelpers + ( addDefaultConvertibleIssuance + , addDefaultEquityCompensationIssuance + , addDefaultStakeholder + , addDefaultStockIssuance + , addDefaultWarrantIssuance + , addPrerequisites + , addStockIssuance + , defaultTestDate + , stockSuccessorIssuance + ) +import Daml.Script + +warrantExerciseSourceIssuance : CT.OcfCreateData +warrantExerciseSourceIssuance = + CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with + id = "WR-ISS-EXERCISE-SOURCE" + custom_id = "WR-EXERCISE-SOURCE" + date = defaultTestDate + purchase_price = OcfMonetary with amount = 1.0, currency = "USD" + security_id = "SEC-WR-EXERCISE-SOURCE" + stakeholder_id = "SH-1" + comments = [] + exercise_triggers = [OcfConversionTrigger with + conversion_right = OcfRightWarrant (OcfWarrantConversionRight with + conversion_mechanism = OcfWarrantMechanismFixedAmount with converts_to_quantity = 100.0 + converts_to_future_round = None + converts_to_stock_class_id = Some "SC_COMMON" + type_ = "WARRANT_CONVERSION_RIGHT") + end_date = None + nickname = None + start_date = None + trigger_condition = None + trigger_date = None + trigger_description = None + trigger_id = "WR-EXERCISE-TRIGGER" + type_ = OcfTriggerTypeTypeElectiveAtWill] + security_law_exemptions = [] + vestings = [] + board_approval_date = None + consideration_text = None + exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + quantity = Some 100.0 + quantity_source = None + stockholder_approval_date = None + vesting_terms_id = None + warrant_expiration_date = None + +testResultingSecurityReferences_MissingStockTransferResultFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + capTableCid <- addDefaultStockIssuance issuer capTableCid + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockTransfer StockTransferOcfData with + id = "TX-STOCK-MISSING-RESULT" + date = defaultTestDate + quantity = 100.0 + security_id = "SEC-STOCK-1" + resulting_security_ids = ["SEC-STOCK-MISSING-RESULT"] + balance_security_id = None + consideration_text = None + comments = []] + edits = [] + deletes = [] + +testResultingSecurityReferences_MissingWarrantTransferResultFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addDefaultStakeholder issuer cap_table + capTableCid <- addDefaultWarrantIssuance issuer capTableCid + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantTransfer WarrantTransferOcfData with + id = "TX-WARRANT-MISSING-RESULT" + date = defaultTestDate + quantity = 100.0 + security_id = "SEC-WR-1" + resulting_security_ids = ["SEC-WARRANT-MISSING-RESULT"] + balance_security_id = None + consideration_text = None + comments = []] + edits = [] + deletes = [] + +testResultingSecurityReferences_MissingConvertibleTransferResultFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addDefaultStakeholder issuer cap_table + capTableCid <- addDefaultConvertibleIssuance issuer capTableCid + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleTransfer ConvertibleTransferOcfData with + id = "TX-CONVERTIBLE-MISSING-RESULT" + date = defaultTestDate + amount = OcfMonetary with amount = 10000.0, currency = "USD" + security_id = "SEC-CV-1" + resulting_security_ids = ["SEC-CONVERTIBLE-MISSING-RESULT"] + balance_security_id = None + consideration_text = None + comments = []] + edits = [] + deletes = [] + +testResultingSecurityReferences_MissingEquityCompensationTransferResultFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + capTableCid <- addDefaultEquityCompensationIssuance issuer capTableCid + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationTransfer EquityCompensationTransferOcfData with + id = "TX-EC-MISSING-RESULT" + date = defaultTestDate + quantity = 100.0 + security_id = "SEC-EC-1" + resulting_security_ids = ["SEC-EC-MISSING-RESULT"] + balance_security_id = None + consideration_text = None + comments = []] + edits = [] + deletes = [] + +testResultingSecurityReferences_StockTransferResultMustBeStock = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + capTableCid <- addDefaultStockIssuance issuer capTableCid + capTableCid <- addDefaultWarrantIssuance issuer capTableCid + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockTransfer StockTransferOcfData with + id = "TX-STOCK-WRONG-RESULT-TYPE" + date = defaultTestDate + quantity = 100.0 + security_id = "SEC-STOCK-1" + resulting_security_ids = ["SEC-WR-1"] + balance_security_id = None + consideration_text = None + comments = []] + edits = [] + deletes = [] + +testResultingSecurityReferences_MissingConsolidationResultFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + capTableCid <- addStockIssuance issuer capTableCid "TX-STOCK-CONSOLIDATE-A" "SEC-STOCK-CONSOLIDATE-A" "SH-1" "SC_COMMON" + capTableCid <- addStockIssuance issuer capTableCid "TX-STOCK-CONSOLIDATE-B" "SEC-STOCK-CONSOLIDATE-B" "SH-1" "SC_COMMON" + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockConsolidation StockConsolidationOcfData with + id = "TX-STOCK-MISSING-CONSOLIDATION-RESULT" + date = defaultTestDate + security_ids = ["SEC-STOCK-CONSOLIDATE-A", "SEC-STOCK-CONSOLIDATE-B"] + resulting_security_id = "SEC-STOCK-MISSING-CONSOLIDATION-RESULT" + reason_text = None + comments = []] + edits = [] + deletes = [] + +testResultingSecurityReferences_MissingStockReissuanceResultFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + capTableCid <- addDefaultStockIssuance issuer capTableCid + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockReissuance StockReissuanceOcfData with + id = "TX-STOCK-MISSING-REISSUANCE-RESULT" + date = defaultTestDate + security_id = "SEC-STOCK-1" + resulting_security_ids = ["SEC-STOCK-MISSING-REISSUANCE-RESULT"] + comments = [] + reason_text = None + split_transaction_id = None] + edits = [] + deletes = [] + +testResultingSecurityReferences_MissingWarrantExerciseResultFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + source <- submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [warrantExerciseSourceIssuance] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantExercise WarrantExerciseOcfData with + id = "TX-WARRANT-MISSING-EXERCISE-RESULT" + date = defaultTestDate + security_id = "SEC-WR-EXERCISE-SOURCE" + trigger_id = "WR-EXERCISE-TRIGGER" + quantity = Some 100.0 + resulting_security_ids = ["SEC-STOCK-MISSING-WARRANT-EXERCISE-RESULT"] + comments = [] + consideration_text = None] + edits = [] + deletes = [] + +testResultingSecurityReferences_MissingConvertibleConversionResultFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + capTableCid <- addDefaultConvertibleIssuance issuer capTableCid + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with + id = "TX-CONVERTIBLE-MISSING-CONVERSION-RESULT" + date = defaultTestDate + security_id = "SEC-CV-1" + trigger_id = "TRIGGER-1" + reason_text = "Conversion test" + resulting_security_ids = ["SEC-STOCK-MISSING-CONVERTIBLE-CONVERSION-RESULT"] + comments = [] + balance_security_id = None + capitalization_definition = None + quantity_converted = Some 100.0] + edits = [] + deletes = [] + +testResultingSecurityReferences_MissingEquityCompensationExerciseResultFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + capTableCid <- addDefaultEquityCompensationIssuance issuer capTableCid + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise EquityCompensationExerciseOcfData with + id = "TX-EC-MISSING-EXERCISE-RESULT" + date = defaultTestDate + security_id = "SEC-EC-1" + quantity = 100.0 + consideration_text = None + resulting_security_ids = ["SEC-STOCK-MISSING-EC-EXERCISE-RESULT"] + comments = []] + edits = [] + deletes = [] + +testResultingSecurityReferences_MissingEquityCompensationReleaseResultFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + capTableCid <- addDefaultEquityCompensationIssuance issuer capTableCid + + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationRelease EquityCompensationReleaseOcfData with + id = "TX-EC-MISSING-RELEASE-RESULT" + date = defaultTestDate + security_id = "SEC-EC-1" + settlement_date = defaultTestDate + release_price = OcfMonetary with amount = 1.0, currency = "USD" + quantity = 100.0 + resulting_security_ids = ["SEC-STOCK-MISSING-EC-RELEASE-RESULT"] + consideration_text = None + comments = []] + edits = [] + deletes = [] + +testResultingSecurityReferences_StockResultCreatedInSameBatchSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + capTableCid <- addDefaultStockIssuance issuer capTableCid + + _ <- submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = + [ stockSuccessorIssuance "TX-STOCK-SAME-BATCH-RESULT" "SEC-STOCK-SAME-BATCH-RESULT" "SH-1" "SC_COMMON" defaultTestDate 100.0 + , CT.OcfCreateStockTransfer StockTransferOcfData with + id = "TX-STOCK-SAME-BATCH-TRANSFER" + date = defaultTestDate + quantity = 100.0 + security_id = "SEC-STOCK-1" + resulting_security_ids = ["SEC-STOCK-SAME-BATCH-RESULT"] + balance_security_id = None + consideration_text = None + comments = [] + ] + edits = [] + deletes = [] + pure () + +testResultingSecurityReferences_ResultDeletionFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + capTableCid <- addDefaultStockIssuance issuer capTableCid + + result <- submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = + [ stockSuccessorIssuance "TX-STOCK-RESULT-TO-DELETE" "SEC-STOCK-RESULT-TO-DELETE" "SH-1" "SC_COMMON" defaultTestDate 100.0 + , CT.OcfCreateStockTransfer StockTransferOcfData with + id = "TX-STOCK-RESULT-DELETE-TRANSFER" + date = defaultTestDate + quantity = 100.0 + security_id = "SEC-STOCK-1" + resulting_security_ids = ["SEC-STOCK-RESULT-TO-DELETE"] + balance_security_id = None + consideration_text = None + comments = [] + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockIssuance "TX-STOCK-RESULT-TO-DELETE"] + pure () + +testResultingSecurityReferences_EditCanReplaceResultInSameBatch = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + capTableCid <- addDefaultStockIssuance issuer capTableCid + let originalTransfer = StockTransferOcfData with + id = "TX-STOCK-EDIT-RESULT" + date = defaultTestDate + quantity = 100.0 + security_id = "SEC-STOCK-1" + resulting_security_ids = ["SEC-STOCK-EDIT-RESULT-OLD"] + balance_security_id = None + consideration_text = None + comments = [] + + created <- submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = + [ stockSuccessorIssuance "TX-STOCK-EDIT-RESULT-OLD" "SEC-STOCK-EDIT-RESULT-OLD" "SH-1" "SC_COMMON" defaultTestDate 100.0 + , CT.OcfCreateStockTransfer originalTransfer + ] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd created.updatedCapTableCid CT.UpdateCapTable with + creates = [stockSuccessorIssuance "TX-STOCK-EDIT-RESULT-NEW" "SEC-STOCK-EDIT-RESULT-NEW" "SH-1" "SC_COMMON" defaultTestDate 100.0] + edits = [CT.OcfEditStockTransfer (originalTransfer with resulting_security_ids = ["SEC-STOCK-EDIT-RESULT-NEW"])] + deletes = [CT.OcfDeleteStockIssuance "TX-STOCK-EDIT-RESULT-OLD"] + pure () diff --git a/Test/daml/OpenCapTable/TestSecurityIdValidation.daml b/Test/daml/OpenCapTable/TestSecurityIdValidation.daml index f43dcb17..3c7d96cf 100644 --- a/Test/daml/OpenCapTable/TestSecurityIdValidation.daml +++ b/Test/daml/OpenCapTable/TestSecurityIdValidation.daml @@ -16,7 +16,15 @@ import qualified Fairmint.OpenCapTable.OCF.EquityCompensationTransfer as ECT import qualified Fairmint.OpenCapTable.CapTable as CT import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addPrerequisites, addDefaultStockIssuance, addDefaultWarrantIssuance, addDefaultConvertibleIssuance, addDefaultEquityCompensationIssuance) +import OpenCapTable.TestHelpers + ( addPrerequisites + , addDefaultStockIssuance + , addDefaultWarrantIssuance + , addDefaultConvertibleIssuance + , addDefaultEquityCompensationIssuance + , stockSuccessorIssuance + , warrantSuccessorIssuance + ) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -218,15 +226,18 @@ testStockTransfer_CorrectSecurityIdSucceeds = script do -- Create a stock transfer using the correct stock security_id result <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with - creates = [CT.OcfCreateStockTransfer ST.StockTransferOcfData with - id = "TX_TRANSFER_VALID" - date = DT.time (DA.date 2024 Jan 01) 0 0 0 - security_id = "SEC-STOCK-1" -- Correct stock security_id - quantity = 50.0 - comments = [] - resulting_security_ids = ["NEW-SEC-1"] - balance_security_id = None - consideration_text = None] + creates = + [ CT.OcfCreateStockTransfer ST.StockTransferOcfData with + id = "TX_TRANSFER_VALID" + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + security_id = "SEC-STOCK-1" -- Correct stock security_id + quantity = 100.0 + comments = [] + resulting_security_ids = ["NEW-SEC-1"] + balance_security_id = None + consideration_text = None + , stockSuccessorIssuance "TX_TRANSFER_VALID_RESULT" "NEW-SEC-1" "SH-1" "SC_COMMON" (DT.time (DA.date 2024 Jan 01) 0 0 0) 100.0 + ] edits = [] deletes = [] pure () @@ -240,15 +251,18 @@ testWarrantTransfer_CorrectSecurityIdSucceeds = script do -- Create a warrant transfer using the correct warrant security_id result <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with - creates = [CT.OcfCreateWarrantTransfer WT.WarrantTransferOcfData with - id = "TX_WARRANT_TRANSFER_VALID" - date = DT.time (DA.date 2024 Jan 01) 0 0 0 - security_id = "SEC-WR-1" -- Correct warrant security_id - quantity = 25.0 - comments = [] - resulting_security_ids = ["NEW-WARRANT-1"] - balance_security_id = None - consideration_text = None] + creates = + [ CT.OcfCreateWarrantTransfer WT.WarrantTransferOcfData with + id = "TX_WARRANT_TRANSFER_VALID" + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + security_id = "SEC-WR-1" -- Correct warrant security_id + quantity = 100.0 + comments = [] + resulting_security_ids = ["NEW-WARRANT-1"] + balance_security_id = None + consideration_text = None + , warrantSuccessorIssuance "TX_WARRANT_TRANSFER_VALID_RESULT" "NEW-WARRANT-1" "SH-1" (DT.time (DA.date 2024 Jan 01) 0 0 0) 100.0 + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 283bec99..70aec903 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -886,7 +886,10 @@ testStockConversionReducesIssuedSharesForCeiling = script do capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_ISSUED" "SEC_CONVERSION_ISSUED" stakeholderId stockClassId) with quantity = 100.0) result <- submit issuer do exerciseCmd capTableIssued CT.UpdateCapTable with - creates = [CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_REDUCE" "SEC_CONVERSION_ISSUED" 50.0)] + creates = + [ CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_REDUCE" "SEC_CONVERSION_ISSUED" 50.0) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CONVERSION_REDUCE_RESULT" "TX_CONVERSION_REDUCE_RESULT" stakeholderId destinationClassId) with quantity = 50.0) + ] edits = [] deletes = [] @@ -906,7 +909,10 @@ testStockConversionCreditsDestinationClassForCeiling = script do submitMustFail issuer do exerciseCmd capTableIssued CT.UpdateCapTable with - creates = [CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_DESTINATION" "SEC_CONVERSION_DESTINATION_SOURCE" 150.0)] + creates = + [ stockSuccessorIssuance "TX_CONVERSION_DESTINATION_OUTPUT" "TX_CONVERSION_DESTINATION_RESULT" stakeholderId commonClassId defaultTestDate 150.0 + , CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_DESTINATION" "SEC_CONVERSION_DESTINATION_SOURCE" 150.0) + ] edits = [] deletes = [] @@ -963,6 +969,7 @@ testStockConversionWithPartialResultingIssuancesFailsForCeiling = script do creates = [ CT.OcfCreateStockConversion conversion , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CONVERSION_PARTIAL_RESULT" "SEC_CONVERSION_PARTIAL_RESULT" stakeholderId commonClassId) with quantity = 75.0) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CONVERSION_PARTIAL_MISSING" "SEC_CONVERSION_PARTIAL_MISSING" stakeholderId commonClassId) with quantity = 75.0) ] edits = [] deletes = [] @@ -1049,11 +1056,14 @@ testStockConversionCannotReduceSecurityBelowZero = script do submitMustFail issuer do exerciseCmd capTableIssued CT.UpdateCapTable with - creates = [CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_OVER_REDUCE" "SEC_CONVERSION_OVER_REDUCE" 150.0)] + creates = + [ stockSuccessorIssuance "TX_CONVERSION_OVER_REDUCE_OUTPUT" "TX_CONVERSION_OVER_REDUCE_RESULT" stakeholderId destinationClassId defaultTestDate 150.0 + , CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_OVER_REDUCE" "SEC_CONVERSION_OVER_REDUCE" 150.0) + ] edits = [] deletes = [] -testStockConversionWithAmbiguousConversionRightsFailsForCeiling = script do +testStockConversionWithAmbiguousConversionRightsUsesExplicitResultingSecurity = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_AMBIG_COMMON" 100.0) @@ -1068,11 +1078,15 @@ testStockConversionWithAmbiguousConversionRightsFailsForCeiling = script do (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_AMBIG" "Ambiguous Conversion Holder" capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_AMBIG_ISSUED" "SEC_CONVERSION_AMBIG" stakeholderId preferredClassId) with quantity = 50.0) - submitMustFail issuer do + _ <- submit issuer do exerciseCmd capTableIssued CT.UpdateCapTable with - creates = [CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_AMBIG" "SEC_CONVERSION_AMBIG" 50.0)] + creates = + [ stockSuccessorIssuance "TX_CONVERSION_AMBIG_OUTPUT" "TX_CONVERSION_AMBIG_RESULT" stakeholderId commonClassId defaultTestDate 50.0 + , CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_AMBIG" "SEC_CONVERSION_AMBIG" 50.0) + ] edits = [] deletes = [] + pure () testStockConversionWithMultipleRightsAcceptsMatchingExplicitResult = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -1110,7 +1124,10 @@ testStockConversionWithoutConversionRightsFailsForCeiling = script do submitMustFail issuer do exerciseCmd capTableIssued CT.UpdateCapTable with - creates = [CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_NO_RIGHTS" "SEC_CONVERSION_NO_RIGHTS" 50.0)] + creates = + [ stockSuccessorIssuance "TX_CONVERSION_NO_RIGHTS_OUTPUT" "TX_CONVERSION_NO_RIGHTS_RESULT" stakeholderId stockClassId defaultTestDate 50.0 + , CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_NO_RIGHTS" "SEC_CONVERSION_NO_RIGHTS" 50.0) + ] edits = [] deletes = [] @@ -1129,6 +1146,7 @@ testStockConversionUsesRatioAdjustmentForDestinationCeiling = script do exerciseCmd capTableIssued CT.UpdateCapTable with creates = [ CT.OcfCreateStockClassConversionRatioAdjustment (stockClassConversionRatioAdjustment "TX_CONVERSION_RATIO_ADJUST" preferredClassId 2.0) + , stockSuccessorIssuance "TX_CONVERSION_RATIO_OUTPUT" "TX_CONVERSION_RATIO_RESULT" stakeholderId commonClassId defaultTestDate 150.0 , CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_RATIO" "SEC_CONVERSION_RATIO" 75.0) ] edits = [] @@ -1150,6 +1168,7 @@ testSameDateHigherStockConversionRatioAdjustmentWinsForCeiling = script do creates = [ CT.OcfCreateStockClassConversionRatioAdjustment (stockClassConversionRatioAdjustment "TX_CONVERSION_RATIO_TIE_LOW" preferredClassId 1.0) , CT.OcfCreateStockClassConversionRatioAdjustment (stockClassConversionRatioAdjustment "TX_CONVERSION_RATIO_TIE_HIGH" preferredClassId 2.0) + , stockSuccessorIssuance "TX_CONVERSION_RATIO_TIE_OUTPUT" "TX_CONVERSION_RATIO_TIE_RESULT" stakeholderId commonClassId defaultTestDate 150.0 , CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_RATIO_TIE" "SEC_CONVERSION_RATIO_TIE" 75.0) ] edits = [] @@ -1172,7 +1191,10 @@ testStockConversionWithNonRatioRightFailsForCeiling = script do submitMustFail issuer do exerciseCmd capTableIssued CT.UpdateCapTable with - creates = [CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_NON_RATIO" "SEC_CONVERSION_NON_RATIO" 50.0)] + creates = + [ stockSuccessorIssuance "TX_CONVERSION_NON_RATIO_OUTPUT" "TX_CONVERSION_NON_RATIO_RESULT" stakeholderId commonClassId defaultTestDate 50.0 + , CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_NON_RATIO" "SEC_CONVERSION_NON_RATIO" 50.0) + ] edits = [] deletes = [] @@ -1217,7 +1239,10 @@ testWarrantExerciseCannotExceedStockClassAuthorizedShares = script do submitMustFail issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateWarrantExercise (warrantExercise "TX_WARRANT_OUTPUT_EXERCISE" "WSEC_OUTPUT" 150.0)] + creates = + [ stockSuccessorIssuance "TX_WARRANT_OUTPUT_EXERCISE_OUTPUT" "TX_WARRANT_OUTPUT_EXERCISE_RESULT" stakeholderId stockClassId defaultTestDate 150.0 + , CT.OcfCreateWarrantExercise (warrantExercise "TX_WARRANT_OUTPUT_EXERCISE" "WSEC_OUTPUT" 150.0) + ] edits = [] deletes = [] @@ -1234,7 +1259,10 @@ testFixedWarrantPartialExerciseUsesExerciseQuantityForCeiling = script do _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateWarrantExercise (warrantExercise "TX_WARRANT_PARTIAL_EXERCISE" "WSEC_PARTIAL" 50.0)] + creates = + [ CT.OcfCreateWarrantExercise (warrantExercise "TX_WARRANT_PARTIAL_EXERCISE" "WSEC_PARTIAL" 50.0) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_WARRANT_PARTIAL_EXERCISE_RESULT" "TX_WARRANT_PARTIAL_EXERCISE_RESULT" stakeholderId stockClassId) with quantity = 50.0) + ] edits = [] deletes = [] pure () @@ -1318,7 +1346,7 @@ testFixedWarrantFullExerciseScalesToRemainingBalance = script do deletes = [] pure () -testWarrantExerciseWithUnknownOutputQuantityFailsForCeiling = script do +testWarrantExerciseWithUnknownInputQuantityUsesResultingSecurity = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_UNKNOWN" 100.0) @@ -1332,11 +1360,15 @@ testWarrantExerciseWithUnknownOutputQuantityFailsForCeiling = script do edits = [] deletes = [] - submitMustFail issuer do + _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateWarrantExercise ((warrantExercise "TX_WARRANT_UNKNOWN_EXERCISE" "WSEC_UNKNOWN" 50.0) with trigger_id = "TRIGGER_CUSTOM", quantity = None)] + creates = + [ stockSuccessorIssuance "TX_WARRANT_UNKNOWN_EXERCISE_OUTPUT" "TX_WARRANT_UNKNOWN_EXERCISE_RESULT" stakeholderId stockClassId defaultTestDate 50.0 + , CT.OcfCreateWarrantExercise ((warrantExercise "TX_WARRANT_UNKNOWN_EXERCISE" "WSEC_UNKNOWN" 50.0) with trigger_id = "TRIGGER_CUSTOM", quantity = None) + ] edits = [] deletes = [] + pure () testNonFixedWarrantExerciseRequiresResultingStockIssuanceForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -1356,7 +1388,8 @@ testNonFixedWarrantExerciseRequiresResultingStockIssuanceForCeiling = script do creates = [ CT.OcfCreateWarrantExercise ((warrantExercise "TX_WARRANT_NON_FIXED_EXERCISE" "WSEC_NON_FIXED" 1.0) with - trigger_id = "TRIGGER_CUSTOM_NON_FIXED") + trigger_id = "TRIGGER_CUSTOM_NON_FIXED" + resulting_security_ids = []) ] edits = [] deletes = [] @@ -1468,7 +1501,10 @@ testWarrantExerciseWithUnknownTriggerFailsForCeiling = script do submitMustFail issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateWarrantExercise ((warrantExercise "TX_WARRANT_MISSING_TRIGGER_EXERCISE" "WSEC_MISSING_TRIGGER" 50.0) with trigger_id = "TRIGGER_DOES_NOT_EXIST")] + creates = + [ stockSuccessorIssuance "TX_WARRANT_MISSING_TRIGGER_OUTPUT" "TX_WARRANT_MISSING_TRIGGER_EXERCISE_RESULT" stakeholderId stockClassId defaultTestDate 50.0 + , CT.OcfCreateWarrantExercise ((warrantExercise "TX_WARRANT_MISSING_TRIGGER_EXERCISE" "WSEC_MISSING_TRIGGER" 50.0) with trigger_id = "TRIGGER_DOES_NOT_EXIST") + ] edits = [] deletes = [] @@ -1485,7 +1521,10 @@ testEquityCompensationExerciseCannotExceedStockClassAuthorizedShares = script do submitMustFail issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationExercise (equityCompensationExercise "TX_EQUITY_COMP_EXERCISE" "ECSEC_OUTPUT" 150.0)] + creates = + [ stockSuccessorIssuance "TX_EQUITY_COMP_EXERCISE_OUTPUT" "TX_EQUITY_COMP_EXERCISE_RESULT" stakeholderId stockClassId defaultTestDate 150.0 + , CT.OcfCreateEquityCompensationExercise (equityCompensationExercise "TX_EQUITY_COMP_EXERCISE" "ECSEC_OUTPUT" 150.0) + ] edits = [] deletes = [] @@ -1642,7 +1681,7 @@ testClasslessEquityCompensationReleaseUsesSinglePlanClassForCeiling = script do let settlementDate = DT.time (DA.date 2024 Jan 03) 0 0 0 submitMustFail issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationRelease (equityCompensationRelease "TX_CLASSLESS_EQUITY_RELEASE" "ECSEC_CLASSLESS_RELEASE" releaseDate settlementDate 150.0)] + creates = [CT.OcfCreateEquityCompensationRelease ((equityCompensationRelease "TX_CLASSLESS_EQUITY_RELEASE" "ECSEC_CLASSLESS_RELEASE" releaseDate settlementDate 150.0) with resulting_security_ids = [])] edits = [] deletes = [] @@ -1724,7 +1763,7 @@ testClasslessEquityCompensationReleaseRejectsAmbiguousPlanClass = script do let settlementDate = DT.time (DA.date 2024 Jan 03) 0 0 0 submitMustFail issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationRelease (equityCompensationRelease "TX_CLASSLESS_RELEASE_AMBIGUOUS" "ECSEC_CLASSLESS_RELEASE_AMBIGUOUS" releaseDate settlementDate 50.0)] + creates = [CT.OcfCreateEquityCompensationRelease ((equityCompensationRelease "TX_CLASSLESS_RELEASE_AMBIGUOUS" "ECSEC_CLASSLESS_RELEASE_AMBIGUOUS" releaseDate settlementDate 50.0) with resulting_security_ids = [])] edits = [] deletes = [] @@ -1741,7 +1780,7 @@ testNegativeEquityCompensationExerciseCannotOffsetIssuedSharesForCeiling = scrip submitMustFail issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationExercise (equityCompensationExercise "TX_EQUITY_COMP_NEGATIVE_EXERCISE" "ECSEC_NEGATIVE" (-50.0))] + creates = [CT.OcfCreateEquityCompensationExercise ((equityCompensationExercise "TX_EQUITY_COMP_NEGATIVE_EXERCISE" "ECSEC_NEGATIVE" (-50.0)) with resulting_security_ids = [])] edits = [] deletes = [] @@ -1806,6 +1845,9 @@ testEquityCompensationReleaseUsesSettlementDateForCeiling = script do creates = [ CT.OcfCreateStockClassAuthorizedSharesAdjustment ((stockClassAuthorizedSharesAdjustment "TX_EQUITY_COMP_RELEASE_AUTH" stockClassId 150.0) with date = settlementDate) , CT.OcfCreateEquityCompensationRelease (equityCompensationRelease "TX_EQUITY_COMP_RELEASE" "ECSEC_RELEASE_DATE" releaseDate settlementDate 150.0) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_EQUITY_COMP_RELEASE_RESULT" "TX_EQUITY_COMP_RELEASE_RESULT" stakeholderId stockClassId) with + date = settlementDate + quantity = 150.0) ] edits = [] deletes = [] diff --git a/Test/daml/OpenCapTable/TestStockConsolidation.daml b/Test/daml/OpenCapTable/TestStockConsolidation.daml index 4598bf2c..b18183f8 100644 --- a/Test/daml/OpenCapTable/TestStockConsolidation.daml +++ b/Test/daml/OpenCapTable/TestStockConsolidation.daml @@ -5,7 +5,7 @@ import Fairmint.OpenCapTable.OCF.StockConsolidation import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addPrerequisites) +import OpenCapTable.TestHelpers (addPrerequisites, stockSuccessorIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -48,7 +48,8 @@ testStockConsolidation_TwoSecurities = script do result2 <- createStockIssuance issuer result.updatedCapTableCid "SSEC-001B" 25000.0 _ <- submit issuer do exerciseCmd result2.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockConsolidation StockConsolidationOcfData with + creates = [stockSuccessorIssuance "STOCK-ISS-CONSOLIDATION-1" "SSEC-001-CONSOLIDATED" "SH-1" "SC_COMMON" (DT.time (DA.date 2025 Apr 15) 0 0 0) 50000.0 + , CT.OcfCreateStockConsolidation StockConsolidationOcfData with id = "TX_STOCK_CONSOLIDATION_1" date = DT.time (DA.date 2025 Apr 15) 0 0 0 security_ids = ["SSEC-001A", "SSEC-001B"] @@ -69,7 +70,8 @@ testStockConsolidation_MultipleSecurities = script do r4 <- createStockIssuance issuer r3.updatedCapTableCid "SSEC-002D" 5000.0 _ <- submit issuer do exerciseCmd r4.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockConsolidation StockConsolidationOcfData with + creates = [stockSuccessorIssuance "STOCK-ISS-CONSOLIDATION-2" "SSEC-002-CONSOLIDATED" "SH-1" "SC_COMMON" (DT.time (DA.date 2025 Jun 30) 0 0 0) 50000.0 + , CT.OcfCreateStockConsolidation StockConsolidationOcfData with id = "TX_STOCK_CONSOLIDATION_2" date = DT.time (DA.date 2025 Jun 30) 0 0 0 security_ids = ["SSEC-002A", "SSEC-002B", "SSEC-002C", "SSEC-002D"] @@ -88,7 +90,8 @@ testStockConsolidation_MinimalFields = script do r2 <- createStockIssuance issuer r1.updatedCapTableCid "SSEC-003B" 70000.0 _ <- submit issuer do exerciseCmd r2.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockConsolidation StockConsolidationOcfData with + creates = [stockSuccessorIssuance "STOCK-ISS-CONSOLIDATION-3" "SSEC-003-CONSOLIDATED" "SH-1" "SC_COMMON" (DT.time (DA.date 2025 Aug 01) 0 0 0) 100000.0 + , CT.OcfCreateStockConsolidation StockConsolidationOcfData with id = "TX_STOCK_CONSOLIDATION_3" date = DT.time (DA.date 2025 Aug 01) 0 0 0 security_ids = ["SSEC-003A", "SSEC-003B"] diff --git a/Test/daml/OpenCapTable/TestStockConversion.daml b/Test/daml/OpenCapTable/TestStockConversion.daml index 98dcca7b..7ecb90d7 100644 --- a/Test/daml/OpenCapTable/TestStockConversion.daml +++ b/Test/daml/OpenCapTable/TestStockConversion.daml @@ -8,7 +8,7 @@ import Fairmint.OpenCapTable.Types.Stock (OcfRatio(..), OcfStockClassType(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.HelpersTriggers (ocfTriggerElectiveAtWill) import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addPrerequisites, defaultStockClass, defaultStockIssuance) +import OpenCapTable.TestHelpers (addPrerequisites, defaultStockClass, defaultStockIssuance, stockSuccessorIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -87,14 +87,17 @@ testStockConversion_PreferredToCommon = script do result <- createStockIssuance issuer capTableCid securityId 100000.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockConversion StockConversionOcfData with - id = "TX_STOCK_CONVERSION_1" - date = DT.time (DA.date 2025 Dec 15) 0 0 0 - security_id = securityId - quantity_converted = 100000.0 - resulting_security_ids = ["SSEC-COMMON-001"] - comments = ["Automatic conversion of Series A Preferred to Common upon IPO"] - balance_security_id = None] + creates = + [ CT.OcfCreateStockConversion StockConversionOcfData with + id = "TX_STOCK_CONVERSION_1" + date = DT.time (DA.date 2025 Dec 15) 0 0 0 + security_id = securityId + quantity_converted = 100000.0 + resulting_security_ids = ["SSEC-COMMON-001"] + comments = ["Automatic conversion of Series A Preferred to Common upon IPO"] + balance_security_id = None + , stockSuccessorIssuance "TX_STOCK_CONVERSION_1_RESULT" "SSEC-COMMON-001" "SH-1" "SC_COMMON" (DT.time (DA.date 2025 Dec 15) 0 0 0) 100000.0 + ] edits = [] deletes = [] pure () @@ -122,6 +125,7 @@ testStockConversion_PartialConversion = script do comments = ["Elective partial conversion by investor"] balance_security_id = Some balanceIssuance.security_id , CT.OcfCreateStockIssuance balanceIssuance + , stockSuccessorIssuance "TX_STOCK_CONVERSION_2_RESULT" "SSEC-COMMON-002" "SH-1" "SC_COMMON" conversionDate 30000.0 ] edits = [] deletes = [] @@ -136,14 +140,18 @@ testStockConversion_MultipleResulting = script do result <- createStockIssuance issuer capTableCid securityId 200000.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockConversion StockConversionOcfData with - id = "TX_STOCK_CONVERSION_3" - date = DT.time (DA.date 2025 Oct 15) 0 0 0 - security_id = securityId - quantity_converted = 200000.0 - resulting_security_ids = ["SSEC-COMMON-003A", "SSEC-COMMON-003B"] - comments = [] - balance_security_id = None] + creates = + [ CT.OcfCreateStockConversion StockConversionOcfData with + id = "TX_STOCK_CONVERSION_3" + date = DT.time (DA.date 2025 Oct 15) 0 0 0 + security_id = securityId + quantity_converted = 200000.0 + resulting_security_ids = ["SSEC-COMMON-003A", "SSEC-COMMON-003B"] + comments = [] + balance_security_id = None + , stockSuccessorIssuance "TX_STOCK_CONVERSION_3_RESULT_A" "SSEC-COMMON-003A" "SH-1" "SC_COMMON" (DT.time (DA.date 2025 Oct 15) 0 0 0) 100000.0 + , stockSuccessorIssuance "TX_STOCK_CONVERSION_3_RESULT_B" "SSEC-COMMON-003B" "SH-1" "SC_COMMON" (DT.time (DA.date 2025 Oct 15) 0 0 0) 100000.0 + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestStockReissuance.daml b/Test/daml/OpenCapTable/TestStockReissuance.daml index 11ab6716..77a23781 100644 --- a/Test/daml/OpenCapTable/TestStockReissuance.daml +++ b/Test/daml/OpenCapTable/TestStockReissuance.daml @@ -5,7 +5,7 @@ import Fairmint.OpenCapTable.OCF.StockReissuance import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addPrerequisites, stockClassSplit) +import OpenCapTable.TestHelpers (addPrerequisites, stockClassSplit, stockSuccessorIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -47,7 +47,8 @@ testStockReissuance_CertificateReplacement = script do result <- createStockIssuance issuer capTableCid securityId 50000.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockReissuance StockReissuanceOcfData with + creates = [stockSuccessorIssuance "STOCK-ISS-REISSUANCE-1" "SSEC-001-REISSUED" "SH-1" "SC_COMMON" (DT.time (DA.date 2025 Mar 15) 0 0 0) 50000.0 + , CT.OcfCreateStockReissuance StockReissuanceOcfData with id = "TX_STOCK_REISSUANCE_1" date = DT.time (DA.date 2025 Mar 15) 0 0 0 security_id = securityId @@ -72,7 +73,8 @@ testStockReissuance_StockSplit = script do deletes = [] _ <- submit issuer do exerciseCmd splitResult.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockReissuance StockReissuanceOcfData with + creates = [stockSuccessorIssuance "STOCK-ISS-REISSUANCE-2" "SSEC-002-SPLIT" "SH-1" "SC_COMMON" (DT.time (DA.date 2025 Jul 01) 0 0 0) 50000.0 + , CT.OcfCreateStockReissuance StockReissuanceOcfData with id = "TX_STOCK_REISSUANCE_2" date = DT.time (DA.date 2025 Jul 01) 0 0 0 security_id = securityId @@ -92,7 +94,9 @@ testStockReissuance_MultipleResulting = script do result <- createStockIssuance issuer capTableCid securityId 100000.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockReissuance StockReissuanceOcfData with + creates = [stockSuccessorIssuance "STOCK-ISS-REISSUANCE-3A" "SSEC-003-FREE-A" "SH-1" "SC_COMMON" (DT.time (DA.date 2025 Sep 01) 0 0 0) 50000.0 + , stockSuccessorIssuance "STOCK-ISS-REISSUANCE-3B" "SSEC-003-FREE-B" "SH-1" "SC_COMMON" (DT.time (DA.date 2025 Sep 01) 0 0 0) 50000.0 + , CT.OcfCreateStockReissuance StockReissuanceOcfData with id = "TX_STOCK_REISSUANCE_3" date = DT.time (DA.date 2025 Sep 01) 0 0 0 security_id = securityId diff --git a/Test/daml/OpenCapTable/TestStockTransfer.daml b/Test/daml/OpenCapTable/TestStockTransfer.daml index d8474d1e..a5a7b7b2 100644 --- a/Test/daml/OpenCapTable/TestStockTransfer.daml +++ b/Test/daml/OpenCapTable/TestStockTransfer.daml @@ -5,7 +5,7 @@ import Fairmint.OpenCapTable.OCF.StockTransfer import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addPrerequisites, defaultStockIssuance) +import OpenCapTable.TestHelpers (addPrerequisites, defaultStockIssuance, stockSuccessorIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -48,15 +48,19 @@ testStockTransfer_SplitTransfer = script do result <- createStockIssuance issuer capTableCid securityId 400000.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockTransfer StockTransferOcfData with - id = "TX_STOCK_TRANSFER_1" - date = DT.time (DA.date 2025 Aug 05) 0 0 0 - quantity = 400000.0 - security_id = securityId - resulting_security_ids = ["SSEC-NEW-001", "SSEC-NEW-002"] - comments = [] - balance_security_id = None -- No remainder: transferring all 400,000 shares - consideration_text = None] + creates = + [ CT.OcfCreateStockTransfer StockTransferOcfData with + id = "TX_STOCK_TRANSFER_1" + date = DT.time (DA.date 2025 Aug 05) 0 0 0 + quantity = 400000.0 + security_id = securityId + resulting_security_ids = ["SSEC-NEW-001", "SSEC-NEW-002"] + comments = [] + balance_security_id = None -- No remainder: transferring all 400,000 shares + consideration_text = None + , stockSuccessorIssuance "TX_STOCK_TRANSFER_1_RESULT_A" "SSEC-NEW-001" "SH-1" "SC_COMMON" (DT.time (DA.date 2025 Aug 05) 0 0 0) 200000.0 + , stockSuccessorIssuance "TX_STOCK_TRANSFER_1_RESULT_B" "SSEC-NEW-002" "SH-1" "SC_COMMON" (DT.time (DA.date 2025 Aug 05) 0 0 0) 200000.0 + ] edits = [] deletes = [] pure () @@ -85,6 +89,7 @@ testStockTransfer_FounderTransferWithConsideration = script do balance_security_id = Some balanceIssuance.security_id consideration_text = Some "Transfer of 75 shares at $0.0001 per share for aggregate consideration of $0.01." , CT.OcfCreateStockIssuance balanceIssuance + , stockSuccessorIssuance "TX_STOCK_TRANSFER_2_RESULT" "SSEC-TRANSFEREE-001" "SH-1" "SC_COMMON" transferDate 75.0 ] edits = [] deletes = [] @@ -98,15 +103,18 @@ testStockTransfer_FullTransfer = script do result <- createStockIssuance issuer capTableCid securityId 10000.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockTransfer StockTransferOcfData with - id = "TX_STOCK_TRANSFER_FULL" - date = DT.time (DA.date 2025 Jan 15) 0 0 0 - quantity = 10000.0 - security_id = securityId - resulting_security_ids = ["SSEC-NEWOWNER-001"] - comments = [] - balance_security_id = None - consideration_text = Some "Secondary sale at $1.50 per share."] + creates = + [ CT.OcfCreateStockTransfer StockTransferOcfData with + id = "TX_STOCK_TRANSFER_FULL" + date = DT.time (DA.date 2025 Jan 15) 0 0 0 + quantity = 10000.0 + security_id = securityId + resulting_security_ids = ["SSEC-NEWOWNER-001"] + comments = [] + balance_security_id = None + consideration_text = Some "Secondary sale at $1.50 per share." + , stockSuccessorIssuance "TX_STOCK_TRANSFER_FULL_RESULT" "SSEC-NEWOWNER-001" "SH-1" "SC_COMMON" (DT.time (DA.date 2025 Jan 15) 0 0 0) 10000.0 + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestWarrantCancellation.daml b/Test/daml/OpenCapTable/TestWarrantCancellation.daml index 1e79b783..079dba69 100644 --- a/Test/daml/OpenCapTable/TestWarrantCancellation.daml +++ b/Test/daml/OpenCapTable/TestWarrantCancellation.daml @@ -12,12 +12,12 @@ import qualified DA.Time as DT import Daml.Script -- Create a warrant issuance to cancel (follows real workflow) -createWarrantIssuance issuer capTableCid securityId quantity = +createWarrantIssuance issuer capTableCid securityId quantity issuanceDate = submit issuer do exerciseCmd capTableCid CT.UpdateCapTable with creates = [CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with - id = "TX_WARRANT_ISS_1" - date = DT.time (DA.date 2023 Jun 01) 0 0 0 + id = "TX_WARRANT_ISS_" <> securityId + date = issuanceDate security_id = securityId custom_id = "W-1" stakeholder_id = "SH-1" @@ -57,7 +57,7 @@ testWarrantCancellation_CreateAndArchiveByIssuer = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "WSEC-001" - result <- createWarrantIssuance issuer capTableCid securityId 50000.0 + result <- createWarrantIssuance issuer capTableCid securityId 50000.0 (DT.time (DA.date 2023 Jun 01) 0 0 0) _ <- createWarrantCancellation issuer result.updatedCapTableCid securityId 50000.0 pure () @@ -66,7 +66,7 @@ testWarrantCancellation_CreateAndArchiveBuiltIn = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "WSEC-002" - result <- createWarrantIssuance issuer capTableCid securityId 100000.0 + result <- createWarrantIssuance issuer capTableCid securityId 100000.0 (DT.time (DA.date 2023 Jun 01) 0 0 0) _ <- createWarrantCancellation issuer result.updatedCapTableCid securityId 100000.0 pure () @@ -75,9 +75,10 @@ testWarrantCancellation_PartialWithOptionals = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "WSEC-PARTIAL" - result <- createWarrantIssuance issuer capTableCid securityId 75000.0 + result <- createWarrantIssuance issuer capTableCid securityId 75000.0 (DT.time (DA.date 2023 Jun 01) 0 0 0) + balanceResult <- createWarrantIssuance issuer result.updatedCapTableCid "WSEC-PARTIAL-BAL" 50000.0 (DT.time (DA.date 2024 Jun 30) 0 0 0) _ <- submit issuer do - exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + exerciseCmd balanceResult.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateWarrantCancellation WarrantCancellationOcfData with id = "TX_WARRANT_CANCEL_PARTIAL" date = DT.time (DA.date 2024 Jun 30) 0 0 0 diff --git a/Test/daml/OpenCapTable/TestWarrantExercise.daml b/Test/daml/OpenCapTable/TestWarrantExercise.daml index 809017b8..40e8664d 100644 --- a/Test/daml/OpenCapTable/TestWarrantExercise.daml +++ b/Test/daml/OpenCapTable/TestWarrantExercise.daml @@ -7,7 +7,7 @@ import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Stock (OcfQuantitySourceType(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addPrerequisites) +import OpenCapTable.TestHelpers (addPrerequisites, stockSuccessorIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -65,7 +65,8 @@ testWarrantExercise_BasicExercise = script do result <- createWarrantIssuance issuer capTableCid securityId 10000.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateWarrantExercise WarrantExerciseOcfData with + creates = [stockSuccessorIssuance "STOCK-ISS-WARRANT-EXERCISE-1" "SSEC-FROM-WARRANT-001" "SH-1" "SC_COMMON" (DT.time (DA.date 2025 Jun 15) 0 0 0) 10000.0 + , CT.OcfCreateWarrantExercise WarrantExerciseOcfData with id = "TX_WARRANT_EXERCISE_1" date = DT.time (DA.date 2025 Jun 15) 0 0 0 security_id = securityId @@ -86,7 +87,8 @@ testWarrantExercise_PartialWithComments = script do result <- createWarrantIssuance issuer capTableCid securityId 50000.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateWarrantExercise WarrantExerciseOcfData with + creates = [stockSuccessorIssuance "STOCK-ISS-WARRANT-EXERCISE-2" "SSEC-FROM-WARRANT-002" "SH-1" "SC_COMMON" (DT.time (DA.date 2025 Aug 01) 0 0 0) 25000.0 + , CT.OcfCreateWarrantExercise WarrantExerciseOcfData with id = "TX_WARRANT_EXERCISE_2" date = DT.time (DA.date 2025 Aug 01) 0 0 0 security_id = securityId @@ -107,7 +109,8 @@ testWarrantExercise_MinimalFields = script do result <- createWarrantIssuance issuer capTableCid securityId 5000.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateWarrantExercise WarrantExerciseOcfData with + creates = [stockSuccessorIssuance "STOCK-ISS-WARRANT-EXERCISE-3" "SSEC-FROM-WARRANT-003" "SH-1" "SC_COMMON" (DT.time (DA.date 2025 Sep 30) 0 0 0) 5000.0 + , CT.OcfCreateWarrantExercise WarrantExerciseOcfData with id = "TX_WARRANT_EXERCISE_3" date = DT.time (DA.date 2025 Sep 30) 0 0 0 security_id = securityId diff --git a/Test/daml/OpenCapTable/TestWarrantTransfer.daml b/Test/daml/OpenCapTable/TestWarrantTransfer.daml index 579a669d..4112803d 100644 --- a/Test/daml/OpenCapTable/TestWarrantTransfer.daml +++ b/Test/daml/OpenCapTable/TestWarrantTransfer.daml @@ -6,19 +6,19 @@ import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import OpenCapTable.HelpersTriggers (ocfTriggerElectiveAtWill) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addDefaultStakeholder) +import OpenCapTable.TestHelpers (addDefaultStakeholder, warrantSuccessorIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT import Daml.Script -- Create a warrant issuance to transfer (follows real workflow) -createWarrantIssuance issuer capTableCid securityId quantity = +createWarrantIssuance issuer capTableCid securityId quantity issuanceDate = submit issuer do exerciseCmd capTableCid CT.UpdateCapTable with creates = [CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with - id = "TX_WARRANT_ISS_1" - date = DT.time (DA.date 2023 Jun 01) 0 0 0 + id = "TX_WARRANT_ISS_" <> securityId + date = issuanceDate security_id = securityId custom_id = "W-1" stakeholder_id = "SH-1" @@ -45,10 +45,11 @@ testWarrantTransfer_SecondarySale = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "WSEC-001" - result <- createWarrantIssuance issuer capTableCid securityId 10000.0 + result <- createWarrantIssuance issuer capTableCid securityId 10000.0 (DT.time (DA.date 2023 Jun 01) 0 0 0) _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateWarrantTransfer WarrantTransferOcfData with + creates = [warrantSuccessorIssuance "WR-ISS-TRANSFERRED-001" "WSEC-TRANSFERRED-001" "SH-1" (DT.time (DA.date 2025 Mar 15) 0 0 0) 10000.0 + , CT.OcfCreateWarrantTransfer WarrantTransferOcfData with id = "TX_WARRANT_TRANSFER_1" date = DT.time (DA.date 2025 Mar 15) 0 0 0 quantity = 10000.0 @@ -66,10 +67,12 @@ testWarrantTransfer_PartialTransfer = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "WSEC-PARTIAL" - result <- createWarrantIssuance issuer capTableCid securityId 50000.0 + result <- createWarrantIssuance issuer capTableCid securityId 50000.0 (DT.time (DA.date 2023 Jun 01) 0 0 0) + balanceResult <- createWarrantIssuance issuer result.updatedCapTableCid "WSEC-PARTIAL-REMAIN" 30000.0 (DT.time (DA.date 2025 Jun 01) 0 0 0) _ <- submit issuer do - exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateWarrantTransfer WarrantTransferOcfData with + exerciseCmd balanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = [warrantSuccessorIssuance "WR-ISS-NEW-HOLDER" "WSEC-NEW-HOLDER" "SH-1" (DT.time (DA.date 2025 Jun 01) 0 0 0) 20000.0 + , CT.OcfCreateWarrantTransfer WarrantTransferOcfData with id = "TX_WARRANT_TRANSFER_PARTIAL" date = DT.time (DA.date 2025 Jun 01) 0 0 0 quantity = 20000.0 @@ -87,10 +90,11 @@ testWarrantTransfer_FullTransferNoConsideration = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "WSEC-FULL" - result <- createWarrantIssuance issuer capTableCid securityId 5000.0 + result <- createWarrantIssuance issuer capTableCid securityId 5000.0 (DT.time (DA.date 2023 Jun 01) 0 0 0) _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateWarrantTransfer WarrantTransferOcfData with + creates = [warrantSuccessorIssuance "WR-ISS-NEWOWNER" "WSEC-NEWOWNER" "SH-1" (DT.time (DA.date 2025 Jul 20) 0 0 0) 5000.0 + , CT.OcfCreateWarrantTransfer WarrantTransferOcfData with id = "TX_WARRANT_TRANSFER_FULL" date = DT.time (DA.date 2025 Jul 20) 0 0 0 quantity = 5000.0 diff --git a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar index 5f816ce6..5e61ba20 100644 --- a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f564040e913fbcad6ff7d7d8aa359dfa6790cdeb7c00b78f9ac3534f5c08407 -size 2940801 +oid sha256:363e38442038eb3f894e628ac8911f9b1c95bc825fe5e2a54a9e9387a9c2f37a +size 2947649 diff --git a/dars/dars.lock b/dars/dars.lock index e883a7cb..f93035df 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,8 +40,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar": { - "sha256": "0f564040e913fbcad6ff7d7d8aa359dfa6790cdeb7c00b78f9ac3534f5c08407", - "size": 2940801, + "sha256": "363e38442038eb3f894e628ac8911f9b1c95bc825fe5e2a54a9e9387a9c2f37a", + "size": 2947649, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-10T10:45:50.000Z", "networks": [] diff --git a/scripts/codegen/captable-config.yaml b/scripts/codegen/captable-config.yaml index 86b43dff..011f7744 100644 --- a/scripts/codegen/captable-config.yaml +++ b/scripts/codegen/captable-config.yaml @@ -96,38 +96,70 @@ validations: # Stock plan adjustments - validate stock plan exists StockPlanPoolAdjustment: [stock_plan_id] StockPlanReturnToPool: [stock_plan_id] - # Stock transactions - validate security_id exists in stock_issuances_by_security_id - StockTransfer: [security_id:stock_issuances_by_security_id] - StockCancellation: [security_id:stock_issuances_by_security_id] + # Security lifecycle transactions validate source, balance, and resulting + # securities against their type-specific final issuance maps. + StockTransfer: + - security_id:stock_issuances_by_security_id + - balance_security_id?:stock_issuances_by_security_id + - 'resulting_security_ids[]:stock_issuances_by_security_id' + StockCancellation: [security_id:stock_issuances_by_security_id, 'balance_security_id?:stock_issuances_by_security_id'] StockAcceptance: [security_id:stock_issuances_by_security_id] StockRetraction: [security_id:stock_issuances_by_security_id] - StockConversion: [security_id:stock_issuances_by_security_id] - StockRepurchase: [security_id:stock_issuances_by_security_id] - StockReissuance: [security_id:stock_issuances_by_security_id] + StockConversion: + - security_id:stock_issuances_by_security_id + - balance_security_id?:stock_issuances_by_security_id + - 'resulting_security_ids[]:stock_issuances_by_security_id' + StockRepurchase: [security_id:stock_issuances_by_security_id, 'balance_security_id?:stock_issuances_by_security_id'] + StockReissuance: + [security_id:stock_issuances_by_security_id, 'resulting_security_ids[]:stock_issuances_by_security_id'] # StockConsolidation: uses security_ids (plural array) - validate each element - StockConsolidation: ['security_ids[]:stock_issuances_by_security_id'] + StockConsolidation: + ['security_ids[]:stock_issuances_by_security_id', 'resulting_security_id:stock_issuances_by_security_id'] # Warrant transactions - validate security_id exists in warrant_issuances_by_security_id - WarrantTransfer: [security_id:warrant_issuances_by_security_id] - WarrantCancellation: [security_id:warrant_issuances_by_security_id] + WarrantTransfer: + - security_id:warrant_issuances_by_security_id + - balance_security_id?:warrant_issuances_by_security_id + - 'resulting_security_ids[]:warrant_issuances_by_security_id' + WarrantCancellation: + [security_id:warrant_issuances_by_security_id, 'balance_security_id?:warrant_issuances_by_security_id'] WarrantAcceptance: [security_id:warrant_issuances_by_security_id] WarrantRetraction: [security_id:warrant_issuances_by_security_id] - WarrantExercise: [security_id:warrant_issuances_by_security_id] + WarrantExercise: + [security_id:warrant_issuances_by_security_id, 'resulting_security_ids[]:stock_issuances_by_security_id'] # Convertible transactions - validate security_id exists in convertible_issuances_by_security_id - ConvertibleTransfer: [security_id:convertible_issuances_by_security_id] - ConvertibleCancellation: [security_id:convertible_issuances_by_security_id] + ConvertibleTransfer: + - security_id:convertible_issuances_by_security_id + - balance_security_id?:convertible_issuances_by_security_id + - 'resulting_security_ids[]:convertible_issuances_by_security_id' + ConvertibleCancellation: + [security_id:convertible_issuances_by_security_id, 'balance_security_id?:convertible_issuances_by_security_id'] ConvertibleAcceptance: [security_id:convertible_issuances_by_security_id] ConvertibleRetraction: [security_id:convertible_issuances_by_security_id] - ConvertibleConversion: [security_id:convertible_issuances_by_security_id] + ConvertibleConversion: + - security_id:convertible_issuances_by_security_id + - balance_security_id?:convertible_issuances_by_security_id + - 'resulting_security_ids[]:stock_issuances_by_security_id' # Equity compensation transactions - validate security_id exists in equity_compensation_issuances_by_security_id - EquityCompensationTransfer: [security_id:equity_compensation_issuances_by_security_id] - EquityCompensationCancellation: [security_id:equity_compensation_issuances_by_security_id] + EquityCompensationTransfer: + - security_id:equity_compensation_issuances_by_security_id + - balance_security_id?:equity_compensation_issuances_by_security_id + - 'resulting_security_ids[]:equity_compensation_issuances_by_security_id' + EquityCompensationCancellation: + [ + security_id:equity_compensation_issuances_by_security_id, + 'balance_security_id?:equity_compensation_issuances_by_security_id', + ] EquityCompensationAcceptance: [security_id:equity_compensation_issuances_by_security_id] EquityCompensationRetraction: [security_id:equity_compensation_issuances_by_security_id] - EquityCompensationExercise: [security_id:equity_compensation_issuances_by_security_id] - EquityCompensationRelease: [security_id:equity_compensation_issuances_by_security_id] + EquityCompensationExercise: + - security_id:equity_compensation_issuances_by_security_id + - 'resulting_security_ids[]:stock_issuances_by_security_id' + EquityCompensationRelease: + - security_id:equity_compensation_issuances_by_security_id + - 'resulting_security_ids[]:stock_issuances_by_security_id' EquityCompensationRepricing: [security_id:equity_compensation_issuances_by_security_id] # Vesting transactions - validate security_id exists in any issuance type that supports vesting diff --git a/scripts/codegen/templates/loops/edit-case.daml b/scripts/codegen/templates/loops/edit-case.daml index 975309fa..288e1c43 100644 --- a/scripts/codegen/templates/loops/edit-case.daml +++ b/scripts/codegen/templates/loops/edit-case.daml @@ -4,30 +4,7 @@ OcfEdit{{name}} d -> do let oldCidOpt = Map.lookup d.id maps.{{map_field}} assertMsg "{{name}} not found" (isSome oldCidOpt) let Some oldCid = oldCidOpt -{{#each validations}} -{{#if is_optional}} -{{#if or_maps}} - case d.{{field}} of - Some refId -> assertMsg ("{{error}}: " <> refId) (isSome (Map.lookup refId maps.{{firstMap}}) || isSome (Map.lookup refId maps.{{secondMap}}) || isSome (Map.lookup refId maps.{{thirdMap}})) - None -> pure () -{{else}} - case d.{{field}} of - Some refId -> assertMsg ("{{error}}: " <> refId) (isSome (Map.lookup refId maps.{{map}})) - None -> pure () -{{/if}} -{{else}} -{{#if is_array}} - -- Validate each element in array field {{field}} - mapA_ (\secId -> assertMsg ("{{error}}: " <> secId) (isSome (Map.lookup secId maps.{{map}}))) d.{{field}} -{{else}} -{{#if or_maps}} - assertMsg ("{{error}}: " <> d.{{field}}) (isSome (Map.lookup d.{{field}} maps.{{firstMap}}) || isSome (Map.lookup d.{{field}} maps.{{secondMap}}) || isSome (Map.lookup d.{{field}} maps.{{thirdMap}})) -{{else}} - assertMsg ("{{error}}: " <> d.{{field}}) (isSome (Map.lookup d.{{field}} maps.{{map}})) -{{/if}} -{{/if}} -{{/if}} -{{/each}} + -- validateConfiguredReferences checks edited contracts after finalMaps includes batch creates. {{#if validates_issuer_id_reference}} validateIssuerReference issuerCid d.issuer_id {{/if}} From 4c0c5a23f3586e5be2dca28474e0ef50006856f4 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 21:06:24 -0400 Subject: [PATCH 24/27] chore: refresh reference integrity DAR after restack --- dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar index e00712c2..7487707b 100644 --- a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26d24d0fbc1e24b33a5ee001f227eecf827d3872948572f964d158ba420c98eb -size 3038906 +oid sha256:239daa6f154a1f97783c2c7f0ffc922a1b1af7b759c0f615d03c4d2f682c5d72 +size 3075700 diff --git a/dars/dars.lock b/dars/dars.lock index 626e43bf..5b761814 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,8 +40,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar": { - "sha256": "26d24d0fbc1e24b33a5ee001f227eecf827d3872948572f964d158ba420c98eb", - "size": 3038906, + "sha256": "239daa6f154a1f97783c2c7f0ffc922a1b1af7b759c0f615d03c4d2f682c5d72", + "size": 3075700, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-10T10:45:50.000Z", "networks": [] From 7ff47b70dc87d6898b17852ac8806bb2e6f29ea4 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 21:13:49 -0400 Subject: [PATCH 25/27] chore: refresh deterministic reference integrity DAR --- dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar index 7487707b..3e3b3e22 100644 --- a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:239daa6f154a1f97783c2c7f0ffc922a1b1af7b759c0f615d03c4d2f682c5d72 -size 3075700 +oid sha256:491016b81d744cf115848a328cdaab4ee9f9f7f4e06635fa9e53df056fc0634b +size 3050211 diff --git a/dars/dars.lock b/dars/dars.lock index c0b104ec..4a6c234b 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,8 +40,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar": { - "sha256": "239daa6f154a1f97783c2c7f0ffc922a1b1af7b759c0f615d03c4d2f682c5d72", - "size": 3075700, + "sha256": "491016b81d744cf115848a328cdaab4ee9f9f7f4e06635fa9e53df056fc0634b", + "size": 3050211, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-10T10:45:50.000Z", "networks": [] From f4788cc61d34949a2d597df77aade295161afa39 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 23:10:44 -0400 Subject: [PATCH 26/27] chore: refresh reference integrity DAR after parent fix --- dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar index 8680ce70..72131352 100644 --- a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d93ca8aa5e52c961df9eaecb55c898324360212b04fce7be4fcc33772b41b6b1 -size 3069371 +oid sha256:f321638da45f5ad87e3f91c8701eaf6ac2b846bcef7be1e8b9221144972e3997 +size 3070303 diff --git a/dars/dars.lock b/dars/dars.lock index 0db3031c..8351cbed 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,10 +40,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar": { - "sha256": "d93ca8aa5e52c961df9eaecb55c898324360212b04fce7be4fcc33772b41b6b1", - "size": 3069371, + "sha256": "f321638da45f5ad87e3f91c8701eaf6ac2b846bcef7be1e8b9221144972e3997", + "size": 3070303, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T02:37:05Z", + "uploadedAt": "2026-07-11T03:07:54Z", "networks": [] } } From 88ae4aa82427f3f1359d497434fa333420912111 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 23:47:34 -0400 Subject: [PATCH 27/27] docs: align stock plan fixture comment --- Test/daml/OpenCapTable/TestStockIssuance.daml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Test/daml/OpenCapTable/TestStockIssuance.daml b/Test/daml/OpenCapTable/TestStockIssuance.daml index 9a0aecea..d09b2824 100644 --- a/Test/daml/OpenCapTable/TestStockIssuance.daml +++ b/Test/daml/OpenCapTable/TestStockIssuance.daml @@ -60,7 +60,7 @@ testStockIssuance_OptionalsSome = script do edits = [] deletes = [] - -- Keep stock_plan_id empty here; plan-backed stock issuance is covered by stock-plan tests. + -- Exercise the optional stock_plan_id reference with the plan created above. let issuance = (defaultStockIssuance "TX_STOCK_ISS_SOME" "SEC-3" stakeholderId classId) with custom_id = "CERT-3" date = DT.time (DA.date 2024 Feb 01) 0 0 0