From 8f498c36436f91509e69c9ef8dd936f71f91eb60 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 12:04:06 -0400 Subject: [PATCH 01/19] Enforce global security id uniqueness --- OpenCapTable-v34/daml.yaml | 2 +- Test/daml.yaml | 2 +- .../TestSecurityIdValidation.daml | 66 ++++++++++++++++++- .../0.0.14/OpenCapTable-v34.dar | 3 + dars/dars.lock | 7 ++ .../codegen/templates/CapTable.daml.template | 11 ++++ .../codegen/templates/loops/create-case.daml | 2 +- .../codegen/templates/loops/edit-case.daml | 2 +- 8 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar diff --git a/OpenCapTable-v34/daml.yaml b/OpenCapTable-v34/daml.yaml index a8cb9483..5a2ffed7 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.13 +version: 0.0.14 build-options: - -Wno-crypto-text-is-alpha - --typecheck-upgrades=no diff --git a/Test/daml.yaml b/Test/daml.yaml index 16f002f3..533768e2 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.13.dar + - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.14.dar build-options: - -Wno-template-interface-depends-on-daml-script diff --git a/Test/daml/OpenCapTable/TestSecurityIdValidation.daml b/Test/daml/OpenCapTable/TestSecurityIdValidation.daml index f43dcb17..90489053 100644 --- a/Test/daml/OpenCapTable/TestSecurityIdValidation.daml +++ b/Test/daml/OpenCapTable/TestSecurityIdValidation.daml @@ -11,17 +11,39 @@ module OpenCapTable.TestSecurityIdValidation where import qualified Fairmint.OpenCapTable.OCF.StockTransfer as ST import qualified Fairmint.OpenCapTable.OCF.WarrantTransfer as WT +import qualified Fairmint.OpenCapTable.OCF.WarrantIssuance as WI import qualified Fairmint.OpenCapTable.OCF.ConvertibleTransfer as CVT import qualified Fairmint.OpenCapTable.OCF.EquityCompensationTransfer as ECT import qualified Fairmint.OpenCapTable.CapTable as CT import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addPrerequisites, addDefaultStockIssuance, addDefaultWarrantIssuance, addDefaultConvertibleIssuance, addDefaultEquityCompensationIssuance) +import OpenCapTable.TestHelpers (addPrerequisites, addDefaultStockIssuance, addDefaultWarrantIssuance, addDefaultConvertibleIssuance, addDefaultEquityCompensationIssuance, addStockIssuance, defaultStockIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT import Daml.Script +warrantIssuanceData : Text -> Text -> Text -> WI.WarrantIssuanceOcfData +warrantIssuanceData issuanceId securityId stakeholderId = WI.WarrantIssuanceOcfData with + id = issuanceId + custom_id = "WR-" <> issuanceId + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + purchase_price = OcfMonetary with amount = 1.0, currency = "USD" + security_id = securityId + stakeholder_id = stakeholderId + comments = [] + exercise_triggers = [] + security_law_exemptions = [] + vestings = [] + board_approval_date = None + consideration_text = None + exercise_price = None + quantity = Some 100.0 + quantity_source = None + stockholder_approval_date = None + vesting_terms_id = None + warrant_expiration_date = None + -- ============================================================================= -- Test: Non-existent security_id fails with clear error @@ -204,6 +226,48 @@ testEquityCompensationTransfer_WrongSecurityTypeFails = script do deletes = [] pure () +-- ============================================================================= +-- Test: security_id values are globally unique across security issuance types +-- ============================================================================= + +testSecurityIssuance_DuplicateSecurityIdAcrossTypesFails = script do + TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp + cap_table <- addPrerequisites issuer cap_table + cap_table <- addDefaultStockIssuance issuer cap_table + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (warrantIssuanceData "WR-DUP-STOCK-SECURITY" "SEC-STOCK-1" "SH-1")] + edits = [] + deletes = [] + pure () + +testSecurityIssuance_DuplicateSecurityIdAcrossTypesFailsInSameBatch = script do + TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp + cap_table <- addPrerequisites issuer cap_table + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance (defaultStockIssuance "TX-BATCH-STOCK" "SEC-BATCH-DUP" "SH-1" "SC_COMMON") + , CT.OcfCreateWarrantIssuance (warrantIssuanceData "TX-BATCH-WARRANT" "SEC-BATCH-DUP" "SH-1") + ] + edits = [] + deletes = [] + pure () + +testSecurityIssuance_EditCannotReuseSecurityIdFromAnotherType = script do + TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp + cap_table <- addPrerequisites issuer cap_table + cap_table <- addDefaultWarrantIssuance issuer cap_table + cap_table <- addStockIssuance issuer cap_table "STOCK-EDIT-SRC" "SEC-STOCK-EDIT" "SH-1" "SC_COMMON" + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockIssuance (defaultStockIssuance "STOCK-EDIT-SRC" "SEC-WR-1" "SH-1" "SC_COMMON")] + deletes = [] + pure () -- ============================================================================= -- Test: Valid security_id succeeds diff --git a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar new file mode 100644 index 00000000..9b6246f8 --- /dev/null +++ b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ce31f6067f2ee809be330b39bb593b01b852217c0ce2a56e368d6f3f2718017 +size 2756537 diff --git a/dars/dars.lock b/dars/dars.lock index 67f05953..b8ab7a29 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -39,6 +39,13 @@ "uploadedAt": "2026-07-09T14:19:15.953Z", "networks": [] }, + "OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar": { + "sha256": "1ce31f6067f2ee809be330b39bb593b01b852217c0ce2a56e368d6f3f2718017", + "size": 2756537, + "sdkVersion": "3.4.10", + "uploadedAt": "2026-07-09T16:01:43.106Z", + "networks": [] + }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { "sha256": "2ce462b8afcb1bcdbe351c56b0a47de478477f3c655da00e41cb397f63e997d6", "size": 2446238, diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index e886b968..2bb94ba0 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -103,6 +103,17 @@ toMaps ct = CapTableMaps with stock_issuances_by_security_id = ct.stock_issuances_by_security_id warrant_issuances_by_security_id = ct.warrant_issuances_by_security_id +securityIdExists : CapTableMaps -> Text -> Bool +securityIdExists maps securityId = + isSome (Map.lookup securityId maps.convertible_issuances_by_security_id) || + isSome (Map.lookup securityId maps.equity_compensation_issuances_by_security_id) || + isSome (Map.lookup securityId maps.stock_issuances_by_security_id) || + isSome (Map.lookup securityId maps.warrant_issuances_by_security_id) + +assertSecurityIdAvailable : Text -> CapTableMaps -> Update () +assertSecurityIdAvailable securityId maps = + assertMsg ("Security ID already exists: " <> securityId) (not (securityIdExists maps securityId)) + data AuthorizedSharesCeiling = AuthorizedSharesFinite Decimal | AuthorizedSharesUnlimited diff --git a/scripts/codegen/templates/loops/create-case.daml b/scripts/codegen/templates/loops/create-case.daml index d218315a..b6d1bb48 100644 --- a/scripts/codegen/templates/loops/create-case.daml +++ b/scripts/codegen/templates/loops/create-case.daml @@ -3,7 +3,7 @@ OcfCreate{{name}} d -> do assertMsg "{{name}} ID already exists" (isNone (Map.lookup d.id maps.{{map_field}})) {{#if security_id_index_map}} - assertMsg "{{name}} security_id already exists" (isNone (Map.lookup d.security_id maps.{{security_id_index_map}})) + assertSecurityIdAvailable d.security_id maps {{/if}} {{#each validations}} {{#if is_optional}} diff --git a/scripts/codegen/templates/loops/edit-case.daml b/scripts/codegen/templates/loops/edit-case.daml index 69745ee7..c28de0cc 100644 --- a/scripts/codegen/templates/loops/edit-case.daml +++ b/scripts/codegen/templates/loops/edit-case.daml @@ -46,7 +46,7 @@ OcfEdit{{name}} d -> do let oldSecurityId = oldContract.{{data_param}}.security_id -- Only check uniqueness if security_id is changing when (d.security_id /= oldSecurityId) do - assertMsg "{{name}} security_id already exists" (isNone (Map.lookup d.security_id maps.{{security_id_index_map}})) + assertSecurityIdAvailable d.security_id maps archive oldCid newCid <- create {{name}} with context = ctx, {{data_param}} = d let newMaps = maps with From 019d40135f3a9513e2b45847b77bdeba8acf49ab Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 13:29:18 -0400 Subject: [PATCH 02/19] Refresh security id DAR after vesting merge --- dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar index 9b6246f8..0314abc1 100644 --- a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1ce31f6067f2ee809be330b39bb593b01b852217c0ce2a56e368d6f3f2718017 -size 2756537 +oid sha256:36cce82a962686854d967cc5aed2e8cd7b8dc40465fa96cfdc9823c0924e9101 +size 2809187 diff --git a/dars/dars.lock b/dars/dars.lock index ab3fccfc..cb1547a8 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,10 +40,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar": { - "sha256": "1ce31f6067f2ee809be330b39bb593b01b852217c0ce2a56e368d6f3f2718017", - "size": 2756537, + "sha256": "36cce82a962686854d967cc5aed2e8cd7b8dc40465fa96cfdc9823c0924e9101", + "size": 2809187, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T16:01:43.106Z", + "uploadedAt": "2026-07-09T17:27:56Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { From b0c4766e067acd2cd46d3b58389389bc275813de Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 13:57:55 -0400 Subject: [PATCH 03/19] Refresh security id DAR after vesting update --- dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar index 0314abc1..36c4f182 100644 --- a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36cce82a962686854d967cc5aed2e8cd7b8dc40465fa96cfdc9823c0924e9101 -size 2809187 +oid sha256:110111ab78fdedf976437e4c09e897a0637f15de3fa8871c96d8329ac2fb1f23 +size 2829625 diff --git a/dars/dars.lock b/dars/dars.lock index 008f4354..a7e88a13 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,10 +40,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar": { - "sha256": "36cce82a962686854d967cc5aed2e8cd7b8dc40465fa96cfdc9823c0924e9101", - "size": 2809187, + "sha256": "110111ab78fdedf976437e4c09e897a0637f15de3fa8871c96d8329ac2fb1f23", + "size": 2829625, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T17:27:56Z", + "uploadedAt": "2026-07-09T17:56:26Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { From bb52fcdddc66392cc88cd9c0872b9029b571b262 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 14:20:46 -0400 Subject: [PATCH 04/19] Refresh security id DAR after vesting fixes --- dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar index 36c4f182..dc4f7f2d 100644 --- a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:110111ab78fdedf976437e4c09e897a0637f15de3fa8871c96d8329ac2fb1f23 -size 2829625 +oid sha256:7b35c9c4c81af1767d05111f9e3834ad56ff8145bbcf99b92325c5ad388c275d +size 2831504 diff --git a/dars/dars.lock b/dars/dars.lock index ff15deac..cf9e555b 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,10 +40,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar": { - "sha256": "110111ab78fdedf976437e4c09e897a0637f15de3fa8871c96d8329ac2fb1f23", - "size": 2829625, + "sha256": "7b35c9c4c81af1767d05111f9e3834ad56ff8145bbcf99b92325c5ad388c275d", + "size": 2831504, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T17:56:26Z", + "uploadedAt": "2026-07-09T18:19:55Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { From dfa0f14194513e683dc538eda533d24679e03285 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 14:51:55 -0400 Subject: [PATCH 05/19] Refresh security id DAR after reference fold --- dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar index dc4f7f2d..2e651c9f 100644 --- a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b35c9c4c81af1767d05111f9e3834ad56ff8145bbcf99b92325c5ad388c275d -size 2831504 +oid sha256:6245acb124034698c3c0e6d67dc3b45f50ebefc0a6e0b4eb033a73287860e57f +size 2843788 diff --git a/dars/dars.lock b/dars/dars.lock index 47e1f732..5e8694bd 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,10 +40,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar": { - "sha256": "7b35c9c4c81af1767d05111f9e3834ad56ff8145bbcf99b92325c5ad388c275d", - "size": 2831504, + "sha256": "6245acb124034698c3c0e6d67dc3b45f50ebefc0a6e0b4eb033a73287860e57f", + "size": 2843788, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T18:19:55Z", + "uploadedAt": "2026-07-09T18:50:58.571Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { From 0126fdbde7c657fc6e7c4b1a3b217702127bd703 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 15:10:26 -0400 Subject: [PATCH 06/19] Enforce transaction lifecycle dates --- OpenCapTable-v34/daml.yaml | 2 +- Test/daml.yaml | 2 +- .../TestTransactionLifecycleDates.daml | 194 ++++++++++++ .../0.0.15/OpenCapTable-v34.dar | 3 + dars/dars.lock | 7 + .../codegen/templates/CapTable.daml.template | 294 ++++++++++++++++++ 6 files changed, 500 insertions(+), 2 deletions(-) create mode 100644 Test/daml/OpenCapTable/TestTransactionLifecycleDates.daml create mode 100644 dars/OpenCapTable-v34/0.0.15/OpenCapTable-v34.dar diff --git a/OpenCapTable-v34/daml.yaml b/OpenCapTable-v34/daml.yaml index 5a2ffed7..454ede48 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.14 +version: 0.0.15 build-options: - -Wno-crypto-text-is-alpha - --typecheck-upgrades=no diff --git a/Test/daml.yaml b/Test/daml.yaml index 533768e2..3cf3a85b 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.14.dar + - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.15.dar build-options: - -Wno-template-interface-depends-on-daml-script diff --git a/Test/daml/OpenCapTable/TestTransactionLifecycleDates.daml b/Test/daml/OpenCapTable/TestTransactionLifecycleDates.daml new file mode 100644 index 00000000..c34ae9f1 --- /dev/null +++ b/Test/daml/OpenCapTable/TestTransactionLifecycleDates.daml @@ -0,0 +1,194 @@ +module OpenCapTable.TestTransactionLifecycleDates where + +import qualified Fairmint.OpenCapTable.CapTable as CT +import Fairmint.OpenCapTable.OCF.ConvertibleAcceptance (ConvertibleAcceptanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.ConvertibleTransfer (ConvertibleTransferOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationAcceptance (EquityCompensationAcceptanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationTransfer (EquityCompensationTransferOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockAcceptance (StockAcceptanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockTransfer (StockTransferOcfData(..)) +import Fairmint.OpenCapTable.OCF.WarrantAcceptance (WarrantAcceptanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.WarrantTransfer (WarrantTransferOcfData(..)) +import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import OpenCapTable.Setup +import OpenCapTable.TestHelpers + ( addConvertibleIssuance + , addDefaultStakeholder + , addEquityCompensationIssuance + , addPrerequisites + , addStockIssuance + , addWarrantIssuance + ) +import qualified DA.Date as DA +import DA.Date (Month(..)) +import qualified DA.Time as DT +import Daml.Script + +dec31 : Time +dec31 = DT.time (DA.date 2023 Dec 31) 0 0 0 + +jan02 : Time +jan02 = DT.time (DA.date 2024 Jan 02) 0 0 0 + +usd : Decimal -> OcfMonetary +usd amount = OcfMonetary with amount = amount, currency = "USD" + +stockTransfer : Text -> Text -> Text -> StockTransferOcfData +stockTransfer transferId sourceSecurityId resultingSecurityId = StockTransferOcfData with + id = transferId + date = jan02 + security_id = sourceSecurityId + quantity = 10.0 + resulting_security_ids = [resultingSecurityId] + balance_security_id = None + consideration_text = None + comments = [] + +convertibleTransfer : Text -> Text -> Text -> ConvertibleTransferOcfData +convertibleTransfer transferId sourceSecurityId resultingSecurityId = ConvertibleTransferOcfData with + id = transferId + date = jan02 + security_id = sourceSecurityId + amount = usd 100.0 + resulting_security_ids = [resultingSecurityId] + balance_security_id = None + consideration_text = None + comments = [] + +warrantTransfer : Text -> Text -> Text -> WarrantTransferOcfData +warrantTransfer transferId sourceSecurityId resultingSecurityId = WarrantTransferOcfData with + id = transferId + date = jan02 + security_id = sourceSecurityId + quantity = 10.0 + resulting_security_ids = [resultingSecurityId] + balance_security_id = None + consideration_text = None + comments = [] + +equityCompensationTransfer : Text -> Text -> Text -> EquityCompensationTransferOcfData +equityCompensationTransfer transferId sourceSecurityId resultingSecurityId = EquityCompensationTransferOcfData with + id = transferId + date = jan02 + security_id = sourceSecurityId + quantity = 10.0 + resulting_security_ids = [resultingSecurityId] + balance_security_id = None + consideration_text = None + comments = [] + +testTransactionLifecycleDates_StockAcceptanceBeforeIssuanceFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + capTable <- addStockIssuance issuer capTable "TX-STOCK-SOURCE" "SEC-STOCK-SOURCE" "SH-1" "SC_COMMON" + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateStockAcceptance StockAcceptanceOcfData with + id = "TX-STOCK-ACCEPT-BEFORE" + date = dec31 + security_id = "SEC-STOCK-SOURCE" + comments = []] + edits = [] + deletes = [] + pure () + +testTransactionLifecycleDates_ConvertibleAcceptanceBeforeIssuanceFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addDefaultStakeholder issuer cap_table + capTable <- addConvertibleIssuance issuer capTable "TX-CV-SOURCE" "SEC-CV-SOURCE" "SH-1" + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleAcceptance ConvertibleAcceptanceOcfData with + id = "TX-CV-ACCEPT-BEFORE" + date = dec31 + security_id = "SEC-CV-SOURCE" + comments = []] + edits = [] + deletes = [] + pure () + +testTransactionLifecycleDates_WarrantAcceptanceBeforeIssuanceFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addDefaultStakeholder issuer cap_table + capTable <- addWarrantIssuance issuer capTable "TX-WR-SOURCE" "SEC-WR-SOURCE" "SH-1" + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantAcceptance WarrantAcceptanceOcfData with + id = "TX-WR-ACCEPT-BEFORE" + date = dec31 + security_id = "SEC-WR-SOURCE" + comments = []] + edits = [] + deletes = [] + pure () + +testTransactionLifecycleDates_EquityCompensationAcceptanceBeforeIssuanceFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addDefaultStakeholder issuer cap_table + capTable <- addEquityCompensationIssuance issuer capTable "TX-EC-SOURCE" "SEC-EC-SOURCE" "SH-1" + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationAcceptance EquityCompensationAcceptanceOcfData with + id = "TX-EC-ACCEPT-BEFORE" + date = dec31 + security_id = "SEC-EC-SOURCE" + comments = []] + edits = [] + deletes = [] + pure () + +testTransactionLifecycleDates_StockTransferResultBeforeTransferFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + capTable <- addStockIssuance issuer capTable "TX-STOCK-SOURCE" "SEC-STOCK-SOURCE" "SH-1" "SC_COMMON" + capTable <- addStockIssuance issuer capTable "TX-STOCK-RESULT" "SEC-STOCK-RESULT" "SH-1" "SC_COMMON" + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateStockTransfer (stockTransfer "TX-STOCK-TRANSFER-LATE" "SEC-STOCK-SOURCE" "SEC-STOCK-RESULT")] + edits = [] + deletes = [] + pure () + +testTransactionLifecycleDates_ConvertibleTransferResultBeforeTransferFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addDefaultStakeholder issuer cap_table + capTable <- addConvertibleIssuance issuer capTable "TX-CV-SOURCE" "SEC-CV-SOURCE" "SH-1" + capTable <- addConvertibleIssuance issuer capTable "TX-CV-RESULT" "SEC-CV-RESULT" "SH-1" + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateConvertibleTransfer (convertibleTransfer "TX-CV-TRANSFER-LATE" "SEC-CV-SOURCE" "SEC-CV-RESULT")] + edits = [] + deletes = [] + pure () + +testTransactionLifecycleDates_WarrantTransferResultBeforeTransferFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addDefaultStakeholder issuer cap_table + capTable <- addWarrantIssuance issuer capTable "TX-WR-SOURCE" "SEC-WR-SOURCE" "SH-1" + capTable <- addWarrantIssuance issuer capTable "TX-WR-RESULT" "SEC-WR-RESULT" "SH-1" + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantTransfer (warrantTransfer "TX-WR-TRANSFER-LATE" "SEC-WR-SOURCE" "SEC-WR-RESULT")] + edits = [] + deletes = [] + pure () + +testTransactionLifecycleDates_EquityCompensationTransferResultBeforeTransferFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addDefaultStakeholder issuer cap_table + capTable <- addEquityCompensationIssuance issuer capTable "TX-EC-SOURCE" "SEC-EC-SOURCE" "SH-1" + capTable <- addEquityCompensationIssuance issuer capTable "TX-EC-RESULT" "SEC-EC-RESULT" "SH-1" + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationTransfer (equityCompensationTransfer "TX-EC-TRANSFER-LATE" "SEC-EC-SOURCE" "SEC-EC-RESULT")] + edits = [] + deletes = [] + pure () diff --git a/dars/OpenCapTable-v34/0.0.15/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.15/OpenCapTable-v34.dar new file mode 100644 index 00000000..c8fd8c33 --- /dev/null +++ b/dars/OpenCapTable-v34/0.0.15/OpenCapTable-v34.dar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea55f7375ad0040a8b234a038bbeb0a64a70d6d858ee06bc9d1fdbe30d1d2961 +size 2878343 diff --git a/dars/dars.lock b/dars/dars.lock index 5e8694bd..7127fa88 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -46,6 +46,13 @@ "uploadedAt": "2026-07-09T18:50:58.571Z", "networks": [] }, + "OpenCapTable-v34/0.0.15/OpenCapTable-v34.dar": { + "sha256": "ea55f7375ad0040a8b234a038bbeb0a64a70d6d858ee06bc9d1fdbe30d1d2961", + "size": 2878343, + "sdkVersion": "3.4.10", + "uploadedAt": "2026-07-09T19:09:14.836Z", + "networks": [] + }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { "sha256": "2ce462b8afcb1bcdbe351c56b0a47de478477f3c655da00e41cb397f63e997d6", "size": 2446238, diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 8a060ac0..49dd1428 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -1673,6 +1673,12 @@ validateOptionalStockPlanClassReference label maps stockPlanIdOpt stockClassId = None -> assertMsg (label <> " not found: " <> stockPlanId) False None -> pure () +assertDateOnOrAfter : Text -> Time -> Text -> Time -> Update () +assertDateOnOrAfter label eventDate referenceLabel referenceDate = + assertMsg + (label <> " date " <> show eventDate <> " cannot precede " <> referenceLabel <> " date " <> show referenceDate) + (eventDate >= referenceDate) + validateStockClassConversionRightReferences : CapTableMaps -> Update () validateStockClassConversionRightReferences maps = mapA_ @@ -1753,6 +1759,293 @@ validateStockIssuanceReferences maps = validateOptionalReference "Stock issuance vesting terms" maps.vesting_terms issuance.issuance_data.vesting_terms_id) (Map.values maps.stock_issuances) +validateStockSourceTransactionDate : CapTableMaps -> Text -> Time -> Text -> Update () +validateStockSourceTransactionDate maps label transactionDate securityId = + case Map.lookup securityId maps.stock_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertDateOnOrAfter label transactionDate ("stock issuance " <> issuance.issuance_data.id) issuance.issuance_data.date + None -> pure () + +validateConvertibleSourceTransactionDate : CapTableMaps -> Text -> Time -> Text -> Update () +validateConvertibleSourceTransactionDate maps label transactionDate securityId = + case Map.lookup securityId maps.convertible_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertDateOnOrAfter label transactionDate ("convertible issuance " <> issuance.issuance_data.id) issuance.issuance_data.date + None -> pure () + +validateWarrantSourceTransactionDate : CapTableMaps -> Text -> Time -> Text -> Update () +validateWarrantSourceTransactionDate maps label transactionDate securityId = + case Map.lookup securityId maps.warrant_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertDateOnOrAfter label transactionDate ("warrant issuance " <> issuance.issuance_data.id) issuance.issuance_data.date + None -> pure () + +validateEquityCompensationSourceTransactionDate : CapTableMaps -> Text -> Time -> Text -> Update () +validateEquityCompensationSourceTransactionDate maps label transactionDate securityId = + case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertDateOnOrAfter label transactionDate ("equity compensation issuance " <> issuance.issuance_data.id) issuance.issuance_data.date + None -> pure () + +validateStockOutputDate : CapTableMaps -> Text -> Time -> Text -> Update () +validateStockOutputDate maps label transactionDate securityId = + case Map.lookup securityId maps.stock_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertDateOnOrAfter (label <> " output stock issuance " <> issuance.issuance_data.id) issuance.issuance_data.date label transactionDate + None -> pure () + +validateConvertibleOutputDate : CapTableMaps -> Text -> Time -> Text -> Update () +validateConvertibleOutputDate maps label transactionDate securityId = + case Map.lookup securityId maps.convertible_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertDateOnOrAfter (label <> " output convertible issuance " <> issuance.issuance_data.id) issuance.issuance_data.date label transactionDate + None -> pure () + +validateWarrantOutputDate : CapTableMaps -> Text -> Time -> Text -> Update () +validateWarrantOutputDate maps label transactionDate securityId = + case Map.lookup securityId maps.warrant_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertDateOnOrAfter (label <> " output warrant issuance " <> issuance.issuance_data.id) issuance.issuance_data.date label transactionDate + None -> pure () + +validateEquityCompensationOutputDate : CapTableMaps -> Text -> Time -> Text -> Update () +validateEquityCompensationOutputDate maps label transactionDate securityId = + case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertDateOnOrAfter (label <> " output equity compensation issuance " <> issuance.issuance_data.id) issuance.issuance_data.date label transactionDate + None -> pure () + +validateOptionalStockOutputDate : CapTableMaps -> Text -> Time -> Optional Text -> Update () +validateOptionalStockOutputDate maps label transactionDate securityIdOpt = case securityIdOpt of + Some securityId -> validateStockOutputDate maps label transactionDate securityId + None -> pure () + +validateOptionalConvertibleOutputDate : CapTableMaps -> Text -> Time -> Optional Text -> Update () +validateOptionalConvertibleOutputDate maps label transactionDate securityIdOpt = case securityIdOpt of + Some securityId -> validateConvertibleOutputDate maps label transactionDate securityId + None -> pure () + +validateOptionalWarrantOutputDate : CapTableMaps -> Text -> Time -> Optional Text -> Update () +validateOptionalWarrantOutputDate maps label transactionDate securityIdOpt = case securityIdOpt of + Some securityId -> validateWarrantOutputDate maps label transactionDate securityId + None -> pure () + +validateOptionalEquityCompensationOutputDate : CapTableMaps -> Text -> Time -> Optional Text -> Update () +validateOptionalEquityCompensationOutputDate maps label transactionDate securityIdOpt = case securityIdOpt of + Some securityId -> validateEquityCompensationOutputDate maps label transactionDate securityId + None -> pure () + +validateStockLifecycleDates : CapTableMaps -> Update () +validateStockLifecycleDates maps = do + mapA_ + (\acceptanceCid -> do + acceptance <- fetch acceptanceCid + validateStockSourceTransactionDate maps ("Stock acceptance " <> acceptance.acceptance_data.id) acceptance.acceptance_data.date acceptance.acceptance_data.security_id) + (Map.values maps.stock_acceptances) + + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + let label = "Stock cancellation " <> cancellation.cancellation_data.id + validateStockSourceTransactionDate maps label cancellation.cancellation_data.date cancellation.cancellation_data.security_id + validateOptionalStockOutputDate maps label cancellation.cancellation_data.date cancellation.cancellation_data.balance_security_id) + (Map.values maps.stock_cancellations) + + mapA_ + (\repurchaseCid -> do + repurchase <- fetch repurchaseCid + let label = "Stock repurchase " <> repurchase.repurchase_data.id + validateStockSourceTransactionDate maps label repurchase.repurchase_data.date repurchase.repurchase_data.security_id + validateOptionalStockOutputDate maps label repurchase.repurchase_data.date repurchase.repurchase_data.balance_security_id) + (Map.values maps.stock_repurchases) + + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + let label = "Stock transfer " <> transfer.transfer_data.id + validateStockSourceTransactionDate maps label transfer.transfer_data.date transfer.transfer_data.security_id + validateOptionalStockOutputDate maps label transfer.transfer_data.date transfer.transfer_data.balance_security_id + mapA_ (validateStockOutputDate maps label transfer.transfer_data.date) transfer.transfer_data.resulting_security_ids) + (Map.values maps.stock_transfers) + + mapA_ + (\conversionCid -> do + conversion <- fetch conversionCid + let label = "Stock conversion " <> conversion.conversion_data.id + validateStockSourceTransactionDate maps label conversion.conversion_data.date conversion.conversion_data.security_id + validateOptionalStockOutputDate maps label conversion.conversion_data.date conversion.conversion_data.balance_security_id + mapA_ (validateStockOutputDate maps label conversion.conversion_data.date) conversion.conversion_data.resulting_security_ids) + (Map.values maps.stock_conversions) + + mapA_ + (\retractionCid -> do + retraction <- fetch retractionCid + validateStockSourceTransactionDate maps ("Stock retraction " <> retraction.retraction_data.id) retraction.retraction_data.date retraction.retraction_data.security_id) + (Map.values maps.stock_retractions) + + mapA_ + (\reissuanceCid -> do + reissuance <- fetch reissuanceCid + let label = "Stock reissuance " <> reissuance.reissuance_data.id + validateStockSourceTransactionDate maps label reissuance.reissuance_data.date reissuance.reissuance_data.security_id + mapA_ (validateStockOutputDate maps label reissuance.reissuance_data.date) reissuance.reissuance_data.resulting_security_ids) + (Map.values maps.stock_reissuances) + + mapA_ + (\consolidationCid -> do + consolidation <- fetch consolidationCid + let label = "Stock consolidation " <> consolidation.consolidation_data.id + mapA_ (validateStockSourceTransactionDate maps label consolidation.consolidation_data.date) consolidation.consolidation_data.security_ids + validateStockOutputDate maps label consolidation.consolidation_data.date consolidation.consolidation_data.resulting_security_id) + (Map.values maps.stock_consolidations) + +validateConvertibleLifecycleDates : CapTableMaps -> Update () +validateConvertibleLifecycleDates maps = do + mapA_ + (\acceptanceCid -> do + acceptance <- fetch acceptanceCid + validateConvertibleSourceTransactionDate maps ("Convertible acceptance " <> acceptance.acceptance_data.id) acceptance.acceptance_data.date acceptance.acceptance_data.security_id) + (Map.values maps.convertible_acceptances) + + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + let label = "Convertible cancellation " <> cancellation.cancellation_data.id + validateConvertibleSourceTransactionDate maps label cancellation.cancellation_data.date cancellation.cancellation_data.security_id + validateOptionalConvertibleOutputDate maps label cancellation.cancellation_data.date cancellation.cancellation_data.balance_security_id) + (Map.values maps.convertible_cancellations) + + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + let label = "Convertible transfer " <> transfer.transfer_data.id + validateConvertibleSourceTransactionDate maps label transfer.transfer_data.date transfer.transfer_data.security_id + validateOptionalConvertibleOutputDate maps label transfer.transfer_data.date transfer.transfer_data.balance_security_id + mapA_ (validateConvertibleOutputDate maps label transfer.transfer_data.date) transfer.transfer_data.resulting_security_ids) + (Map.values maps.convertible_transfers) + + mapA_ + (\conversionCid -> do + conversion <- fetch conversionCid + let label = "Convertible conversion " <> conversion.conversion_data.id + validateConvertibleSourceTransactionDate maps label conversion.conversion_data.date conversion.conversion_data.security_id + validateOptionalConvertibleOutputDate maps label conversion.conversion_data.date conversion.conversion_data.balance_security_id + mapA_ (validateStockOutputDate maps label conversion.conversion_data.date) conversion.conversion_data.resulting_security_ids) + (Map.values maps.convertible_conversions) + + mapA_ + (\retractionCid -> do + retraction <- fetch retractionCid + validateConvertibleSourceTransactionDate maps ("Convertible retraction " <> retraction.retraction_data.id) retraction.retraction_data.date retraction.retraction_data.security_id) + (Map.values maps.convertible_retractions) + +validateWarrantLifecycleDates : CapTableMaps -> Update () +validateWarrantLifecycleDates maps = do + mapA_ + (\acceptanceCid -> do + acceptance <- fetch acceptanceCid + validateWarrantSourceTransactionDate maps ("Warrant acceptance " <> acceptance.acceptance_data.id) acceptance.acceptance_data.date acceptance.acceptance_data.security_id) + (Map.values maps.warrant_acceptances) + + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + let label = "Warrant cancellation " <> cancellation.cancellation_data.id + validateWarrantSourceTransactionDate maps label cancellation.cancellation_data.date cancellation.cancellation_data.security_id + validateOptionalWarrantOutputDate maps label cancellation.cancellation_data.date cancellation.cancellation_data.balance_security_id) + (Map.values maps.warrant_cancellations) + + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + let label = "Warrant transfer " <> transfer.transfer_data.id + validateWarrantSourceTransactionDate maps label transfer.transfer_data.date transfer.transfer_data.security_id + validateOptionalWarrantOutputDate maps label transfer.transfer_data.date transfer.transfer_data.balance_security_id + mapA_ (validateWarrantOutputDate maps label transfer.transfer_data.date) transfer.transfer_data.resulting_security_ids) + (Map.values maps.warrant_transfers) + + mapA_ + (\exerciseCid -> do + exercise <- fetch exerciseCid + let label = "Warrant exercise " <> exercise.exercise_data.id + validateWarrantSourceTransactionDate maps label exercise.exercise_data.date exercise.exercise_data.security_id + mapA_ (validateStockOutputDate maps label exercise.exercise_data.date) exercise.exercise_data.resulting_security_ids) + (Map.values maps.warrant_exercises) + + mapA_ + (\retractionCid -> do + retraction <- fetch retractionCid + validateWarrantSourceTransactionDate maps ("Warrant retraction " <> retraction.retraction_data.id) retraction.retraction_data.date retraction.retraction_data.security_id) + (Map.values maps.warrant_retractions) + +validateEquityCompensationLifecycleDates : CapTableMaps -> Update () +validateEquityCompensationLifecycleDates maps = do + mapA_ + (\acceptanceCid -> do + acceptance <- fetch acceptanceCid + validateEquityCompensationSourceTransactionDate maps ("Equity compensation acceptance " <> acceptance.acceptance_data.id) acceptance.acceptance_data.date acceptance.acceptance_data.security_id) + (Map.values maps.equity_compensation_acceptances) + + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + let label = "Equity compensation cancellation " <> cancellation.cancellation_data.id + validateEquityCompensationSourceTransactionDate maps label cancellation.cancellation_data.date cancellation.cancellation_data.security_id + validateOptionalEquityCompensationOutputDate maps label cancellation.cancellation_data.date cancellation.cancellation_data.balance_security_id) + (Map.values maps.equity_compensation_cancellations) + + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + let label = "Equity compensation transfer " <> transfer.transfer_data.id + validateEquityCompensationSourceTransactionDate maps label transfer.transfer_data.date transfer.transfer_data.security_id + validateOptionalEquityCompensationOutputDate maps label transfer.transfer_data.date transfer.transfer_data.balance_security_id + mapA_ (validateEquityCompensationOutputDate maps label transfer.transfer_data.date) transfer.transfer_data.resulting_security_ids) + (Map.values maps.equity_compensation_transfers) + + mapA_ + (\exerciseCid -> do + exercise <- fetch exerciseCid + let label = "Equity compensation exercise " <> exercise.exercise_data.id + validateEquityCompensationSourceTransactionDate maps label exercise.exercise_data.date exercise.exercise_data.security_id + mapA_ (validateStockOutputDate maps label exercise.exercise_data.date) exercise.exercise_data.resulting_security_ids) + (Map.values maps.equity_compensation_exercises) + + mapA_ + (\releaseCid -> do + release <- fetch releaseCid + let label = "Equity compensation release " <> release.release_data.id + validateEquityCompensationSourceTransactionDate maps label release.release_data.date release.release_data.security_id + mapA_ (validateStockOutputDate maps label release.release_data.settlement_date) release.release_data.resulting_security_ids) + (Map.values maps.equity_compensation_releases) + + mapA_ + (\repricingCid -> do + repricing <- fetch repricingCid + validateEquityCompensationSourceTransactionDate maps ("Equity compensation repricing " <> repricing.repricing_data.id) repricing.repricing_data.date repricing.repricing_data.security_id) + (Map.values maps.equity_compensation_repricings) + + mapA_ + (\retractionCid -> do + retraction <- fetch retractionCid + validateEquityCompensationSourceTransactionDate maps ("Equity compensation retraction " <> retraction.retraction_data.id) retraction.retraction_data.date retraction.retraction_data.security_id) + (Map.values maps.equity_compensation_retractions) + +validateTransactionLifecycleDates : CapTableMaps -> Update () +validateTransactionLifecycleDates maps = do + validateStockLifecycleDates maps + validateConvertibleLifecycleDates maps + validateWarrantLifecycleDates maps + validateEquityCompensationLifecycleDates maps + data StockIssuanceShareNumberLot = StockIssuanceShareNumberLot with stock_class_id: Text issuance_id: Text @@ -2860,6 +3153,7 @@ template CapTable validateWarrantExerciseTriggerReferences finalMaps validateStockReissuanceSplitReferences finalMaps validateConfiguredReferences finalMaps + validateTransactionLifecycleDates finalMaps validateStockSecurityBalances finalMaps validateWarrantSecurityBalances finalMaps validateEquityCompensationSecurityBalances finalMaps From 1ae97135ed3cb779868875b0d903787c1301afc9 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 15:36:17 -0400 Subject: [PATCH 07/19] Validate final security ID uniqueness --- .../TestSecurityIdValidation.daml | 25 ++++++++++++++ .../0.0.14/OpenCapTable-v34.dar | 4 +-- dars/dars.lock | 4 +-- .../codegen/templates/CapTable.daml.template | 34 +++++++++++++++++++ 4 files changed, 63 insertions(+), 4 deletions(-) diff --git a/Test/daml/OpenCapTable/TestSecurityIdValidation.daml b/Test/daml/OpenCapTable/TestSecurityIdValidation.daml index 90489053..1cfd9ce3 100644 --- a/Test/daml/OpenCapTable/TestSecurityIdValidation.daml +++ b/Test/daml/OpenCapTable/TestSecurityIdValidation.daml @@ -20,6 +20,7 @@ import OpenCapTable.Setup import OpenCapTable.TestHelpers (addPrerequisites, addDefaultStockIssuance, addDefaultWarrantIssuance, addDefaultConvertibleIssuance, addDefaultEquityCompensationIssuance, addStockIssuance, defaultStockIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) +import qualified DA.Map as Map import qualified DA.Time as DT import Daml.Script @@ -269,6 +270,30 @@ testSecurityIssuance_EditCannotReuseSecurityIdFromAnotherType = script do deletes = [] pure () +-- A final-state validation must reject legacy or upgraded cap tables whose +-- security ID index is stale and therefore hides a duplicate issuance. +testSecurityIssuance_FinalStateDuplicateSecurityIdsFail = script do + TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp + cap_table <- addPrerequisites issuer cap_table + cap_table <- addDefaultStockIssuance issuer cap_table + capTableDataOpt <- queryContractId issuer cap_table + let Some capTableData = capTableDataOpt + + duplicateWarrantCid <- submit (actAs issuer <> actAs system_operator) $ createCmd $ WI.WarrantIssuance with + context = ctx + issuance_data = warrantIssuanceData "WR-UPGRADED-DUPLICATE" "SEC-STOCK-1" "SH-1" + + let corruptedCapTable = capTableData with + warrant_issuances = Map.insert "WR-UPGRADED-DUPLICATE" duplicateWarrantCid capTableData.warrant_issuances + corruptedCapTableCid <- submit (actAs issuer <> actAs system_operator) $ createCmd corruptedCapTable + + submitMustFail issuer do + exerciseCmd corruptedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [] + pure () + -- ============================================================================= -- Test: Valid security_id succeeds -- ============================================================================= diff --git a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar index 2e651c9f..c064dfbc 100644 --- a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6245acb124034698c3c0e6d67dc3b45f50ebefc0a6e0b4eb033a73287860e57f -size 2843788 +oid sha256:23296c13eb95bde2b3e82a0935f5829998fcccc1c7cef6f38990eb54a2f0c17c +size 2847745 diff --git a/dars/dars.lock b/dars/dars.lock index 5e8694bd..9c4cd1ee 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,8 +40,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar": { - "sha256": "6245acb124034698c3c0e6d67dc3b45f50ebefc0a6e0b4eb033a73287860e57f", - "size": 2843788, + "sha256": "23296c13eb95bde2b3e82a0935f5829998fcccc1c7cef6f38990eb54a2f0c17c", + "size": 2847745, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T18:50:58.571Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 8a060ac0..3bed5aca 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -115,6 +115,39 @@ assertSecurityIdAvailable : Text -> CapTableMaps -> Update () assertSecurityIdAvailable securityId maps = assertMsg ("Security ID already exists: " <> securityId) (not (securityIdExists maps securityId)) +securityIdsInFinalMaps : CapTableMaps -> Update [Text] +securityIdsInFinalMaps maps = do + convertibleSecurityIds <- foldlA + (\securityIds issuanceCid -> do + issuance <- fetch issuanceCid + pure (issuance.issuance_data.security_id :: securityIds)) + [] + (Map.values maps.convertible_issuances) + equityCompensationSecurityIds <- foldlA + (\securityIds issuanceCid -> do + issuance <- fetch issuanceCid + pure (issuance.issuance_data.security_id :: securityIds)) + [] + (Map.values maps.equity_compensation_issuances) + stockSecurityIds <- foldlA + (\securityIds issuanceCid -> do + issuance <- fetch issuanceCid + pure (issuance.issuance_data.security_id :: securityIds)) + [] + (Map.values maps.stock_issuances) + warrantSecurityIds <- foldlA + (\securityIds issuanceCid -> do + issuance <- fetch issuanceCid + pure (issuance.issuance_data.security_id :: securityIds)) + [] + (Map.values maps.warrant_issuances) + pure (convertibleSecurityIds ++ equityCompensationSecurityIds ++ stockSecurityIds ++ warrantSecurityIds) + +validateFinalSecurityIdUniqueness : CapTableMaps -> Update () +validateFinalSecurityIdUniqueness maps = do + securityIds <- securityIdsInFinalMaps maps + assertMsg "Security IDs must be globally unique" (not (VT.hasDuplicateTexts securityIds)) + data AuthorizedSharesCeiling = AuthorizedSharesFinite Decimal | AuthorizedSharesUnlimited @@ -2844,6 +2877,7 @@ template CapTable (mapsAfterEdits, []) sortedCreates + validateFinalSecurityIdUniqueness finalMaps validateDocumentRelatedObjects finalMaps newIssuerCid validateStockClassConversionRightReferences finalMaps validateConvertibleIssuanceReferences finalMaps From 5fd583a3fa4d0966762dfda6d810171dc33a8780 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 15:46:36 -0400 Subject: [PATCH 08/19] Refresh security ID DAR after reference updates --- dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar index c064dfbc..581c3d56 100644 --- a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23296c13eb95bde2b3e82a0935f5829998fcccc1c7cef6f38990eb54a2f0c17c -size 2847745 +oid sha256:6596a60d1dce6dad668cabca85e3e78de0741ea2e8e53dd557804214a3639be3 +size 2841848 diff --git a/dars/dars.lock b/dars/dars.lock index b0ea7deb..828b4863 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,8 +40,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar": { - "sha256": "23296c13eb95bde2b3e82a0935f5829998fcccc1c7cef6f38990eb54a2f0c17c", - "size": 2847745, + "sha256": "6596a60d1dce6dad668cabca85e3e78de0741ea2e8e53dd557804214a3639be3", + "size": 2841848, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T18:50:58.571Z", "networks": [] From f27eec64b6511092cd3327df61975a4e86c44b2c Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 15:49:18 -0400 Subject: [PATCH 09/19] Refresh lifecycle DAR after reference updates --- dars/OpenCapTable-v34/0.0.15/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.15/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.15/OpenCapTable-v34.dar index c8fd8c33..55212316 100644 --- a/dars/OpenCapTable-v34/0.0.15/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.15/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea55f7375ad0040a8b234a038bbeb0a64a70d6d858ee06bc9d1fdbe30d1d2961 -size 2878343 +oid sha256:4c4658bf29b56917f41c5f7b697128b08626cc70a954ef35e1c04270e2397089 +size 2875074 diff --git a/dars/dars.lock b/dars/dars.lock index 7a6583e4..0ecfe4ea 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -47,8 +47,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.15/OpenCapTable-v34.dar": { - "sha256": "ea55f7375ad0040a8b234a038bbeb0a64a70d6d858ee06bc9d1fdbe30d1d2961", - "size": 2878343, + "sha256": "4c4658bf29b56917f41c5f7b697128b08626cc70a954ef35e1c04270e2397089", + "size": 2875074, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T19:09:14.836Z", "networks": [] From 133300b07cca652e89da107ce2ab687dbc98e13b Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 16:03:50 -0400 Subject: [PATCH 10/19] Refresh security ID DAR after vesting updates --- dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar index 581c3d56..2eb88cfc 100644 --- a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6596a60d1dce6dad668cabca85e3e78de0741ea2e8e53dd557804214a3639be3 -size 2841848 +oid sha256:2387d4aed5669f321d6abee21b1f79ec9962f0034b6d6c067f745c96a04c3003 +size 2844254 diff --git a/dars/dars.lock b/dars/dars.lock index cb33e9b5..1e8d3ae5 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,8 +40,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar": { - "sha256": "6596a60d1dce6dad668cabca85e3e78de0741ea2e8e53dd557804214a3639be3", - "size": 2841848, + "sha256": "2387d4aed5669f321d6abee21b1f79ec9962f0034b6d6c067f745c96a04c3003", + "size": 2844254, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T18:50:58.571Z", "networks": [] From e6a0e8c93a1fba71306f031295e3cc3a23ca45e6 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 16:05:39 -0400 Subject: [PATCH 11/19] Refresh lifecycle DAR after security updates --- dars/OpenCapTable-v34/0.0.15/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.15/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.15/OpenCapTable-v34.dar index 55212316..8d9d0c7b 100644 --- a/dars/OpenCapTable-v34/0.0.15/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.15/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c4658bf29b56917f41c5f7b697128b08626cc70a954ef35e1c04270e2397089 -size 2875074 +oid sha256:67c80e43ac07a6a3f03e8da58d1f62d812e3ddeadc9c50dbea120a2d54649457 +size 2877632 diff --git a/dars/dars.lock b/dars/dars.lock index ae4d6d06..83b94bb9 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -47,8 +47,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.15/OpenCapTable-v34.dar": { - "sha256": "4c4658bf29b56917f41c5f7b697128b08626cc70a954ef35e1c04270e2397089", - "size": 2875074, + "sha256": "67c80e43ac07a6a3f03e8da58d1f62d812e3ddeadc9c50dbea120a2d54649457", + "size": 2877632, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T19:09:14.836Z", "networks": [] From d08558be727ba1f95129efa807070555072adc7c Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 16:08:26 -0400 Subject: [PATCH 12/19] Expand lifecycle date validation coverage --- .../TestTransactionLifecycleDates.daml | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/Test/daml/OpenCapTable/TestTransactionLifecycleDates.daml b/Test/daml/OpenCapTable/TestTransactionLifecycleDates.daml index c34ae9f1..ac897261 100644 --- a/Test/daml/OpenCapTable/TestTransactionLifecycleDates.daml +++ b/Test/daml/OpenCapTable/TestTransactionLifecycleDates.daml @@ -18,6 +18,10 @@ import OpenCapTable.TestHelpers , addPrerequisites , addStockIssuance , addWarrantIssuance + , createStockIssuanceWith + , defaultStockIssuance + , equityCompensationRelease + , stockCancellationOn ) import qualified DA.Date as DA import DA.Date (Month(..)) @@ -192,3 +196,41 @@ testTransactionLifecycleDates_EquityCompensationTransferResultBeforeTransferFail edits = [] deletes = [] pure () + +testTransactionLifecycleDates_StockCancellationBalanceBeforeCancellationFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + capTable <- addStockIssuance issuer capTable "TX-STOCK-CANCEL-SOURCE" "SEC-STOCK-CANCEL-SOURCE" "SH-1" "SC_COMMON" + capTable <- createStockIssuanceWith issuer capTable + ((defaultStockIssuance "TX-STOCK-CANCEL-BALANCE" "SEC-STOCK-CANCEL-BALANCE" "SH-1" "SC_COMMON") with + date = dec31 + quantity = 90.0) + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockCancellation + ((stockCancellationOn "TX-STOCK-CANCEL-LATE" "SEC-STOCK-CANCEL-SOURCE" 10.0 jan02) with + balance_security_id = Some "SEC-STOCK-CANCEL-BALANCE") + ] + edits = [] + deletes = [] + +testTransactionLifecycleDates_EquityCompensationReleaseResultBeforeSettlementFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + capTable <- addEquityCompensationIssuance issuer capTable "TX-EC-RELEASE-SOURCE" "SEC-EC-RELEASE-SOURCE" "SH-1" + capTable <- createStockIssuanceWith issuer capTable + ((defaultStockIssuance "TX-EC-RELEASE-RESULT" "SEC-EC-RELEASE-RESULT" "SH-1" "SC_COMMON") with + date = jan02 + quantity = 10.0) + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationRelease + ((equityCompensationRelease "TX-EC-RELEASE-LATE" "SEC-EC-RELEASE-SOURCE" jan02 (DT.time (DA.date 2024 Jan 03) 0 0 0) 10.0) with + resulting_security_ids = ["SEC-EC-RELEASE-RESULT"]) + ] + edits = [] + deletes = [] From 603d9dd7a60bd78166aecb2c344b0ad5d803d6e1 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 08:08:43 -0400 Subject: [PATCH 13/19] fix: close lifecycle validation gaps --- .../TestTransactionLifecycleDates.daml | 106 ++++++++++++++++++ .../0.0.15/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- .../codegen/templates/CapTable.daml.template | 52 ++++++++- 4 files changed, 159 insertions(+), 9 deletions(-) diff --git a/Test/daml/OpenCapTable/TestTransactionLifecycleDates.daml b/Test/daml/OpenCapTable/TestTransactionLifecycleDates.daml index ac897261..a4bd3287 100644 --- a/Test/daml/OpenCapTable/TestTransactionLifecycleDates.daml +++ b/Test/daml/OpenCapTable/TestTransactionLifecycleDates.daml @@ -4,9 +4,13 @@ import qualified Fairmint.OpenCapTable.CapTable as CT import Fairmint.OpenCapTable.OCF.ConvertibleAcceptance (ConvertibleAcceptanceOcfData(..)) import Fairmint.OpenCapTable.OCF.ConvertibleTransfer (ConvertibleTransferOcfData(..)) import Fairmint.OpenCapTable.OCF.EquityCompensationAcceptance (EquityCompensationAcceptanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationRepricing (EquityCompensationRepricingOcfData(..)) import Fairmint.OpenCapTable.OCF.EquityCompensationTransfer (EquityCompensationTransferOcfData(..)) import Fairmint.OpenCapTable.OCF.StockAcceptance (StockAcceptanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockReissuance (StockReissuanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockRetraction (StockRetractionOcfData(..)) import Fairmint.OpenCapTable.OCF.StockTransfer (StockTransferOcfData(..)) +import Fairmint.OpenCapTable.OCF.VestingAcceleration (VestingAccelerationOcfData(..)) import Fairmint.OpenCapTable.OCF.WarrantAcceptance (WarrantAcceptanceOcfData(..)) import Fairmint.OpenCapTable.OCF.WarrantTransfer (WarrantTransferOcfData(..)) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) @@ -34,6 +38,9 @@ dec31 = DT.time (DA.date 2023 Dec 31) 0 0 0 jan02 : Time jan02 = DT.time (DA.date 2024 Jan 02) 0 0 0 +jan03 : Time +jan03 = DT.time (DA.date 2024 Jan 03) 0 0 0 + usd : Decimal -> OcfMonetary usd amount = OcfMonetary with amount = amount, currency = "USD" @@ -234,3 +241,102 @@ testTransactionLifecycleDates_EquityCompensationReleaseResultBeforeSettlementFai ] edits = [] deletes = [] + +testTransactionLifecycleDates_StockReissuanceResultBeforeTransactionFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + capTable <- addStockIssuance issuer capTable "TX-REISSUE-SOURCE" "SEC-REISSUE-SOURCE" "SH-1" "SC_COMMON" + capTable <- addStockIssuance issuer capTable "TX-REISSUE-RESULT" "SEC-REISSUE-RESULT" "SH-1" "SC_COMMON" + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateStockReissuance StockReissuanceOcfData with + id = "TX-REISSUE-LATE" + date = jan02 + security_id = "SEC-REISSUE-SOURCE" + resulting_security_ids = ["SEC-REISSUE-RESULT"] + split_transaction_id = None + reason_text = None + comments = []] + edits = [] + deletes = [] + +testTransactionLifecycleDates_StockRetractionBeforeIssuanceFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + capTable <- addStockIssuance issuer capTable "TX-RETRACTION-SOURCE" "SEC-RETRACTION-SOURCE" "SH-1" "SC_COMMON" + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateStockRetraction StockRetractionOcfData with + id = "TX-RETRACTION-BEFORE" + date = dec31 + security_id = "SEC-RETRACTION-SOURCE" + reason_text = "Predates its source issuance" + comments = []] + edits = [] + deletes = [] + +testTransactionLifecycleDates_EquityCompensationRepricingBeforeIssuanceFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addDefaultStakeholder issuer cap_table + capTable <- addEquityCompensationIssuance issuer capTable "TX-REPRICE-SOURCE" "SEC-REPRICE-SOURCE" "SH-1" + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationRepricing EquityCompensationRepricingOcfData with + id = "TX-REPRICE-BEFORE" + date = dec31 + security_id = "SEC-REPRICE-SOURCE" + new_exercise_price = usd 1.0 + comments = []] + edits = [] + deletes = [] + +testTransactionLifecycleDates_WrongTypeStockOutputFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + capTable <- addStockIssuance issuer capTable "TX-WRONG-OUTPUT-SOURCE" "SEC-WRONG-OUTPUT-SOURCE" "SH-1" "SC_COMMON" + capTable <- addConvertibleIssuance issuer capTable "TX-WRONG-OUTPUT" "SEC-WRONG-OUTPUT" "SH-1" + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateStockTransfer (stockTransfer "TX-WRONG-OUTPUT-TRANSFER" "SEC-WRONG-OUTPUT-SOURCE" "SEC-WRONG-OUTPUT")] + edits = [] + deletes = [] + +testTransactionLifecycleDates_ReleaseSettlementBeforeTransactionFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + capTable <- addEquityCompensationIssuance issuer capTable "TX-RELEASE-ORDER-SOURCE" "SEC-RELEASE-ORDER-SOURCE" "SH-1" + capTable <- createStockIssuanceWith issuer capTable + ((defaultStockIssuance "TX-RELEASE-ORDER-RESULT" "SEC-RELEASE-ORDER-RESULT" "SH-1" "SC_COMMON") with + date = jan02 + quantity = 10.0) + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationRelease + ((equityCompensationRelease "TX-RELEASE-ORDER" "SEC-RELEASE-ORDER-SOURCE" jan03 jan02 10.0) with + resulting_security_ids = ["SEC-RELEASE-ORDER-RESULT"]) + ] + edits = [] + deletes = [] + +testTransactionLifecycleDates_VestingAccelerationBeforeIssuanceFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + capTable <- addStockIssuance issuer capTable "TX-VESTING-SOURCE" "SEC-VESTING-SOURCE" "SH-1" "SC_COMMON" + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration VestingAccelerationOcfData with + id = "TX-VESTING-ACCELERATION-BEFORE" + date = dec31 + security_id = "SEC-VESTING-SOURCE" + quantity = 10.0 + reason_text = "Predates its source issuance" + comments = []] + edits = [] + deletes = [] diff --git a/dars/OpenCapTable-v34/0.0.15/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.15/OpenCapTable-v34.dar index 8d9d0c7b..f4d6449e 100644 --- a/dars/OpenCapTable-v34/0.0.15/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.15/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67c80e43ac07a6a3f03e8da58d1f62d812e3ddeadc9c50dbea120a2d54649457 -size 2877632 +oid sha256:ecad8964dd342e4bf36eb46384831bcb91a63da9fa05cac6437be82988a22670 +size 2883246 diff --git a/dars/dars.lock b/dars/dars.lock index 83b94bb9..bd652c5d 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -47,10 +47,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.15/OpenCapTable-v34.dar": { - "sha256": "67c80e43ac07a6a3f03e8da58d1f62d812e3ddeadc9c50dbea120a2d54649457", - "size": 2877632, + "sha256": "ecad8964dd342e4bf36eb46384831bcb91a63da9fa05cac6437be82988a22670", + "size": 2883246, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T19:09:14.836Z", + "uploadedAt": "2026-07-10T12:02:38.000Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 179d84b0..827d5971 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -1855,7 +1855,10 @@ validateStockOutputDate maps label transactionDate securityId = Some issuanceCid -> do issuance <- fetch issuanceCid assertDateOnOrAfter (label <> " output stock issuance " <> issuance.issuance_data.id) issuance.issuance_data.date label transactionDate - None -> pure () + None -> + assertMsg + (label <> " output security is not a stock issuance: " <> securityId) + (not (securityIdExists maps securityId)) validateConvertibleOutputDate : CapTableMaps -> Text -> Time -> Text -> Update () validateConvertibleOutputDate maps label transactionDate securityId = @@ -1863,7 +1866,10 @@ validateConvertibleOutputDate maps label transactionDate securityId = Some issuanceCid -> do issuance <- fetch issuanceCid assertDateOnOrAfter (label <> " output convertible issuance " <> issuance.issuance_data.id) issuance.issuance_data.date label transactionDate - None -> pure () + None -> + assertMsg + (label <> " output security is not a convertible issuance: " <> securityId) + (not (securityIdExists maps securityId)) validateWarrantOutputDate : CapTableMaps -> Text -> Time -> Text -> Update () validateWarrantOutputDate maps label transactionDate securityId = @@ -1871,7 +1877,10 @@ validateWarrantOutputDate maps label transactionDate securityId = Some issuanceCid -> do issuance <- fetch issuanceCid assertDateOnOrAfter (label <> " output warrant issuance " <> issuance.issuance_data.id) issuance.issuance_data.date label transactionDate - None -> pure () + None -> + assertMsg + (label <> " output security is not a warrant issuance: " <> securityId) + (not (securityIdExists maps securityId)) validateEquityCompensationOutputDate : CapTableMaps -> Text -> Time -> Text -> Update () validateEquityCompensationOutputDate maps label transactionDate securityId = @@ -1879,7 +1888,20 @@ validateEquityCompensationOutputDate maps label transactionDate securityId = Some issuanceCid -> do issuance <- fetch issuanceCid assertDateOnOrAfter (label <> " output equity compensation issuance " <> issuance.issuance_data.id) issuance.issuance_data.date label transactionDate - None -> pure () + None -> + assertMsg + (label <> " output security is not an equity compensation issuance: " <> securityId) + (not (securityIdExists maps securityId)) + +validateVestingSourceTransactionDate : CapTableMaps -> Text -> Time -> Text -> Update () +validateVestingSourceTransactionDate maps label transactionDate securityId = + case Map.lookup securityId maps.stock_issuances_by_security_id of + Some _ -> validateStockSourceTransactionDate maps label transactionDate securityId + None -> case Map.lookup securityId maps.warrant_issuances_by_security_id of + Some _ -> validateWarrantSourceTransactionDate maps label transactionDate securityId + None -> case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of + Some _ -> validateEquityCompensationSourceTransactionDate maps label transactionDate securityId + None -> pure () validateOptionalStockOutputDate : CapTableMaps -> Text -> Time -> Optional Text -> Update () validateOptionalStockOutputDate maps label transactionDate securityIdOpt = case securityIdOpt of @@ -2082,6 +2104,7 @@ validateEquityCompensationLifecycleDates maps = do release <- fetch releaseCid let label = "Equity compensation release " <> release.release_data.id validateEquityCompensationSourceTransactionDate maps label release.release_data.date release.release_data.security_id + assertDateOnOrAfter (label <> " settlement") release.release_data.settlement_date label release.release_data.date mapA_ (validateStockOutputDate maps label release.release_data.settlement_date) release.release_data.resulting_security_ids) (Map.values maps.equity_compensation_releases) @@ -2097,12 +2120,33 @@ validateEquityCompensationLifecycleDates maps = do validateEquityCompensationSourceTransactionDate maps ("Equity compensation retraction " <> retraction.retraction_data.id) retraction.retraction_data.date retraction.retraction_data.security_id) (Map.values maps.equity_compensation_retractions) +validateVestingLifecycleDates : CapTableMaps -> Update () +validateVestingLifecycleDates maps = do + mapA_ + (\startCid -> do + start <- fetch startCid + validateVestingSourceTransactionDate maps ("Vesting start " <> start.vesting_data.id) start.vesting_data.date start.vesting_data.security_id) + (Map.values maps.vesting_starts) + + mapA_ + (\eventCid -> do + event <- fetch eventCid + validateVestingSourceTransactionDate maps ("Vesting event " <> event.vesting_data.id) event.vesting_data.date event.vesting_data.security_id) + (Map.values maps.vesting_events) + + mapA_ + (\accelerationCid -> do + acceleration <- fetch accelerationCid + validateVestingSourceTransactionDate maps ("Vesting acceleration " <> acceleration.acceleration_data.id) acceleration.acceleration_data.date acceleration.acceleration_data.security_id) + (Map.values maps.vesting_accelerations) + validateTransactionLifecycleDates : CapTableMaps -> Update () validateTransactionLifecycleDates maps = do validateStockLifecycleDates maps validateConvertibleLifecycleDates maps validateWarrantLifecycleDates maps validateEquityCompensationLifecycleDates maps + validateVestingLifecycleDates maps data StockIssuanceShareNumberLot = StockIssuanceShareNumberLot with stock_class_id: Text From bae63d47c946ccf535d0e16c19e6f5e2b9c0fd93 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 18:23:53 -0400 Subject: [PATCH 14/19] feat: enforce lifecycle successor ID uniqueness --- .../TestSecurityIdValidation.daml | 81 +++++ .../0.0.14/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 276 ++++++++++++++++++ 4 files changed, 361 insertions(+), 4 deletions(-) diff --git a/Test/daml/OpenCapTable/TestSecurityIdValidation.daml b/Test/daml/OpenCapTable/TestSecurityIdValidation.daml index 1cfd9ce3..a40eb650 100644 --- a/Test/daml/OpenCapTable/TestSecurityIdValidation.daml +++ b/Test/daml/OpenCapTable/TestSecurityIdValidation.daml @@ -294,6 +294,87 @@ testSecurityIssuance_FinalStateDuplicateSecurityIdsFail = script do deletes = [] pure () +-- Lifecycle outputs identify newly created securities, so an output cannot +-- reuse the source security's identity. +testSuccessorSecurityId_CannotReuseSourceSecurityId = script do + TestOcp{issuer, cap_table} <- setupTestOcp + cap_table <- addPrerequisites issuer cap_table + cap_table <- addStockIssuance issuer cap_table "STOCK-SOURCE-REUSE" "SEC-SOURCE-REUSE" "SH-1" "SC_COMMON" + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateStockTransfer ST.StockTransferOcfData with + id = "TX-SOURCE-REUSE" + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + security_id = "SEC-SOURCE-REUSE" + quantity = 100.0 + comments = [] + resulting_security_ids = ["SEC-SOURCE-REUSE"] + balance_security_id = None + consideration_text = None] + edits = [] + deletes = [] + pure () + +-- A partial transaction creates distinct recipient and remainder securities. +testSuccessorSecurityId_BalanceCannotAlsoBeResult = script do + TestOcp{issuer, cap_table} <- setupTestOcp + cap_table <- addPrerequisites issuer cap_table + cap_table <- addStockIssuance issuer cap_table "STOCK-BALANCE-RESULT-SOURCE" "SEC-BALANCE-RESULT-SOURCE" "SH-1" "SC_COMMON" + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance + ((defaultStockIssuance "STOCK-BALANCE-RESULT" "SEC-BALANCE-RESULT" "SH-1" "SC_COMMON") with quantity = 50.0) + , CT.OcfCreateStockTransfer ST.StockTransferOcfData with + id = "TX-BALANCE-RESULT" + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + security_id = "SEC-BALANCE-RESULT-SOURCE" + quantity = 50.0 + comments = [] + resulting_security_ids = ["SEC-BALANCE-RESULT"] + balance_security_id = Some "SEC-BALANCE-RESULT" + consideration_text = None + ] + edits = [] + deletes = [] + pure () + +-- Even when successor issuances are not materialized, one security identity +-- cannot be claimed as the output of multiple lifecycle transactions. +testSuccessorSecurityId_CannotBeProducedByMultipleTransactionTypes = script do + TestOcp{issuer, cap_table} <- setupTestOcp + cap_table <- addPrerequisites issuer cap_table + cap_table <- addStockIssuance issuer cap_table "STOCK-SHARED-RESULT-A" "SEC-SHARED-RESULT-A" "SH-1" "SC_COMMON" + cap_table <- addDefaultWarrantIssuance issuer cap_table + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer ST.StockTransferOcfData with + id = "TX-SHARED-RESULT-A" + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + security_id = "SEC-SHARED-RESULT-A" + quantity = 100.0 + comments = [] + resulting_security_ids = ["SEC-SHARED-RESULT"] + balance_security_id = None + consideration_text = None + , CT.OcfCreateWarrantTransfer WT.WarrantTransferOcfData with + id = "TX-SHARED-RESULT-B" + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + security_id = "SEC-WR-1" + quantity = 100.0 + comments = [] + resulting_security_ids = ["SEC-SHARED-RESULT"] + balance_security_id = None + consideration_text = None + ] + edits = [] + deletes = [] + pure () + -- ============================================================================= -- Test: Valid security_id succeeds -- ============================================================================= diff --git a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar index 2eb88cfc..d3e1e0af 100644 --- a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2387d4aed5669f321d6abee21b1f79ec9962f0034b6d6c067f745c96a04c3003 -size 2844254 +oid sha256:8d173853748b69a7b4bbb1c291cdf27bde89bcdce1f29ccd98c7601521e28050 +size 3094585 diff --git a/dars/dars.lock b/dars/dars.lock index cf6c68f4..a4a18c86 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,8 +40,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar": { - "sha256": "2387d4aed5669f321d6abee21b1f79ec9962f0034b6d6c067f745c96a04c3003", - "size": 2844254, + "sha256": "8d173853748b69a7b4bbb1c291cdf27bde89bcdce1f29ccd98c7601521e28050", + "size": 3094585, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T18:50:58.571Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index a397a628..600ebfb6 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -149,6 +149,281 @@ validateFinalSecurityIdUniqueness maps = do securityIds <- securityIdsInFinalMaps maps assertMsg "Security IDs must be globally unique" (not (VT.hasDuplicateTexts securityIds)) +assertSuccessorSecurityIdDistinct : Text -> Text -> Text -> Update () +assertSuccessorSecurityIdDistinct label sourceSecurityId successorSecurityId = + assertMsg + (label <> " must use a new successor security ID instead of reusing " <> sourceSecurityId) + (successorSecurityId /= sourceSecurityId) + +validateSuccessorSecurityIds : Text -> Text -> Optional Text -> [Text] -> Update () +validateSuccessorSecurityIds label sourceSecurityId balanceSecurityId resultingSecurityIds = do + mapA_ (assertSuccessorSecurityIdDistinct label sourceSecurityId) resultingSecurityIds + case balanceSecurityId of + Some balanceId -> do + assertSuccessorSecurityIdDistinct label sourceSecurityId balanceId + assertMsg + (label <> " cannot use its balance security as a resulting security: " <> balanceId) + (balanceId `notElem` resultingSecurityIds) + None -> pure () + +successorSecurityIdsFor : Optional Text -> [Text] -> [Text] +successorSecurityIdsFor balanceSecurityId resultingSecurityIds = + case balanceSecurityId of + Some balanceId -> balanceId :: resultingSecurityIds + None -> resultingSecurityIds + +validateGlobalSuccessorSecurityIdUniqueness : CapTableMaps -> Update () +validateGlobalSuccessorSecurityIdUniqueness maps = do + stockTransferIds <- foldlA + (\securityIds transferCid -> do + transfer <- fetch transferCid + pure (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids ++ securityIds)) + [] + (Map.values maps.stock_transfers) + stockCancellationIds <- foldlA + (\securityIds cancellationCid -> do + cancellation <- fetch cancellationCid + pure (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [] ++ securityIds)) + stockTransferIds + (Map.values maps.stock_cancellations) + stockRepurchaseIds <- foldlA + (\securityIds repurchaseCid -> do + repurchase <- fetch repurchaseCid + pure (successorSecurityIdsFor repurchase.repurchase_data.balance_security_id [] ++ securityIds)) + stockCancellationIds + (Map.values maps.stock_repurchases) + stockConversionIds <- foldlA + (\securityIds conversionCid -> do + conversion <- fetch conversionCid + pure (successorSecurityIdsFor conversion.conversion_data.balance_security_id conversion.conversion_data.resulting_security_ids ++ securityIds)) + stockRepurchaseIds + (Map.values maps.stock_conversions) + stockReissuanceIds <- foldlA + (\securityIds reissuanceCid -> do + reissuance <- fetch reissuanceCid + pure (reissuance.reissuance_data.resulting_security_ids ++ securityIds)) + stockConversionIds + (Map.values maps.stock_reissuances) + stockConsolidationIds <- foldlA + (\securityIds consolidationCid -> do + consolidation <- fetch consolidationCid + pure (consolidation.consolidation_data.resulting_security_id :: securityIds)) + stockReissuanceIds + (Map.values maps.stock_consolidations) + warrantTransferIds <- foldlA + (\securityIds transferCid -> do + transfer <- fetch transferCid + pure (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids ++ securityIds)) + stockConsolidationIds + (Map.values maps.warrant_transfers) + warrantCancellationIds <- foldlA + (\securityIds cancellationCid -> do + cancellation <- fetch cancellationCid + pure (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [] ++ securityIds)) + warrantTransferIds + (Map.values maps.warrant_cancellations) + warrantExerciseIds <- foldlA + (\securityIds exerciseCid -> do + exercise <- fetch exerciseCid + pure (exercise.exercise_data.resulting_security_ids ++ securityIds)) + warrantCancellationIds + (Map.values maps.warrant_exercises) + convertibleTransferIds <- foldlA + (\securityIds transferCid -> do + transfer <- fetch transferCid + pure (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids ++ securityIds)) + warrantExerciseIds + (Map.values maps.convertible_transfers) + convertibleCancellationIds <- foldlA + (\securityIds cancellationCid -> do + cancellation <- fetch cancellationCid + pure (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [] ++ securityIds)) + convertibleTransferIds + (Map.values maps.convertible_cancellations) + convertibleConversionIds <- foldlA + (\securityIds conversionCid -> do + conversion <- fetch conversionCid + pure (successorSecurityIdsFor conversion.conversion_data.balance_security_id conversion.conversion_data.resulting_security_ids ++ securityIds)) + convertibleCancellationIds + (Map.values maps.convertible_conversions) + equityCompensationTransferIds <- foldlA + (\securityIds transferCid -> do + transfer <- fetch transferCid + pure (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids ++ securityIds)) + convertibleConversionIds + (Map.values maps.equity_compensation_transfers) + equityCompensationCancellationIds <- foldlA + (\securityIds cancellationCid -> do + cancellation <- fetch cancellationCid + pure (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [] ++ securityIds)) + equityCompensationTransferIds + (Map.values maps.equity_compensation_cancellations) + equityCompensationExerciseIds <- foldlA + (\securityIds exerciseCid -> do + exercise <- fetch exerciseCid + pure (exercise.exercise_data.resulting_security_ids ++ securityIds)) + equityCompensationCancellationIds + (Map.values maps.equity_compensation_exercises) + allSuccessorIds <- foldlA + (\securityIds releaseCid -> do + release <- fetch releaseCid + pure (release.release_data.resulting_security_ids ++ securityIds)) + equityCompensationExerciseIds + (Map.values maps.equity_compensation_releases) + assertMsg + "Each successor security ID may be produced by only one lifecycle transaction" + (not (VT.hasDuplicateTexts allSuccessorIds)) + +validateSuccessorSecurityIdUniqueness : CapTableMaps -> Update () +validateSuccessorSecurityIdUniqueness maps = do + validateGlobalSuccessorSecurityIdUniqueness maps + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + validateSuccessorSecurityIds + ("Stock transfer " <> transfer.transfer_data.id) + transfer.transfer_data.security_id + transfer.transfer_data.balance_security_id + transfer.transfer_data.resulting_security_ids) + (Map.values maps.stock_transfers) + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + validateSuccessorSecurityIds + ("Stock cancellation " <> cancellation.cancellation_data.id) + cancellation.cancellation_data.security_id + cancellation.cancellation_data.balance_security_id + []) + (Map.values maps.stock_cancellations) + mapA_ + (\repurchaseCid -> do + repurchase <- fetch repurchaseCid + validateSuccessorSecurityIds + ("Stock repurchase " <> repurchase.repurchase_data.id) + repurchase.repurchase_data.security_id + repurchase.repurchase_data.balance_security_id + []) + (Map.values maps.stock_repurchases) + mapA_ + (\conversionCid -> do + conversion <- fetch conversionCid + validateSuccessorSecurityIds + ("Stock conversion " <> conversion.conversion_data.id) + conversion.conversion_data.security_id + conversion.conversion_data.balance_security_id + conversion.conversion_data.resulting_security_ids) + (Map.values maps.stock_conversions) + mapA_ + (\reissuanceCid -> do + reissuance <- fetch reissuanceCid + validateSuccessorSecurityIds + ("Stock reissuance " <> reissuance.reissuance_data.id) + reissuance.reissuance_data.security_id + None + reissuance.reissuance_data.resulting_security_ids) + (Map.values maps.stock_reissuances) + mapA_ + (\consolidationCid -> do + consolidation <- fetch consolidationCid + mapA_ + (\sourceSecurityId -> + assertSuccessorSecurityIdDistinct + ("Stock consolidation " <> consolidation.consolidation_data.id) + sourceSecurityId + consolidation.consolidation_data.resulting_security_id) + consolidation.consolidation_data.security_ids) + (Map.values maps.stock_consolidations) + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + validateSuccessorSecurityIds + ("Warrant transfer " <> transfer.transfer_data.id) + transfer.transfer_data.security_id + transfer.transfer_data.balance_security_id + transfer.transfer_data.resulting_security_ids) + (Map.values maps.warrant_transfers) + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + validateSuccessorSecurityIds + ("Warrant cancellation " <> cancellation.cancellation_data.id) + cancellation.cancellation_data.security_id + cancellation.cancellation_data.balance_security_id + []) + (Map.values maps.warrant_cancellations) + mapA_ + (\exerciseCid -> do + exercise <- fetch exerciseCid + validateSuccessorSecurityIds + ("Warrant exercise " <> exercise.exercise_data.id) + exercise.exercise_data.security_id + None + exercise.exercise_data.resulting_security_ids) + (Map.values maps.warrant_exercises) + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + validateSuccessorSecurityIds + ("Convertible transfer " <> transfer.transfer_data.id) + transfer.transfer_data.security_id + transfer.transfer_data.balance_security_id + transfer.transfer_data.resulting_security_ids) + (Map.values maps.convertible_transfers) + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + validateSuccessorSecurityIds + ("Convertible cancellation " <> cancellation.cancellation_data.id) + cancellation.cancellation_data.security_id + cancellation.cancellation_data.balance_security_id + []) + (Map.values maps.convertible_cancellations) + mapA_ + (\conversionCid -> do + conversion <- fetch conversionCid + validateSuccessorSecurityIds + ("Convertible conversion " <> conversion.conversion_data.id) + conversion.conversion_data.security_id + conversion.conversion_data.balance_security_id + conversion.conversion_data.resulting_security_ids) + (Map.values maps.convertible_conversions) + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + validateSuccessorSecurityIds + ("Equity compensation transfer " <> transfer.transfer_data.id) + transfer.transfer_data.security_id + transfer.transfer_data.balance_security_id + transfer.transfer_data.resulting_security_ids) + (Map.values maps.equity_compensation_transfers) + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + validateSuccessorSecurityIds + ("Equity compensation cancellation " <> cancellation.cancellation_data.id) + cancellation.cancellation_data.security_id + cancellation.cancellation_data.balance_security_id + []) + (Map.values maps.equity_compensation_cancellations) + mapA_ + (\exerciseCid -> do + exercise <- fetch exerciseCid + validateSuccessorSecurityIds + ("Equity compensation exercise " <> exercise.exercise_data.id) + exercise.exercise_data.security_id + None + exercise.exercise_data.resulting_security_ids) + (Map.values maps.equity_compensation_exercises) + mapA_ + (\releaseCid -> do + release <- fetch releaseCid + validateSuccessorSecurityIds + ("Equity compensation release " <> release.release_data.id) + release.release_data.security_id + None + release.release_data.resulting_security_ids) + (Map.values maps.equity_compensation_releases) + data AuthorizedSharesCeiling = AuthorizedSharesFinite Decimal | AuthorizedSharesUnlimited @@ -5012,6 +5287,7 @@ template CapTable sortedCreates validateFinalSecurityIdUniqueness finalMaps + validateSuccessorSecurityIdUniqueness finalMaps validateDocumentRelatedObjects finalMaps newIssuerCid validateStockClassConversionRightReferences finalMaps validateConvertibleIssuanceReferences finalMaps From 98118c2ecf3caeeddc8076631a214e5ddda0e2c3 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 02:16:13 -0400 Subject: [PATCH 15/19] chore: refresh lifecycle DAR after parent update --- dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar index 876e5d32..9d8f31e5 100644 --- a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f95145fc2fa99758c05f94d1b0b16817276aabd18395ce0e52740ded6d07515 -size 3217689 +oid sha256:091e40c27f69c4b4c7da24fd14e5a9560238e6259dc2d82043eeef524a2158db +size 3220340 diff --git a/dars/dars.lock b/dars/dars.lock index a2f619fc..2842e2e7 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,10 +40,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar": { - "sha256": "3f95145fc2fa99758c05f94d1b0b16817276aabd18395ce0e52740ded6d07515", - "size": 3217689, + "sha256": "091e40c27f69c4b4c7da24fd14e5a9560238e6259dc2d82043eeef524a2158db", + "size": 3220340, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T05:30:53.089Z", + "uploadedAt": "2026-07-11T06:15:34.441Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { From c6726ecf5049cab1ce34b43ebe73d3e6c3cffe4b Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 04:53:07 -0400 Subject: [PATCH 16/19] chore: refresh lifecycle DAR after parent update --- dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar index 9d8f31e5..41a2d4ff 100644 --- a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:091e40c27f69c4b4c7da24fd14e5a9560238e6259dc2d82043eeef524a2158db -size 3220340 +oid sha256:77771152ca7bf851174692ce98e6edebcabb992e0c4490dfca56c8f1d41300cc +size 3234052 diff --git a/dars/dars.lock b/dars/dars.lock index 3c3b8ee7..3bdcd64f 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,10 +40,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar": { - "sha256": "091e40c27f69c4b4c7da24fd14e5a9560238e6259dc2d82043eeef524a2158db", - "size": 3220340, + "sha256": "77771152ca7bf851174692ce98e6edebcabb992e0c4490dfca56c8f1d41300cc", + "size": 3234052, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T06:15:34.441Z", + "uploadedAt": "2026-07-11T08:49:29.435Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { From 1a042c5087b417c0602e6075b8afbdbd554fe97d Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 11:55:05 -0400 Subject: [PATCH 17/19] chore: refresh lifecycle date DAR --- dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar index 41a2d4ff..2e2e0751 100644 --- a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77771152ca7bf851174692ce98e6edebcabb992e0c4490dfca56c8f1d41300cc -size 3234052 +oid sha256:20ef98b9f5c7c580bd56437fcd2eaec3bd2ef67054e75fb6ce0f06fca7f27917 +size 3234399 diff --git a/dars/dars.lock b/dars/dars.lock index d3ae149a..e524b743 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,10 +40,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar": { - "sha256": "77771152ca7bf851174692ce98e6edebcabb992e0c4490dfca56c8f1d41300cc", - "size": 3234052, + "sha256": "20ef98b9f5c7c580bd56437fcd2eaec3bd2ef67054e75fb6ce0f06fca7f27917", + "size": 3234399, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T08:49:29.435Z", + "uploadedAt": "2026-07-11T15:54:24.000Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { From 4d90aa722a7209eaabf0d9d3d74a309e276a16c8 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 12:31:31 -0400 Subject: [PATCH 18/19] fix: require exact convertible successor dates --- .../TestTransactionLifecycleDates.daml | 17 +++++++++++++++++ .../0.0.14/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- .../codegen/templates/CapTable.daml.template | 4 +++- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/Test/daml/OpenCapTable/TestTransactionLifecycleDates.daml b/Test/daml/OpenCapTable/TestTransactionLifecycleDates.daml index 08a419f0..0fe3bc2d 100644 --- a/Test/daml/OpenCapTable/TestTransactionLifecycleDates.daml +++ b/Test/daml/OpenCapTable/TestTransactionLifecycleDates.daml @@ -22,6 +22,7 @@ import OpenCapTable.TestHelpers , addPrerequisites , addStockIssuance , addWarrantIssuance + , convertibleSuccessorIssuance , createStockIssuanceWith , defaultStockIssuance , equityCompensationRelease @@ -178,6 +179,22 @@ testTransactionLifecycleDates_ConvertibleTransferResultBeforeTransferFails = scr deletes = [] pure () +testTransactionLifecycleDates_ConvertibleTransferResultAfterTransferFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addDefaultStakeholder issuer cap_table + capTable <- addConvertibleIssuance issuer capTable "TX-CV-AFTER-SOURCE" "SEC-CV-AFTER-SOURCE" "SH-1" + + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = + [ convertibleSuccessorIssuance "TX-CV-AFTER-RESULT" "SEC-CV-AFTER-RESULT" "SH-1" jan03 100.0 + , CT.OcfCreateConvertibleTransfer + (convertibleTransfer "TX-CV-AFTER-TRANSFER" "SEC-CV-AFTER-SOURCE" "SEC-CV-AFTER-RESULT") + ] + edits = [] + deletes = [] + pure () + testTransactionLifecycleDates_WarrantTransferResultBeforeTransferFails = script do TestOcp{issuer, cap_table} <- setupTestOcp capTable <- addDefaultStakeholder issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar index 2e2e0751..825ebacd 100644 --- a/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20ef98b9f5c7c580bd56437fcd2eaec3bd2ef67054e75fb6ce0f06fca7f27917 -size 3234399 +oid sha256:099c610b1248e1ef312cb15b0715e791088671c70ac13f44e7e103b22a20d88c +size 3235156 diff --git a/dars/dars.lock b/dars/dars.lock index e524b743..aab88f15 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,10 +40,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar": { - "sha256": "20ef98b9f5c7c580bd56437fcd2eaec3bd2ef67054e75fb6ce0f06fca7f27917", - "size": 3234399, + "sha256": "099c610b1248e1ef312cb15b0715e791088671c70ac13f44e7e103b22a20d88c", + "size": 3235156, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T15:54:24.000Z", + "uploadedAt": "2026-07-11T16:30:30.000Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index b7b0065c..939526ba 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -3486,7 +3486,9 @@ validateConvertibleOutputDate maps label transactionDate securityId = case Map.lookup securityId maps.convertible_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid - assertDateOnOrAfter (label <> " output convertible issuance " <> issuance.issuance_data.id) issuance.issuance_data.date label transactionDate + assertMsg + (label <> " output convertible issuance date mismatch for " <> securityId <> ": expected " <> show transactionDate <> ", found " <> show issuance.issuance_data.date) + (issuance.issuance_data.date == transactionDate) None -> assertMsg (label <> " output security is not a convertible issuance: " <> securityId) From a0306fa316d5ffb1f6372bfdf4833960880e4886 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Mon, 13 Jul 2026 15:05:44 -0400 Subject: [PATCH 19/19] Validate approval dates against event dates --- .../TestApprovalDateConsistency.daml | 181 ++++++++++++++++++ .../TestIssuerAuthorizedSharesAdjustment.daml | 4 +- ...tStockClassAuthorizedSharesAdjustment.daml | 4 +- .../TestStockPlanPoolAdjustment.daml | 4 +- .../codegen/templates/CapTable.daml.template | 73 +++++++ 5 files changed, 260 insertions(+), 6 deletions(-) create mode 100644 Test/daml/OpenCapTable/TestApprovalDateConsistency.daml diff --git a/Test/daml/OpenCapTable/TestApprovalDateConsistency.daml b/Test/daml/OpenCapTable/TestApprovalDateConsistency.daml new file mode 100644 index 00000000..659800b1 --- /dev/null +++ b/Test/daml/OpenCapTable/TestApprovalDateConsistency.daml @@ -0,0 +1,181 @@ +module OpenCapTable.TestApprovalDateConsistency where + +import qualified Fairmint.OpenCapTable.CapTable as CT +import Fairmint.OpenCapTable.OCF.IssuerAuthorizedSharesAdjustment (IssuerAuthorizedSharesAdjustmentOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockClassAuthorizedSharesAdjustment (StockClassAuthorizedSharesAdjustmentOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockPlanPoolAdjustment (StockPlanPoolAdjustmentOcfData(..)) +import Fairmint.OpenCapTable.OCF.Valuation (ValuationOcfData(..), OcfValuationType(..)) +import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import OpenCapTable.Setup +import OpenCapTable.TestHelpers + ( addDefaultStakeholder + , addDefaultStockClass + , addPrerequisites + , convertibleSuccessorIssuance + , defaultStockIssuance + , defaultStockPlan + , equityCompensationSuccessorIssuance + , warrantSuccessorIssuance + ) +import qualified DA.Date as DA +import DA.Date (Month(..)) +import qualified DA.Time as DT +import Daml.Script + +jan01 : Time +jan01 = DT.time (DA.date 2024 Jan 01) 0 0 0 + +jan02 : Time +jan02 = DT.time (DA.date 2024 Jan 02) 0 0 0 + +assertCreateFails : Party -> ContractId CT.CapTable -> CT.OcfCreateData -> Script () +assertCreateFails issuer capTable createData = do + submitMustFail issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [createData] + edits = [] + deletes = [] + pure () + +convertibleWithLateBoardApproval : CT.OcfCreateData +convertibleWithLateBoardApproval = + case convertibleSuccessorIssuance "CV-LATE-BOARD" "CSEC-LATE-BOARD" "SH-1" jan01 100.0 of + CT.OcfCreateConvertibleIssuance issuance -> + CT.OcfCreateConvertibleIssuance (issuance with board_approval_date = Some jan02) + _ -> error "Expected convertible issuance" + +equityCompensationWithLateStockholderApproval : CT.OcfCreateData +equityCompensationWithLateStockholderApproval = + case equityCompensationSuccessorIssuance "EC-LATE-STOCKHOLDER" "ECSEC-LATE-STOCKHOLDER" "SH-1" jan01 100.0 of + CT.OcfCreateEquityCompensationIssuance issuance -> + CT.OcfCreateEquityCompensationIssuance (issuance with stockholder_approval_date = Some jan02) + _ -> error "Expected equity compensation issuance" + +warrantWithLateStockholderApproval : CT.OcfCreateData +warrantWithLateStockholderApproval = + case warrantSuccessorIssuance "WR-LATE-STOCKHOLDER" "WSEC-LATE-STOCKHOLDER" "SH-1" jan01 100.0 of + CT.OcfCreateWarrantIssuance issuance -> + CT.OcfCreateWarrantIssuance (issuance with stockholder_approval_date = Some jan02) + _ -> error "Expected warrant issuance" + +issuerAdjustment : Text -> Optional Time -> Optional Time -> IssuerAuthorizedSharesAdjustmentOcfData +issuerAdjustment adjustmentId boardApprovalDate stockholderApprovalDate = + IssuerAuthorizedSharesAdjustmentOcfData with + id = adjustmentId + date = jan01 + issuer_id = "ISSUER_1" + new_shares_authorized = 200_000_000.0 + board_approval_date = boardApprovalDate + stockholder_approval_date = stockholderApprovalDate + comments = [] + +stockClassAdjustment : Optional Time -> Optional Time -> StockClassAuthorizedSharesAdjustmentOcfData +stockClassAdjustment boardApprovalDate stockholderApprovalDate = + StockClassAuthorizedSharesAdjustmentOcfData with + id = "SC-ADJ-LATE-APPROVAL" + date = jan01 + stock_class_id = "SC_COMMON" + new_shares_authorized = 2_000_000.0 + board_approval_date = boardApprovalDate + stockholder_approval_date = stockholderApprovalDate + comments = [] + +stockPlanAdjustment : Optional Time -> Optional Time -> StockPlanPoolAdjustmentOcfData +stockPlanAdjustment boardApprovalDate stockholderApprovalDate = + StockPlanPoolAdjustmentOcfData with + id = "PLAN-ADJ-LATE-APPROVAL" + date = jan01 + stock_plan_id = "PLAN-APPROVAL-DATES" + shares_reserved = 1_200.0 + board_approval_date = boardApprovalDate + stockholder_approval_date = stockholderApprovalDate + comments = [] + +valuation : Optional Time -> Optional Time -> ValuationOcfData +valuation boardApprovalDate stockholderApprovalDate = + ValuationOcfData with + id = "VALUATION-LATE-APPROVAL" + effective_date = jan01 + price_per_share = OcfMonetary with amount = 1.0, currency = "USD" + stock_class_id = "SC_COMMON" + valuation_type = OcfValuationType409A + comments = [] + board_approval_date = boardApprovalDate + provider = None + stockholder_approval_date = stockholderApprovalDate + +testApprovalDateConsistency_ConvertibleLateBoardApprovalFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addDefaultStakeholder issuer cap_table + assertCreateFails issuer capTable convertibleWithLateBoardApproval + +testApprovalDateConsistency_EquityCompensationLateStockholderApprovalFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addDefaultStakeholder issuer cap_table + assertCreateFails issuer capTable equityCompensationWithLateStockholderApproval + +testApprovalDateConsistency_IssuerAdjustmentLateBoardApprovalFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + assertCreateFails issuer cap_table (CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAdjustment "ISSUER-ADJ-LATE-BOARD" (Some jan02) None)) + +testApprovalDateConsistency_StockClassAdjustmentLateStockholderApprovalFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addDefaultStockClass issuer cap_table + assertCreateFails issuer capTable (CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAdjustment None (Some jan02))) + +testApprovalDateConsistency_StockIssuanceLateBoardApprovalFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + let issuance = (defaultStockIssuance "STOCK-LATE-BOARD" "SSEC-LATE-BOARD" "SH-1" "SC_COMMON") with + board_approval_date = Some jan02 + assertCreateFails issuer capTable (CT.OcfCreateStockIssuance issuance) + +testApprovalDateConsistency_StockPlanAdjustmentLateStockholderApprovalFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addDefaultStockClass issuer cap_table + result <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateStockPlan (defaultStockPlan "PLAN-APPROVAL-DATES" "SC_COMMON")] + edits = [] + deletes = [] + assertCreateFails issuer result.updatedCapTableCid (CT.OcfCreateStockPlanPoolAdjustment (stockPlanAdjustment None (Some jan02))) + +testApprovalDateConsistency_ValuationLateBoardApprovalFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addDefaultStockClass issuer cap_table + assertCreateFails issuer capTable (CT.OcfCreateValuation (valuation (Some jan02) None)) + +testApprovalDateConsistency_WarrantLateStockholderApprovalFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addDefaultStakeholder issuer cap_table + assertCreateFails issuer capTable warrantWithLateStockholderApproval + +testApprovalDateConsistency_ApprovalsOnTransactionDateSucceed = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTable <- addPrerequisites issuer cap_table + let issuance = (defaultStockIssuance "STOCK-SAME-DAY-APPROVAL" "SSEC-SAME-DAY-APPROVAL" "SH-1" "SC_COMMON") with + board_approval_date = Some jan01 + stockholder_approval_date = Some jan01 + + _ <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance issuance] + edits = [] + deletes = [] + pure () + +testApprovalDateConsistency_EditIsRevalidated = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let validAdjustment = issuerAdjustment "ISSUER-ADJ-EDIT-APPROVAL" (Some jan01) None + result <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateIssuerAuthorizedSharesAdjustment validAdjustment] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditIssuerAuthorizedSharesAdjustment (validAdjustment with board_approval_date = Some jan02)] + deletes = [] + pure () diff --git a/Test/daml/OpenCapTable/TestIssuerAuthorizedSharesAdjustment.daml b/Test/daml/OpenCapTable/TestIssuerAuthorizedSharesAdjustment.daml index c7f4356f..8f8be129 100644 --- a/Test/daml/OpenCapTable/TestIssuerAuthorizedSharesAdjustment.daml +++ b/Test/daml/OpenCapTable/TestIssuerAuthorizedSharesAdjustment.daml @@ -66,8 +66,8 @@ testIssuerAuthorizedSharesAdjustment_OptionalsSome = script do date = DT.time (DA.date 2024 Jan 02) 0 0 0 issuer_id = "ISSUER_1" new_shares_authorized = 2500.0 - board_approval_date = Some (DT.time (DA.date 2024 Jan 03) 0 0 0) - stockholder_approval_date = Some (DT.time (DA.date 2024 Jan 04) 0 0 0) + board_approval_date = Some (DT.time (DA.date 2024 Jan 01) 0 0 0) + stockholder_approval_date = Some (DT.time (DA.date 2024 Jan 02) 0 0 0) comments = ["Increase"]] edits = [] deletes = [] diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedSharesAdjustment.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedSharesAdjustment.daml index bab22c9e..6ab01af1 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedSharesAdjustment.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedSharesAdjustment.daml @@ -74,8 +74,8 @@ testCreateAndArchiveStockClassAuthorizedSharesAdjustmentBuiltIn = script do date = DT.time (DA.date 2024 Jan 02) 0 0 0 stock_class_id = "SC_COMMON_2" new_shares_authorized = 1_500_000.0 - board_approval_date = Some (DT.time (DA.date 2024 Jan 03) 0 0 0) - stockholder_approval_date = Some (DT.time (DA.date 2024 Jan 05) 0 0 0) + board_approval_date = Some (DT.time (DA.date 2024 Jan 01) 0 0 0) + stockholder_approval_date = Some (DT.time (DA.date 2024 Jan 02) 0 0 0) comments = ["Authorized change"]] edits = [] deletes = [] diff --git a/Test/daml/OpenCapTable/TestStockPlanPoolAdjustment.daml b/Test/daml/OpenCapTable/TestStockPlanPoolAdjustment.daml index 2f9f8ed5..acff98a7 100644 --- a/Test/daml/OpenCapTable/TestStockPlanPoolAdjustment.daml +++ b/Test/daml/OpenCapTable/TestStockPlanPoolAdjustment.daml @@ -53,8 +53,8 @@ testStockPlanPoolAdjustment_OptionalsSome = script do id = "TX_STOCK_PLAN_POOL_ADJ_2" date = DT.time (DA.date 2024 Jan 02) 0 0 0 stock_plan_id = "PLAN_1" - board_approval_date = Some (DT.time (DA.date 2024 Jan 03) 0 0 0) - stockholder_approval_date = Some (DT.time (DA.date 2024 Jan 04) 0 0 0) + board_approval_date = Some (DT.time (DA.date 2024 Jan 01) 0 0 0) + stockholder_approval_date = Some (DT.time (DA.date 2024 Jan 02) 0 0 0) shares_reserved = 1750.0 comments = ["Top up"]] edits = [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 939526ba..0a0a93d9 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -3352,6 +3352,12 @@ assertDateOnOrAfter label eventDate referenceLabel referenceDate = (label <> " date " <> show eventDate <> " cannot precede " <> referenceLabel <> " date " <> show referenceDate) (eventDate >= referenceDate) +assertOptionalApprovalDateOnOrBefore : Text -> Text -> Optional Time -> Text -> Time -> Update () +assertOptionalApprovalDateOnOrBefore label approvalLabel approvalDate eventLabel eventDate = + case approvalDate of + Some date -> assertDateOnOrAfter (label <> " " <> eventLabel) eventDate (label <> " " <> approvalLabel) date + None -> pure () + validateStockClassConversionRightReferences : CapTableMaps -> Update () validateStockClassConversionRightReferences maps = mapA_ @@ -3765,12 +3771,79 @@ validateVestingLifecycleDates maps = do validateTransactionLifecycleDates : CapTableMaps -> Update () validateTransactionLifecycleDates maps = do + validateApprovalDateConsistency maps validateStockLifecycleDates maps validateConvertibleLifecycleDates maps validateWarrantLifecycleDates maps validateEquityCompensationLifecycleDates maps validateVestingLifecycleDates maps +validateApprovalDateConsistency : CapTableMaps -> Update () +validateApprovalDateConsistency maps = do + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + let label = "Convertible issuance " <> issuance.issuance_data.id + assertOptionalApprovalDateOnOrBefore label "board approval" issuance.issuance_data.board_approval_date "transaction" issuance.issuance_data.date + assertOptionalApprovalDateOnOrBefore label "stockholder approval" issuance.issuance_data.stockholder_approval_date "transaction" issuance.issuance_data.date) + (Map.values maps.convertible_issuances) + + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + let label = "Equity compensation issuance " <> issuance.issuance_data.id + assertOptionalApprovalDateOnOrBefore label "board approval" issuance.issuance_data.board_approval_date "transaction" issuance.issuance_data.date + assertOptionalApprovalDateOnOrBefore label "stockholder approval" issuance.issuance_data.stockholder_approval_date "transaction" issuance.issuance_data.date) + (Map.values maps.equity_compensation_issuances) + + mapA_ + (\adjustmentCid -> do + adjustment <- fetch adjustmentCid + let label = "Issuer authorized shares adjustment " <> adjustment.adjustment_data.id + assertOptionalApprovalDateOnOrBefore label "board approval" adjustment.adjustment_data.board_approval_date "transaction" adjustment.adjustment_data.date + assertOptionalApprovalDateOnOrBefore label "stockholder approval" adjustment.adjustment_data.stockholder_approval_date "transaction" adjustment.adjustment_data.date) + (Map.values maps.issuer_authorized_shares_adjustments) + + mapA_ + (\adjustmentCid -> do + adjustment <- fetch adjustmentCid + let label = "Stock class authorized shares adjustment " <> adjustment.adjustment_data.id + assertOptionalApprovalDateOnOrBefore label "board approval" adjustment.adjustment_data.board_approval_date "transaction" adjustment.adjustment_data.date + assertOptionalApprovalDateOnOrBefore label "stockholder approval" adjustment.adjustment_data.stockholder_approval_date "transaction" adjustment.adjustment_data.date) + (Map.values maps.stock_class_authorized_shares_adjustments) + + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + let label = "Stock issuance " <> issuance.issuance_data.id + assertOptionalApprovalDateOnOrBefore label "board approval" issuance.issuance_data.board_approval_date "transaction" issuance.issuance_data.date + assertOptionalApprovalDateOnOrBefore label "stockholder approval" issuance.issuance_data.stockholder_approval_date "transaction" issuance.issuance_data.date) + (Map.values maps.stock_issuances) + + mapA_ + (\adjustmentCid -> do + adjustment <- fetch adjustmentCid + let label = "Stock plan pool adjustment " <> adjustment.adjustment_data.id + assertOptionalApprovalDateOnOrBefore label "board approval" adjustment.adjustment_data.board_approval_date "transaction" adjustment.adjustment_data.date + assertOptionalApprovalDateOnOrBefore label "stockholder approval" adjustment.adjustment_data.stockholder_approval_date "transaction" adjustment.adjustment_data.date) + (Map.values maps.stock_plan_pool_adjustments) + + mapA_ + (\valuationCid -> do + valuation <- fetch valuationCid + let label = "Valuation " <> valuation.valuation_data.id + assertOptionalApprovalDateOnOrBefore label "board approval" valuation.valuation_data.board_approval_date "effective" valuation.valuation_data.effective_date + assertOptionalApprovalDateOnOrBefore label "stockholder approval" valuation.valuation_data.stockholder_approval_date "effective" valuation.valuation_data.effective_date) + (Map.values maps.valuations) + + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + let label = "Warrant issuance " <> issuance.issuance_data.id + assertOptionalApprovalDateOnOrBefore label "board approval" issuance.issuance_data.board_approval_date "transaction" issuance.issuance_data.date + assertOptionalApprovalDateOnOrBefore label "stockholder approval" issuance.issuance_data.stockholder_approval_date "transaction" issuance.issuance_data.date) + (Map.values maps.warrant_issuances) + data StockIssuanceShareNumberLot = StockIssuanceShareNumberLot with stock_class_id: Text issuance_id: Text