From 4e07ce3cfedc08890b0b0ae866d0e43cdf79a045 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 20:20:25 -0400 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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)