From de4daaf2404917e50cd38a6da268086efe80235e Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 22:36:27 -0400 Subject: [PATCH 01/38] Enforce stock security balance invariants --- OpenCapTable-v34/daml.yaml | 2 +- Test/daml.yaml | 2 +- .../TestStockSecurityBalances.daml | 244 ++++++++++++++++++ .../0.0.6/OpenCapTable-v34.dar | 3 + dars/dars.lock | 7 + .../codegen/templates/CapTable.daml.template | 83 +++++- 6 files changed, 338 insertions(+), 3 deletions(-) create mode 100644 Test/daml/OpenCapTable/TestStockSecurityBalances.daml create mode 100644 dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar diff --git a/OpenCapTable-v34/daml.yaml b/OpenCapTable-v34/daml.yaml index f16d912a..70b5e670 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.5 +version: 0.0.6 build-options: - -Wno-crypto-text-is-alpha - --typecheck-upgrades=no diff --git a/Test/daml.yaml b/Test/daml.yaml index 64cd009c..0c90f894 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.5.dar + - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.6.dar build-options: - -Wno-template-interface-depends-on-daml-script diff --git a/Test/daml/OpenCapTable/TestStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestStockSecurityBalances.daml new file mode 100644 index 00000000..39e0a589 --- /dev/null +++ b/Test/daml/OpenCapTable/TestStockSecurityBalances.daml @@ -0,0 +1,244 @@ +module OpenCapTable.TestStockSecurityBalances where + +import qualified Fairmint.OpenCapTable.CapTable as CT +import Fairmint.OpenCapTable.OCF.StockCancellation (StockCancellationOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockClassSplit (StockClassSplitOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockConsolidation (StockConsolidationOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockConversion (StockConversionOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockRepurchase (StockRepurchaseOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockReissuance (StockReissuanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockRetraction (StockRetractionOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockTransfer (StockTransferOcfData(..)) +import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import Fairmint.OpenCapTable.Types.Stock (OcfRatio(..)) +import OpenCapTable.Setup +import OpenCapTable.TestHelpers +import qualified DA.Date as DA +import DA.Date (Month(..)) +import qualified DA.Time as DT +import Daml.Script + +stockCancellation : Text -> Text -> Decimal -> Time -> StockCancellationOcfData +stockCancellation cancellationId securityId quantity date = StockCancellationOcfData with + id = cancellationId + date = date + security_id = securityId + quantity = quantity + balance_security_id = None + reason_text = "Cancelled shares" + comments = [] + +stockRepurchase : Text -> Text -> Decimal -> Time -> StockRepurchaseOcfData +stockRepurchase repurchaseId securityId quantity date = StockRepurchaseOcfData with + id = repurchaseId + date = date + security_id = securityId + price = OcfMonetary with amount = 1.0, currency = "USD" + quantity = quantity + balance_security_id = None + consideration_text = None + comments = [] + +stockTransfer : Text -> Text -> Decimal -> Time -> StockTransferOcfData +stockTransfer transferId securityId quantity date = StockTransferOcfData with + id = transferId + date = date + security_id = securityId + quantity = quantity + resulting_security_ids = [transferId <> "_RESULT"] + balance_security_id = None + consideration_text = None + comments = [] + +stockConversion : Text -> Text -> Decimal -> Time -> StockConversionOcfData +stockConversion conversionId securityId quantity date = StockConversionOcfData with + id = conversionId + date = date + security_id = securityId + quantity_converted = quantity + resulting_security_ids = [conversionId <> "_RESULT"] + balance_security_id = None + comments = [] + +stockRetraction : Text -> Text -> Time -> StockRetractionOcfData +stockRetraction retractionId securityId date = StockRetractionOcfData with + id = retractionId + date = date + security_id = securityId + reason_text = "Retracted stock issuance" + comments = [] + +stockReissuance : Text -> Text -> Time -> StockReissuanceOcfData +stockReissuance reissuanceId securityId date = StockReissuanceOcfData with + id = reissuanceId + date = date + security_id = securityId + resulting_security_ids = [reissuanceId <> "_RESULT"] + split_transaction_id = None + reason_text = None + comments = [] + +stockConsolidation : Text -> [Text] -> Time -> StockConsolidationOcfData +stockConsolidation consolidationId securityIds date = StockConsolidationOcfData with + id = consolidationId + date = date + security_ids = securityIds + resulting_security_id = consolidationId <> "_RESULT" + reason_text = None + comments = [] + +stockClassSplit : Text -> Text -> Decimal -> Decimal -> Time -> StockClassSplitOcfData +stockClassSplit splitId stockClassId numerator denominator date = StockClassSplitOcfData with + id = splitId + date = date + stock_class_id = stockClassId + split_ratio = OcfRatio with numerator = numerator, denominator = denominator + comments = [] + +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 + +testStockSecurityBalance_CancellationCannotExceedIssuedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let securityId = "SEC_STOCK_BAL_CANCEL" + let issuance = (defaultStockIssuance "TX_STOCK_BAL_CANCEL" securityId stakeholderId stockClassId) with quantity = 100.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockCancellation (stockCancellation "TX_CANCEL_TOO_MUCH" securityId 101.0 jan02) + ] + edits = [] + deletes = [] + +testStockSecurityBalance_CumulativeReductionsCannotExceedIssuedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let securityId = "SEC_STOCK_BAL_CUMULATIVE" + let issuance = (defaultStockIssuance "TX_STOCK_BAL_CUMULATIVE" securityId stakeholderId stockClassId) with quantity = 100.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockCancellation (stockCancellation "TX_CANCEL_60" securityId 60.0 jan02) + , CT.OcfCreateStockRepurchase (stockRepurchase "TX_REPURCHASE_50" securityId 50.0 jan03) + ] + edits = [] + deletes = [] + +testStockSecurityBalance_TransferCannotExceedIssuedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let securityId = "SEC_STOCK_BAL_TRANSFER" + let issuance = (defaultStockIssuance "TX_STOCK_BAL_TRANSFER" securityId stakeholderId stockClassId) with quantity = 100.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockTransfer (stockTransfer "TX_TRANSFER_TOO_MUCH" securityId 101.0 jan02) + ] + edits = [] + deletes = [] + +testStockSecurityBalance_ConversionCannotExceedIssuedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let securityId = "SEC_STOCK_BAL_CONVERT" + let issuance = (defaultStockIssuance "TX_STOCK_BAL_CONVERT" securityId stakeholderId stockClassId) with quantity = 100.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockConversion (stockConversion "TX_CONVERT_TOO_MUCH" securityId 101.0 jan02) + ] + edits = [] + deletes = [] + +testStockSecurityBalance_ReductionAfterRetractionFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let securityId = "SEC_STOCK_BAL_RETRACTED" + let issuance = (defaultStockIssuance "TX_STOCK_BAL_RETRACTED" securityId stakeholderId stockClassId) with quantity = 100.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockRetraction (stockRetraction "TX_RETRACT_ALL" securityId jan02) + , CT.OcfCreateStockCancellation (stockCancellation "TX_CANCEL_AFTER_RETRACT" securityId 1.0 jan03) + ] + edits = [] + deletes = [] + +testStockSecurityBalance_ReductionAfterReissuanceFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let securityId = "SEC_STOCK_BAL_REISSUED" + let issuance = (defaultStockIssuance "TX_STOCK_BAL_REISSUED" securityId stakeholderId stockClassId) with quantity = 100.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockReissuance (stockReissuance "TX_REISSUE_ALL" securityId jan02) + , CT.OcfCreateStockCancellation (stockCancellation "TX_CANCEL_AFTER_REISSUE" securityId 1.0 jan03) + ] + edits = [] + deletes = [] + +testStockSecurityBalance_ReductionAfterConsolidationFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let securityId = "SEC_STOCK_BAL_CONSOLIDATED" + let issuance = (defaultStockIssuance "TX_STOCK_BAL_CONSOLIDATED" securityId stakeholderId stockClassId) with quantity = 100.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockConsolidation (stockConsolidation "TX_CONSOLIDATE_ALL" [securityId] jan02) + , CT.OcfCreateStockCancellation (stockCancellation "TX_CANCEL_AFTER_CONSOLIDATE" securityId 1.0 jan03) + ] + edits = [] + deletes = [] + +testStockSecurityBalance_SplitAdjustedBalanceAllowsReduction = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let securityId = "SEC_STOCK_BAL_SPLIT" + let issuance = (defaultStockIssuance "TX_STOCK_BAL_SPLIT" securityId stakeholderId stockClassId) with quantity = 100.0 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockClassSplit (stockClassSplit "TX_SPLIT_2_FOR_1" stockClassId 2.0 1.0 jan02) + , CT.OcfCreateStockRepurchase (stockRepurchase "TX_REPURCHASE_AFTER_SPLIT" securityId 150.0 jan03) + ] + edits = [] + deletes = [] + pure () + +testStockSecurityBalance_ReverseSplitReducesAvailableBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let securityId = "SEC_STOCK_BAL_REVERSE_SPLIT" + let issuance = (defaultStockIssuance "TX_STOCK_BAL_REVERSE_SPLIT" securityId stakeholderId stockClassId) with quantity = 100.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockClassSplit (stockClassSplit "TX_REVERSE_SPLIT" stockClassId 1.0 2.0 jan02) + , CT.OcfCreateStockCancellation (stockCancellation "TX_CANCEL_AFTER_REVERSE_SPLIT" securityId 75.0 jan03) + ] + edits = [] + deletes = [] diff --git a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar new file mode 100644 index 00000000..fd300054 --- /dev/null +++ b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b985a286216493e6b330c2417897b6d0529b2f6015550646955051d1fa554a51 +size 2564665 diff --git a/dars/dars.lock b/dars/dars.lock index d14c99df..c38cb5f9 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -38,6 +38,13 @@ "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T02:27:43.397Z", "networks": [] + }, + "OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar": { + "sha256": "b985a286216493e6b330c2417897b6d0529b2f6015550646955051d1fa554a51", + "size": 2564665, + "sdkVersion": "3.4.10", + "uploadedAt": "2026-07-09T02:35:27.968Z", + "networks": [] } } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 4d4d2452..26f043f8 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -289,6 +289,32 @@ stockSecurityShareEvents maps issuanceData = do [] (Map.values maps.stock_repurchases) + transferEvents <- foldlA + (\events transferCid -> do + transfer <- fetch transferCid + if transfer.transfer_data.security_id == issuanceData.security_id + then pure ((StockClassShareDelta with + event_date = transfer.transfer_data.date + event_priority = 1 + quantity_delta = 0.0 - transfer.transfer_data.quantity) + :: events) + else pure events) + [] + (Map.values maps.stock_transfers) + + conversionEvents <- foldlA + (\events conversionCid -> do + conversion <- fetch conversionCid + if conversion.conversion_data.security_id == issuanceData.security_id + then pure ((StockClassShareDelta with + event_date = conversion.conversion_data.date + event_priority = 1 + quantity_delta = 0.0 - conversion.conversion_data.quantity_converted) + :: events) + else pure events) + [] + (Map.values maps.stock_conversions) + retractionEvents <- foldlA (\events retractionCid -> do retraction <- fetch retractionCid @@ -301,6 +327,30 @@ stockSecurityShareEvents maps issuanceData = do [] (Map.values maps.stock_retractions) + reissuanceEvents <- foldlA + (\events reissuanceCid -> do + reissuance <- fetch reissuanceCid + if reissuance.reissuance_data.security_id == issuanceData.security_id + then pure ((StockClassShareRetraction with + event_date = reissuance.reissuance_data.date + event_priority = 3) + :: events) + else pure events) + [] + (Map.values maps.stock_reissuances) + + consolidationEvents <- foldlA + (\events consolidationCid -> do + consolidation <- fetch consolidationCid + if issuanceData.security_id `elem` consolidation.consolidation_data.security_ids + then pure ((StockClassShareRetraction with + event_date = consolidation.consolidation_data.date + event_priority = 3) + :: events) + else pure events) + [] + (Map.values maps.stock_consolidations) + splitEvents <- foldlA (\events splitCid -> do split <- fetch splitCid @@ -318,13 +368,43 @@ stockSecurityShareEvents maps issuanceData = do event_date = issuanceData.date event_priority = 0 quantity_delta = issuanceData.quantity) - :: (cancellationEvents ++ repurchaseEvents ++ retractionEvents ++ splitEvents)) + :: (cancellationEvents ++ repurchaseEvents ++ transferEvents ++ conversionEvents ++ retractionEvents ++ reissuanceEvents ++ consolidationEvents ++ splitEvents)) stockIssuanceCurrentQuantity : CapTableMaps -> StockIssuanceOcfData -> Update Decimal stockIssuanceCurrentQuantity maps issuanceData = do events <- stockSecurityShareEvents maps issuanceData pure $ foldl applyStockClassShareEvent 0.0 (sortOn stockClassShareEventSortKey events) +validateStockSecurityBalanceEvent : Text -> Decimal -> StockClassShareEvent -> Update Decimal +validateStockSecurityBalanceEvent securityId current event = case event of + StockClassShareDelta with quantity_delta -> do + let next = current + quantity_delta + assertMsg + ("Stock security balance cannot be negative for " <> securityId <> ": applying delta " <> show quantity_delta <> " leaves " <> show next) + (next >= 0.0) + pure next + StockClassShareMultiplier with ratio -> + pure (current * ratio.numerator / ratio.denominator) + StockClassShareRetraction _ _ -> + pure 0.0 + +validateStockSecurityBalance : CapTableMaps -> StockIssuanceOcfData -> Update () +validateStockSecurityBalance maps issuanceData = do + events <- stockSecurityShareEvents maps issuanceData + _ <- foldlA + (validateStockSecurityBalanceEvent issuanceData.security_id) + 0.0 + (sortOn stockClassShareEventSortKey events) + pure () + +validateStockSecurityBalances : CapTableMaps -> Update () +validateStockSecurityBalances maps = + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + validateStockSecurityBalance maps issuance.issuance_data) + (Map.values maps.stock_issuances) + stockClassIssuedQuantity : CapTableMaps -> Text -> Update Decimal stockClassIssuedQuantity maps stockClassId = do quantities <- foldlA @@ -829,6 +909,7 @@ template CapTable validateValuationStockClassReferences finalMaps validateEquityCompensationIssuanceReferences finalMaps validateWarrantIssuanceReferences finalMaps + validateStockSecurityBalances finalMaps authorizedShareAdjustmentIndex <- stockClassAuthorizedAdjustmentIndex finalMaps validateAuthorizedShareCeilings finalMaps authorizedShareAdjustmentIndex newIssuerCid validateAuthorizedShareCeilingsForEdits From 329a596f18889f8b04ed2c33247da779ac8a9862 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 22:50:14 -0400 Subject: [PATCH 02/38] Cover non-stock security balances --- .../TestNonStockSecurityBalances.daml | 272 ++++++++++++++++++ .../0.0.6/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- .../codegen/templates/CapTable.daml.template | 216 ++++++++++++++ 4 files changed, 493 insertions(+), 5 deletions(-) create mode 100644 Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml diff --git a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml new file mode 100644 index 00000000..fce16564 --- /dev/null +++ b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml @@ -0,0 +1,272 @@ +module OpenCapTable.TestNonStockSecurityBalances where + +import qualified Fairmint.OpenCapTable.CapTable as CT +import Fairmint.OpenCapTable.OCF.EquityCompensationCancellation (EquityCompensationCancellationOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationExercise (EquityCompensationExerciseOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance (EquityCompensationIssuanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationRelease (EquityCompensationReleaseOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationRetraction (EquityCompensationRetractionOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationTransfer (EquityCompensationTransferOcfData(..)) +import Fairmint.OpenCapTable.OCF.WarrantCancellation (WarrantCancellationOcfData(..)) +import Fairmint.OpenCapTable.OCF.WarrantExercise (WarrantExerciseOcfData(..)) +import Fairmint.OpenCapTable.OCF.WarrantIssuance (WarrantIssuanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.WarrantRetraction (WarrantRetractionOcfData(..)) +import Fairmint.OpenCapTable.OCF.WarrantTransfer (WarrantTransferOcfData(..)) +import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import Fairmint.OpenCapTable.Types.Stock (OcfQuantitySourceType(..)) +import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) +import OpenCapTable.Setup +import OpenCapTable.TestHelpers +import qualified DA.Date as DA +import DA.Date (Month(..)) +import qualified DA.Time as DT +import Daml.Script + +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 + +jan04 : Time +jan04 = DT.time (DA.date 2024 Jan 04) 0 0 0 + +warrantIssuance : Text -> Text -> Text -> Decimal -> WarrantIssuanceOcfData +warrantIssuance issuanceId securityId stakeholderId quantity = WarrantIssuanceOcfData with + id = issuanceId + date = defaultTestDate + security_id = securityId + custom_id = "WR-" <> issuanceId + stakeholder_id = stakeholderId + purchase_price = OcfMonetary with amount = 0.0, currency = "USD" + quantity = Some quantity + quantity_source = Some OcfQuantityUnspecified + exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + exercise_triggers = [] + security_law_exemptions = [] + vestings = [] + board_approval_date = None + consideration_text = None + stockholder_approval_date = None + vesting_terms_id = None + warrant_expiration_date = None + comments = [] + +warrantCancellation : Text -> Text -> Decimal -> Time -> WarrantCancellationOcfData +warrantCancellation cancellationId securityId quantity date = WarrantCancellationOcfData with + id = cancellationId + date = date + security_id = securityId + quantity = quantity + balance_security_id = None + reason_text = "Cancelled warrants" + comments = [] + +warrantTransfer : Text -> Text -> Decimal -> Time -> WarrantTransferOcfData +warrantTransfer transferId securityId quantity date = WarrantTransferOcfData with + id = transferId + date = date + security_id = securityId + quantity = quantity + resulting_security_ids = [transferId <> "_RESULT"] + balance_security_id = None + consideration_text = None + comments = [] + +warrantExercise : Text -> Text -> Optional Decimal -> Time -> WarrantExerciseOcfData +warrantExercise exerciseId securityId quantity date = WarrantExerciseOcfData with + id = exerciseId + date = date + security_id = securityId + trigger_id = "TEST_TRIGGER" + quantity = quantity + resulting_security_ids = [exerciseId <> "_RESULT"] + consideration_text = None + comments = [] + +warrantRetraction : Text -> Text -> Time -> WarrantRetractionOcfData +warrantRetraction retractionId securityId date = WarrantRetractionOcfData with + id = retractionId + date = date + security_id = securityId + reason_text = "Retracted warrant issuance" + comments = [] + +equityCompensationIssuance : Text -> Text -> Text -> Decimal -> EquityCompensationIssuanceOcfData +equityCompensationIssuance issuanceId securityId stakeholderId quantity = EquityCompensationIssuanceOcfData with + id = issuanceId + date = defaultTestDate + security_id = securityId + custom_id = "EC-" <> issuanceId + stakeholder_id = stakeholderId + compensation_type = OcfCompensationTypeOptionNSO + quantity = quantity + exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + base_price = None + early_exercisable = None + stock_class_id = None + stock_plan_id = None + vesting_terms_id = None + expiration_date = None + termination_exercise_windows = [] + security_law_exemptions = [] + vestings = [] + board_approval_date = None + consideration_text = None + stockholder_approval_date = None + comments = [] + +equityCompensationCancellation : Text -> Text -> Decimal -> Time -> EquityCompensationCancellationOcfData +equityCompensationCancellation cancellationId securityId quantity date = EquityCompensationCancellationOcfData with + id = cancellationId + date = date + security_id = securityId + quantity = quantity + balance_security_id = None + reason_text = "Cancelled equity compensation" + comments = [] + +equityCompensationTransfer : Text -> Text -> Decimal -> Time -> EquityCompensationTransferOcfData +equityCompensationTransfer transferId securityId quantity date = EquityCompensationTransferOcfData with + id = transferId + date = date + security_id = securityId + quantity = quantity + resulting_security_ids = [transferId <> "_RESULT"] + balance_security_id = None + consideration_text = None + comments = [] + +equityCompensationExercise : Text -> Text -> Decimal -> Time -> EquityCompensationExerciseOcfData +equityCompensationExercise exerciseId securityId quantity date = EquityCompensationExerciseOcfData with + id = exerciseId + date = date + security_id = securityId + quantity = quantity + resulting_security_ids = [exerciseId <> "_RESULT"] + consideration_text = None + comments = [] + +equityCompensationRelease : Text -> Text -> Decimal -> Time -> EquityCompensationReleaseOcfData +equityCompensationRelease releaseId securityId quantity date = EquityCompensationReleaseOcfData with + id = releaseId + date = date + security_id = securityId + quantity = quantity + settlement_date = date + release_price = OcfMonetary with amount = 1.0, currency = "USD" + resulting_security_ids = [releaseId <> "_RESULT"] + consideration_text = None + comments = [] + +equityCompensationRetraction : Text -> Text -> Time -> EquityCompensationRetractionOcfData +equityCompensationRetraction retractionId securityId date = EquityCompensationRetractionOcfData with + id = retractionId + date = date + security_id = securityId + reason_text = "Retracted equity compensation issuance" + comments = [] + +testWarrantSecurityBalance_CancellationCannotExceedIssuedQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let securityId = "WSEC_BAL_CANCEL" + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance (warrantIssuance "TX_W_BAL_CANCEL" securityId defaultStakeholderId 100.0) + , CT.OcfCreateWarrantCancellation (warrantCancellation "TX_W_CANCEL_TOO_MUCH" securityId 101.0 jan02) + ] + edits = [] + deletes = [] + +testWarrantSecurityBalance_CumulativeExerciseAndTransferCannotExceedIssuedQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let securityId = "WSEC_BAL_CUMULATIVE" + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance (warrantIssuance "TX_W_BAL_CUMULATIVE" securityId defaultStakeholderId 100.0) + , CT.OcfCreateWarrantExercise (warrantExercise "TX_W_EXERCISE_60" securityId (Some 60.0) jan02) + , CT.OcfCreateWarrantTransfer (warrantTransfer "TX_W_TRANSFER_50" securityId 50.0 jan03) + ] + edits = [] + deletes = [] + +testWarrantSecurityBalance_ReductionAfterFullExerciseFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let securityId = "WSEC_BAL_FULL_EXERCISE" + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance (warrantIssuance "TX_W_BAL_FULL_EXERCISE" securityId defaultStakeholderId 100.0) + , CT.OcfCreateWarrantExercise (warrantExercise "TX_W_FULL_EXERCISE" securityId None jan02) + , CT.OcfCreateWarrantCancellation (warrantCancellation "TX_W_CANCEL_AFTER_EXERCISE" securityId 1.0 jan03) + ] + edits = [] + deletes = [] + +testWarrantSecurityBalance_ReductionAfterRetractionFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let securityId = "WSEC_BAL_RETRACTED" + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance (warrantIssuance "TX_W_BAL_RETRACTED" securityId defaultStakeholderId 100.0) + , CT.OcfCreateWarrantRetraction (warrantRetraction "TX_W_RETRACT_ALL" securityId jan02) + , CT.OcfCreateWarrantTransfer (warrantTransfer "TX_W_TRANSFER_AFTER_RETRACT" securityId 1.0 jan03) + ] + edits = [] + deletes = [] + +testEquityCompensationSecurityBalance_CancellationCannotExceedIssuedQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let securityId = "ESEC_BAL_CANCEL" + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_E_BAL_CANCEL" securityId defaultStakeholderId 100.0) + , CT.OcfCreateEquityCompensationCancellation (equityCompensationCancellation "TX_E_CANCEL_TOO_MUCH" securityId 101.0 jan02) + ] + edits = [] + deletes = [] + +testEquityCompensationSecurityBalance_CumulativeReductionsCannotExceedIssuedQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let securityId = "ESEC_BAL_CUMULATIVE" + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_E_BAL_CUMULATIVE" securityId defaultStakeholderId 100.0) + , CT.OcfCreateEquityCompensationExercise (equityCompensationExercise "TX_E_EXERCISE_40" securityId 40.0 jan02) + , CT.OcfCreateEquityCompensationRelease (equityCompensationRelease "TX_E_RELEASE_40" securityId 40.0 jan03) + , CT.OcfCreateEquityCompensationTransfer (equityCompensationTransfer "TX_E_TRANSFER_25" securityId 25.0 jan04) + ] + edits = [] + deletes = [] + +testEquityCompensationSecurityBalance_ReductionAfterRetractionFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let securityId = "ESEC_BAL_RETRACTED" + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_E_BAL_RETRACTED" securityId defaultStakeholderId 100.0) + , CT.OcfCreateEquityCompensationRetraction (equityCompensationRetraction "TX_E_RETRACT_ALL" securityId jan02) + , CT.OcfCreateEquityCompensationCancellation (equityCompensationCancellation "TX_E_CANCEL_AFTER_RETRACT" securityId 1.0 jan03) + ] + edits = [] + deletes = [] diff --git a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar index fd300054..d08f436f 100644 --- a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b985a286216493e6b330c2417897b6d0529b2f6015550646955051d1fa554a51 -size 2564665 +oid sha256:bbee97a0d5bb2e0bd7b822628ff62a66b8b7c8076026667d0f3df3c0340619fa +size 2586123 diff --git a/dars/dars.lock b/dars/dars.lock index c38cb5f9..39307cc6 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,10 +40,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar": { - "sha256": "b985a286216493e6b330c2417897b6d0529b2f6015550646955051d1fa554a51", - "size": 2564665, + "sha256": "bbee97a0d5bb2e0bd7b822628ff62a66b8b7c8076026667d0f3df3c0340619fa", + "size": 2586123, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T02:35:27.968Z", + "uploadedAt": "2026-07-09T02:49:48.638Z", "networks": [] } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 26f043f8..52372686 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -123,6 +123,16 @@ data StockClassShareEvent event_priority: Int deriving (Eq, Show) +data SecurityQuantityEvent + = SecurityQuantityDelta with + event_date: Time + event_priority: Int + quantity_delta: Decimal + | SecurityQuantityRetraction with + event_date: Time + event_priority: Int + deriving (Eq, Show) + authorizedSharesCeiling : OcfInitialSharesAuthorized -> AuthorizedSharesCeiling authorizedSharesCeiling auth = case auth of OcfInitialSharesNumeric n -> AuthorizedSharesFinite n @@ -405,6 +415,210 @@ validateStockSecurityBalances maps = validateStockSecurityBalance maps issuance.issuance_data) (Map.values maps.stock_issuances) +securityQuantityEventSortKey : SecurityQuantityEvent -> (Time, Int) +securityQuantityEventSortKey event = case event of + SecurityQuantityDelta with event_date; event_priority -> (event_date, event_priority) + SecurityQuantityRetraction with event_date; event_priority -> (event_date, event_priority) + +validateSecurityQuantityBalanceEvent : Text -> Optional Decimal -> SecurityQuantityEvent -> Update (Optional Decimal) +validateSecurityQuantityBalanceEvent securityId currentOpt event = case currentOpt of + Some current -> case event of + SecurityQuantityDelta with quantity_delta -> do + let next = current + quantity_delta + assertMsg + ("Security balance cannot be negative for " <> securityId <> ": applying delta " <> show quantity_delta <> " leaves " <> show next) + (next >= 0.0) + pure (Some next) + SecurityQuantityRetraction _ _ -> + pure (Some 0.0) + None -> case event of + SecurityQuantityDelta {} -> + pure None + SecurityQuantityRetraction _ _ -> + pure (Some 0.0) + +validateSecurityQuantityBalance : Text -> Optional Decimal -> [SecurityQuantityEvent] -> Update () +validateSecurityQuantityBalance securityId initialQuantity events = do + _ <- foldlA + (validateSecurityQuantityBalanceEvent securityId) + initialQuantity + (sortOn securityQuantityEventSortKey events) + pure () + +warrantSecurityQuantityEvents : CapTableMaps -> WarrantIssuanceOcfData -> Update [SecurityQuantityEvent] +warrantSecurityQuantityEvents maps issuanceData = do + transferEvents <- foldlA + (\events transferCid -> do + transfer <- fetch transferCid + if transfer.transfer_data.security_id == issuanceData.security_id + then pure ((SecurityQuantityDelta with + event_date = transfer.transfer_data.date + event_priority = 1 + quantity_delta = 0.0 - transfer.transfer_data.quantity) + :: events) + else pure events) + [] + (Map.values maps.warrant_transfers) + + cancellationEvents <- foldlA + (\events cancellationCid -> do + cancellation <- fetch cancellationCid + if cancellation.cancellation_data.security_id == issuanceData.security_id + then pure ((SecurityQuantityDelta with + event_date = cancellation.cancellation_data.date + event_priority = 1 + quantity_delta = 0.0 - cancellation.cancellation_data.quantity) + :: events) + else pure events) + [] + (Map.values maps.warrant_cancellations) + + exerciseEvents <- foldlA + (\events exerciseCid -> do + exercise <- fetch exerciseCid + if exercise.exercise_data.security_id == issuanceData.security_id + then case exercise.exercise_data.quantity of + Some quantity -> + pure ((SecurityQuantityDelta with + event_date = exercise.exercise_data.date + event_priority = 1 + quantity_delta = 0.0 - quantity) + :: events) + None -> + pure ((SecurityQuantityRetraction with + event_date = exercise.exercise_data.date + event_priority = 3) + :: events) + else pure events) + [] + (Map.values maps.warrant_exercises) + + retractionEvents <- foldlA + (\events retractionCid -> do + retraction <- fetch retractionCid + if retraction.retraction_data.security_id == issuanceData.security_id + then pure ((SecurityQuantityRetraction with + event_date = retraction.retraction_data.date + event_priority = 3) + :: events) + else pure events) + [] + (Map.values maps.warrant_retractions) + + pure (transferEvents ++ cancellationEvents ++ exerciseEvents ++ retractionEvents) + +validateWarrantSecurityBalance : CapTableMaps -> WarrantIssuanceOcfData -> Update () +validateWarrantSecurityBalance maps issuanceData = do + events <- warrantSecurityQuantityEvents maps issuanceData + case issuanceData.quantity of + Some quantity -> + validateSecurityQuantityBalance + issuanceData.security_id + (Some 0.0) + ((SecurityQuantityDelta with + event_date = issuanceData.date + event_priority = 0 + quantity_delta = quantity) + :: events) + None -> + validateSecurityQuantityBalance issuanceData.security_id None events + +validateWarrantSecurityBalances : CapTableMaps -> Update () +validateWarrantSecurityBalances maps = + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + validateWarrantSecurityBalance maps issuance.issuance_data) + (Map.values maps.warrant_issuances) + +equityCompensationSecurityQuantityEvents : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update [SecurityQuantityEvent] +equityCompensationSecurityQuantityEvents maps issuanceData = do + transferEvents <- foldlA + (\events transferCid -> do + transfer <- fetch transferCid + if transfer.transfer_data.security_id == issuanceData.security_id + then pure ((SecurityQuantityDelta with + event_date = transfer.transfer_data.date + event_priority = 1 + quantity_delta = 0.0 - transfer.transfer_data.quantity) + :: events) + else pure events) + [] + (Map.values maps.equity_compensation_transfers) + + cancellationEvents <- foldlA + (\events cancellationCid -> do + cancellation <- fetch cancellationCid + if cancellation.cancellation_data.security_id == issuanceData.security_id + then pure ((SecurityQuantityDelta with + event_date = cancellation.cancellation_data.date + event_priority = 1 + quantity_delta = 0.0 - cancellation.cancellation_data.quantity) + :: events) + else pure events) + [] + (Map.values maps.equity_compensation_cancellations) + + exerciseEvents <- foldlA + (\events exerciseCid -> do + exercise <- fetch exerciseCid + if exercise.exercise_data.security_id == issuanceData.security_id + then pure ((SecurityQuantityDelta with + event_date = exercise.exercise_data.date + event_priority = 1 + quantity_delta = 0.0 - exercise.exercise_data.quantity) + :: events) + else pure events) + [] + (Map.values maps.equity_compensation_exercises) + + releaseEvents <- foldlA + (\events releaseCid -> do + release <- fetch releaseCid + if release.release_data.security_id == issuanceData.security_id + then pure ((SecurityQuantityDelta with + event_date = release.release_data.date + event_priority = 1 + quantity_delta = 0.0 - release.release_data.quantity) + :: events) + else pure events) + [] + (Map.values maps.equity_compensation_releases) + + retractionEvents <- foldlA + (\events retractionCid -> do + retraction <- fetch retractionCid + if retraction.retraction_data.security_id == issuanceData.security_id + then pure ((SecurityQuantityRetraction with + event_date = retraction.retraction_data.date + event_priority = 3) + :: events) + else pure events) + [] + (Map.values maps.equity_compensation_retractions) + + pure (transferEvents ++ cancellationEvents ++ exerciseEvents ++ releaseEvents ++ retractionEvents) + +validateEquityCompensationSecurityBalance : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update () +validateEquityCompensationSecurityBalance maps issuanceData = do + events <- equityCompensationSecurityQuantityEvents maps issuanceData + validateSecurityQuantityBalance + issuanceData.security_id + (Some 0.0) + ((SecurityQuantityDelta with + event_date = issuanceData.date + event_priority = 0 + quantity_delta = issuanceData.quantity) + :: events) + +validateEquityCompensationSecurityBalances : CapTableMaps -> Update () +validateEquityCompensationSecurityBalances maps = + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + validateEquityCompensationSecurityBalance maps issuance.issuance_data) + (Map.values maps.equity_compensation_issuances) + stockClassIssuedQuantity : CapTableMaps -> Text -> Update Decimal stockClassIssuedQuantity maps stockClassId = do quantities <- foldlA @@ -910,6 +1124,8 @@ template CapTable validateEquityCompensationIssuanceReferences finalMaps validateWarrantIssuanceReferences finalMaps validateStockSecurityBalances finalMaps + validateWarrantSecurityBalances finalMaps + validateEquityCompensationSecurityBalances finalMaps authorizedShareAdjustmentIndex <- stockClassAuthorizedAdjustmentIndex finalMaps validateAuthorizedShareCeilings finalMaps authorizedShareAdjustmentIndex newIssuerCid validateAuthorizedShareCeilingsForEdits From 039da67de0dd793c19476e8ca4a6ee9b06d2b90d Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 23:03:25 -0400 Subject: [PATCH 03/38] Keep equity compensation release fixtures valid --- .../TestEquityCompensationRelease.daml | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml b/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml index e676d86e..44e748dc 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml @@ -1,22 +1,54 @@ module OpenCapTable.TestEquityCompensationRelease where +import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance import Fairmint.OpenCapTable.OCF.EquityCompensationRelease import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addDefaultStakeholder, addEquityCompensationIssuance) +import OpenCapTable.TestHelpers (addDefaultStakeholder) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT import Daml.Script +addEquityCompensationReleaseIssuance : Party -> ContractId CT.CapTable -> Text -> Text -> Text -> Decimal -> Script (ContractId CT.CapTable) +addEquityCompensationReleaseIssuance issuer capTableCid issuanceId securityId stakeholderId quantity = do + result <- submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance EquityCompensationIssuanceOcfData with + id = issuanceId + custom_id = "EC-" <> issuanceId + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + compensation_type = OcfCompensationTypeRSU + quantity = quantity + security_id = securityId + stakeholder_id = stakeholderId + comments = [] + security_law_exemptions = [] + vestings = [] + termination_exercise_windows = [] + base_price = None + board_approval_date = None + consideration_text = None + early_exercisable = None + exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + expiration_date = None + stock_class_id = None + stock_plan_id = None + stockholder_approval_date = None + vesting_terms_id = None] + edits = [] + deletes = [] + pure result.updatedCapTableCid + -- Test: Create and archive RSU release/settlement testCreateAndArchiveEquityCompensationRelease = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp -- Add stakeholder and equity compensation issuance before releasing cap_table <- addDefaultStakeholder issuer cap_table - cap_table <- addEquityCompensationIssuance issuer cap_table "EC-ISS-RSU-1" "SEC_RSU_001" "SH-1" + cap_table <- addEquityCompensationReleaseIssuance issuer cap_table "EC-ISS-RSU-1" "SEC_RSU_001" "SH-1" 20000.0 -- RSU vesting and release _ <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with @@ -40,7 +72,7 @@ testCreateAndArchiveEquityCompensationReleaseQuarterly = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp -- Add stakeholder and equity compensation issuance before releasing cap_table <- addDefaultStakeholder issuer cap_table - cap_table <- addEquityCompensationIssuance issuer cap_table "EC-ISS-RSU-2" "SEC_RSU_002" "SH-1" + cap_table <- addEquityCompensationReleaseIssuance issuer cap_table "EC-ISS-RSU-2" "SEC_RSU_002" "SH-1" 5000.0 -- Quarterly RSU vest with tax withholding _ <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with From c9c1ca6dc5b1ea78ff4c40246789076a137e73a6 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 23:11:40 -0400 Subject: [PATCH 04/38] Address stock balance review edge cases --- .../TestStockClassAuthorizedShares.daml | 31 +++++ .../TestStockSecurityBalances.daml | 37 ++++++ .../0.0.6/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 116 +++++++++++++++--- 5 files changed, 174 insertions(+), 18 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 4d6ac05d..52f404c1 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -4,6 +4,7 @@ import Fairmint.OpenCapTable.OCF.StockCancellation import Fairmint.OpenCapTable.OCF.StockClassSplit import Fairmint.OpenCapTable.OCF.StockRepurchase import Fairmint.OpenCapTable.OCF.StockRetraction +import Fairmint.OpenCapTable.OCF.StockTransfer import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..)) import qualified Fairmint.OpenCapTable.CapTable as CT @@ -37,6 +38,18 @@ stockRepurchase repurchaseId securityId quantity = consideration_text = None comments = [] +stockTransfer : Text -> Text -> Decimal -> StockTransferOcfData +stockTransfer transferId securityId quantity = + StockTransferOcfData with + id = transferId + date = defaultTestDate + security_id = securityId + quantity = quantity + resulting_security_ids = [transferId <> "_RESULT"] + balance_security_id = None + consideration_text = None + comments = [] + stockRetraction : Text -> Text -> StockRetractionOcfData stockRetraction retractionId securityId = StockRetractionOcfData with @@ -303,6 +316,24 @@ testStockRepurchaseReducesIssuedSharesForCeiling = script do _ <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_REPURCHASE_REISSUE" "SEC_REPURCHASE_REISSUE" stakeholderId stockClassId) with quantity = 50.0) pure () +testStockTransferDoesNotReduceIssuedSharesForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_TRANSFER" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_TRANSFER" "Transfer Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_TRANSFER_ISSUED" "SEC_TRANSFER_ISSUED" stakeholderId stockClassId) with quantity = 100.0) + result <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockTransfer (stockTransfer "TX_TRANSFER_REPLACE" "SEC_TRANSFER_ISSUED" 100.0)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_TRANSFER_EXTRA" "SEC_TRANSFER_EXTRA" stakeholderId stockClassId) with quantity = 1.0)] + edits = [] + deletes = [] + testStockRetractionReducesIssuedSharesForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestStockSecurityBalances.daml index 39e0a589..5f8c4469 100644 --- a/Test/daml/OpenCapTable/TestStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestStockSecurityBalances.daml @@ -50,6 +50,11 @@ stockTransfer transferId securityId quantity date = StockTransferOcfData with consideration_text = None comments = [] +stockTransferWithBalance : Text -> Text -> Decimal -> Text -> Time -> StockTransferOcfData +stockTransferWithBalance transferId securityId quantity balanceSecurityId date = + (stockTransfer transferId securityId quantity date) with + balance_security_id = Some balanceSecurityId + stockConversion : Text -> Text -> Decimal -> Time -> StockConversionOcfData stockConversion conversionId securityId quantity date = StockConversionOcfData with id = conversionId @@ -147,6 +152,22 @@ testStockSecurityBalance_TransferCannotExceedIssuedShares = script do edits = [] deletes = [] +testStockSecurityBalance_BalanceSecurityRetiresSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let securityId = "SEC_STOCK_BAL_TRANSFER_BALANCE" + let issuance = (defaultStockIssuance "TX_STOCK_BAL_TRANSFER_BALANCE" securityId stakeholderId stockClassId) with quantity = 100.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockTransfer (stockTransferWithBalance "TX_TRANSFER_WITH_BALANCE" securityId 40.0 "SEC_STOCK_BAL_REMAINDER" jan02) + , CT.OcfCreateStockCancellation (stockCancellation "TX_CANCEL_OLD_SOURCE_AFTER_BALANCE" securityId 1.0 jan03) + ] + edits = [] + deletes = [] + testStockSecurityBalance_ConversionCannotExceedIssuedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table @@ -194,6 +215,22 @@ testStockSecurityBalance_ReductionAfterReissuanceFails = script do edits = [] deletes = [] +testStockSecurityBalance_TerminalEventRequiresRemainingBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let securityId = "SEC_STOCK_BAL_TERMINAL_ONCE" + let issuance = (defaultStockIssuance "TX_STOCK_BAL_TERMINAL_ONCE" securityId stakeholderId stockClassId) with quantity = 100.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockReissuance (stockReissuance "TX_REISSUE_FIRST" securityId jan02) + , CT.OcfCreateStockReissuance (stockReissuance "TX_REISSUE_SECOND" securityId jan03) + ] + edits = [] + deletes = [] + testStockSecurityBalance_ReductionAfterConsolidationFails = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar index d08f436f..6092553f 100644 --- a/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bbee97a0d5bb2e0bd7b822628ff62a66b8b7c8076026667d0f3df3c0340619fa -size 2586123 +oid sha256:204f820b59eefc0abef97171fe1f5cdfeafa957dd8ad05a81d26cb70194b811c +size 2593623 diff --git a/dars/dars.lock b/dars/dars.lock index 39307cc6..1fe774ab 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -40,8 +40,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.6/OpenCapTable-v34.dar": { - "sha256": "bbee97a0d5bb2e0bd7b822628ff62a66b8b7c8076026667d0f3df3c0340619fa", - "size": 2586123, + "sha256": "204f820b59eefc0abef97171fe1f5cdfeafa957dd8ad05a81d26cb70194b811c", + "size": 2593623, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T02:49:48.638Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 52372686..75ce4294 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -271,8 +271,17 @@ applyStockClassShareEvent current event = case event of StockClassShareMultiplier with ratio -> current * ratio.numerator / ratio.denominator StockClassShareRetraction _ _ -> 0.0 -stockSecurityShareEvents : CapTableMaps -> StockIssuanceOcfData -> Update [StockClassShareEvent] -stockSecurityShareEvents maps issuanceData = do +balanceSecurityTerminalEvents : Time -> Optional Text -> [StockClassShareEvent] +balanceSecurityTerminalEvents eventDate balanceSecurityId = case balanceSecurityId of + Some _ -> + [ StockClassShareRetraction with + event_date = eventDate + event_priority = 3 + ] + None -> [] + +stockIssuedShareEvents : CapTableMaps -> StockIssuanceOcfData -> Update [StockClassShareEvent] +stockIssuedShareEvents maps issuanceData = do cancellationEvents <- foldlA (\events cancellationCid -> do cancellation <- fetch cancellationCid @@ -299,15 +308,90 @@ stockSecurityShareEvents maps issuanceData = do [] (Map.values maps.stock_repurchases) + conversionEvents <- foldlA + (\events conversionCid -> do + conversion <- fetch conversionCid + if conversion.conversion_data.security_id == issuanceData.security_id + then pure ((StockClassShareDelta with + event_date = conversion.conversion_data.date + event_priority = 1 + quantity_delta = 0.0 - conversion.conversion_data.quantity_converted) + :: events) + else pure events) + [] + (Map.values maps.stock_conversions) + + retractionEvents <- foldlA + (\events retractionCid -> do + retraction <- fetch retractionCid + if retraction.retraction_data.security_id == issuanceData.security_id + then pure ((StockClassShareRetraction with + event_date = retraction.retraction_data.date + event_priority = 3) + :: events) + else pure events) + [] + (Map.values maps.stock_retractions) + + splitEvents <- foldlA + (\events splitCid -> do + split <- fetch splitCid + if split.split_data.stock_class_id == issuanceData.stock_class_id && split.split_data.date >= issuanceData.date + then pure ((StockClassShareMultiplier with + event_date = split.split_data.date + event_priority = 2 + ratio = split.split_data.split_ratio) + :: events) + else pure events) + [] + (Map.values maps.stock_class_splits) + + pure ((StockClassShareDelta with + event_date = issuanceData.date + event_priority = 0 + quantity_delta = issuanceData.quantity) + :: (cancellationEvents ++ repurchaseEvents ++ conversionEvents ++ retractionEvents ++ splitEvents)) + +stockSecurityShareEvents : CapTableMaps -> StockIssuanceOcfData -> Update [StockClassShareEvent] +stockSecurityShareEvents maps issuanceData = do + cancellationEvents <- foldlA + (\events cancellationCid -> do + cancellation <- fetch cancellationCid + if cancellation.cancellation_data.security_id == issuanceData.security_id + then + let delta = StockClassShareDelta with + event_date = cancellation.cancellation_data.date + event_priority = 1 + quantity_delta = 0.0 - cancellation.cancellation_data.quantity + in pure (delta :: (balanceSecurityTerminalEvents cancellation.cancellation_data.date cancellation.cancellation_data.balance_security_id ++ events)) + else pure events) + [] + (Map.values maps.stock_cancellations) + + repurchaseEvents <- foldlA + (\events repurchaseCid -> do + repurchase <- fetch repurchaseCid + if repurchase.repurchase_data.security_id == issuanceData.security_id + then + let delta = StockClassShareDelta with + event_date = repurchase.repurchase_data.date + event_priority = 1 + quantity_delta = 0.0 - repurchase.repurchase_data.quantity + in pure (delta :: (balanceSecurityTerminalEvents repurchase.repurchase_data.date repurchase.repurchase_data.balance_security_id ++ events)) + else pure events) + [] + (Map.values maps.stock_repurchases) + transferEvents <- foldlA (\events transferCid -> do transfer <- fetch transferCid if transfer.transfer_data.security_id == issuanceData.security_id - then pure ((StockClassShareDelta with - event_date = transfer.transfer_data.date - event_priority = 1 - quantity_delta = 0.0 - transfer.transfer_data.quantity) - :: events) + then + let delta = StockClassShareDelta with + event_date = transfer.transfer_data.date + event_priority = 1 + quantity_delta = 0.0 - transfer.transfer_data.quantity + in pure (delta :: (balanceSecurityTerminalEvents transfer.transfer_data.date transfer.transfer_data.balance_security_id ++ events)) else pure events) [] (Map.values maps.stock_transfers) @@ -316,11 +400,12 @@ stockSecurityShareEvents maps issuanceData = do (\events conversionCid -> do conversion <- fetch conversionCid if conversion.conversion_data.security_id == issuanceData.security_id - then pure ((StockClassShareDelta with - event_date = conversion.conversion_data.date - event_priority = 1 - quantity_delta = 0.0 - conversion.conversion_data.quantity_converted) - :: events) + then + let delta = StockClassShareDelta with + event_date = conversion.conversion_data.date + event_priority = 1 + quantity_delta = 0.0 - conversion.conversion_data.quantity_converted + in pure (delta :: (balanceSecurityTerminalEvents conversion.conversion_data.date conversion.conversion_data.balance_security_id ++ events)) else pure events) [] (Map.values maps.stock_conversions) @@ -382,7 +467,7 @@ stockSecurityShareEvents maps issuanceData = do stockIssuanceCurrentQuantity : CapTableMaps -> StockIssuanceOcfData -> Update Decimal stockIssuanceCurrentQuantity maps issuanceData = do - events <- stockSecurityShareEvents maps issuanceData + events <- stockIssuedShareEvents maps issuanceData pure $ foldl applyStockClassShareEvent 0.0 (sortOn stockClassShareEventSortKey events) validateStockSecurityBalanceEvent : Text -> Decimal -> StockClassShareEvent -> Update Decimal @@ -395,7 +480,10 @@ validateStockSecurityBalanceEvent securityId current event = case event of pure next StockClassShareMultiplier with ratio -> pure (current * ratio.numerator / ratio.denominator) - StockClassShareRetraction _ _ -> + StockClassShareRetraction _ _ -> do + assertMsg + ("Stock security balance cannot be retired for " <> securityId <> " because it has no remaining balance") + (current > 0.0) pure 0.0 validateStockSecurityBalance : CapTableMaps -> StockIssuanceOcfData -> Update () From 13bf9a4f6b705cb3b157167332f535571c334111 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:43:52 +0000 Subject: [PATCH 05/38] Fix non-stock balance terminal-event invariants --- .../TestNonStockSecurityBalances.daml | 75 +++++++++++++++++++ .../codegen/templates/CapTable.daml.template | 58 ++++++++------ 2 files changed, 112 insertions(+), 21 deletions(-) diff --git a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml index fce16564..49378abd 100644 --- a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml @@ -270,3 +270,78 @@ testEquityCompensationSecurityBalance_ReductionAfterRetractionFails = script do ] edits = [] deletes = [] + +testWarrantSecurityBalance_RetractionAfterFullExerciseFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let securityId = "WSEC_BAL_REPEAT_TERMINAL" + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance (warrantIssuance "TX_W_BAL_REPEAT_TERMINAL" securityId defaultStakeholderId 100.0) + , CT.OcfCreateWarrantExercise (warrantExercise "TX_W_FULL_EXERCISE_REPEAT" securityId None jan02) + , CT.OcfCreateWarrantRetraction (warrantRetraction "TX_W_RETRACT_AFTER_FULL_EXERCISE" securityId jan03) + ] + edits = [] + deletes = [] + +testWarrantSecurityBalance_TransferWithBalanceSecurityRetiresSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let securityId = "WSEC_BAL_TRANSFER_BALANCE_SECURITY" + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance (warrantIssuance "TX_W_BAL_TRANSFER_BALANCE_SECURITY" securityId defaultStakeholderId 100.0) + , CT.OcfCreateWarrantTransfer ((warrantTransfer "TX_W_TRANSFER_BALANCE_SECURITY" securityId 60.0 jan02) with balance_security_id = Some "WSEC_BALANCE_TRANSFER") + , CT.OcfCreateWarrantCancellation (warrantCancellation "TX_W_CANCEL_AFTER_BALANCE_TRANSFER" securityId 1.0 jan03) + ] + edits = [] + deletes = [] + +testWarrantSecurityBalance_CancellationWithBalanceSecurityRetiresSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let securityId = "WSEC_BAL_CANCELLATION_BALANCE_SECURITY" + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance (warrantIssuance "TX_W_BAL_CANCELLATION_BALANCE_SECURITY" securityId defaultStakeholderId 100.0) + , CT.OcfCreateWarrantCancellation ((warrantCancellation "TX_W_CANCEL_BALANCE_SECURITY" securityId 60.0 jan02) with balance_security_id = Some "WSEC_BALANCE_CANCELLATION") + , CT.OcfCreateWarrantTransfer (warrantTransfer "TX_W_TRANSFER_AFTER_BALANCE_CANCELLATION" securityId 1.0 jan03) + ] + edits = [] + deletes = [] + +testEquityCompensationSecurityBalance_TransferWithBalanceSecurityRetiresSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let securityId = "ESEC_BAL_TRANSFER_BALANCE_SECURITY" + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_E_BAL_TRANSFER_BALANCE_SECURITY" securityId defaultStakeholderId 100.0) + , CT.OcfCreateEquityCompensationTransfer ((equityCompensationTransfer "TX_E_TRANSFER_BALANCE_SECURITY" securityId 60.0 jan02) with balance_security_id = Some "ESEC_BALANCE_TRANSFER") + , CT.OcfCreateEquityCompensationCancellation (equityCompensationCancellation "TX_E_CANCEL_AFTER_BALANCE_TRANSFER" securityId 1.0 jan03) + ] + edits = [] + deletes = [] + +testEquityCompensationSecurityBalance_CancellationWithBalanceSecurityRetiresSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let securityId = "ESEC_BAL_CANCELLATION_BALANCE_SECURITY" + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_E_BAL_CANCELLATION_BALANCE_SECURITY" securityId defaultStakeholderId 100.0) + , CT.OcfCreateEquityCompensationCancellation ((equityCompensationCancellation "TX_E_CANCEL_BALANCE_SECURITY" securityId 60.0 jan02) with balance_security_id = Some "ESEC_BALANCE_CANCELLATION") + , CT.OcfCreateEquityCompensationTransfer (equityCompensationTransfer "TX_E_TRANSFER_AFTER_BALANCE_CANCELLATION" securityId 1.0 jan03) + ] + edits = [] + deletes = [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index e41a8206..b9370905 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -970,7 +970,10 @@ validateSecurityQuantityBalanceEvent securityId currentOpt event = case currentO ("Security balance cannot be negative for " <> securityId <> ": applying delta " <> show quantity_delta <> " leaves " <> show next) (next >= 0.0) pure (Some next) - SecurityQuantityRetraction _ _ -> + SecurityQuantityRetraction _ _ -> do + assertMsg + ("Security retraction requires a positive balance for " <> securityId) + (current > 0.0) pure (Some 0.0) None -> case event of SecurityQuantityDelta {} -> @@ -986,17 +989,27 @@ validateSecurityQuantityBalance securityId initialQuantity events = do (sortOn securityQuantityEventSortKey events) pure () +securityQuantityBalanceTerminalEvents : Time -> Optional Text -> [SecurityQuantityEvent] +securityQuantityBalanceTerminalEvents eventDate balanceSecurityId = case balanceSecurityId of + Some _ -> + [ SecurityQuantityRetraction with + event_date = eventDate + event_priority = 3 + ] + None -> [] + warrantSecurityQuantityEvents : CapTableMaps -> WarrantIssuanceOcfData -> Update [SecurityQuantityEvent] warrantSecurityQuantityEvents maps issuanceData = do transferEvents <- foldlA (\events transferCid -> do transfer <- fetch transferCid if transfer.transfer_data.security_id == issuanceData.security_id - then pure ((SecurityQuantityDelta with - event_date = transfer.transfer_data.date - event_priority = 1 - quantity_delta = 0.0 - transfer.transfer_data.quantity) - :: events) + then + let delta = SecurityQuantityDelta with + event_date = transfer.transfer_data.date + event_priority = 1 + quantity_delta = 0.0 - transfer.transfer_data.quantity + in pure (delta :: (securityQuantityBalanceTerminalEvents transfer.transfer_data.date transfer.transfer_data.balance_security_id ++ events)) else pure events) [] (Map.values maps.warrant_transfers) @@ -1005,11 +1018,12 @@ warrantSecurityQuantityEvents maps issuanceData = do (\events cancellationCid -> do cancellation <- fetch cancellationCid if cancellation.cancellation_data.security_id == issuanceData.security_id - then pure ((SecurityQuantityDelta with - event_date = cancellation.cancellation_data.date - event_priority = 1 - quantity_delta = 0.0 - cancellation.cancellation_data.quantity) - :: events) + then + let delta = SecurityQuantityDelta with + event_date = cancellation.cancellation_data.date + event_priority = 1 + quantity_delta = 0.0 - cancellation.cancellation_data.quantity + in pure (delta :: (securityQuantityBalanceTerminalEvents cancellation.cancellation_data.date cancellation.cancellation_data.balance_security_id ++ events)) else pure events) [] (Map.values maps.warrant_cancellations) @@ -1078,11 +1092,12 @@ equityCompensationSecurityQuantityEvents maps issuanceData = do (\events transferCid -> do transfer <- fetch transferCid if transfer.transfer_data.security_id == issuanceData.security_id - then pure ((SecurityQuantityDelta with - event_date = transfer.transfer_data.date - event_priority = 1 - quantity_delta = 0.0 - transfer.transfer_data.quantity) - :: events) + then + let delta = SecurityQuantityDelta with + event_date = transfer.transfer_data.date + event_priority = 1 + quantity_delta = 0.0 - transfer.transfer_data.quantity + in pure (delta :: (securityQuantityBalanceTerminalEvents transfer.transfer_data.date transfer.transfer_data.balance_security_id ++ events)) else pure events) [] (Map.values maps.equity_compensation_transfers) @@ -1091,11 +1106,12 @@ equityCompensationSecurityQuantityEvents maps issuanceData = do (\events cancellationCid -> do cancellation <- fetch cancellationCid if cancellation.cancellation_data.security_id == issuanceData.security_id - then pure ((SecurityQuantityDelta with - event_date = cancellation.cancellation_data.date - event_priority = 1 - quantity_delta = 0.0 - cancellation.cancellation_data.quantity) - :: events) + then + let delta = SecurityQuantityDelta with + event_date = cancellation.cancellation_data.date + event_priority = 1 + quantity_delta = 0.0 - cancellation.cancellation_data.quantity + in pure (delta :: (securityQuantityBalanceTerminalEvents cancellation.cancellation_data.date cancellation.cancellation_data.balance_security_id ++ events)) else pure events) [] (Map.values maps.equity_compensation_cancellations) From 4c47c34c5f6d1157a8a8a12b7b085287193726bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:44:39 +0000 Subject: [PATCH 06/38] Refine security retraction error details --- scripts/codegen/templates/CapTable.daml.template | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index b9370905..bd7a72ef 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -972,7 +972,7 @@ validateSecurityQuantityBalanceEvent securityId currentOpt event = case currentO pure (Some next) SecurityQuantityRetraction _ _ -> do assertMsg - ("Security retraction requires a positive balance for " <> securityId) + ("Security retraction requires a positive balance for " <> securityId <> ": current balance is " <> show current) (current > 0.0) pure (Some 0.0) None -> case event of @@ -989,8 +989,8 @@ validateSecurityQuantityBalance securityId initialQuantity events = do (sortOn securityQuantityEventSortKey events) pure () -securityQuantityBalanceTerminalEvents : Time -> Optional Text -> [SecurityQuantityEvent] -securityQuantityBalanceTerminalEvents eventDate balanceSecurityId = case balanceSecurityId of +balanceSecurityQuantityTerminalEvents : Time -> Optional Text -> [SecurityQuantityEvent] +balanceSecurityQuantityTerminalEvents eventDate balanceSecurityId = case balanceSecurityId of Some _ -> [ SecurityQuantityRetraction with event_date = eventDate @@ -1009,7 +1009,7 @@ warrantSecurityQuantityEvents maps issuanceData = do event_date = transfer.transfer_data.date event_priority = 1 quantity_delta = 0.0 - transfer.transfer_data.quantity - in pure (delta :: (securityQuantityBalanceTerminalEvents transfer.transfer_data.date transfer.transfer_data.balance_security_id ++ events)) + in pure (delta :: (balanceSecurityQuantityTerminalEvents transfer.transfer_data.date transfer.transfer_data.balance_security_id ++ events)) else pure events) [] (Map.values maps.warrant_transfers) @@ -1023,7 +1023,7 @@ warrantSecurityQuantityEvents maps issuanceData = do event_date = cancellation.cancellation_data.date event_priority = 1 quantity_delta = 0.0 - cancellation.cancellation_data.quantity - in pure (delta :: (securityQuantityBalanceTerminalEvents cancellation.cancellation_data.date cancellation.cancellation_data.balance_security_id ++ events)) + in pure (delta :: (balanceSecurityQuantityTerminalEvents cancellation.cancellation_data.date cancellation.cancellation_data.balance_security_id ++ events)) else pure events) [] (Map.values maps.warrant_cancellations) @@ -1097,7 +1097,7 @@ equityCompensationSecurityQuantityEvents maps issuanceData = do event_date = transfer.transfer_data.date event_priority = 1 quantity_delta = 0.0 - transfer.transfer_data.quantity - in pure (delta :: (securityQuantityBalanceTerminalEvents transfer.transfer_data.date transfer.transfer_data.balance_security_id ++ events)) + in pure (delta :: (balanceSecurityQuantityTerminalEvents transfer.transfer_data.date transfer.transfer_data.balance_security_id ++ events)) else pure events) [] (Map.values maps.equity_compensation_transfers) @@ -1111,7 +1111,7 @@ equityCompensationSecurityQuantityEvents maps issuanceData = do event_date = cancellation.cancellation_data.date event_priority = 1 quantity_delta = 0.0 - cancellation.cancellation_data.quantity - in pure (delta :: (securityQuantityBalanceTerminalEvents cancellation.cancellation_data.date cancellation.cancellation_data.balance_security_id ++ events)) + in pure (delta :: (balanceSecurityQuantityTerminalEvents cancellation.cancellation_data.date cancellation.cancellation_data.balance_security_id ++ events)) else pure events) [] (Map.values maps.equity_compensation_cancellations) From e448b17edc00036576648e9b914dac0299a0c71c Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 10:10:40 -0400 Subject: [PATCH 07/38] Allow terminal balance reductions to consume remaining source --- .../TestNonStockSecurityBalances.daml | 32 ++++++++++ .../0.0.12/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- .../codegen/templates/CapTable.daml.template | 59 ++++++++++--------- 4 files changed, 69 insertions(+), 32 deletions(-) diff --git a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml index f967c5f9..3012761a 100644 --- a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml @@ -286,6 +286,22 @@ testWarrantSecurityBalance_CancellationWithBalanceSecurityRetiresSource = script edits = [] deletes = [] +testWarrantSecurityBalance_BalanceSecurityReductionCanConsumeRemainingSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let securityId = "WSEC_BAL_ZERO_REMAINDER" + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance (warrantIssuance "TX_W_BAL_ZERO_REMAINDER" securityId defaultStakeholderId 100.0) + , CT.OcfCreateWarrantCancellation (warrantCancellation "TX_W_CANCEL_BEFORE_BALANCE_TRANSFER" securityId 40.0 jan02) + , CT.OcfCreateWarrantTransfer ((warrantTransfer "TX_W_BALANCE_TRANSFER_REMAINDER" securityId 60.0 jan03) with balance_security_id = Some "WSEC_BALANCE_ZERO_REMAINDER") + ] + edits = [] + deletes = [] + pure () + testEquityCompensationSecurityBalance_CancellationCannotExceedIssuedQuantity = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableReady <- addDefaultStakeholder issuer cap_table @@ -375,3 +391,19 @@ testEquityCompensationSecurityBalance_CancellationWithBalanceSecurityRetiresSour ] edits = [] deletes = [] + +testEquityCompensationSecurityBalance_BalanceSecurityReductionCanConsumeRemainingSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let securityId = "ESEC_BAL_ZERO_REMAINDER" + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_E_BAL_ZERO_REMAINDER" securityId defaultStakeholderId 100.0) + , CT.OcfCreateEquityCompensationTransfer (equityCompensationTransfer "TX_E_TRANSFER_BEFORE_BALANCE_CANCEL" securityId 40.0 jan02) + , CT.OcfCreateEquityCompensationCancellation ((equityCompensationCancellation "TX_E_BALANCE_CANCEL_REMAINDER" securityId 60.0 jan03) with balance_security_id = Some "ESEC_BALANCE_ZERO_REMAINDER") + ] + edits = [] + deletes = [] + pure () diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index 37ff650d..9101bd3a 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78c5fca6c8c6757a63ffdc84f06ced4aebf054aaa88e4b134404025198f67fc4 -size 2720381 +oid sha256:196c49c9a56a7e5d5c32f8c970f1b78727617d36cac3bfaa3144206300839c63 +size 2721645 diff --git a/dars/dars.lock b/dars/dars.lock index 1e9ca561..a9d449ae 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,10 +26,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "78c5fca6c8c6757a63ffdc84f06ced4aebf054aaa88e4b134404025198f67fc4", - "size": 2720381, + "sha256": "196c49c9a56a7e5d5c32f8c970f1b78727617d36cac3bfaa3144206300839c63", + "size": 2721645, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T13:56:10.592Z", + "uploadedAt": "2026-07-09T14:09:26.775Z", "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 4dff9d30..08766753 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -140,6 +140,10 @@ data SecurityQuantityEvent event_date: Time event_priority: Int quantity_delta: Decimal + | SecurityQuantityTerminalDelta with + event_date: Time + event_priority: Int + quantity_delta: Decimal | SecurityQuantityRetraction with event_date: Time event_priority: Int @@ -748,14 +752,19 @@ balanceSecurityTerminalEvents eventDate balanceSecurityId = case balanceSecurity ] None -> [] -balanceSecurityQuantityTerminalEvents : Time -> Optional Text -> [SecurityQuantityEvent] -balanceSecurityQuantityTerminalEvents eventDate balanceSecurityId = case balanceSecurityId of - Some _ -> - [ SecurityQuantityRetraction with +securityQuantityReductionEvent : Time -> Decimal -> Optional Text -> SecurityQuantityEvent +securityQuantityReductionEvent eventDate quantity balanceSecurityId = + case balanceSecurityId of + Some _ -> + SecurityQuantityTerminalDelta with event_date = eventDate - event_priority = 3 - ] - None -> [] + event_priority = 1 + quantity_delta = 0.0 - quantity + None -> + SecurityQuantityDelta with + event_date = eventDate + event_priority = 1 + quantity_delta = 0.0 - quantity addStockClassShareEvent : Text -> StockClassShareEvent -> Map Text [StockClassShareEvent] -> Map Text [StockClassShareEvent] addStockClassShareEvent key event index = @@ -1026,6 +1035,7 @@ validateStockSecurityBalances maps = securityQuantityEventSortKey : SecurityQuantityEvent -> (Time, Int) securityQuantityEventSortKey event = case event of SecurityQuantityDelta with event_date; event_priority -> (event_date, event_priority) + SecurityQuantityTerminalDelta with event_date; event_priority -> (event_date, event_priority) SecurityQuantityRetraction with event_date; event_priority -> (event_date, event_priority) validateSecurityQuantityBalanceEvent : Text -> Optional Decimal -> SecurityQuantityEvent -> Update (Optional Decimal) @@ -1037,6 +1047,15 @@ validateSecurityQuantityBalanceEvent securityId currentOpt event = case currentO ("Security balance cannot be negative for " <> securityId <> ": applying delta " <> show quantity_delta <> " leaves " <> show next) (next >= 0.0) pure (Some next) + SecurityQuantityTerminalDelta with quantity_delta -> do + let next = current + quantity_delta + assertMsg + ("Security terminal reduction requires a positive balance for " <> securityId <> ": current balance is " <> show current) + (current > 0.0) + assertMsg + ("Security balance cannot be negative for " <> securityId <> ": applying delta " <> show quantity_delta <> " leaves " <> show next) + (next >= 0.0) + pure (Some 0.0) SecurityQuantityRetraction _ _ -> do assertMsg ("Security retraction requires a positive balance for " <> securityId <> ": current balance is " <> show current) @@ -1045,6 +1064,8 @@ validateSecurityQuantityBalanceEvent securityId currentOpt event = case currentO None -> case event of SecurityQuantityDelta {} -> pure None + SecurityQuantityTerminalDelta {} -> + pure (Some 0.0) SecurityQuantityRetraction _ _ -> pure (Some 0.0) @@ -1063,11 +1084,7 @@ warrantSecurityQuantityEvents maps issuanceData = do transfer <- fetch transferCid if transfer.transfer_data.security_id == issuanceData.security_id then - let delta = SecurityQuantityDelta with - event_date = transfer.transfer_data.date - event_priority = 1 - quantity_delta = 0.0 - transfer.transfer_data.quantity - in pure (delta :: (balanceSecurityQuantityTerminalEvents transfer.transfer_data.date transfer.transfer_data.balance_security_id ++ events)) + pure (securityQuantityReductionEvent transfer.transfer_data.date transfer.transfer_data.quantity transfer.transfer_data.balance_security_id :: events) else pure events) [] (Map.values maps.warrant_transfers) @@ -1077,11 +1094,7 @@ warrantSecurityQuantityEvents maps issuanceData = do cancellation <- fetch cancellationCid if cancellation.cancellation_data.security_id == issuanceData.security_id then - let delta = SecurityQuantityDelta with - event_date = cancellation.cancellation_data.date - event_priority = 1 - quantity_delta = 0.0 - cancellation.cancellation_data.quantity - in pure (delta :: (balanceSecurityQuantityTerminalEvents cancellation.cancellation_data.date cancellation.cancellation_data.balance_security_id ++ events)) + pure (securityQuantityReductionEvent cancellation.cancellation_data.date cancellation.cancellation_data.quantity cancellation.cancellation_data.balance_security_id :: events) else pure events) [] (Map.values maps.warrant_cancellations) @@ -1151,11 +1164,7 @@ equityCompensationSecurityQuantityEvents maps issuanceData = do transfer <- fetch transferCid if transfer.transfer_data.security_id == issuanceData.security_id then - let delta = SecurityQuantityDelta with - event_date = transfer.transfer_data.date - event_priority = 1 - quantity_delta = 0.0 - transfer.transfer_data.quantity - in pure (delta :: (balanceSecurityQuantityTerminalEvents transfer.transfer_data.date transfer.transfer_data.balance_security_id ++ events)) + pure (securityQuantityReductionEvent transfer.transfer_data.date transfer.transfer_data.quantity transfer.transfer_data.balance_security_id :: events) else pure events) [] (Map.values maps.equity_compensation_transfers) @@ -1165,11 +1174,7 @@ equityCompensationSecurityQuantityEvents maps issuanceData = do cancellation <- fetch cancellationCid if cancellation.cancellation_data.security_id == issuanceData.security_id then - let delta = SecurityQuantityDelta with - event_date = cancellation.cancellation_data.date - event_priority = 1 - quantity_delta = 0.0 - cancellation.cancellation_data.quantity - in pure (delta :: (balanceSecurityQuantityTerminalEvents cancellation.cancellation_data.date cancellation.cancellation_data.balance_security_id ++ events)) + pure (securityQuantityReductionEvent cancellation.cancellation_data.date cancellation.cancellation_data.quantity cancellation.cancellation_data.balance_security_id :: events) else pure events) [] (Map.values maps.equity_compensation_cancellations) From 9df404196469d4504cc6e9ca17b35532f5c96f13 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 10:17:59 -0400 Subject: [PATCH 08/38] Order same-day terminal balance reductions after deltas --- Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml | 4 ++-- dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- scripts/codegen/templates/CapTable.daml.template | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml index 3012761a..227a9490 100644 --- a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml @@ -296,7 +296,7 @@ testWarrantSecurityBalance_BalanceSecurityReductionCanConsumeRemainingSource = s creates = [ CT.OcfCreateWarrantIssuance (warrantIssuance "TX_W_BAL_ZERO_REMAINDER" securityId defaultStakeholderId 100.0) , CT.OcfCreateWarrantCancellation (warrantCancellation "TX_W_CANCEL_BEFORE_BALANCE_TRANSFER" securityId 40.0 jan02) - , CT.OcfCreateWarrantTransfer ((warrantTransfer "TX_W_BALANCE_TRANSFER_REMAINDER" securityId 60.0 jan03) with balance_security_id = Some "WSEC_BALANCE_ZERO_REMAINDER") + , CT.OcfCreateWarrantTransfer ((warrantTransfer "TX_W_BALANCE_TRANSFER_REMAINDER" securityId 60.0 jan02) with balance_security_id = Some "WSEC_BALANCE_ZERO_REMAINDER") ] edits = [] deletes = [] @@ -402,7 +402,7 @@ testEquityCompensationSecurityBalance_BalanceSecurityReductionCanConsumeRemainin creates = [ CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_E_BAL_ZERO_REMAINDER" securityId defaultStakeholderId 100.0) , CT.OcfCreateEquityCompensationTransfer (equityCompensationTransfer "TX_E_TRANSFER_BEFORE_BALANCE_CANCEL" securityId 40.0 jan02) - , CT.OcfCreateEquityCompensationCancellation ((equityCompensationCancellation "TX_E_BALANCE_CANCEL_REMAINDER" securityId 60.0 jan03) with balance_security_id = Some "ESEC_BALANCE_ZERO_REMAINDER") + , CT.OcfCreateEquityCompensationCancellation ((equityCompensationCancellation "TX_E_BALANCE_CANCEL_REMAINDER" securityId 60.0 jan02) with balance_security_id = Some "ESEC_BALANCE_ZERO_REMAINDER") ] edits = [] deletes = [] diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index 9101bd3a..f9106385 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:196c49c9a56a7e5d5c32f8c970f1b78727617d36cac3bfaa3144206300839c63 -size 2721645 +oid sha256:81405f77f871beef33e306dc88288b51aced1e9758f02b547b58991b0487b498 +size 2721652 diff --git a/dars/dars.lock b/dars/dars.lock index a9d449ae..69ae0249 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,10 +26,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "196c49c9a56a7e5d5c32f8c970f1b78727617d36cac3bfaa3144206300839c63", - "size": 2721645, + "sha256": "81405f77f871beef33e306dc88288b51aced1e9758f02b547b58991b0487b498", + "size": 2721652, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T14:09:26.775Z", + "uploadedAt": "2026-07-09T14:17:17.823Z", "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 08766753..3583f0ed 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -758,7 +758,7 @@ securityQuantityReductionEvent eventDate quantity balanceSecurityId = Some _ -> SecurityQuantityTerminalDelta with event_date = eventDate - event_priority = 1 + event_priority = 2 quantity_delta = 0.0 - quantity None -> SecurityQuantityDelta with From aaf8d268936be4c2192af4ddbc2316aad6c43e3b Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 10:54:47 -0400 Subject: [PATCH 09/38] Address stock security balance review feedback --- .../TestEquityCompensationRelease.daml | 2 +- Test/daml/OpenCapTable/TestHelpers.daml | 33 +++++++++++++--- .../TestNonStockSecurityBalances.daml | 30 +++++++------- .../TestStockClassAuthorizedShares.daml | 14 ------- .../TestStockSecurityBalances.daml | 39 +++++-------------- .../codegen/templates/CapTable.daml.template | 2 +- 6 files changed, 54 insertions(+), 66 deletions(-) diff --git a/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml b/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml index 44e748dc..233b7d6b 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml @@ -32,7 +32,7 @@ addEquityCompensationReleaseIssuance issuer capTableCid issuanceId securityId st board_approval_date = None consideration_text = None early_exercisable = None - exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + exercise_price = None expiration_date = None stock_class_id = None stock_plan_id = None diff --git a/Test/daml/OpenCapTable/TestHelpers.daml b/Test/daml/OpenCapTable/TestHelpers.daml index 9928dda0..b95e7d66 100644 --- a/Test/daml/OpenCapTable/TestHelpers.daml +++ b/Test/daml/OpenCapTable/TestHelpers.daml @@ -13,6 +13,7 @@ import Fairmint.OpenCapTable.OCF.VestingTerms (VestingTermsOcfData(..), OcfAlloc import Fairmint.OpenCapTable.OCF.WarrantIssuance (WarrantIssuanceOcfData(..)) import Fairmint.OpenCapTable.OCF.ConvertibleIssuance (ConvertibleIssuanceOcfData(..), OcfConvertibleConversionTrigger(..)) import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance (EquityCompensationIssuanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationRelease (EquityCompensationReleaseOcfData(..)) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Stock (OcfStockClassType(..), OcfInitialSharesAuthorized(..), OcfRatio(..)) import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) @@ -92,24 +93,32 @@ stockClassAuthorizedSharesAdjustment adjustmentId stockClassId authorizedShares stockholder_approval_date = None comments = [] -stockCancellation : Text -> Text -> Decimal -> StockCancellationOcfData -stockCancellation cancellationId securityId quantity = StockCancellationOcfData with +stockCancellationOn : Text -> Text -> Decimal -> Time -> StockCancellationOcfData +stockCancellationOn cancellationId securityId quantity date = StockCancellationOcfData with id = cancellationId - date = defaultTestDate + date = date security_id = securityId quantity = quantity balance_security_id = None reason_text = "Cancelled shares" comments = [] -stockClassSplit : Text -> Text -> Decimal -> Decimal -> StockClassSplitOcfData -stockClassSplit splitId stockClassId numerator denominator = StockClassSplitOcfData with +stockCancellation : Text -> Text -> Decimal -> StockCancellationOcfData +stockCancellation cancellationId securityId quantity = + stockCancellationOn cancellationId securityId quantity defaultTestDate + +stockClassSplitOn : Text -> Text -> Decimal -> Decimal -> Time -> StockClassSplitOcfData +stockClassSplitOn splitId stockClassId numerator denominator date = StockClassSplitOcfData with id = splitId - date = defaultTestDate + date = date stock_class_id = stockClassId split_ratio = OcfRatio with numerator = numerator, denominator = denominator comments = [] +stockClassSplit : Text -> Text -> Decimal -> Decimal -> StockClassSplitOcfData +stockClassSplit splitId stockClassId numerator denominator = + stockClassSplitOn splitId stockClassId numerator denominator defaultTestDate + stockPlanEquityCompensationIssuance : Text -> Text -> Text -> Text -> Text -> Decimal -> EquityCompensationIssuanceOcfData stockPlanEquityCompensationIssuance issuanceId securityId stakeholderId stockClassId stockPlanId quantity = EquityCompensationIssuanceOcfData with id = issuanceId @@ -134,6 +143,18 @@ stockPlanEquityCompensationIssuance issuanceId securityId stakeholderId stockCla quantity = quantity comments = [] +equityCompensationRelease : Text -> Text -> Time -> Time -> Decimal -> EquityCompensationReleaseOcfData +equityCompensationRelease releaseId securityId releaseDate settlementDate quantity = EquityCompensationReleaseOcfData with + id = releaseId + date = releaseDate + security_id = securityId + settlement_date = settlementDate + release_price = OcfMonetary with amount = 1.0, currency = "USD" + quantity = quantity + resulting_security_ids = [releaseId <> "_RESULT"] + consideration_text = None + comments = [] + -- | Returns valid StakeholderOcfData with sensible defaults. -- Provide ID and name; all other fields use reasonable test values. -- diff --git a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml index 227a9490..5cdee12b 100644 --- a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml @@ -4,7 +4,6 @@ import qualified Fairmint.OpenCapTable.CapTable as CT import Fairmint.OpenCapTable.OCF.EquityCompensationCancellation (EquityCompensationCancellationOcfData(..)) import Fairmint.OpenCapTable.OCF.EquityCompensationExercise (EquityCompensationExerciseOcfData(..)) import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance (EquityCompensationIssuanceOcfData(..)) -import Fairmint.OpenCapTable.OCF.EquityCompensationRelease (EquityCompensationReleaseOcfData(..)) import Fairmint.OpenCapTable.OCF.EquityCompensationRetraction (EquityCompensationRetractionOcfData(..)) import Fairmint.OpenCapTable.OCF.EquityCompensationTransfer (EquityCompensationTransferOcfData(..)) import Fairmint.OpenCapTable.OCF.WarrantCancellation (WarrantCancellationOcfData(..)) @@ -147,18 +146,6 @@ equityCompensationExercise exerciseId securityId quantity date = EquityCompensat consideration_text = None comments = [] -equityCompensationRelease : Text -> Text -> Decimal -> Time -> EquityCompensationReleaseOcfData -equityCompensationRelease releaseId securityId quantity date = EquityCompensationReleaseOcfData with - id = releaseId - date = date - security_id = securityId - quantity = quantity - settlement_date = date - release_price = OcfMonetary with amount = 1.0, currency = "USD" - resulting_security_ids = [releaseId <> "_RESULT"] - consideration_text = None - comments = [] - equityCompensationRetraction : Text -> Text -> Time -> EquityCompensationRetractionOcfData equityCompensationRetraction retractionId securityId date = EquityCompensationRetractionOcfData with id = retractionId @@ -326,7 +313,7 @@ testEquityCompensationSecurityBalance_CumulativeReductionsCannotExceedIssuedQuan creates = [ CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_E_BAL_CUMULATIVE" securityId defaultStakeholderId 100.0) , CT.OcfCreateEquityCompensationExercise (equityCompensationExercise "TX_E_EXERCISE_40" securityId 40.0 jan02) - , CT.OcfCreateEquityCompensationRelease (equityCompensationRelease "TX_E_RELEASE_40" securityId 40.0 jan03) + , CT.OcfCreateEquityCompensationRelease (equityCompensationRelease "TX_E_RELEASE_40" securityId jan03 jan03 40.0) , CT.OcfCreateEquityCompensationTransfer (equityCompensationTransfer "TX_E_TRANSFER_25" securityId 25.0 jan04) ] edits = [] @@ -347,6 +334,21 @@ testEquityCompensationSecurityBalance_ReductionAfterRetractionFails = script do edits = [] deletes = [] +testEquityCompensationSecurityBalance_ReleaseSettlingAfterRetractionFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let securityId = "ESEC_BAL_RELEASE_SETTLES_AFTER_RETRACTION" + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_E_BAL_RELEASE_AFTER_RETRACTION" securityId defaultStakeholderId 100.0) + , CT.OcfCreateEquityCompensationRelease (equityCompensationRelease "TX_E_RELEASE_AFTER_RETRACTION" securityId jan02 jan04 50.0) + , CT.OcfCreateEquityCompensationRetraction (equityCompensationRetraction "TX_E_RETRACT_BEFORE_SETTLEMENT" securityId jan03) + ] + edits = [] + deletes = [] + testEquityCompensationSecurityBalance_RepeatedRetractionFails = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableReady <- addDefaultStakeholder issuer cap_table diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index f89a3f60..f35aa4f2 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -3,7 +3,6 @@ module OpenCapTable.TestStockClassAuthorizedShares where import Fairmint.OpenCapTable.OCF.StockConversion import Fairmint.OpenCapTable.OCF.EquityCompensationExercise import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance -import Fairmint.OpenCapTable.OCF.EquityCompensationRelease import Fairmint.OpenCapTable.OCF.StockRepurchase import Fairmint.OpenCapTable.OCF.StockRetraction import Fairmint.OpenCapTable.OCF.StockTransfer @@ -203,19 +202,6 @@ equityCompensationExercise exerciseId securityId quantity = resulting_security_ids = [exerciseId <> "_RESULT"] consideration_text = None -equityCompensationRelease : Text -> Text -> Time -> Time -> Decimal -> EquityCompensationReleaseOcfData -equityCompensationRelease releaseId securityId releaseDate settlementDate quantity = - EquityCompensationReleaseOcfData with - id = releaseId - date = releaseDate - security_id = securityId - settlement_date = settlementDate - release_price = OcfMonetary with amount = 1.0, currency = "USD" - quantity = quantity - resulting_security_ids = [releaseId <> "_RESULT"] - consideration_text = None - comments = [] - testStockIssuanceCannotExceedStockClassInitialAuthorizedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestStockSecurityBalances.daml index 5f8c4469..0334fd58 100644 --- a/Test/daml/OpenCapTable/TestStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestStockSecurityBalances.daml @@ -1,8 +1,6 @@ module OpenCapTable.TestStockSecurityBalances where import qualified Fairmint.OpenCapTable.CapTable as CT -import Fairmint.OpenCapTable.OCF.StockCancellation (StockCancellationOcfData(..)) -import Fairmint.OpenCapTable.OCF.StockClassSplit (StockClassSplitOcfData(..)) import Fairmint.OpenCapTable.OCF.StockConsolidation (StockConsolidationOcfData(..)) import Fairmint.OpenCapTable.OCF.StockConversion (StockConversionOcfData(..)) import Fairmint.OpenCapTable.OCF.StockRepurchase (StockRepurchaseOcfData(..)) @@ -10,7 +8,6 @@ import Fairmint.OpenCapTable.OCF.StockReissuance (StockReissuanceOcfData(..)) import Fairmint.OpenCapTable.OCF.StockRetraction (StockRetractionOcfData(..)) import Fairmint.OpenCapTable.OCF.StockTransfer (StockTransferOcfData(..)) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) -import Fairmint.OpenCapTable.Types.Stock (OcfRatio(..)) import OpenCapTable.Setup import OpenCapTable.TestHelpers import qualified DA.Date as DA @@ -18,16 +15,6 @@ import DA.Date (Month(..)) import qualified DA.Time as DT import Daml.Script -stockCancellation : Text -> Text -> Decimal -> Time -> StockCancellationOcfData -stockCancellation cancellationId securityId quantity date = StockCancellationOcfData with - id = cancellationId - date = date - security_id = securityId - quantity = quantity - balance_security_id = None - reason_text = "Cancelled shares" - comments = [] - stockRepurchase : Text -> Text -> Decimal -> Time -> StockRepurchaseOcfData stockRepurchase repurchaseId securityId quantity date = StockRepurchaseOcfData with id = repurchaseId @@ -92,14 +79,6 @@ stockConsolidation consolidationId securityIds date = StockConsolidationOcfData reason_text = None comments = [] -stockClassSplit : Text -> Text -> Decimal -> Decimal -> Time -> StockClassSplitOcfData -stockClassSplit splitId stockClassId numerator denominator date = StockClassSplitOcfData with - id = splitId - date = date - stock_class_id = stockClassId - split_ratio = OcfRatio with numerator = numerator, denominator = denominator - comments = [] - jan02 : Time jan02 = DT.time (DA.date 2024 Jan 02) 0 0 0 @@ -116,7 +95,7 @@ testStockSecurityBalance_CancellationCannotExceedIssuedShares = script do exerciseCmd capTableReady CT.UpdateCapTable with creates = [ CT.OcfCreateStockIssuance issuance - , CT.OcfCreateStockCancellation (stockCancellation "TX_CANCEL_TOO_MUCH" securityId 101.0 jan02) + , CT.OcfCreateStockCancellation (stockCancellationOn "TX_CANCEL_TOO_MUCH" securityId 101.0 jan02) ] edits = [] deletes = [] @@ -131,7 +110,7 @@ testStockSecurityBalance_CumulativeReductionsCannotExceedIssuedShares = script d exerciseCmd capTableReady CT.UpdateCapTable with creates = [ CT.OcfCreateStockIssuance issuance - , CT.OcfCreateStockCancellation (stockCancellation "TX_CANCEL_60" securityId 60.0 jan02) + , CT.OcfCreateStockCancellation (stockCancellationOn "TX_CANCEL_60" securityId 60.0 jan02) , CT.OcfCreateStockRepurchase (stockRepurchase "TX_REPURCHASE_50" securityId 50.0 jan03) ] edits = [] @@ -163,7 +142,7 @@ testStockSecurityBalance_BalanceSecurityRetiresSource = script do creates = [ CT.OcfCreateStockIssuance issuance , CT.OcfCreateStockTransfer (stockTransferWithBalance "TX_TRANSFER_WITH_BALANCE" securityId 40.0 "SEC_STOCK_BAL_REMAINDER" jan02) - , CT.OcfCreateStockCancellation (stockCancellation "TX_CANCEL_OLD_SOURCE_AFTER_BALANCE" securityId 1.0 jan03) + , CT.OcfCreateStockCancellation (stockCancellationOn "TX_CANCEL_OLD_SOURCE_AFTER_BALANCE" securityId 1.0 jan03) ] edits = [] deletes = [] @@ -194,7 +173,7 @@ testStockSecurityBalance_ReductionAfterRetractionFails = script do creates = [ CT.OcfCreateStockIssuance issuance , CT.OcfCreateStockRetraction (stockRetraction "TX_RETRACT_ALL" securityId jan02) - , CT.OcfCreateStockCancellation (stockCancellation "TX_CANCEL_AFTER_RETRACT" securityId 1.0 jan03) + , CT.OcfCreateStockCancellation (stockCancellationOn "TX_CANCEL_AFTER_RETRACT" securityId 1.0 jan03) ] edits = [] deletes = [] @@ -210,7 +189,7 @@ testStockSecurityBalance_ReductionAfterReissuanceFails = script do creates = [ CT.OcfCreateStockIssuance issuance , CT.OcfCreateStockReissuance (stockReissuance "TX_REISSUE_ALL" securityId jan02) - , CT.OcfCreateStockCancellation (stockCancellation "TX_CANCEL_AFTER_REISSUE" securityId 1.0 jan03) + , CT.OcfCreateStockCancellation (stockCancellationOn "TX_CANCEL_AFTER_REISSUE" securityId 1.0 jan03) ] edits = [] deletes = [] @@ -242,7 +221,7 @@ testStockSecurityBalance_ReductionAfterConsolidationFails = script do creates = [ CT.OcfCreateStockIssuance issuance , CT.OcfCreateStockConsolidation (stockConsolidation "TX_CONSOLIDATE_ALL" [securityId] jan02) - , CT.OcfCreateStockCancellation (stockCancellation "TX_CANCEL_AFTER_CONSOLIDATE" securityId 1.0 jan03) + , CT.OcfCreateStockCancellation (stockCancellationOn "TX_CANCEL_AFTER_CONSOLIDATE" securityId 1.0 jan03) ] edits = [] deletes = [] @@ -257,7 +236,7 @@ testStockSecurityBalance_SplitAdjustedBalanceAllowsReduction = script do exerciseCmd capTableReady CT.UpdateCapTable with creates = [ CT.OcfCreateStockIssuance issuance - , CT.OcfCreateStockClassSplit (stockClassSplit "TX_SPLIT_2_FOR_1" stockClassId 2.0 1.0 jan02) + , CT.OcfCreateStockClassSplit (stockClassSplitOn "TX_SPLIT_2_FOR_1" stockClassId 2.0 1.0 jan02) , CT.OcfCreateStockRepurchase (stockRepurchase "TX_REPURCHASE_AFTER_SPLIT" securityId 150.0 jan03) ] edits = [] @@ -274,8 +253,8 @@ testStockSecurityBalance_ReverseSplitReducesAvailableBalance = script do exerciseCmd capTableReady CT.UpdateCapTable with creates = [ CT.OcfCreateStockIssuance issuance - , CT.OcfCreateStockClassSplit (stockClassSplit "TX_REVERSE_SPLIT" stockClassId 1.0 2.0 jan02) - , CT.OcfCreateStockCancellation (stockCancellation "TX_CANCEL_AFTER_REVERSE_SPLIT" securityId 75.0 jan03) + , CT.OcfCreateStockClassSplit (stockClassSplitOn "TX_REVERSE_SPLIT" stockClassId 1.0 2.0 jan02) + , CT.OcfCreateStockCancellation (stockCancellationOn "TX_CANCEL_AFTER_REVERSE_SPLIT" securityId 75.0 jan03) ] edits = [] deletes = [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 363d7d94..e676bca2 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -1201,7 +1201,7 @@ equityCompensationSecurityQuantityEvents maps issuanceData = do release <- fetch releaseCid if release.release_data.security_id == issuanceData.security_id then pure ((SecurityQuantityDelta with - event_date = release.release_data.date + event_date = release.release_data.settlement_date event_priority = 1 quantity_delta = 0.0 - release.release_data.quantity) :: events) From d1ba8fbbf16b25efaa3db702552faacbb2e3e52a Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 10:59:56 -0400 Subject: [PATCH 10/38] Update stock security DAR backup --- dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index f9106385..b566ebd3 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81405f77f871beef33e306dc88288b51aced1e9758f02b547b58991b0487b498 -size 2721652 +oid sha256:42cde6e37b6ab3d750f1d70f77f3403de23c0c15c8c51b572a5b8df73a0adada +size 2726091 diff --git a/dars/dars.lock b/dars/dars.lock index e98667d7..eb417cbb 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,8 +26,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "81405f77f871beef33e306dc88288b51aced1e9758f02b547b58991b0487b498", - "size": 2721652, + "sha256": "42cde6e37b6ab3d750f1d70f77f3403de23c0c15c8c51b572a5b8df73a0adada", + "size": 2726091, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T14:17:17.823Z", "networks": [] From 38a46b200a3b1ca8774cba6fda2b1ee5ca59e086 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 11:28:32 -0400 Subject: [PATCH 11/38] Refresh security balance DAR after base update --- dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index b566ebd3..f6818b2c 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42cde6e37b6ab3d750f1d70f77f3403de23c0c15c8c51b572a5b8df73a0adada -size 2726091 +oid sha256:c9ede9d3d6bb7e869b9e26ae8376c50b5ef3ef56c8ba282922a809bc82b4c206 +size 2733102 diff --git a/dars/dars.lock b/dars/dars.lock index 70cd8542..faf07b7f 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,8 +26,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "42cde6e37b6ab3d750f1d70f77f3403de23c0c15c8c51b572a5b8df73a0adada", - "size": 2726091, + "sha256": "c9ede9d3d6bb7e869b9e26ae8376c50b5ef3ef56c8ba282922a809bc82b4c206", + "size": 2733102, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T14:17:17.823Z", "networks": [] From 088a9e8fa1526c85e3b7a4d0d570ea8a38aecaaf Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 11:43:56 -0400 Subject: [PATCH 12/38] Refresh security balance DAR after ceiling update --- dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index f6818b2c..3961976a 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9ede9d3d6bb7e869b9e26ae8376c50b5ef3ef56c8ba282922a809bc82b4c206 -size 2733102 +oid sha256:e617877e6f0cf19e9b5c351da12c11c5f96db8f2de1f9cc90ea71886fd09ff04 +size 2733510 diff --git a/dars/dars.lock b/dars/dars.lock index af3ee40b..3b7b936f 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,8 +26,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "c9ede9d3d6bb7e869b9e26ae8376c50b5ef3ef56c8ba282922a809bc82b4c206", - "size": 2733102, + "sha256": "e617877e6f0cf19e9b5c351da12c11c5f96db8f2de1f9cc90ea71886fd09ff04", + "size": 2733510, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T14:17:17.823Z", "networks": [] From 5191f61481c3d5ba8ea1f598deada73e1b85bfb1 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 12:34:02 -0400 Subject: [PATCH 13/38] Address security balance review feedback --- .../TestNonStockSecurityBalances.daml | 38 +++++++++ .../TestStockSecurityBalances.daml | 17 ++++ .../0.0.12/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- .../codegen/templates/CapTable.daml.template | 77 ++++++++++++------- 5 files changed, 111 insertions(+), 31 deletions(-) diff --git a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml index 5cdee12b..44a341ac 100644 --- a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml @@ -11,6 +11,7 @@ import Fairmint.OpenCapTable.OCF.WarrantExercise (WarrantExerciseOcfData(..)) import Fairmint.OpenCapTable.OCF.WarrantIssuance (WarrantIssuanceOcfData(..)) import Fairmint.OpenCapTable.OCF.WarrantRetraction (WarrantRetractionOcfData(..)) import Fairmint.OpenCapTable.OCF.WarrantTransfer (WarrantTransferOcfData(..)) +import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfConversionTrigger(..), OcfConversionTriggerType(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Stock (OcfQuantitySourceType(..)) import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) @@ -30,6 +31,26 @@ jan03 = DT.time (DA.date 2024 Jan 03) 0 0 0 jan04 : Time jan04 = DT.time (DA.date 2024 Jan 04) 0 0 0 +preIssuanceDate : Time +preIssuanceDate = DT.time (DA.date 2023 Dec 31) 0 0 0 + +warrantFixedExerciseTrigger : Text -> Text -> Decimal -> OcfConversionTrigger +warrantFixedExerciseTrigger triggerId stockClassId convertsToQuantity = + OcfConversionTrigger with + conversion_right = OcfRightWarrant (OcfWarrantConversionRight with + conversion_mechanism = OcfWarrantMechanismFixedAmount with converts_to_quantity = convertsToQuantity + converts_to_future_round = None + converts_to_stock_class_id = Some stockClassId + type_ = "WARRANT_CONVERSION_RIGHT") + end_date = None + nickname = None + start_date = None + trigger_condition = None + trigger_date = None + trigger_description = None + trigger_id = triggerId + type_ = OcfTriggerTypeTypeElectiveAtWill + warrantIssuance : Text -> Text -> Text -> Decimal -> WarrantIssuanceOcfData warrantIssuance issuanceId securityId stakeholderId quantity = WarrantIssuanceOcfData with id = issuanceId @@ -289,6 +310,23 @@ testWarrantSecurityBalance_BalanceSecurityReductionCanConsumeRemainingSource = s deletes = [] pure () +testWarrantSecurityBalance_UnknownQuantityExerciseBeforeIssuanceFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let securityId = "WSEC_BAL_UNKNOWN_PRE_ISSUE" + let issuance = (warrantIssuance "TX_W_BAL_UNKNOWN_PRE_ISSUE" securityId stakeholderId 100.0) with + quantity = None + exercise_triggers = [warrantFixedExerciseTrigger "TEST_TRIGGER" stockClassId 25.0] + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance issuance + , CT.OcfCreateWarrantExercise (warrantExercise "TX_W_EXERCISE_BEFORE_ISSUE" securityId (Some 25.0) preIssuanceDate) + ] + edits = [] + deletes = [] + testEquityCompensationSecurityBalance_CancellationCannotExceedIssuedQuantity = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableReady <- addDefaultStakeholder issuer cap_table diff --git a/Test/daml/OpenCapTable/TestStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestStockSecurityBalances.daml index 0334fd58..548a29df 100644 --- a/Test/daml/OpenCapTable/TestStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestStockSecurityBalances.daml @@ -147,6 +147,23 @@ testStockSecurityBalance_BalanceSecurityRetiresSource = script do edits = [] deletes = [] +testStockSecurityBalance_BalanceSecurityReductionCanConsumeRemainingSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let securityId = "SEC_STOCK_BAL_ZERO_REMAINDER" + let issuance = (defaultStockIssuance "TX_STOCK_BAL_ZERO_REMAINDER" securityId stakeholderId stockClassId) with quantity = 100.0 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockCancellation (stockCancellationOn "TX_CANCEL_BEFORE_BALANCE_TRANSFER" securityId 40.0 jan02) + , CT.OcfCreateStockTransfer (stockTransferWithBalance "TX_TRANSFER_BALANCE_REMAINDER" securityId 60.0 "SEC_STOCK_BAL_REMAINDER" jan03) + ] + edits = [] + deletes = [] + pure () + testStockSecurityBalance_ConversionCannotExceedIssuedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index 3961976a..1fc56c7d 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e617877e6f0cf19e9b5c351da12c11c5f96db8f2de1f9cc90ea71886fd09ff04 -size 2733510 +oid sha256:b1d5e284d4b9d89c15a8d693e050ab6e9af8c3cf9c5dfda816df89086476da5c +size 2737137 diff --git a/dars/dars.lock b/dars/dars.lock index 3b7b936f..16af9971 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,10 +26,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "e617877e6f0cf19e9b5c351da12c11c5f96db8f2de1f9cc90ea71886fd09ff04", - "size": 2733510, + "sha256": "b1d5e284d4b9d89c15a8d693e050ab6e9af8c3cf9c5dfda816df89086476da5c", + "size": 2737137, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T14:17:17.823Z", + "uploadedAt": "2026-07-09T16:32:54Z", "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 e886b968..eb792b80 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -121,6 +121,10 @@ data StockClassShareEvent | StockClassShareRetraction with event_date: Time event_priority: Int + | StockClassShareTerminalDelta with + event_date: Time + event_priority: Int + quantity_delta: Decimal deriving (Eq, Show) data StockClassShareLot = StockClassShareLot with @@ -710,18 +714,21 @@ issuerAuthorizedLimitAt adjustmentHistory issuerData asOf = stockClassShareEventSortKey : StockClassShareEvent -> (Time, Int) stockClassShareEventSortKey event = case event of StockClassShareDelta with event_date; event_priority -> (event_date, event_priority) + StockClassShareTerminalDelta with event_date; event_priority -> (event_date, event_priority) StockClassShareMultiplier with event_date; event_priority -> (event_date, event_priority) StockClassShareRetraction with event_date; event_priority -> (event_date, event_priority) stockClassShareEventOnOrAfter : Time -> StockClassShareEvent -> Bool stockClassShareEventOnOrAfter earliest event = case event of StockClassShareDelta with event_date -> event_date >= earliest + StockClassShareTerminalDelta with event_date -> event_date >= earliest StockClassShareMultiplier with event_date -> event_date >= earliest StockClassShareRetraction with event_date -> event_date >= earliest stockClassShareEventOnOrBefore : Time -> StockClassShareEvent -> Bool stockClassShareEventOnOrBefore latest event = case event of StockClassShareDelta with event_date -> event_date <= latest + StockClassShareTerminalDelta with event_date -> event_date <= latest StockClassShareMultiplier with event_date -> event_date <= latest StockClassShareRetraction with event_date -> event_date <= latest @@ -730,6 +737,9 @@ applyStockClassShareEvent current event = case event of StockClassShareDelta with quantity_delta -> let next = current + quantity_delta in if next < 0.0 then None else Some next + StockClassShareTerminalDelta with quantity_delta -> + let next = current + quantity_delta + in if next < 0.0 then None else Some 0.0 StockClassShareMultiplier with ratio -> Some (current * ratio.numerator / ratio.denominator) StockClassShareRetraction _ _ -> Some 0.0 @@ -739,14 +749,20 @@ applyStockClassShareEventOptional currentOpt event = Some current -> applyStockClassShareEvent current event None -> None -balanceSecurityTerminalEvents : Time -> Optional Text -> [StockClassShareEvent] -balanceSecurityTerminalEvents eventDate balanceSecurityId = case balanceSecurityId of +stockSecurityReductionEvents : Time -> Decimal -> Optional Text -> [StockClassShareEvent] +stockSecurityReductionEvents eventDate quantity balanceSecurityId = case balanceSecurityId of Some _ -> - [ StockClassShareRetraction with + [ StockClassShareTerminalDelta with + event_date = eventDate + event_priority = 2 + quantity_delta = 0.0 - quantity + ] + None -> + [ StockClassShareDelta with event_date = eventDate - event_priority = 3 + event_priority = 1 + quantity_delta = 0.0 - quantity ] - None -> [] securityQuantityReductionEvent : Time -> Decimal -> Optional Text -> SecurityQuantityEvent securityQuantityReductionEvent eventDate quantity balanceSecurityId = @@ -897,11 +913,7 @@ stockSecurityShareEvents maps issuanceData = do cancellation <- fetch cancellationCid if cancellation.cancellation_data.security_id == issuanceData.security_id then - let delta = StockClassShareDelta with - event_date = cancellation.cancellation_data.date - event_priority = 1 - quantity_delta = 0.0 - cancellation.cancellation_data.quantity - in pure (delta :: (balanceSecurityTerminalEvents cancellation.cancellation_data.date cancellation.cancellation_data.balance_security_id ++ events)) + pure (stockSecurityReductionEvents cancellation.cancellation_data.date cancellation.cancellation_data.quantity cancellation.cancellation_data.balance_security_id ++ events) else pure events) [] (Map.values maps.stock_cancellations) @@ -911,11 +923,7 @@ stockSecurityShareEvents maps issuanceData = do repurchase <- fetch repurchaseCid if repurchase.repurchase_data.security_id == issuanceData.security_id then - let delta = StockClassShareDelta with - event_date = repurchase.repurchase_data.date - event_priority = 1 - quantity_delta = 0.0 - repurchase.repurchase_data.quantity - in pure (delta :: (balanceSecurityTerminalEvents repurchase.repurchase_data.date repurchase.repurchase_data.balance_security_id ++ events)) + pure (stockSecurityReductionEvents repurchase.repurchase_data.date repurchase.repurchase_data.quantity repurchase.repurchase_data.balance_security_id ++ events) else pure events) [] (Map.values maps.stock_repurchases) @@ -925,11 +933,7 @@ stockSecurityShareEvents maps issuanceData = do transfer <- fetch transferCid if transfer.transfer_data.security_id == issuanceData.security_id then - let delta = StockClassShareDelta with - event_date = transfer.transfer_data.date - event_priority = 1 - quantity_delta = 0.0 - transfer.transfer_data.quantity - in pure (delta :: (balanceSecurityTerminalEvents transfer.transfer_data.date transfer.transfer_data.balance_security_id ++ events)) + pure (stockSecurityReductionEvents transfer.transfer_data.date transfer.transfer_data.quantity transfer.transfer_data.balance_security_id ++ events) else pure events) [] (Map.values maps.stock_transfers) @@ -939,11 +943,7 @@ stockSecurityShareEvents maps issuanceData = do conversion <- fetch conversionCid if conversion.conversion_data.security_id == issuanceData.security_id then - let delta = StockClassShareDelta with - event_date = conversion.conversion_data.date - event_priority = 1 - quantity_delta = 0.0 - conversion.conversion_data.quantity_converted - in pure (delta :: (balanceSecurityTerminalEvents conversion.conversion_data.date conversion.conversion_data.balance_security_id ++ events)) + pure (stockSecurityReductionEvents conversion.conversion_data.date conversion.conversion_data.quantity_converted conversion.conversion_data.balance_security_id ++ events) else pure events) [] (Map.values maps.stock_conversions) @@ -1011,6 +1011,15 @@ validateStockSecurityBalanceEvent securityId current event = case event of ("Stock security balance cannot be negative for " <> securityId <> ": applying delta " <> show quantity_delta <> " leaves " <> show next) (next >= 0.0) pure next + StockClassShareTerminalDelta with quantity_delta -> do + let next = current + quantity_delta + assertMsg + ("Stock security terminal reduction requires a positive balance for " <> securityId <> ": current balance is " <> show current) + (current > 0.0) + assertMsg + ("Stock security balance cannot be negative for " <> securityId <> ": applying delta " <> show quantity_delta <> " leaves " <> show next) + (next >= 0.0) + pure 0.0 StockClassShareMultiplier with ratio -> pure (current * ratio.numerator / ratio.denominator) StockClassShareRetraction _ _ -> do @@ -1042,6 +1051,21 @@ securityQuantityEventSortKey event = case event of SecurityQuantityTerminalDelta with event_date; event_priority -> (event_date, event_priority) SecurityQuantityRetraction with event_date; event_priority -> (event_date, event_priority) +securityQuantityEventDate : SecurityQuantityEvent -> Time +securityQuantityEventDate event = case event of + SecurityQuantityDelta with event_date -> event_date + SecurityQuantityTerminalDelta with event_date -> event_date + SecurityQuantityRetraction with event_date -> event_date + +assertSecurityQuantityEventsOnOrAfter : Text -> Time -> [SecurityQuantityEvent] -> Update () +assertSecurityQuantityEventsOnOrAfter securityId earliest events = + mapA_ + (\event -> + assertMsg + ("Security quantity event cannot precede issuance for " <> securityId <> ": event date " <> show (securityQuantityEventDate event) <> " is before " <> show earliest) + (securityQuantityEventDate event >= earliest)) + events + validateSecurityQuantityBalanceEvent : Text -> Optional Decimal -> SecurityQuantityEvent -> Update (Optional Decimal) validateSecurityQuantityBalanceEvent securityId currentOpt event = case currentOpt of Some current -> case event of @@ -1150,7 +1174,8 @@ validateWarrantSecurityBalance maps issuanceData = do event_priority = 0 quantity_delta = quantity) :: events) - None -> + None -> do + assertSecurityQuantityEventsOnOrAfter issuanceData.security_id issuanceData.date events validateSecurityQuantityBalance issuanceData.security_id None events validateWarrantSecurityBalances : CapTableMaps -> Update () From 338c33fc76d287eb4dee0bb04b6b57a61ac9968b Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 13:23:27 -0400 Subject: [PATCH 14/38] Address security balance review feedback --- .../TestStockPlanReservedShares.daml | 35 ++++++++++++++++ .../TestStockSecurityBalances.daml | 25 +++++++++++ .../0.0.12/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +-- .../codegen/templates/CapTable.daml.template | 41 ++++++++++--------- 5 files changed, 86 insertions(+), 25 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index f0f22c05..904e50ad 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -3,6 +3,7 @@ module OpenCapTable.TestStockPlanReservedShares where import qualified Fairmint.OpenCapTable.CapTable as CT import Fairmint.OpenCapTable.OCF.EquityCompensationCancellation (EquityCompensationCancellationOcfData(..)) import Fairmint.OpenCapTable.OCF.EquityCompensationRetraction (EquityCompensationRetractionOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationTransfer (EquityCompensationTransferOcfData(..)) import Fairmint.OpenCapTable.OCF.StockRetraction (StockRetractionOcfData(..)) import Fairmint.OpenCapTable.OCF.StockPlanPoolAdjustment (StockPlanPoolAdjustmentOcfData(..)) import Fairmint.OpenCapTable.OCF.StockPlanReturnToPool (StockPlanReturnToPoolOcfData(..)) @@ -43,6 +44,17 @@ equityCompensationCancellation cancellationId securityId quantity = EquityCompen reason_text = "Cancelled equity compensation" comments = [] +equityCompensationTransfer : Text -> Text -> Decimal -> EquityCompensationTransferOcfData +equityCompensationTransfer transferId securityId quantity = EquityCompensationTransferOcfData with + id = transferId + date = defaultTestDate + security_id = securityId + quantity = quantity + resulting_security_ids = [transferId <> "_RESULT"] + balance_security_id = None + consideration_text = None + comments = [] + createPlanAndStockIssuance : Party -> ContractId CT.CapTable -> @@ -232,6 +244,29 @@ testStockPlanReservedShares_ReturnToPoolAllowsReuse = script do deletes = [] pure () +testStockPlanReservedShares_EquityCompensationTransferReducesReturnableSourceBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EC_TRANSFER_RETURN" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let firstIssuance = stockPlanEquityCompensationIssuance "TX_EC_TRANSFER_RETURN_POOL" "SEC-EC-TRANSFER-RETURN-POOL" stakeholderId stockClassId stockPlanId 100.0 + let transfer = equityCompensationTransfer "TX_EC_TRANSFER_RETURN" firstIssuance.security_id 40.0 + let returnToPool = stockPlanReturnToPool "TX_RETURN_TRANSFERRED_EC" firstIssuance.security_id stockPlanId 40.0 + let secondIssuance = stockPlanEquityCompensationIssuance "TX_EC_REUSE_TRANSFER_RETURN" "SEC-EC-REUSE-TRANSFER-RETURN" stakeholderId stockClassId stockPlanId 40.0 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance firstIssuance + , CT.OcfCreateEquityCompensationTransfer transfer + , CT.OcfCreateStockPlanReturnToPool returnToPool + , CT.OcfCreateEquityCompensationIssuance secondIssuance + ] + edits = [] + deletes = [] + pure () + testStockPlanReservedShares_EquityCompensationReturnRequiresReduction = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/Test/daml/OpenCapTable/TestStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestStockSecurityBalances.daml index 548a29df..3487f2e8 100644 --- a/Test/daml/OpenCapTable/TestStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestStockSecurityBalances.daml @@ -164,6 +164,31 @@ testStockSecurityBalance_BalanceSecurityReductionCanConsumeRemainingSource = scr deletes = [] pure () +testStockSecurityBalance_BalanceSecurityRetirementUpdatesIssuedQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockClassId = "SC_STOCK_BAL_TERMINAL_INDEX" + (capTableWithClass, _) <- setupStockClassWith issuer cap_table (limitedStockClass stockClassId 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_STOCK_BAL_TERMINAL_INDEX" "Balance Holder" + let securityId = "SEC_STOCK_BAL_TERMINAL_INDEX" + let issuance = (defaultStockIssuance "TX_STOCK_BAL_TERMINAL_INDEX" securityId stakeholderId stockClassId) with quantity = 100.0 + capTableIssued <- createStockIssuanceWith issuer capTableReady issuance + + let cancellation = (stockCancellationOn "TX_STOCK_BAL_TERMINAL_CANCEL" securityId 40.0 jan02) with + balance_security_id = Some "SEC_STOCK_BAL_TERMINAL_REMAINDER" + let replacement = (defaultStockIssuance "TX_STOCK_BAL_TERMINAL_REPLACE" "SEC_STOCK_BAL_TERMINAL_REPLACE" stakeholderId stockClassId) with + date = jan03 + quantity = 100.0 + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockIssuance replacement + ] + edits = [] + deletes = [] + pure () + testStockSecurityBalance_ConversionCannotExceedIssuedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index 5ce6174c..a3f8fc70 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa9aa0dd89bbcceedd019b8c7c3e313d00bae4d65c3fafa1b7fe6faa30e810e4 -size 2784470 +oid sha256:c94d6aa4e3733d89b3d525862ab01910daa3344557ecda6c21fec44227c06ecb +size 2785948 diff --git a/dars/dars.lock b/dars/dars.lock index 61eee758..c51290ee 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,10 +26,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "fa9aa0dd89bbcceedd019b8c7c3e313d00bae4d65c3fafa1b7fe6faa30e810e4", - "size": 2784470, + "sha256": "c94d6aa4e3733d89b3d525862ab01910daa3344557ecda6c21fec44227c06ecb", + "size": 2785948, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T17:08:19Z", + "uploadedAt": "2026-07-09T17:19:42Z", "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 e4ae5396..f40356bc 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -789,17 +789,18 @@ addStockClassShareEvent : Text -> StockClassShareEvent -> Map Text [StockClassSh addStockClassShareEvent key event index = Map.insert key (event :: fromOptional [] (Map.lookup key index)) index +addStockClassShareEvents : Text -> [StockClassShareEvent] -> Map Text [StockClassShareEvent] -> Map Text [StockClassShareEvent] +addStockClassShareEvents key events index = + foldl (\updatedIndex event -> addStockClassShareEvent key event updatedIndex) index events + stockClassShareEventIndex : CapTableMaps -> Update StockClassShareEventIndex stockClassShareEventIndex maps = do cancellationEventsBySecurityId <- foldlA (\index cancellationCid -> do cancellation <- fetch cancellationCid - pure (addStockClassShareEvent + pure (addStockClassShareEvents cancellation.cancellation_data.security_id - (StockClassShareDelta with - event_date = cancellation.cancellation_data.date - event_priority = 1 - quantity_delta = 0.0 - cancellation.cancellation_data.quantity) + (stockSecurityReductionEvents cancellation.cancellation_data.date cancellation.cancellation_data.quantity cancellation.cancellation_data.balance_security_id) index)) Map.empty (Map.values maps.stock_cancellations) @@ -807,12 +808,9 @@ stockClassShareEventIndex maps = do repurchaseEventsBySecurityId <- foldlA (\index repurchaseCid -> do repurchase <- fetch repurchaseCid - pure (addStockClassShareEvent + pure (addStockClassShareEvents repurchase.repurchase_data.security_id - (StockClassShareDelta with - event_date = repurchase.repurchase_data.date - event_priority = 1 - quantity_delta = 0.0 - repurchase.repurchase_data.quantity) + (stockSecurityReductionEvents repurchase.repurchase_data.date repurchase.repurchase_data.quantity repurchase.repurchase_data.balance_security_id) index)) cancellationEventsBySecurityId (Map.values maps.stock_repurchases) @@ -820,12 +818,9 @@ stockClassShareEventIndex maps = do conversionEventsBySecurityId <- foldlA (\index conversionCid -> do conversion <- fetch conversionCid - pure (addStockClassShareEvent + pure (addStockClassShareEvents conversion.conversion_data.security_id - (StockClassShareDelta with - event_date = conversion.conversion_data.date - event_priority = 1 - quantity_delta = 0.0 - conversion.conversion_data.quantity_converted) + (stockSecurityReductionEvents conversion.conversion_data.date conversion.conversion_data.quantity_converted conversion.conversion_data.balance_security_id) index)) repurchaseEventsBySecurityId (Map.values maps.stock_conversions) @@ -2013,16 +2008,22 @@ stockPlanReturnedQuantityBySecurityAt maps securityId asOf = equityCompensationSecurityEvents : CapTableMaps -> Text -> Update [StockClassShareEvent] equityCompensationSecurityEvents maps securityId = do + transferEvents <- foldlA + (\events transferCid -> do + transfer <- fetch transferCid + if transfer.transfer_data.security_id == securityId + then pure (stockSecurityReductionEvents transfer.transfer_data.date transfer.transfer_data.quantity transfer.transfer_data.balance_security_id ++ events) + else pure events) + [] + (Map.values maps.equity_compensation_transfers) + cancellationEvents <- foldlA (\events cancellationCid -> do cancellation <- fetch cancellationCid if cancellation.cancellation_data.security_id == securityId - then pure ((StockClassShareDelta with - event_date = cancellation.cancellation_data.date - event_priority = 1 - quantity_delta = 0.0 - cancellation.cancellation_data.quantity) :: events) + then pure (stockSecurityReductionEvents cancellation.cancellation_data.date cancellation.cancellation_data.quantity cancellation.cancellation_data.balance_security_id ++ events) else pure events) - [] + transferEvents (Map.values maps.equity_compensation_cancellations) exerciseEvents <- foldlA From 8c8e90762ceceee000c428daf06dbbbcb33ba22e Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 14:11:08 -0400 Subject: [PATCH 15/38] Refresh security balance after schema fixes --- dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index 15a9a1f7..2e6d0ce6 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d6ac1efd7425f476e4cae901d09a11e060bf3f20badf16b33dc8b00911b8a05 -size 2806437 +oid sha256:1055735906590c88ccb864497c52500cf74d43286c04b52dd88c90adf18b4266 +size 2808301 diff --git a/dars/dars.lock b/dars/dars.lock index ebf39431..3bbdb93b 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,10 +26,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "2d6ac1efd7425f476e4cae901d09a11e060bf3f20badf16b33dc8b00911b8a05", - "size": 2806437, + "sha256": "1055735906590c88ccb864497c52500cf74d43286c04b52dd88c90adf18b4266", + "size": 2808301, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T17:49:51Z", + "uploadedAt": "2026-07-09T18:10:05Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { From 93663443ed8f885afc4179411f1839f4a003ed6e Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 14:47:35 -0400 Subject: [PATCH 16/38] Refresh security balance DAR after reference fold --- dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index 2e6d0ce6..45118b41 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1055735906590c88ccb864497c52500cf74d43286c04b52dd88c90adf18b4266 -size 2808301 +oid sha256:a1fc1b4a100214a35c8c3587c927ec8c45627aab0621247ce9ba544044c7f757 +size 2820900 diff --git a/dars/dars.lock b/dars/dars.lock index bb4b2cd0..acdc1d68 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,10 +26,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "1055735906590c88ccb864497c52500cf74d43286c04b52dd88c90adf18b4266", - "size": 2808301, + "sha256": "a1fc1b4a100214a35c8c3587c927ec8c45627aab0621247ce9ba544044c7f757", + "size": 2820900, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T18:10:05Z", + "uploadedAt": "2026-07-09T18:46:38.106Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { From 828b971697989c8c017aecad8f49e1e2e8176f30 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 15:43:59 -0400 Subject: [PATCH 17/38] Refresh security balance DAR after reference updates --- dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index 45118b41..ae0e6cc9 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1fc1b4a100214a35c8c3587c927ec8c45627aab0621247ce9ba544044c7f757 -size 2820900 +oid sha256:7b7b74b16ca9ba1c68e5e72120de564ec8e42a26199a0041acaaace6d2cc5b1d +size 2815058 diff --git a/dars/dars.lock b/dars/dars.lock index 44fe4db9..f9a6fe2d 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,8 +26,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "a1fc1b4a100214a35c8c3587c927ec8c45627aab0621247ce9ba544044c7f757", - "size": 2820900, + "sha256": "7b7b74b16ca9ba1c68e5e72120de564ec8e42a26199a0041acaaace6d2cc5b1d", + "size": 2815058, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T18:46:38.106Z", "networks": [] From 0f1547e1f6d247ab404c06324d2f0465806a5b61 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 15:55:19 -0400 Subject: [PATCH 18/38] Align security balances with lifecycle events --- .../TestNonStockSecurityBalances.daml | 24 +++++++- .../TestStockPlanReservedShares.daml | 5 +- .../0.0.12/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 60 ++++++++++++------- 5 files changed, 66 insertions(+), 31 deletions(-) diff --git a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml index 44a341ac..fb45e175 100644 --- a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml @@ -372,12 +372,12 @@ testEquityCompensationSecurityBalance_ReductionAfterRetractionFails = script do edits = [] deletes = [] -testEquityCompensationSecurityBalance_ReleaseSettlingAfterRetractionFails = script do +testEquityCompensationSecurityBalance_ReleaseBeforeRetractionUsesReleaseDate = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableReady <- addDefaultStakeholder issuer cap_table let securityId = "ESEC_BAL_RELEASE_SETTLES_AFTER_RETRACTION" - submitMustFail issuer do + _ <- submit issuer do exerciseCmd capTableReady CT.UpdateCapTable with creates = [ CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_E_BAL_RELEASE_AFTER_RETRACTION" securityId defaultStakeholderId 100.0) @@ -386,6 +386,7 @@ testEquityCompensationSecurityBalance_ReleaseSettlingAfterRetractionFails = scri ] edits = [] deletes = [] + pure () testEquityCompensationSecurityBalance_RepeatedRetractionFails = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -447,3 +448,22 @@ testEquityCompensationSecurityBalance_BalanceSecurityReductionCanConsumeRemainin edits = [] deletes = [] pure () + +testEquityCompensationSecurityBalance_SplitAdjustedExerciseSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let securityId = "ESEC_BAL_SPLIT" + let issuance = (equityCompensationIssuance "TX_E_BAL_SPLIT" securityId stakeholderId 100.0) with + stock_class_id = Some stockClassId + let split = (stockClassSplit "TX_E_BAL_SPLIT" stockClassId 2.0 1.0) with date = jan02 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationIssuance issuance + , CT.OcfCreateStockClassSplit split + , CT.OcfCreateEquityCompensationExercise (equityCompensationExercise "TX_E_EXERCISE_AFTER_SPLIT" securityId 200.0 jan03) + ] + edits = [] + deletes = [] + pure () diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index 741a0eaa..9e33c1ae 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -314,7 +314,7 @@ testStockPlanReservedShares_ReturnToPoolAllowsReuse = script do deletes = [] pure () -testStockPlanReservedShares_EquityCompensationTransferReducesReturnableSourceBalance = script do +testStockPlanReservedShares_TransferredEquityCompensationCannotReturnToPool = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table let stockPlanId = "PLAN_EC_TRANSFER_RETURN" @@ -324,7 +324,7 @@ testStockPlanReservedShares_EquityCompensationTransferReducesReturnableSourceBal let returnToPool = stockPlanReturnToPool "TX_RETURN_TRANSFERRED_EC" firstIssuance.security_id stockPlanId 40.0 let secondIssuance = stockPlanEquityCompensationIssuance "TX_EC_REUSE_TRANSFER_RETURN" "SEC-EC-REUSE-TRANSFER-RETURN" stakeholderId stockClassId stockPlanId 40.0 - _ <- submit issuer do + submitMustFail issuer do exerciseCmd capTableReady CT.UpdateCapTable with creates = [ CT.OcfCreateStockPlan plan @@ -335,7 +335,6 @@ testStockPlanReservedShares_EquityCompensationTransferReducesReturnableSourceBal ] edits = [] deletes = [] - pure () testStockPlanReservedShares_EquityCompensationReturnRequiresReduction = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index ae0e6cc9..d0e34c6b 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b7b74b16ca9ba1c68e5e72120de564ec8e42a26199a0041acaaace6d2cc5b1d -size 2815058 +oid sha256:110764e5c0805a433ffab3af5d7c572bd7ea050752b0440db85446523d1f2d2a +size 2817458 diff --git a/dars/dars.lock b/dars/dars.lock index f9a6fe2d..fdde677f 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,8 +26,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "7b7b74b16ca9ba1c68e5e72120de564ec8e42a26199a0041acaaace6d2cc5b1d", - "size": 2815058, + "sha256": "110764e5c0805a433ffab3af5d7c572bd7ea050752b0440db85446523d1f2d2a", + "size": 2817458, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T18:46:38.106Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 67e19378..544e4bf2 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -149,6 +149,10 @@ data SecurityQuantityEvent event_date: Time event_priority: Int quantity_delta: Decimal + | SecurityQuantityMultiplier with + event_date: Time + event_priority: Int + ratio: OcfRatio | SecurityQuantityRetraction with event_date: Time event_priority: Int @@ -734,12 +738,12 @@ issuerAuthorizedLimitAt adjustmentHistory issuerData asOf = Some adjustment -> Some adjustment.new_shares_authorized None -> issuerAuthorizedSharesLimit issuerData.initial_shares_authorized -stockClassShareEventSortKey : StockClassShareEvent -> (Time, Int) +stockClassShareEventSortKey : StockClassShareEvent -> (Time, Int, Int) stockClassShareEventSortKey event = case event of - StockClassShareDelta with event_date; event_priority -> (event_date, event_priority) - StockClassShareTerminalDelta with event_date; event_priority -> (event_date, event_priority) - StockClassShareMultiplier with event_date; event_priority -> (event_date, event_priority) - StockClassShareRetraction with event_date; event_priority -> (event_date, event_priority) + StockClassShareDelta with event_date; event_priority -> (event_date, event_priority, 0) + StockClassShareTerminalDelta with event_date; event_priority -> (event_date, event_priority, 1) + StockClassShareMultiplier with event_date; event_priority -> (event_date, event_priority, 2) + StockClassShareRetraction with event_date; event_priority -> (event_date, event_priority, 3) stockClassShareEventDate : StockClassShareEvent -> Time stockClassShareEventDate event = case event of @@ -1117,16 +1121,18 @@ validateStockSecurityBalances maps = validateStockSecurityBalance maps issuance.issuance_data) (Map.values maps.stock_issuances) -securityQuantityEventSortKey : SecurityQuantityEvent -> (Time, Int) +securityQuantityEventSortKey : SecurityQuantityEvent -> (Time, Int, Int) securityQuantityEventSortKey event = case event of - SecurityQuantityDelta with event_date; event_priority -> (event_date, event_priority) - SecurityQuantityTerminalDelta with event_date; event_priority -> (event_date, event_priority) - SecurityQuantityRetraction with event_date; event_priority -> (event_date, event_priority) + SecurityQuantityDelta with event_date; event_priority -> (event_date, event_priority, 0) + SecurityQuantityTerminalDelta with event_date; event_priority -> (event_date, event_priority, 1) + SecurityQuantityMultiplier with event_date; event_priority -> (event_date, event_priority, 2) + SecurityQuantityRetraction with event_date; event_priority -> (event_date, event_priority, 3) securityQuantityEventDate : SecurityQuantityEvent -> Time securityQuantityEventDate event = case event of SecurityQuantityDelta with event_date -> event_date SecurityQuantityTerminalDelta with event_date -> event_date + SecurityQuantityMultiplier with event_date -> event_date SecurityQuantityRetraction with event_date -> event_date assertSecurityQuantityEventsOnOrAfter : Text -> Time -> [SecurityQuantityEvent] -> Update () @@ -1156,6 +1162,8 @@ validateSecurityQuantityBalanceEvent securityId currentOpt event = case currentO ("Security balance cannot be negative for " <> securityId <> ": applying delta " <> show quantity_delta <> " leaves " <> show next) (next >= 0.0) pure (Some 0.0) + SecurityQuantityMultiplier with ratio -> + pure (Some (current * ratio.numerator / ratio.denominator)) SecurityQuantityRetraction _ _ -> do assertMsg ("Security retraction requires a positive balance for " <> securityId <> ": current balance is " <> show current) @@ -1166,6 +1174,8 @@ validateSecurityQuantityBalanceEvent securityId currentOpt event = case currentO pure None SecurityQuantityTerminalDelta {} -> pure (Some 0.0) + SecurityQuantityMultiplier {} -> + pure None SecurityQuantityRetraction _ _ -> pure (Some 0.0) @@ -1298,7 +1308,7 @@ equityCompensationSecurityQuantityEvents maps issuanceData = do release <- fetch releaseCid if release.release_data.security_id == issuanceData.security_id then pure ((SecurityQuantityDelta with - event_date = release.release_data.settlement_date + event_date = release.release_data.date event_priority = 1 quantity_delta = 0.0 - release.release_data.quantity) :: events) @@ -1306,6 +1316,21 @@ equityCompensationSecurityQuantityEvents maps issuanceData = do [] (Map.values maps.equity_compensation_releases) + splitEvents <- case issuanceData.stock_class_id of + Some stockClassId -> + foldlA + (\events splitCid -> do + split <- fetch splitCid + if split.split_data.stock_class_id == stockClassId && split.split_data.date >= issuanceData.date + then pure ((SecurityQuantityMultiplier with + event_date = split.split_data.date + event_priority = 2 + ratio = split.split_data.split_ratio) :: events) + else pure events) + [] + (Map.values maps.stock_class_splits) + None -> pure [] + retractionEvents <- foldlA (\events retractionCid -> do retraction <- fetch retractionCid @@ -1318,7 +1343,7 @@ equityCompensationSecurityQuantityEvents maps issuanceData = do [] (Map.values maps.equity_compensation_retractions) - pure (transferEvents ++ cancellationEvents ++ exerciseEvents ++ releaseEvents ++ retractionEvents) + pure (transferEvents ++ cancellationEvents ++ exerciseEvents ++ releaseEvents ++ retractionEvents ++ splitEvents) validateEquityCompensationSecurityBalance : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update () validateEquityCompensationSecurityBalance maps issuanceData = do @@ -2283,22 +2308,13 @@ stockPlanReturnedQuantityBySecurityAt maps securityId asOf = equityCompensationSecurityEvents : CapTableMaps -> Text -> Update [StockClassShareEvent] equityCompensationSecurityEvents maps securityId = do - transferEvents <- foldlA - (\events transferCid -> do - transfer <- fetch transferCid - if transfer.transfer_data.security_id == securityId - then pure (stockSecurityReductionEvents transfer.transfer_data.date transfer.transfer_data.quantity transfer.transfer_data.balance_security_id ++ events) - else pure events) - [] - (Map.values maps.equity_compensation_transfers) - cancellationEvents <- foldlA (\events cancellationCid -> do cancellation <- fetch cancellationCid if cancellation.cancellation_data.security_id == securityId then pure (stockSecurityReductionEvents cancellation.cancellation_data.date cancellation.cancellation_data.quantity cancellation.cancellation_data.balance_security_id ++ events) else pure events) - transferEvents + [] (Map.values maps.equity_compensation_cancellations) exerciseEvents <- foldlA @@ -2318,7 +2334,7 @@ equityCompensationSecurityEvents maps securityId = do release <- fetch releaseCid if release.release_data.security_id == securityId then pure ((StockClassShareDelta with - event_date = release.release_data.settlement_date + event_date = release.release_data.date event_priority = 1 quantity_delta = 0.0 - release.release_data.quantity) :: events) else pure events) From d0f399c40693bbe697997c7401afe81096a5c4fd Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 16:00:58 -0400 Subject: [PATCH 19/38] Refresh security balance DAR after schema updates --- dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index d0e34c6b..feff916d 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:110764e5c0805a433ffab3af5d7c572bd7ea050752b0440db85446523d1f2d2a -size 2817458 +oid sha256:f75e54641b32e3da7c9716e91b48709f2a8080940fa74fe98f7857fc4ba29981 +size 2817511 diff --git a/dars/dars.lock b/dars/dars.lock index 56e0d42f..7e998d4f 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,8 +26,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "110764e5c0805a433ffab3af5d7c572bd7ea050752b0440db85446523d1f2d2a", - "size": 2817458, + "sha256": "f75e54641b32e3da7c9716e91b48709f2a8080940fa74fe98f7857fc4ba29981", + "size": 2817511, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T18:46:38.106Z", "networks": [] From 15633ec21422104cec81b1d9f09f5e86763188d2 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 07:33:20 -0400 Subject: [PATCH 20/38] fix: index security balance validation events --- .../TestStockSecurityBalances.daml | 30 + .../0.0.12/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- .../codegen/templates/CapTable.daml.template | 554 ++++++++---------- 4 files changed, 289 insertions(+), 305 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestStockSecurityBalances.daml index 3487f2e8..8dae3cec 100644 --- a/Test/daml/OpenCapTable/TestStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestStockSecurityBalances.daml @@ -189,6 +189,36 @@ testStockSecurityBalance_BalanceSecurityRetirementUpdatesIssuedQuantity = script deletes = [] pure () +testStockSecurityBalance_TransferBalanceRetirementUpdatesIssuedQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockClassId = "SC_STOCK_BAL_TRANSFER_TERMINAL_INDEX" + (capTableWithClass, _) <- setupStockClassWith issuer cap_table (limitedStockClass stockClassId 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_STOCK_BAL_TRANSFER_TERMINAL_INDEX" "Transfer Holder" + let securityId = "SEC_STOCK_BAL_TRANSFER_TERMINAL_INDEX" + let transferId = "TX_STOCK_BAL_TERMINAL_TRANSFER" + let resultSecurityId = transferId <> "_RESULT" + let balanceSecurityId = "SEC_STOCK_BAL_TRANSFER_TERMINAL_REMAINDER" + let issuance = (defaultStockIssuance "TX_STOCK_BAL_TRANSFER_TERMINAL_INDEX" securityId stakeholderId stockClassId) with quantity = 100.0 + let transfer = stockTransferWithBalance transferId securityId 40.0 balanceSecurityId jan02 + let transferredIssuance = (defaultStockIssuance "TX_STOCK_BAL_TRANSFERRED" resultSecurityId stakeholderId stockClassId) with + date = jan02 + quantity = 40.0 + let balanceIssuance = (defaultStockIssuance "TX_STOCK_BAL_TRANSFER_REMAINDER" balanceSecurityId stakeholderId stockClassId) with + date = jan02 + quantity = 60.0 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockTransfer transfer + , CT.OcfCreateStockIssuance transferredIssuance + , CT.OcfCreateStockIssuance balanceIssuance + ] + edits = [] + deletes = [] + pure () + testStockSecurityBalance_ConversionCannotExceedIssuedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index feff916d..5c889d77 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f75e54641b32e3da7c9716e91b48709f2a8080940fa74fe98f7857fc4ba29981 -size 2817511 +oid sha256:027d2a93e0145336a8fc6d61ccee56d42ecd4655e5aa9251fa5e233ebb6bd53f +size 2818268 diff --git a/dars/dars.lock b/dars/dars.lock index 7e998d4f..ead778d1 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,10 +26,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "f75e54641b32e3da7c9716e91b48709f2a8080940fa74fe98f7857fc4ba29981", - "size": 2817511, + "sha256": "027d2a93e0145336a8fc6d61ccee56d42ecd4655e5aa9251fa5e233ebb6bd53f", + "size": 2818268, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T18:46:38.106Z", + "uploadedAt": "2026-07-10T11:31:33.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 544e4bf2..b4bc2202 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -138,6 +138,7 @@ data StockClassShareLot = StockClassShareLot with data StockClassShareEventIndex = StockClassShareEventIndex with stock_security_events_by_security_id: Map Text [StockClassShareEvent] stock_class_split_events_by_stock_class_id: Map Text [StockClassShareEvent] + stock_security_balance_events_by_security_id: Optional (Map Text [StockClassShareEvent]) deriving (Eq, Show) data SecurityQuantityEvent @@ -158,6 +159,11 @@ data SecurityQuantityEvent event_priority: Int deriving (Eq, Show) +data NonStockSecurityBalanceEventIndex = NonStockSecurityBalanceEventIndex with + warrant_events_by_security_id: Map Text [SecurityQuantityEvent] + equity_compensation_events_by_security_id: Map Text [SecurityQuantityEvent] + deriving (Eq, Show) + authorizedSharesCeiling : OcfInitialSharesAuthorized -> AuthorizedSharesCeiling authorizedSharesCeiling auth = case auth of OcfInitialSharesNumeric n -> AuthorizedSharesFinite n @@ -826,89 +832,100 @@ addStockClassShareEventForKeys keys event index = stockClassShareEventIndex : CapTableMaps -> Update StockClassShareEventIndex stockClassShareEventIndex maps = do - cancellationEventsBySecurityId <- foldlA - (\index cancellationCid -> do + (cancellationEventsBySecurityId, cancellationBalanceEventsBySecurityId) <- foldlA + (\(shareIndex, balanceIndex) cancellationCid -> do cancellation <- fetch cancellationCid - pure (addStockClassShareEvents - cancellation.cancellation_data.security_id - (stockSecurityReductionEvents cancellation.cancellation_data.date cancellation.cancellation_data.quantity cancellation.cancellation_data.balance_security_id) - index)) - Map.empty + let events = stockSecurityReductionEvents cancellation.cancellation_data.date cancellation.cancellation_data.quantity cancellation.cancellation_data.balance_security_id + pure + ( addStockClassShareEvents cancellation.cancellation_data.security_id events shareIndex + , addStockClassShareEvents cancellation.cancellation_data.security_id events balanceIndex + )) + (Map.empty, Map.empty) (Map.values maps.stock_cancellations) - repurchaseEventsBySecurityId <- foldlA - (\index repurchaseCid -> do + (repurchaseEventsBySecurityId, repurchaseBalanceEventsBySecurityId) <- foldlA + (\(shareIndex, balanceIndex) repurchaseCid -> do repurchase <- fetch repurchaseCid - pure (addStockClassShareEvents - repurchase.repurchase_data.security_id - (stockSecurityReductionEvents repurchase.repurchase_data.date repurchase.repurchase_data.quantity repurchase.repurchase_data.balance_security_id) - index)) - cancellationEventsBySecurityId + let events = stockSecurityReductionEvents repurchase.repurchase_data.date repurchase.repurchase_data.quantity repurchase.repurchase_data.balance_security_id + pure + ( addStockClassShareEvents repurchase.repurchase_data.security_id events shareIndex + , addStockClassShareEvents repurchase.repurchase_data.security_id events balanceIndex + )) + (cancellationEventsBySecurityId, cancellationBalanceEventsBySecurityId) (Map.values maps.stock_repurchases) - conversionEventsBySecurityId <- foldlA - (\index conversionCid -> do + (conversionEventsBySecurityId, conversionBalanceEventsBySecurityId) <- foldlA + (\(shareIndex, balanceIndex) conversionCid -> do conversion <- fetch conversionCid - pure (addStockClassShareEvents - conversion.conversion_data.security_id - (stockSecurityReductionEvents conversion.conversion_data.date conversion.conversion_data.quantity_converted conversion.conversion_data.balance_security_id) - index)) - repurchaseEventsBySecurityId + let events = stockSecurityReductionEvents conversion.conversion_data.date conversion.conversion_data.quantity_converted conversion.conversion_data.balance_security_id + pure + ( addStockClassShareEvents conversion.conversion_data.security_id events shareIndex + , addStockClassShareEvents conversion.conversion_data.security_id events balanceIndex + )) + (repurchaseEventsBySecurityId, repurchaseBalanceEventsBySecurityId) (Map.values maps.stock_conversions) - transferEventsBySecurityId <- foldlA - (\index transferCid -> do + (transferEventsBySecurityId, transferBalanceEventsBySecurityId) <- foldlA + (\(shareIndex, balanceIndex) transferCid -> do transfer <- fetch transferCid - if allResultingStockIssuancesExist maps transfer.transfer_data.resulting_security_ids - then pure (addStockClassShareEvent - transfer.transfer_data.security_id - (StockClassShareDelta with - event_date = transfer.transfer_data.date - event_priority = 1 - quantity_delta = 0.0 - transfer.transfer_data.quantity) - index) - else pure index) - conversionEventsBySecurityId + let shareEvents = stockSecurityReductionEvents transfer.transfer_data.date transfer.transfer_data.quantity transfer.transfer_data.balance_security_id + let nextShareIndex = + if allResultingStockIssuancesExist maps transfer.transfer_data.resulting_security_ids + then addStockClassShareEvents transfer.transfer_data.security_id shareEvents shareIndex + else shareIndex + let balanceEvents = stockSecurityReductionEvents transfer.transfer_data.date transfer.transfer_data.quantity transfer.transfer_data.balance_security_id + pure + ( nextShareIndex + , addStockClassShareEvents transfer.transfer_data.security_id balanceEvents balanceIndex + )) + (conversionEventsBySecurityId, conversionBalanceEventsBySecurityId) (Map.values maps.stock_transfers) - reissuanceEventsBySecurityId <- foldlA - (\index reissuanceCid -> do + (reissuanceEventsBySecurityId, reissuanceBalanceEventsBySecurityId) <- foldlA + (\(shareIndex, balanceIndex) reissuanceCid -> do reissuance <- fetch reissuanceCid - if allResultingStockIssuancesExist maps reissuance.reissuance_data.resulting_security_ids - then pure (addStockClassShareEvent - reissuance.reissuance_data.security_id - (StockClassShareRetraction with + let event = StockClassShareRetraction with event_date = reissuance.reissuance_data.date - event_priority = 3) - index) - else pure index) - transferEventsBySecurityId + event_priority = 3 + let nextShareIndex = + if allResultingStockIssuancesExist maps reissuance.reissuance_data.resulting_security_ids + then addStockClassShareEvent reissuance.reissuance_data.security_id event shareIndex + else shareIndex + pure + ( nextShareIndex + , addStockClassShareEvent reissuance.reissuance_data.security_id event balanceIndex + )) + (transferEventsBySecurityId, transferBalanceEventsBySecurityId) (Map.values maps.stock_reissuances) - consolidationEventsBySecurityId <- foldlA - (\index consolidationCid -> do + (consolidationEventsBySecurityId, consolidationBalanceEventsBySecurityId) <- foldlA + (\(shareIndex, balanceIndex) consolidationCid -> do consolidation <- fetch consolidationCid - if isSome (Map.lookup consolidation.consolidation_data.resulting_security_id maps.stock_issuances_by_security_id) - then pure (addStockClassShareEventForKeys - consolidation.consolidation_data.security_ids - (StockClassShareRetraction with + let event = StockClassShareRetraction with event_date = consolidation.consolidation_data.date - event_priority = 3) - index) - else pure index) - reissuanceEventsBySecurityId + event_priority = 3 + let nextShareIndex = + if isSome (Map.lookup consolidation.consolidation_data.resulting_security_id maps.stock_issuances_by_security_id) + then addStockClassShareEventForKeys consolidation.consolidation_data.security_ids event shareIndex + else shareIndex + pure + ( nextShareIndex + , addStockClassShareEventForKeys consolidation.consolidation_data.security_ids event balanceIndex + )) + (reissuanceEventsBySecurityId, reissuanceBalanceEventsBySecurityId) (Map.values maps.stock_consolidations) - retractionEventsBySecurityId <- foldlA - (\index retractionCid -> do + (retractionEventsBySecurityId, retractionBalanceEventsBySecurityId) <- foldlA + (\(shareIndex, balanceIndex) retractionCid -> do retraction <- fetch retractionCid - pure (addStockClassShareEvent - retraction.retraction_data.security_id - (StockClassShareRetraction with + let event = StockClassShareRetraction with event_date = retraction.retraction_data.date - event_priority = 3) - index)) - consolidationEventsBySecurityId + event_priority = 3 + pure + ( addStockClassShareEvent retraction.retraction_data.security_id event shareIndex + , addStockClassShareEvent retraction.retraction_data.security_id event balanceIndex + )) + (consolidationEventsBySecurityId, consolidationBalanceEventsBySecurityId) (Map.values maps.stock_retractions) splitEventsByStockClassId <- foldlA @@ -926,6 +943,7 @@ stockClassShareEventIndex maps = do pure StockClassShareEventIndex with stock_security_events_by_security_id = retractionEventsBySecurityId + stock_security_balance_events_by_security_id = Some retractionBalanceEventsBySecurityId stock_class_split_events_by_stock_class_id = splitEventsByStockClassId stockClassShareLotEvents : StockClassShareEventIndex -> StockClassShareLot -> [StockClassShareEvent] @@ -982,102 +1000,19 @@ stockClassShareLotPlanAllocatedQuantityAt eventIndex lot asOf = (filter (stockClassShareEventOnOrBefore asOf) (stockClassShareLotPlanAllocationEvents eventIndex lot))) else Some 0.0 -stockSecurityShareEvents : CapTableMaps -> StockIssuanceOcfData -> Update [StockClassShareEvent] -stockSecurityShareEvents maps issuanceData = do - cancellationEvents <- foldlA - (\events cancellationCid -> do - cancellation <- fetch cancellationCid - if cancellation.cancellation_data.security_id == issuanceData.security_id - then - pure (stockSecurityReductionEvents cancellation.cancellation_data.date cancellation.cancellation_data.quantity cancellation.cancellation_data.balance_security_id ++ events) - else pure events) - [] - (Map.values maps.stock_cancellations) - - repurchaseEvents <- foldlA - (\events repurchaseCid -> do - repurchase <- fetch repurchaseCid - if repurchase.repurchase_data.security_id == issuanceData.security_id - then - pure (stockSecurityReductionEvents repurchase.repurchase_data.date repurchase.repurchase_data.quantity repurchase.repurchase_data.balance_security_id ++ events) - else pure events) - [] - (Map.values maps.stock_repurchases) - - transferEvents <- foldlA - (\events transferCid -> do - transfer <- fetch transferCid - if transfer.transfer_data.security_id == issuanceData.security_id - then - pure (stockSecurityReductionEvents transfer.transfer_data.date transfer.transfer_data.quantity transfer.transfer_data.balance_security_id ++ events) - else pure events) - [] - (Map.values maps.stock_transfers) - - conversionEvents <- foldlA - (\events conversionCid -> do - conversion <- fetch conversionCid - if conversion.conversion_data.security_id == issuanceData.security_id - then - pure (stockSecurityReductionEvents conversion.conversion_data.date conversion.conversion_data.quantity_converted conversion.conversion_data.balance_security_id ++ events) - else pure events) - [] - (Map.values maps.stock_conversions) - - retractionEvents <- foldlA - (\events retractionCid -> do - retraction <- fetch retractionCid - if retraction.retraction_data.security_id == issuanceData.security_id - then pure ((StockClassShareRetraction with - event_date = retraction.retraction_data.date - event_priority = 3) - :: events) - else pure events) - [] - (Map.values maps.stock_retractions) - - reissuanceEvents <- foldlA - (\events reissuanceCid -> do - reissuance <- fetch reissuanceCid - if reissuance.reissuance_data.security_id == issuanceData.security_id - then pure ((StockClassShareRetraction with - event_date = reissuance.reissuance_data.date - event_priority = 3) - :: events) - else pure events) - [] - (Map.values maps.stock_reissuances) - - consolidationEvents <- foldlA - (\events consolidationCid -> do - consolidation <- fetch consolidationCid - if issuanceData.security_id `elem` consolidation.consolidation_data.security_ids - then pure ((StockClassShareRetraction with - event_date = consolidation.consolidation_data.date - event_priority = 3) - :: events) - else pure events) - [] - (Map.values maps.stock_consolidations) - - splitEvents <- foldlA - (\events splitCid -> do - split <- fetch splitCid - if split.split_data.stock_class_id == issuanceData.stock_class_id && split.split_data.date >= issuanceData.date - then pure ((StockClassShareMultiplier with - event_date = split.split_data.date - event_priority = 2 - ratio = split.split_data.split_ratio) - :: events) - else pure events) - [] - (Map.values maps.stock_class_splits) - - pure ((StockClassShareDelta with +stockSecurityShareEvents : StockClassShareEventIndex -> StockIssuanceOcfData -> [StockClassShareEvent] +stockSecurityShareEvents eventIndex issuanceData = + let balanceEventsBySecurityId = fromOptional Map.empty eventIndex.stock_security_balance_events_by_security_id + securityEvents = fromOptional [] (Map.lookup issuanceData.security_id balanceEventsBySecurityId) + splitEvents = + filter + (stockClassShareEventOnOrAfter issuanceData.date) + (fromOptional [] (Map.lookup issuanceData.stock_class_id eventIndex.stock_class_split_events_by_stock_class_id)) + in (StockClassShareDelta with event_date = issuanceData.date event_priority = 0 quantity_delta = issuanceData.quantity) - :: (cancellationEvents ++ repurchaseEvents ++ transferEvents ++ conversionEvents ++ retractionEvents ++ reissuanceEvents ++ consolidationEvents ++ splitEvents)) + :: (securityEvents ++ splitEvents) validateStockSecurityBalanceEvent : Text -> Decimal -> StockClassShareEvent -> Update Decimal validateStockSecurityBalanceEvent securityId current event = case event of @@ -1104,21 +1039,21 @@ validateStockSecurityBalanceEvent securityId current event = case event of (current > 0.0) pure 0.0 -validateStockSecurityBalance : CapTableMaps -> StockIssuanceOcfData -> Update () -validateStockSecurityBalance maps issuanceData = do - events <- stockSecurityShareEvents maps issuanceData +validateStockSecurityBalance : StockClassShareEventIndex -> StockIssuanceOcfData -> Update () +validateStockSecurityBalance eventIndex issuanceData = do + let events = stockSecurityShareEvents eventIndex issuanceData _ <- foldlA (validateStockSecurityBalanceEvent issuanceData.security_id) 0.0 (sortOn stockClassShareEventSortKey events) pure () -validateStockSecurityBalances : CapTableMaps -> Update () -validateStockSecurityBalances maps = +validateStockSecurityBalances : StockClassShareEventIndex -> CapTableMaps -> Update () +validateStockSecurityBalances eventIndex maps = mapA_ (\issuanceCid -> do issuance <- fetch issuanceCid - validateStockSecurityBalance maps issuance.issuance_data) + validateStockSecurityBalance eventIndex issuance.issuance_data) (Map.values maps.stock_issuances) securityQuantityEventSortKey : SecurityQuantityEvent -> (Time, Int, Int) @@ -1187,65 +1122,128 @@ validateSecurityQuantityBalance securityId initialQuantity events = do (sortOn securityQuantityEventSortKey events) pure () -warrantSecurityQuantityEvents : CapTableMaps -> WarrantIssuanceOcfData -> Update [SecurityQuantityEvent] -warrantSecurityQuantityEvents maps issuanceData = do - transferEvents <- foldlA - (\events transferCid -> do +addSecurityQuantityEvent : Text -> SecurityQuantityEvent -> Map Text [SecurityQuantityEvent] -> Map Text [SecurityQuantityEvent] +addSecurityQuantityEvent key event index = + Map.insert key (event :: fromOptional [] (Map.lookup key index)) index + +nonStockSecurityBalanceEventIndex : CapTableMaps -> Update NonStockSecurityBalanceEventIndex +nonStockSecurityBalanceEventIndex maps = do + warrantTransferEvents <- foldlA + (\index transferCid -> do transfer <- fetch transferCid - if transfer.transfer_data.security_id == issuanceData.security_id - then - pure (securityQuantityReductionEvent transfer.transfer_data.date transfer.transfer_data.quantity transfer.transfer_data.balance_security_id :: events) - else pure events) - [] + pure (addSecurityQuantityEvent + transfer.transfer_data.security_id + (securityQuantityReductionEvent transfer.transfer_data.date transfer.transfer_data.quantity transfer.transfer_data.balance_security_id) + index)) + Map.empty (Map.values maps.warrant_transfers) - cancellationEvents <- foldlA - (\events cancellationCid -> do + warrantCancellationEvents <- foldlA + (\index cancellationCid -> do cancellation <- fetch cancellationCid - if cancellation.cancellation_data.security_id == issuanceData.security_id - then - pure (securityQuantityReductionEvent cancellation.cancellation_data.date cancellation.cancellation_data.quantity cancellation.cancellation_data.balance_security_id :: events) - else pure events) - [] + pure (addSecurityQuantityEvent + cancellation.cancellation_data.security_id + (securityQuantityReductionEvent cancellation.cancellation_data.date cancellation.cancellation_data.quantity cancellation.cancellation_data.balance_security_id) + index)) + warrantTransferEvents (Map.values maps.warrant_cancellations) - exerciseEvents <- foldlA - (\events exerciseCid -> do + warrantExerciseEvents <- foldlA + (\index exerciseCid -> do exercise <- fetch exerciseCid - if exercise.exercise_data.security_id == issuanceData.security_id - then case exercise.exercise_data.quantity of - Some quantity -> - pure ((SecurityQuantityDelta with - event_date = exercise.exercise_data.date - event_priority = 1 - quantity_delta = 0.0 - quantity) - :: events) - None -> - pure ((SecurityQuantityRetraction with - event_date = exercise.exercise_data.date - event_priority = 3) - :: events) - else pure events) - [] + let event = case exercise.exercise_data.quantity of + Some quantity -> SecurityQuantityDelta with + event_date = exercise.exercise_data.date + event_priority = 1 + quantity_delta = 0.0 - quantity + None -> SecurityQuantityRetraction with + event_date = exercise.exercise_data.date + event_priority = 3 + pure (addSecurityQuantityEvent exercise.exercise_data.security_id event index)) + warrantCancellationEvents (Map.values maps.warrant_exercises) - retractionEvents <- foldlA - (\events retractionCid -> do + warrantEvents <- foldlA + (\index retractionCid -> do retraction <- fetch retractionCid - if retraction.retraction_data.security_id == issuanceData.security_id - then pure ((SecurityQuantityRetraction with - event_date = retraction.retraction_data.date - event_priority = 3) - :: events) - else pure events) - [] + pure (addSecurityQuantityEvent + retraction.retraction_data.security_id + (SecurityQuantityRetraction with + event_date = retraction.retraction_data.date + event_priority = 3) + index)) + warrantExerciseEvents (Map.values maps.warrant_retractions) - pure (transferEvents ++ cancellationEvents ++ exerciseEvents ++ retractionEvents) + equityTransferEvents <- foldlA + (\index transferCid -> do + transfer <- fetch transferCid + pure (addSecurityQuantityEvent + transfer.transfer_data.security_id + (securityQuantityReductionEvent transfer.transfer_data.date transfer.transfer_data.quantity transfer.transfer_data.balance_security_id) + index)) + Map.empty + (Map.values maps.equity_compensation_transfers) -validateWarrantSecurityBalance : CapTableMaps -> WarrantIssuanceOcfData -> Update () -validateWarrantSecurityBalance maps issuanceData = do - events <- warrantSecurityQuantityEvents maps issuanceData + equityCancellationEvents <- foldlA + (\index cancellationCid -> do + cancellation <- fetch cancellationCid + pure (addSecurityQuantityEvent + cancellation.cancellation_data.security_id + (securityQuantityReductionEvent cancellation.cancellation_data.date cancellation.cancellation_data.quantity cancellation.cancellation_data.balance_security_id) + index)) + equityTransferEvents + (Map.values maps.equity_compensation_cancellations) + + equityExerciseEvents <- foldlA + (\index exerciseCid -> do + exercise <- fetch exerciseCid + pure (addSecurityQuantityEvent + exercise.exercise_data.security_id + (SecurityQuantityDelta with + event_date = exercise.exercise_data.date + event_priority = 1 + quantity_delta = 0.0 - exercise.exercise_data.quantity) + index)) + equityCancellationEvents + (Map.values maps.equity_compensation_exercises) + + equityReleaseEvents <- foldlA + (\index releaseCid -> do + release <- fetch releaseCid + pure (addSecurityQuantityEvent + release.release_data.security_id + (SecurityQuantityDelta with + event_date = release.release_data.date + event_priority = 1 + quantity_delta = 0.0 - release.release_data.quantity) + index)) + equityExerciseEvents + (Map.values maps.equity_compensation_releases) + + equityEvents <- foldlA + (\index retractionCid -> do + retraction <- fetch retractionCid + pure (addSecurityQuantityEvent + retraction.retraction_data.security_id + (SecurityQuantityRetraction with + event_date = retraction.retraction_data.date + event_priority = 3) + index)) + equityReleaseEvents + (Map.values maps.equity_compensation_retractions) + + pure NonStockSecurityBalanceEventIndex with + warrant_events_by_security_id = warrantEvents + equity_compensation_events_by_security_id = equityEvents + +warrantSecurityQuantityEvents : NonStockSecurityBalanceEventIndex -> WarrantIssuanceOcfData -> [SecurityQuantityEvent] +warrantSecurityQuantityEvents eventIndex issuanceData = + fromOptional [] (Map.lookup issuanceData.security_id eventIndex.warrant_events_by_security_id) + +validateWarrantSecurityBalance : NonStockSecurityBalanceEventIndex -> WarrantIssuanceOcfData -> Update () +validateWarrantSecurityBalance eventIndex issuanceData = do + let events = warrantSecurityQuantityEvents eventIndex issuanceData case issuanceData.quantity of Some quantity -> validateSecurityQuantityBalance @@ -1260,94 +1258,44 @@ validateWarrantSecurityBalance maps issuanceData = do assertSecurityQuantityEventsOnOrAfter issuanceData.security_id issuanceData.date events validateSecurityQuantityBalance issuanceData.security_id None events -validateWarrantSecurityBalances : CapTableMaps -> Update () -validateWarrantSecurityBalances maps = +validateWarrantSecurityBalances : NonStockSecurityBalanceEventIndex -> CapTableMaps -> Update () +validateWarrantSecurityBalances eventIndex maps = mapA_ (\issuanceCid -> do issuance <- fetch issuanceCid - validateWarrantSecurityBalance maps issuance.issuance_data) + validateWarrantSecurityBalance eventIndex issuance.issuance_data) (Map.values maps.warrant_issuances) -equityCompensationSecurityQuantityEvents : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update [SecurityQuantityEvent] -equityCompensationSecurityQuantityEvents maps issuanceData = do - transferEvents <- foldlA - (\events transferCid -> do - transfer <- fetch transferCid - if transfer.transfer_data.security_id == issuanceData.security_id - then - pure (securityQuantityReductionEvent transfer.transfer_data.date transfer.transfer_data.quantity transfer.transfer_data.balance_security_id :: events) - else pure events) - [] - (Map.values maps.equity_compensation_transfers) - - cancellationEvents <- foldlA - (\events cancellationCid -> do - cancellation <- fetch cancellationCid - if cancellation.cancellation_data.security_id == issuanceData.security_id - then - pure (securityQuantityReductionEvent cancellation.cancellation_data.date cancellation.cancellation_data.quantity cancellation.cancellation_data.balance_security_id :: events) - else pure events) - [] - (Map.values maps.equity_compensation_cancellations) - - exerciseEvents <- foldlA - (\events exerciseCid -> do - exercise <- fetch exerciseCid - if exercise.exercise_data.security_id == issuanceData.security_id - then pure ((SecurityQuantityDelta with - event_date = exercise.exercise_data.date - event_priority = 1 - quantity_delta = 0.0 - exercise.exercise_data.quantity) - :: events) - else pure events) - [] - (Map.values maps.equity_compensation_exercises) - - releaseEvents <- foldlA - (\events releaseCid -> do - release <- fetch releaseCid - if release.release_data.security_id == issuanceData.security_id - then pure ((SecurityQuantityDelta with - event_date = release.release_data.date - event_priority = 1 - quantity_delta = 0.0 - release.release_data.quantity) - :: events) - else pure events) - [] - (Map.values maps.equity_compensation_releases) - - splitEvents <- case issuanceData.stock_class_id of - Some stockClassId -> - foldlA - (\events splitCid -> do - split <- fetch splitCid - if split.split_data.stock_class_id == stockClassId && split.split_data.date >= issuanceData.date - then pure ((SecurityQuantityMultiplier with - event_date = split.split_data.date - event_priority = 2 - ratio = split.split_data.split_ratio) :: events) - else pure events) - [] - (Map.values maps.stock_class_splits) - None -> pure [] - - retractionEvents <- foldlA - (\events retractionCid -> do - retraction <- fetch retractionCid - if retraction.retraction_data.security_id == issuanceData.security_id - then pure ((SecurityQuantityRetraction with - event_date = retraction.retraction_data.date - event_priority = 3) - :: events) - else pure events) - [] - (Map.values maps.equity_compensation_retractions) - - pure (transferEvents ++ cancellationEvents ++ exerciseEvents ++ releaseEvents ++ retractionEvents ++ splitEvents) +stockClassSplitSecurityQuantityEvents : StockClassShareEventIndex -> Text -> Time -> [SecurityQuantityEvent] +stockClassSplitSecurityQuantityEvents eventIndex stockClassId earliest = + concatMap + (\event -> case event of + StockClassShareMultiplier with event_date; event_priority; ratio -> + if event_date >= earliest + then [SecurityQuantityMultiplier with event_date; event_priority; ratio] + else [] + _ -> []) + (fromOptional [] (Map.lookup stockClassId eventIndex.stock_class_split_events_by_stock_class_id)) + +equityCompensationSecurityQuantityEvents : + StockClassShareEventIndex -> + NonStockSecurityBalanceEventIndex -> + EquityCompensationIssuanceOcfData -> + [SecurityQuantityEvent] +equityCompensationSecurityQuantityEvents stockEventIndex balanceEventIndex issuanceData = + let securityEvents = fromOptional [] (Map.lookup issuanceData.security_id balanceEventIndex.equity_compensation_events_by_security_id) + splitEvents = case issuanceData.stock_class_id of + Some stockClassId -> stockClassSplitSecurityQuantityEvents stockEventIndex stockClassId issuanceData.date + None -> [] + in securityEvents ++ splitEvents -validateEquityCompensationSecurityBalance : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update () -validateEquityCompensationSecurityBalance maps issuanceData = do - events <- equityCompensationSecurityQuantityEvents maps issuanceData +validateEquityCompensationSecurityBalance : + StockClassShareEventIndex -> + NonStockSecurityBalanceEventIndex -> + EquityCompensationIssuanceOcfData -> + Update () +validateEquityCompensationSecurityBalance stockEventIndex balanceEventIndex issuanceData = do + let events = equityCompensationSecurityQuantityEvents stockEventIndex balanceEventIndex issuanceData validateSecurityQuantityBalance issuanceData.security_id (Some 0.0) @@ -1357,12 +1305,16 @@ validateEquityCompensationSecurityBalance maps issuanceData = do quantity_delta = issuanceData.quantity) :: events) -validateEquityCompensationSecurityBalances : CapTableMaps -> Update () -validateEquityCompensationSecurityBalances maps = +validateEquityCompensationSecurityBalances : + StockClassShareEventIndex -> + NonStockSecurityBalanceEventIndex -> + CapTableMaps -> + Update () +validateEquityCompensationSecurityBalances stockEventIndex balanceEventIndex maps = mapA_ (\issuanceCid -> do issuance <- fetch issuanceCid - validateEquityCompensationSecurityBalance maps issuance.issuance_data) + validateEquityCompensationSecurityBalance stockEventIndex balanceEventIndex issuance.issuance_data) (Map.values maps.equity_compensation_issuances) stockClassShareLotCurrentQuantity : StockClassShareEventIndex -> StockClassShareLot -> Optional Decimal stockClassShareLotCurrentQuantity eventIndex lot = @@ -1889,9 +1841,8 @@ validateStockIssuanceShareNumberLotsDoNotOverlap eventIndex lots = (validateStockClassShareNumberLotsDoNotOverlap eventIndex) (Map.values (stockIssuanceShareNumberLotIndex lots)) -validateStockIssuanceShareNumberRanges : CapTableMaps -> Update () -validateStockIssuanceShareNumberRanges maps = do - eventIndex <- stockClassShareEventIndex maps +validateStockIssuanceShareNumberRanges : StockClassShareEventIndex -> CapTableMaps -> Update () +validateStockIssuanceShareNumberRanges eventIndex maps = do lots <- stockIssuanceShareNumberLots eventIndex maps validateStockIssuanceShareNumberLotsDoNotOverlap eventIndex lots @@ -2702,12 +2653,12 @@ validateStockPlanPoolCeilings eventIndex maps = do validateStockPlanReservedSharesAtDates eventIndex maps adjustmentHistory validateAuthorizedShareCeilings : + StockClassShareEventIndex -> CapTableMaps -> Map Text StockClassAuthorizedSharesAdjustmentOcfData -> ContractId Issuer -> Update () -validateAuthorizedShareCeilings maps adjustmentIndex issuerCid = do - eventIndex <- stockClassShareEventIndex maps +validateAuthorizedShareCeilings eventIndex maps adjustmentIndex issuerCid = do stockClassIndex <- stockClassDataIndex maps stockClasses <- stockClassDataList maps lotsByStockClass <- stockClassShareLotsByStockClass maps stockClassIndex @@ -2861,7 +2812,9 @@ template CapTable validateValuationStockClassReferences finalMaps validateStockPlanReferences finalMaps validateStockIssuanceReferences finalMaps - validateStockIssuanceShareNumberRanges finalMaps + stockEventIndex <- stockClassShareEventIndex finalMaps + nonStockBalanceEventIndex <- nonStockSecurityBalanceEventIndex finalMaps + validateStockIssuanceShareNumberRanges stockEventIndex finalMaps validateEquityCompensationIssuanceReferences finalMaps validateWarrantIssuanceReferences finalMaps validateVestingTransactionConditionReferences finalMaps @@ -2869,11 +2822,12 @@ template CapTable validateWarrantExerciseTriggerReferences finalMaps validateStockReissuanceSplitReferences finalMaps validateConfiguredReferences finalMaps - validateStockSecurityBalances finalMaps - validateWarrantSecurityBalances finalMaps - validateEquityCompensationSecurityBalances finalMaps + validateStockSecurityBalances stockEventIndex finalMaps + validateWarrantSecurityBalances nonStockBalanceEventIndex finalMaps + validateEquityCompensationSecurityBalances stockEventIndex nonStockBalanceEventIndex finalMaps authorizedShareAdjustmentIndex <- stockClassAuthorizedAdjustmentIndex finalMaps validateAuthorizedShareCeilings + stockEventIndex finalMaps authorizedShareAdjustmentIndex newIssuerCid From 50eb41d7adcd8e12005e48a3314a728c2c359490 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 07:54:12 -0400 Subject: [PATCH 21/38] fix: preserve ceilings when balances are missing --- .../TestStockSecurityBalances.daml | 30 ++++++++++++++++++- .../0.0.12/OpenCapTable-v34.dar | 4 +-- dars/dars.lock | 6 ++-- .../codegen/templates/CapTable.daml.template | 24 +++++++++++++-- 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestStockSecurityBalances.daml index 8dae3cec..14152f93 100644 --- a/Test/daml/OpenCapTable/TestStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestStockSecurityBalances.daml @@ -175,20 +175,48 @@ testStockSecurityBalance_BalanceSecurityRetirementUpdatesIssuedQuantity = script let cancellation = (stockCancellationOn "TX_STOCK_BAL_TERMINAL_CANCEL" securityId 40.0 jan02) with balance_security_id = Some "SEC_STOCK_BAL_TERMINAL_REMAINDER" + let balanceIssuance = (defaultStockIssuance "TX_STOCK_BAL_TERMINAL_REMAINDER" "SEC_STOCK_BAL_TERMINAL_REMAINDER" stakeholderId stockClassId) with + date = jan02 + quantity = 60.0 let replacement = (defaultStockIssuance "TX_STOCK_BAL_TERMINAL_REPLACE" "SEC_STOCK_BAL_TERMINAL_REPLACE" stakeholderId stockClassId) with date = jan03 - quantity = 100.0 + quantity = 40.0 _ <- submit issuer do exerciseCmd capTableIssued CT.UpdateCapTable with creates = [ CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockIssuance balanceIssuance , CT.OcfCreateStockIssuance replacement ] edits = [] deletes = [] pure () +testStockSecurityBalance_MissingTransferBalanceCannotFreeAuthorizedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockClassId = "SC_STOCK_BAL_MISSING_TRANSFER_BALANCE" + (capTableWithClass, _) <- setupStockClassWith issuer cap_table (limitedStockClass stockClassId 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_STOCK_BAL_MISSING_TRANSFER_BALANCE" "Transfer Holder" + let securityId = "SEC_STOCK_BAL_MISSING_TRANSFER_BALANCE" + let transferId = "TX_STOCK_BAL_MISSING_TRANSFER_BALANCE" + let resultSecurityId = transferId <> "_RESULT" + let issuance = (defaultStockIssuance "TX_STOCK_BAL_MISSING_TRANSFER_BALANCE_SOURCE" securityId stakeholderId stockClassId) with quantity = 100.0 + let transfer = stockTransferWithBalance transferId securityId 40.0 "SEC_STOCK_BAL_MISSING_TRANSFER_REMAINDER" jan02 + let transferredIssuance = (defaultStockIssuance "TX_STOCK_BAL_MISSING_TRANSFER_RESULT" resultSecurityId stakeholderId stockClassId) with + date = jan02 + quantity = 40.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockTransfer transfer + , CT.OcfCreateStockIssuance transferredIssuance + ] + edits = [] + deletes = [] + testStockSecurityBalance_TransferBalanceRetirementUpdatesIssuedQuantity = script do TestOcp{issuer, cap_table} <- setupTestOcp let stockClassId = "SC_STOCK_BAL_TRANSFER_TERMINAL_INDEX" diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index 5c889d77..00eb5a4a 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:027d2a93e0145336a8fc6d61ccee56d42ecd4655e5aa9251fa5e233ebb6bd53f -size 2818268 +oid sha256:8e6117ab75cd825b9f74ec0208721bd773ece46866f79191a73c89e0f278e397 +size 2820303 diff --git a/dars/dars.lock b/dars/dars.lock index ead778d1..56434b94 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,10 +26,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "027d2a93e0145336a8fc6d61ccee56d42ecd4655e5aa9251fa5e233ebb6bd53f", - "size": 2818268, + "sha256": "8e6117ab75cd825b9f74ec0208721bd773ece46866f79191a73c89e0f278e397", + "size": 2820303, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-10T11:31:33.000Z", + "uploadedAt": "2026-07-10T11:52:45.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 b4bc2202..f8601ea5 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -259,6 +259,11 @@ allResultingStockIssuancesExist : CapTableMaps -> [Text] -> Bool allResultingStockIssuancesExist maps securityIds = not (null securityIds) && all (\securityId -> isSome (Map.lookup securityId maps.stock_issuances_by_security_id)) securityIds +stockBalanceSecurityIssuanceExists : CapTableMaps -> Optional Text -> Bool +stockBalanceSecurityIssuanceExists maps balanceSecurityId = case balanceSecurityId of + Some securityId -> isSome (Map.lookup securityId maps.stock_issuances_by_security_id) + None -> True + noResultingStockIssuancesExist : CapTableMaps -> [Text] -> Bool noResultingStockIssuancesExist maps securityIds = all (\securityId -> isNone (Map.lookup securityId maps.stock_issuances_by_security_id)) securityIds @@ -836,8 +841,12 @@ stockClassShareEventIndex maps = do (\(shareIndex, balanceIndex) cancellationCid -> do cancellation <- fetch cancellationCid let events = stockSecurityReductionEvents cancellation.cancellation_data.date cancellation.cancellation_data.quantity cancellation.cancellation_data.balance_security_id + let nextShareIndex = + if stockBalanceSecurityIssuanceExists maps cancellation.cancellation_data.balance_security_id + then addStockClassShareEvents cancellation.cancellation_data.security_id events shareIndex + else shareIndex pure - ( addStockClassShareEvents cancellation.cancellation_data.security_id events shareIndex + ( nextShareIndex , addStockClassShareEvents cancellation.cancellation_data.security_id events balanceIndex )) (Map.empty, Map.empty) @@ -847,8 +856,12 @@ stockClassShareEventIndex maps = do (\(shareIndex, balanceIndex) repurchaseCid -> do repurchase <- fetch repurchaseCid let events = stockSecurityReductionEvents repurchase.repurchase_data.date repurchase.repurchase_data.quantity repurchase.repurchase_data.balance_security_id + let nextShareIndex = + if stockBalanceSecurityIssuanceExists maps repurchase.repurchase_data.balance_security_id + then addStockClassShareEvents repurchase.repurchase_data.security_id events shareIndex + else shareIndex pure - ( addStockClassShareEvents repurchase.repurchase_data.security_id events shareIndex + ( nextShareIndex , addStockClassShareEvents repurchase.repurchase_data.security_id events balanceIndex )) (cancellationEventsBySecurityId, cancellationBalanceEventsBySecurityId) @@ -858,8 +871,12 @@ stockClassShareEventIndex maps = do (\(shareIndex, balanceIndex) conversionCid -> do conversion <- fetch conversionCid let events = stockSecurityReductionEvents conversion.conversion_data.date conversion.conversion_data.quantity_converted conversion.conversion_data.balance_security_id + let nextShareIndex = + if stockBalanceSecurityIssuanceExists maps conversion.conversion_data.balance_security_id + then addStockClassShareEvents conversion.conversion_data.security_id events shareIndex + else shareIndex pure - ( addStockClassShareEvents conversion.conversion_data.security_id events shareIndex + ( nextShareIndex , addStockClassShareEvents conversion.conversion_data.security_id events balanceIndex )) (repurchaseEventsBySecurityId, repurchaseBalanceEventsBySecurityId) @@ -871,6 +888,7 @@ stockClassShareEventIndex maps = do let shareEvents = stockSecurityReductionEvents transfer.transfer_data.date transfer.transfer_data.quantity transfer.transfer_data.balance_security_id let nextShareIndex = if allResultingStockIssuancesExist maps transfer.transfer_data.resulting_security_ids + && stockBalanceSecurityIssuanceExists maps transfer.transfer_data.balance_security_id then addStockClassShareEvents transfer.transfer_data.security_id shareEvents shareIndex else shareIndex let balanceEvents = stockSecurityReductionEvents transfer.transfer_data.date transfer.transfer_data.quantity transfer.transfer_data.balance_security_id From d5870bfa8a839017548d625541cc11f7d6b3fee8 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 09:51:57 -0400 Subject: [PATCH 22/38] fix: validate stock balance successors --- .../TestFinalReferenceValidation.daml | 13 +- .../OpenCapTable/TestStockCancellation.daml | 24 ++- .../OpenCapTable/TestStockConversion.daml | 29 ++- .../TestStockPlanReservedShares.daml | 25 +++ .../OpenCapTable/TestStockRepurchase.daml | 27 ++- .../TestStockSecurityBalances.daml | 69 ++++++- Test/daml/OpenCapTable/TestStockTransfer.daml | 60 ++++-- .../codegen/templates/CapTable.daml.template | 178 +++++++++++++++++- 8 files changed, 371 insertions(+), 54 deletions(-) diff --git a/Test/daml/OpenCapTable/TestFinalReferenceValidation.daml b/Test/daml/OpenCapTable/TestFinalReferenceValidation.daml index 7e8f8d16..a4a36779 100644 --- a/Test/daml/OpenCapTable/TestFinalReferenceValidation.daml +++ b/Test/daml/OpenCapTable/TestFinalReferenceValidation.daml @@ -14,6 +14,7 @@ import OpenCapTable.TestHelpers , addDefaultWarrantIssuance , addPrerequisites , addStockIssuance + , defaultStockIssuance ) import qualified DA.Date as DA import DA.Date (Month(..)) @@ -38,9 +39,19 @@ testFinalReferenceValidation_DeleteReferencedStockIssuanceFails = script do TestOcp{issuer, cap_table} <- setupTestOcp capTable <- addPrerequisites issuer cap_table capTable <- addStockIssuance issuer capTable "TX-STOCK-FINAL-REF" "SEC-FINAL-REF" "SH-1" "SC_COMMON" + let resultIssuance = (defaultStockIssuance "TX-STOCK-TRANSFER-FINAL-RESULT" "SEC-TRANSFER-RESULT" "SH-1" "SC_COMMON") with + date = defaultReferenceTestTime + quantity = 10.0 + let balanceIssuance = (defaultStockIssuance "TX-STOCK-TRANSFER-FINAL-BALANCE" "SEC-TRANSFER-BALANCE" "SH-1" "SC_COMMON") with + date = defaultReferenceTestTime + quantity = 90.0 result <- submit issuer do exerciseCmd capTable CT.UpdateCapTable with - creates = [CT.OcfCreateStockTransfer (stockTransferData "TX-STOCK-TRANSFER-FINAL-REF" "SEC-FINAL-REF")] + creates = + [ CT.OcfCreateStockTransfer (stockTransferData "TX-STOCK-TRANSFER-FINAL-REF" "SEC-FINAL-REF") + , CT.OcfCreateStockIssuance resultIssuance + , CT.OcfCreateStockIssuance balanceIssuance + ] edits = [] deletes = [] diff --git a/Test/daml/OpenCapTable/TestStockCancellation.daml b/Test/daml/OpenCapTable/TestStockCancellation.daml index 3853f15e..f587ff43 100644 --- a/Test/daml/OpenCapTable/TestStockCancellation.daml +++ b/Test/daml/OpenCapTable/TestStockCancellation.daml @@ -6,7 +6,7 @@ import qualified Fairmint.OpenCapTable.CapTable as CT import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Stock (OcfStockClassType(..), OcfInitialSharesAuthorized(..)) import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addPrerequisites) +import OpenCapTable.TestHelpers (addPrerequisites, defaultStockIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -95,16 +95,22 @@ testStockCancellation_OptionalsSome = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addPrerequisites issuer cap_table result <- createStockIssuance issuer capTableCid + let balanceIssuance = (defaultStockIssuance "TX_STOCK_CANCEL_2_BALANCE" "SEC-1-BAL2" "SH-1" "SC_COMMON") with + date = DT.time (DA.date 2024 Jan 03) 0 0 0 + quantity = 75.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockCancellation StockCancellationOcfData with - id = "TX_STOCK_CANCEL_2" - date = DT.time (DA.date 2024 Jan 03) 0 0 0 - security_id = "SEC-1" - quantity = 25.0 - balance_security_id = Some "SEC-1-BAL2" - reason_text = "Return to pool" - comments = ["Admin correction"]] + creates = + [ CT.OcfCreateStockCancellation StockCancellationOcfData with + id = "TX_STOCK_CANCEL_2" + date = DT.time (DA.date 2024 Jan 03) 0 0 0 + security_id = "SEC-1" + quantity = 25.0 + balance_security_id = Some "SEC-1-BAL2" + reason_text = "Return to pool" + comments = ["Admin correction"] + , CT.OcfCreateStockIssuance balanceIssuance + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestStockConversion.daml b/Test/daml/OpenCapTable/TestStockConversion.daml index 6799bd5f..8e4ac2db 100644 --- a/Test/daml/OpenCapTable/TestStockConversion.daml +++ b/Test/daml/OpenCapTable/TestStockConversion.daml @@ -8,7 +8,7 @@ import Fairmint.OpenCapTable.Types.Stock (OcfRatio(..), OcfStockClassType(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.HelpersTriggers (ocfTriggerElectiveAtWill) import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addPrerequisites, defaultStockClass) +import OpenCapTable.TestHelpers (addPrerequisites, defaultStockClass, defaultStockIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -106,16 +106,27 @@ testStockConversion_PartialConversion = script do capTableCid <- addPreferredStockClass issuer capTableCid let securityId = "SSEC-PREF-002" result <- createStockIssuance issuer capTableCid securityId 50000.0 + let conversionDate = DT.time (DA.date 2025 Nov 01) 0 0 0 + let resultIssuance = (defaultStockIssuance "TX_STOCK_CONVERSION_2_RESULT" "SSEC-COMMON-002" "SH-1" "SC_COMMON") with + date = conversionDate + quantity = 30000.0 + let balanceIssuance = (defaultStockIssuance "TX_STOCK_CONVERSION_2_BALANCE" "SSEC-PREF-002-REMAIN" "SH-1" "SC_PREF") with + date = conversionDate + quantity = 20000.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockConversion StockConversionOcfData with - id = "TX_STOCK_CONVERSION_2" - date = DT.time (DA.date 2025 Nov 01) 0 0 0 - security_id = securityId - quantity_converted = 30000.0 - resulting_security_ids = ["SSEC-COMMON-002"] - comments = ["Elective partial conversion by investor"] - balance_security_id = Some "SSEC-PREF-002-REMAIN"] + creates = + [ CT.OcfCreateStockIssuance resultIssuance + , CT.OcfCreateStockIssuance balanceIssuance + , CT.OcfCreateStockConversion StockConversionOcfData with + id = "TX_STOCK_CONVERSION_2" + date = conversionDate + security_id = securityId + quantity_converted = 30000.0 + resulting_security_ids = ["SSEC-COMMON-002"] + comments = ["Elective partial conversion by investor"] + balance_security_id = Some "SSEC-PREF-002-REMAIN" + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index 9e33c1ae..33be3fe3 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -314,6 +314,31 @@ testStockPlanReservedShares_ReturnToPoolAllowsReuse = script do deletes = [] pure () +testStockPlanReservedShares_BalanceCancellationOnlyReturnsCancelledQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EC_BALANCE_RETURN" + let sourceSecurityId = "SEC-EC-BALANCE-RETURN-SOURCE" + let balanceSecurityId = "SEC-EC-BALANCE-RETURN-REMAINDER" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EC_BALANCE_RETURN_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + let cancellation = (equityCompensationCancellation "TX_EC_BALANCE_RETURN_CANCEL" sourceSecurityId 40.0) with + balance_security_id = Some balanceSecurityId + let balanceIssuance = stockPlanEquityCompensationIssuance "TX_EC_BALANCE_RETURN_REMAINDER" balanceSecurityId stakeholderId stockClassId stockPlanId 60.0 + let returnToPool = stockPlanReturnToPool "TX_EC_BALANCE_EXCESS_RETURN" sourceSecurityId stockPlanId 100.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + , CT.OcfCreateEquityCompensationCancellation cancellation + , CT.OcfCreateEquityCompensationIssuance balanceIssuance + , CT.OcfCreateStockPlanReturnToPool returnToPool + ] + edits = [] + deletes = [] + testStockPlanReservedShares_TransferredEquityCompensationCannotReturnToPool = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/Test/daml/OpenCapTable/TestStockRepurchase.daml b/Test/daml/OpenCapTable/TestStockRepurchase.daml index fc8aa75b..23f8a137 100644 --- a/Test/daml/OpenCapTable/TestStockRepurchase.daml +++ b/Test/daml/OpenCapTable/TestStockRepurchase.daml @@ -5,7 +5,7 @@ import Fairmint.OpenCapTable.OCF.StockRepurchase import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addPrerequisites) +import OpenCapTable.TestHelpers (addPrerequisites, defaultStockIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -67,17 +67,24 @@ testStockRepurchase_PartialRepurchase = script do capTableCid <- addPrerequisites issuer cap_table let securityId = "SSEC-002" result <- createStockIssuance issuer capTableCid securityId 100000.0 + let repurchaseDate = DT.time (DA.date 2023 Jun 15) 0 0 0 + let balanceIssuance = (defaultStockIssuance "TX_STOCK_REPURCHASE_2_BALANCE" "SSEC-002-REMAIN" "SH-1" "SC_COMMON") with + date = repurchaseDate + quantity = 50000.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockRepurchase StockRepurchaseOcfData with - id = "TX_STOCK_REPURCHASE_2" - date = DT.time (DA.date 2023 Jun 15) 0 0 0 - security_id = securityId - price = OcfMonetary with amount = 0.01, currency = "USD" - quantity = 50000.0 - comments = ["Partial repurchase of unvested shares upon voluntary termination."] - balance_security_id = Some "SSEC-002-REMAIN" - consideration_text = Some "Repurchase at original issue price per Stock Restriction Agreement Section 3."] + creates = + [ CT.OcfCreateStockRepurchase StockRepurchaseOcfData with + id = "TX_STOCK_REPURCHASE_2" + date = repurchaseDate + security_id = securityId + price = OcfMonetary with amount = 0.01, currency = "USD" + quantity = 50000.0 + comments = ["Partial repurchase of unvested shares upon voluntary termination."] + balance_security_id = Some "SSEC-002-REMAIN" + consideration_text = Some "Repurchase at original issue price per Stock Restriction Agreement Section 3." + , CT.OcfCreateStockIssuance balanceIssuance + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestStockSecurityBalances.daml index 14152f93..8680542e 100644 --- a/Test/daml/OpenCapTable/TestStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestStockSecurityBalances.daml @@ -135,30 +135,47 @@ testStockSecurityBalance_BalanceSecurityRetiresSource = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table let securityId = "SEC_STOCK_BAL_TRANSFER_BALANCE" + let transferId = "TX_TRANSFER_WITH_BALANCE" + let resultSecurityId = transferId <> "_RESULT" + let balanceSecurityId = "SEC_STOCK_BAL_REMAINDER" let issuance = (defaultStockIssuance "TX_STOCK_BAL_TRANSFER_BALANCE" securityId stakeholderId stockClassId) with quantity = 100.0 + let resultIssuance = (defaultStockIssuance "TX_STOCK_BAL_TRANSFER_RESULT" resultSecurityId stakeholderId stockClassId) with + date = jan02 + quantity = 40.0 + let balanceIssuance = (defaultStockIssuance "TX_STOCK_BAL_TRANSFER_REMAINDER" balanceSecurityId stakeholderId stockClassId) with + date = jan02 + quantity = 60.0 submitMustFail issuer do exerciseCmd capTableReady CT.UpdateCapTable with creates = [ CT.OcfCreateStockIssuance issuance - , CT.OcfCreateStockTransfer (stockTransferWithBalance "TX_TRANSFER_WITH_BALANCE" securityId 40.0 "SEC_STOCK_BAL_REMAINDER" jan02) + , CT.OcfCreateStockTransfer (stockTransferWithBalance transferId securityId 40.0 balanceSecurityId jan02) + , CT.OcfCreateStockIssuance resultIssuance + , CT.OcfCreateStockIssuance balanceIssuance , CT.OcfCreateStockCancellation (stockCancellationOn "TX_CANCEL_OLD_SOURCE_AFTER_BALANCE" securityId 1.0 jan03) ] edits = [] deletes = [] -testStockSecurityBalance_BalanceSecurityReductionCanConsumeRemainingSource = script do +testStockSecurityBalance_FullReductionWithoutBalanceCanConsumeRemainingSource = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table let securityId = "SEC_STOCK_BAL_ZERO_REMAINDER" + let transferId = "TX_TRANSFER_FINAL_REMAINDER" + let resultSecurityId = transferId <> "_RESULT" let issuance = (defaultStockIssuance "TX_STOCK_BAL_ZERO_REMAINDER" securityId stakeholderId stockClassId) with quantity = 100.0 + let resultIssuance = (defaultStockIssuance "TX_STOCK_BAL_FINAL_RESULT" resultSecurityId stakeholderId stockClassId) with + date = jan03 + quantity = 60.0 _ <- submit issuer do exerciseCmd capTableReady CT.UpdateCapTable with creates = [ CT.OcfCreateStockIssuance issuance , CT.OcfCreateStockCancellation (stockCancellationOn "TX_CANCEL_BEFORE_BALANCE_TRANSFER" securityId 40.0 jan02) - , CT.OcfCreateStockTransfer (stockTransferWithBalance "TX_TRANSFER_BALANCE_REMAINDER" securityId 60.0 "SEC_STOCK_BAL_REMAINDER" jan03) + , CT.OcfCreateStockTransfer (stockTransfer transferId securityId 60.0 jan03) + , CT.OcfCreateStockIssuance resultIssuance ] edits = [] deletes = [] @@ -193,6 +210,52 @@ testStockSecurityBalance_BalanceSecurityRetirementUpdatesIssuedQuantity = script deletes = [] pure () +testStockSecurityBalance_BalanceSecurityMustMatchRemainingQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let securityId = "SEC_STOCK_BAL_WRONG_REMAINDER" + let balanceSecurityId = "SEC_STOCK_BAL_WRONG_REMAINDER_RESULT" + let issuance = (defaultStockIssuance "TX_STOCK_BAL_WRONG_REMAINDER" securityId stakeholderId stockClassId) with quantity = 100.0 + let cancellation = (stockCancellationOn "TX_STOCK_BAL_WRONG_REMAINDER_CANCEL" securityId 40.0 jan02) with + balance_security_id = Some balanceSecurityId + let balanceIssuance = (defaultStockIssuance "TX_STOCK_BAL_WRONG_REMAINDER_RESULT" balanceSecurityId stakeholderId stockClassId) with + date = jan02 + quantity = 1.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockIssuance balanceIssuance + ] + edits = [] + deletes = [] + +testStockSecurityBalance_BalanceSecurityMustMatchSourceStockClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let otherStockClassId = "SC_STOCK_BAL_WRONG_CLASS" + (capTableWithOtherClass, _) <- setupStockClassWith issuer capTableReady (defaultStockClass otherStockClassId) + let securityId = "SEC_STOCK_BAL_WRONG_CLASS" + let balanceSecurityId = "SEC_STOCK_BAL_WRONG_CLASS_RESULT" + let issuance = (defaultStockIssuance "TX_STOCK_BAL_WRONG_CLASS" securityId stakeholderId stockClassId) with quantity = 100.0 + let cancellation = (stockCancellationOn "TX_STOCK_BAL_WRONG_CLASS_CANCEL" securityId 40.0 jan02) with + balance_security_id = Some balanceSecurityId + let balanceIssuance = (defaultStockIssuance "TX_STOCK_BAL_WRONG_CLASS_RESULT" balanceSecurityId stakeholderId otherStockClassId) with + date = jan02 + quantity = 60.0 + + submitMustFail issuer do + exerciseCmd capTableWithOtherClass CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockIssuance balanceIssuance + ] + edits = [] + deletes = [] + testStockSecurityBalance_MissingTransferBalanceCannotFreeAuthorizedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp let stockClassId = "SC_STOCK_BAL_MISSING_TRANSFER_BALANCE" diff --git a/Test/daml/OpenCapTable/TestStockTransfer.daml b/Test/daml/OpenCapTable/TestStockTransfer.daml index b4b2ffd7..5077b3b6 100644 --- a/Test/daml/OpenCapTable/TestStockTransfer.daml +++ b/Test/daml/OpenCapTable/TestStockTransfer.daml @@ -5,7 +5,7 @@ import Fairmint.OpenCapTable.OCF.StockTransfer import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addPrerequisites) +import OpenCapTable.TestHelpers (addPrerequisites, defaultStockIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -66,17 +66,28 @@ testStockTransfer_DuplicateResultingSecurityIdsRejected = script do capTableCid <- addPrerequisites issuer cap_table let securityId = "SSEC-DUP-TRANSFER-001" result <- createStockIssuance issuer capTableCid securityId 100.0 + let transferDate = DT.time (DA.date 2025 Aug 05) 0 0 0 + let resultIssuance = (defaultStockIssuance "TX_STOCK_TRANSFER_DUP_RESULT" "SSEC-DUP-RESULT" "SH-1" "SC_COMMON") with + date = transferDate + quantity = 50.0 + let balanceIssuance = (defaultStockIssuance "TX_STOCK_TRANSFER_DUP_BALANCE" "SSEC-DUP-BALANCE" "SH-1" "SC_COMMON") with + date = transferDate + quantity = 50.0 submitMustFail issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockTransfer StockTransferOcfData with - id = "TX_STOCK_TRANSFER_DUP_RESULT" - date = DT.time (DA.date 2025 Aug 05) 0 0 0 - quantity = 50.0 - security_id = securityId - resulting_security_ids = ["SSEC-DUP-RESULT", "SSEC-DUP-RESULT"] - comments = [] - balance_security_id = Some "SSEC-DUP-BALANCE" - consideration_text = None] + creates = + [ CT.OcfCreateStockTransfer StockTransferOcfData with + id = "TX_STOCK_TRANSFER_DUP_RESULT" + date = transferDate + quantity = 50.0 + security_id = securityId + resulting_security_ids = ["SSEC-DUP-RESULT", "SSEC-DUP-RESULT"] + comments = [] + balance_security_id = Some "SSEC-DUP-BALANCE" + consideration_text = None + , CT.OcfCreateStockIssuance resultIssuance + , CT.OcfCreateStockIssuance balanceIssuance + ] edits = [] deletes = [] pure () @@ -88,17 +99,28 @@ testStockTransfer_FounderTransferWithConsideration = script do capTableCid <- addPrerequisites issuer cap_table let securityId = "SSEC-FOUNDER-001" result <- createStockIssuance issuer capTableCid securityId 1000.0 + let transferDate = DT.time (DA.date 2024 Apr 18) 0 0 0 + let resultIssuance = (defaultStockIssuance "TX_STOCK_TRANSFER_2_RESULT" "SSEC-TRANSFEREE-001" "SH-1" "SC_COMMON") with + date = transferDate + quantity = 75.0 + let balanceIssuance = (defaultStockIssuance "TX_STOCK_TRANSFER_2_BALANCE" "SSEC-FOUNDER-REMAIN" "SH-1" "SC_COMMON") with + date = transferDate + quantity = 925.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockTransfer StockTransferOcfData with - id = "TX_STOCK_TRANSFER_2" - date = DT.time (DA.date 2024 Apr 18) 0 0 0 - quantity = 75.0 - security_id = securityId - resulting_security_ids = ["SSEC-TRANSFEREE-001"] - comments = ["Stock transfer from Founder A to Founder B per Stock Transfer Agreement. Transferee assumes all obligations including vesting schedule and repurchase option."] - balance_security_id = Some "SSEC-FOUNDER-REMAIN" - consideration_text = Some "Transfer of 75 shares at $0.0001 per share for aggregate consideration of $0.01."] + creates = + [ CT.OcfCreateStockTransfer StockTransferOcfData with + id = "TX_STOCK_TRANSFER_2" + date = transferDate + quantity = 75.0 + security_id = securityId + resulting_security_ids = ["SSEC-TRANSFEREE-001"] + comments = ["Stock transfer from Founder A to Founder B per Stock Transfer Agreement. Transferee assumes all obligations including vesting schedule and repurchase option."] + balance_security_id = Some "SSEC-FOUNDER-REMAIN" + consideration_text = Some "Transfer of 75 shares at $0.0001 per share for aggregate consideration of $0.01." + , CT.OcfCreateStockIssuance resultIssuance + , CT.OcfCreateStockIssuance balanceIssuance + ] edits = [] deletes = [] pure () diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index f8601ea5..bc0004cb 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -891,10 +891,9 @@ stockClassShareEventIndex maps = do && stockBalanceSecurityIssuanceExists maps transfer.transfer_data.balance_security_id then addStockClassShareEvents transfer.transfer_data.security_id shareEvents shareIndex else shareIndex - let balanceEvents = stockSecurityReductionEvents transfer.transfer_data.date transfer.transfer_data.quantity transfer.transfer_data.balance_security_id pure ( nextShareIndex - , addStockClassShareEvents transfer.transfer_data.security_id balanceEvents balanceIndex + , addStockClassShareEvents transfer.transfer_data.security_id shareEvents balanceIndex )) (conversionEventsBySecurityId, conversionBalanceEventsBySecurityId) (Map.values maps.stock_transfers) @@ -1074,6 +1073,175 @@ validateStockSecurityBalances eventIndex maps = validateStockSecurityBalance eventIndex issuance.issuance_data) (Map.values maps.stock_issuances) +stockSecurityQuantityBeforeEvent : + StockClassShareEventIndex -> + StockIssuanceOcfData -> + (Time, Int, Int) -> + Update Decimal +stockSecurityQuantityBeforeEvent eventIndex issuanceData targetSortKey = + foldlA + (validateStockSecurityBalanceEvent issuanceData.security_id) + 0.0 + (filter + (\event -> stockClassShareEventSortKey event < targetSortKey) + (sortOn stockClassShareEventSortKey (stockSecurityShareEvents eventIndex issuanceData))) + +stockClassQuantityAtSortKey : + CapTableMaps -> + Text -> + Time -> + Decimal -> + (Time, Int, Int) -> + Update Decimal +stockClassQuantityAtSortKey maps stockClassId quantityDate quantity targetSortKey = + let (targetDate, _, _) = targetSortKey + quantitySortKey = if quantityDate == targetDate then targetSortKey else (quantityDate, 0, 0) + in foldlA + (\adjustedQuantity splitCid -> do + split <- fetch splitCid + let splitSortKey = (split.split_data.date, 2, 2) + ratio = split.split_data.split_ratio + if split.split_data.stock_class_id /= stockClassId + then pure adjustedQuantity + else if quantitySortKey < targetSortKey && quantitySortKey < splitSortKey && splitSortKey <= targetSortKey + then pure (adjustedQuantity * ratio.numerator / ratio.denominator) + else if targetSortKey < quantitySortKey && targetSortKey < splitSortKey && splitSortKey < quantitySortKey + then pure (adjustedQuantity * ratio.denominator / ratio.numerator) + else pure adjustedQuantity) + quantity + (Map.values maps.stock_class_splits) + +validateStockBalanceSuccessorQuantity : + CapTableMaps -> + Text -> + Text -> + (Time, Int, Int) -> + Decimal -> + Text -> + Update () +validateStockBalanceSuccessorQuantity maps label expectedStockClassId targetSortKey expectedQuantity securityId = + case Map.lookup securityId maps.stock_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertMsg + (label <> " balance stock class mismatch for " <> securityId <> ": expected " <> expectedStockClassId <> ", found " <> issuance.issuance_data.stock_class_id) + (issuance.issuance_data.stock_class_id == expectedStockClassId) + actualQuantity <- stockClassQuantityAtSortKey + maps + issuance.issuance_data.stock_class_id + issuance.issuance_data.date + issuance.issuance_data.quantity + targetSortKey + assertMsg + (label <> " balance quantity mismatch for " <> securityId <> ": expected " <> show expectedQuantity <> ", found " <> show actualQuantity) + (actualQuantity == expectedQuantity) + None -> + assertMsg (label <> " balance stock issuance not found: " <> securityId) False + +validateStockTerminalBalanceSuccessor : + StockClassShareEventIndex -> + CapTableMaps -> + Text -> + StockIssuanceOcfData -> + Time -> + Decimal -> + Optional Text -> + Update () +validateStockTerminalBalanceSuccessor eventIndex maps label sourceIssuanceData eventDate reductionQuantity balanceSecurityId = + case balanceSecurityId of + Some securityId -> do + let targetSortKey = (eventDate, 2, 1) + currentQuantity <- stockSecurityQuantityBeforeEvent eventIndex sourceIssuanceData targetSortKey + let expectedQuantity = currentQuantity - reductionQuantity + assertMsg + (label <> " exceeds the current source quantity for " <> sourceIssuanceData.security_id) + (expectedQuantity >= 0.0) + validateStockBalanceSuccessorQuantity + maps + label + sourceIssuanceData.stock_class_id + targetSortKey + expectedQuantity + securityId + None -> pure () + +validateStockTerminalBalanceSuccessorForSource : + StockClassShareEventIndex -> + CapTableMaps -> + Text -> + Text -> + Time -> + Decimal -> + Optional Text -> + Update () +validateStockTerminalBalanceSuccessorForSource eventIndex maps label sourceSecurityId eventDate reductionQuantity balanceSecurityId = + case Map.lookup sourceSecurityId maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + validateStockTerminalBalanceSuccessor + eventIndex + maps + label + sourceIssuance.issuance_data + eventDate + reductionQuantity + balanceSecurityId + None -> assertMsg (label <> " source stock issuance not found: " <> sourceSecurityId) False + +validateStockBalanceSuccessorQuantities : StockClassShareEventIndex -> CapTableMaps -> Update () +validateStockBalanceSuccessorQuantities eventIndex maps = do + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + validateStockTerminalBalanceSuccessorForSource + eventIndex + maps + ("Stock cancellation " <> cancellation.cancellation_data.id) + cancellation.cancellation_data.security_id + cancellation.cancellation_data.date + cancellation.cancellation_data.quantity + cancellation.cancellation_data.balance_security_id) + (Map.values maps.stock_cancellations) + + mapA_ + (\repurchaseCid -> do + repurchase <- fetch repurchaseCid + validateStockTerminalBalanceSuccessorForSource + eventIndex + maps + ("Stock repurchase " <> repurchase.repurchase_data.id) + repurchase.repurchase_data.security_id + repurchase.repurchase_data.date + repurchase.repurchase_data.quantity + repurchase.repurchase_data.balance_security_id) + (Map.values maps.stock_repurchases) + + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + validateStockTerminalBalanceSuccessorForSource + eventIndex + maps + ("Stock transfer " <> transfer.transfer_data.id) + transfer.transfer_data.security_id + transfer.transfer_data.date + transfer.transfer_data.quantity + transfer.transfer_data.balance_security_id) + (Map.values maps.stock_transfers) + + mapA_ + (\conversionCid -> do + conversion <- fetch conversionCid + validateStockTerminalBalanceSuccessorForSource + eventIndex + maps + ("Stock conversion " <> conversion.conversion_data.id) + conversion.conversion_data.security_id + conversion.conversion_data.date + conversion.conversion_data.quantity_converted + conversion.conversion_data.balance_security_id) + (Map.values maps.stock_conversions) + securityQuantityEventSortKey : SecurityQuantityEvent -> (Time, Int, Int) securityQuantityEventSortKey event = case event of SecurityQuantityDelta with event_date; event_priority -> (event_date, event_priority, 0) @@ -2281,7 +2449,10 @@ equityCompensationSecurityEvents maps securityId = do (\events cancellationCid -> do cancellation <- fetch cancellationCid if cancellation.cancellation_data.security_id == securityId - then pure (stockSecurityReductionEvents cancellation.cancellation_data.date cancellation.cancellation_data.quantity cancellation.cancellation_data.balance_security_id ++ events) + then pure ((StockClassShareDelta with + event_date = cancellation.cancellation_data.date + event_priority = 1 + quantity_delta = 0.0 - cancellation.cancellation_data.quantity) :: events) else pure events) [] (Map.values maps.equity_compensation_cancellations) @@ -2840,6 +3011,7 @@ template CapTable validateWarrantExerciseTriggerReferences finalMaps validateStockReissuanceSplitReferences finalMaps validateConfiguredReferences finalMaps + validateStockBalanceSuccessorQuantities stockEventIndex finalMaps validateStockSecurityBalances stockEventIndex finalMaps validateWarrantSecurityBalances nonStockBalanceEventIndex finalMaps validateEquityCompensationSecurityBalances stockEventIndex nonStockBalanceEventIndex finalMaps From 85c7617a990c3dfb7b692289a7346b0dc754df2d Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 13:05:18 -0400 Subject: [PATCH 23/38] Cover missing stock balance successors --- .../OpenCapTable/TestStockSecurityBalances.daml | 17 +++++++++++++++++ .../0.0.12/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestStockSecurityBalances.daml index 8680542e..bedafcfe 100644 --- a/Test/daml/OpenCapTable/TestStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestStockSecurityBalances.daml @@ -210,6 +210,23 @@ testStockSecurityBalance_BalanceSecurityRetirementUpdatesIssuedQuantity = script deletes = [] pure () +testStockSecurityBalance_MissingCancellationBalanceSecurityFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let securityId = "SEC_STOCK_BAL_MISSING_CANCEL_REMAINDER" + let issuance = (defaultStockIssuance "TX_STOCK_BAL_MISSING_CANCEL_REMAINDER" securityId stakeholderId stockClassId) with quantity = 100.0 + let cancellation = (stockCancellationOn "TX_STOCK_BAL_MISSING_CANCEL_REMAINDER_CANCEL" securityId 1.0 jan02) with + balance_security_id = Some "SEC_STOCK_BAL_MISSING_CANCEL_REMAINDER_RESULT" + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockCancellation cancellation + ] + edits = [] + deletes = [] + testStockSecurityBalance_BalanceSecurityMustMatchRemainingQuantity = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index 00eb5a4a..79eb6592 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e6117ab75cd825b9f74ec0208721bd773ece46866f79191a73c89e0f278e397 -size 2820303 +oid sha256:0837eef46d17a611b9e0e65381b04b3d3a807d9d0090cac1bdbef50424cd3857 +size 2834348 diff --git a/dars/dars.lock b/dars/dars.lock index 56434b94..02cfd357 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,8 +26,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "8e6117ab75cd825b9f74ec0208721bd773ece46866f79191a73c89e0f278e397", - "size": 2820303, + "sha256": "0837eef46d17a611b9e0e65381b04b3d3a807d9d0090cac1bdbef50424cd3857", + "size": 2834348, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-10T11:52:45.000Z", "networks": [] From dcc84ec0b3b303d815b57f242bfc367f6ed258b6 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 15:29:49 -0400 Subject: [PATCH 24/38] fix: enforce non-stock balance successor continuity --- .../TestEquityCompensationCancellation.daml | 18 +- .../TestEquityCompensationTransfer.daml | 24 +- .../TestNonStockSecurityBalances.daml | 157 ++++++++++++- .../OpenCapTable/TestWarrantCancellation.daml | 18 +- .../OpenCapTable/TestWarrantTransfer.daml | 18 +- .../0.0.12/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 213 +++++++++++++++++- 8 files changed, 410 insertions(+), 46 deletions(-) diff --git a/Test/daml/OpenCapTable/TestEquityCompensationCancellation.daml b/Test/daml/OpenCapTable/TestEquityCompensationCancellation.daml index e1995dd5..2ad41054 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationCancellation.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationCancellation.daml @@ -12,12 +12,12 @@ import qualified DA.Time as DT import Daml.Script -- Create an equity compensation issuance to cancel (follows real workflow) -createEquityCompensationIssuance issuer capTableCid securityId quantity = +createEquityCompensationIssuance issuer capTableCid securityId quantity issuanceDate = submit issuer do exerciseCmd capTableCid CT.UpdateCapTable with creates = [CT.OcfCreateEquityCompensationIssuance EquityCompensationIssuanceOcfData with - id = "TX_EC_ISS_1" - date = DT.time (DA.date 2020 Jan 15) 0 0 0 + id = "TX_EC_ISS_" <> securityId + date = issuanceDate security_id = securityId custom_id = "GRANT-001" stakeholder_id = "SH-1" @@ -61,7 +61,7 @@ testEquityCompensationCancellation_CreateAndArchiveByIssuer = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "db5a26d6-3912-4400-87b0-bda74de361af" - result <- createEquityCompensationIssuance issuer capTableCid securityId 250000.0 + result <- createEquityCompensationIssuance issuer capTableCid securityId 250000.0 (DT.time (DA.date 2020 Jan 15) 0 0 0) _ <- createEquityCompensationCancellation issuer result.updatedCapTableCid securityId 214200.0 pure () @@ -70,7 +70,7 @@ testEquityCompensationCancellation_CreateAndArchiveBuiltIn = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "SEC-EC-002" - result <- createEquityCompensationIssuance issuer capTableCid securityId 100000.0 + result <- createEquityCompensationIssuance issuer capTableCid securityId 100000.0 (DT.time (DA.date 2020 Jan 15) 0 0 0) _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateEquityCompensationCancellation EquityCompensationCancellationOcfData with @@ -90,12 +90,14 @@ testEquityCompensationCancellation_PartialWithOptionals = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "SEC-EC-PARTIAL" - result <- createEquityCompensationIssuance issuer capTableCid securityId 500000.0 + result <- createEquityCompensationIssuance issuer capTableCid securityId 500000.0 (DT.time (DA.date 2020 Jan 15) 0 0 0) + let cancellationDate = DT.time (DA.date 2024 Jun 30) 0 0 0 + balanceResult <- createEquityCompensationIssuance issuer result.updatedCapTableCid "SEC-EC-PARTIAL-BAL" 350000.0 cancellationDate _ <- submit issuer do - exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + exerciseCmd balanceResult.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateEquityCompensationCancellation EquityCompensationCancellationOcfData with id = "TX_EC_CANCEL_PARTIAL" - date = DT.time (DA.date 2024 Jun 30) 0 0 0 + date = cancellationDate security_id = securityId quantity = 150000.0 reason_text = "Partial forfeiture due to early departure" diff --git a/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml b/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml index b6ba7cf1..402c3e56 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml @@ -13,12 +13,12 @@ import qualified DA.Time as DT import Daml.Script -- Create an equity compensation issuance to transfer (follows real workflow) -createEquityCompensationIssuance issuer capTableCid securityId quantity = +createEquityCompensationIssuance issuer capTableCid securityId quantity issuanceDate = submit issuer do exerciseCmd capTableCid CT.UpdateCapTable with creates = [CT.OcfCreateEquityCompensationIssuance EquityCompensationIssuanceOcfData with - id = "TX_EQUITY_COMP_ISS_1" - date = DT.time (DA.date 2022 Jan 15) 0 0 0 + id = "TX_EQUITY_COMP_ISS_" <> securityId + date = issuanceDate security_id = securityId custom_id = "OPT-1" stakeholder_id = "SH-1" @@ -48,12 +48,14 @@ testEquityCompensationTransfer_NSOToFamilyTrust = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "ESEC-NSO-001" - result <- createEquityCompensationIssuance issuer capTableCid securityId 10000.0 + result <- createEquityCompensationIssuance issuer capTableCid securityId 10000.0 (DT.time (DA.date 2022 Jan 15) 0 0 0) + let transferDate = DT.time (DA.date 2025 Jun 01) 0 0 0 + balanceResult <- createEquityCompensationIssuance issuer result.updatedCapTableCid "ESEC-NSO-REMAIN" 5000.0 transferDate _ <- submit issuer do - exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + exerciseCmd balanceResult.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateEquityCompensationTransfer EquityCompensationTransferOcfData with id = "TX_EQUITY_COMP_TRANSFER_1" - date = DT.time (DA.date 2025 Jun 01) 0 0 0 + date = transferDate quantity = 5000.0 security_id = securityId resulting_security_ids = ["ESEC-NSO-TRANSFERRED-001"] @@ -69,7 +71,7 @@ testEquityCompensationTransfer_FullTransfer = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "ESEC-NSO-FULL" - result <- createEquityCompensationIssuance issuer capTableCid securityId 2500.0 + result <- createEquityCompensationIssuance issuer capTableCid securityId 2500.0 (DT.time (DA.date 2022 Jan 15) 0 0 0) _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateEquityCompensationTransfer EquityCompensationTransferOcfData with @@ -90,12 +92,14 @@ testEquityCompensationTransfer_WithApprovalComments = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "ESEC-NSO-APPROVAL" - result <- createEquityCompensationIssuance issuer capTableCid securityId 15000.0 + result <- createEquityCompensationIssuance issuer capTableCid securityId 15000.0 (DT.time (DA.date 2022 Jan 15) 0 0 0) + let transferDate = DT.time (DA.date 2025 Nov 01) 0 0 0 + balanceResult <- createEquityCompensationIssuance issuer result.updatedCapTableCid "ESEC-NSO-REMAINING" 7000.0 transferDate _ <- submit issuer do - exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + exerciseCmd balanceResult.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateEquityCompensationTransfer EquityCompensationTransferOcfData with id = "TX_EQUITY_COMP_TRANSFER_APPROVED" - date = DT.time (DA.date 2025 Nov 01) 0 0 0 + date = transferDate quantity = 8000.0 security_id = securityId resulting_security_ids = ["ESEC-NSO-TRUST"] diff --git a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml index fb45e175..37f96aa9 100644 --- a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml @@ -268,11 +268,15 @@ testWarrantSecurityBalance_TransferWithBalanceSecurityRetiresSource = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableReady <- addDefaultStakeholder issuer cap_table let securityId = "WSEC_BAL_TRANSFER_BALANCE_SECURITY" + let resultIssuance = (warrantIssuance "TX_W_TRANSFER_BALANCE_RESULT" "TX_W_TRANSFER_BALANCE_SECURITY_RESULT" defaultStakeholderId 60.0) with date = jan02 + let balanceIssuance = (warrantIssuance "TX_W_TRANSFER_BALANCE_REMAINDER" "WSEC_BALANCE_TRANSFER" defaultStakeholderId 40.0) with date = jan02 submitMustFail issuer do exerciseCmd capTableReady CT.UpdateCapTable with creates = [ CT.OcfCreateWarrantIssuance (warrantIssuance "TX_W_BAL_TRANSFER_BALANCE_SECURITY" securityId defaultStakeholderId 100.0) + , CT.OcfCreateWarrantIssuance resultIssuance + , CT.OcfCreateWarrantIssuance balanceIssuance , CT.OcfCreateWarrantTransfer ((warrantTransfer "TX_W_TRANSFER_BALANCE_SECURITY" securityId 60.0 jan02) with balance_security_id = Some "WSEC_BALANCE_TRANSFER") , CT.OcfCreateWarrantCancellation (warrantCancellation "TX_W_CANCEL_AFTER_BALANCE_TRANSFER" securityId 1.0 jan03) ] @@ -283,23 +287,27 @@ testWarrantSecurityBalance_CancellationWithBalanceSecurityRetiresSource = script TestOcp{issuer, cap_table} <- setupTestOcp capTableReady <- addDefaultStakeholder issuer cap_table let securityId = "WSEC_BAL_CANCELLATION_BALANCE_SECURITY" + let balanceIssuance = (warrantIssuance "TX_W_CANCEL_BALANCE_REMAINDER" "WSEC_BALANCE_CANCELLATION" defaultStakeholderId 40.0) with date = jan02 + let laterResult = (warrantIssuance "TX_W_TRANSFER_AFTER_CANCEL_RESULT" "TX_W_TRANSFER_AFTER_BALANCE_CANCELLATION_RESULT" defaultStakeholderId 1.0) with date = jan03 submitMustFail issuer do exerciseCmd capTableReady CT.UpdateCapTable with creates = [ CT.OcfCreateWarrantIssuance (warrantIssuance "TX_W_BAL_CANCELLATION_BALANCE_SECURITY" securityId defaultStakeholderId 100.0) + , CT.OcfCreateWarrantIssuance balanceIssuance + , CT.OcfCreateWarrantIssuance laterResult , CT.OcfCreateWarrantCancellation ((warrantCancellation "TX_W_CANCEL_BALANCE_SECURITY" securityId 60.0 jan02) with balance_security_id = Some "WSEC_BALANCE_CANCELLATION") , CT.OcfCreateWarrantTransfer (warrantTransfer "TX_W_TRANSFER_AFTER_BALANCE_CANCELLATION" securityId 1.0 jan03) ] edits = [] deletes = [] -testWarrantSecurityBalance_BalanceSecurityReductionCanConsumeRemainingSource = script do +testWarrantSecurityBalance_BalanceSecurityRequiresPositiveRemainder = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableReady <- addDefaultStakeholder issuer cap_table let securityId = "WSEC_BAL_ZERO_REMAINDER" - _ <- submit issuer do + submitMustFail issuer do exerciseCmd capTableReady CT.UpdateCapTable with creates = [ CT.OcfCreateWarrantIssuance (warrantIssuance "TX_W_BAL_ZERO_REMAINDER" securityId defaultStakeholderId 100.0) @@ -308,8 +316,56 @@ testWarrantSecurityBalance_BalanceSecurityReductionCanConsumeRemainingSource = s ] edits = [] deletes = [] + +testWarrantSecurityBalance_ExactBalanceSuccessorSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let securityId = "WSEC_BAL_EXACT_SUCCESSOR" + let balanceSecurityId = "WSEC_BAL_EXACT_REMAINDER" + let balanceIssuance = (warrantIssuance "TX_W_BAL_EXACT_REMAINDER" balanceSecurityId defaultStakeholderId 40.0) with date = jan02 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance (warrantIssuance "TX_W_BAL_EXACT_SOURCE" securityId defaultStakeholderId 100.0) + , CT.OcfCreateWarrantIssuance balanceIssuance + , CT.OcfCreateWarrantCancellation ((warrantCancellation "TX_W_BAL_EXACT_CANCEL" securityId 60.0 jan02) with balance_security_id = Some balanceSecurityId) + ] + edits = [] + deletes = [] pure () +testWarrantSecurityBalance_WrongBalanceSuccessorQuantityFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let securityId = "WSEC_BAL_WRONG_SUCCESSOR" + let balanceSecurityId = "WSEC_BAL_WRONG_REMAINDER" + let wrongBalance = (warrantIssuance "TX_W_BAL_WRONG_REMAINDER" balanceSecurityId defaultStakeholderId 39.0) with date = jan02 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance (warrantIssuance "TX_W_BAL_WRONG_SOURCE" securityId defaultStakeholderId 100.0) + , CT.OcfCreateWarrantIssuance wrongBalance + , CT.OcfCreateWarrantCancellation ((warrantCancellation "TX_W_BAL_WRONG_CANCEL" securityId 60.0 jan02) with balance_security_id = Some balanceSecurityId) + ] + edits = [] + deletes = [] + +testWarrantSecurityBalance_MissingBalanceSuccessorFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let securityId = "WSEC_BAL_MISSING_SUCCESSOR" + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance (warrantIssuance "TX_W_BAL_MISSING_SOURCE" securityId defaultStakeholderId 100.0) + , CT.OcfCreateWarrantCancellation ((warrantCancellation "TX_W_BAL_MISSING_CANCEL" securityId 60.0 jan02) with balance_security_id = Some "WSEC_BAL_MISSING_REMAINDER") + ] + edits = [] + deletes = [] + testWarrantSecurityBalance_UnknownQuantityExerciseBeforeIssuanceFails = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table @@ -407,11 +463,15 @@ testEquityCompensationSecurityBalance_TransferWithBalanceSecurityRetiresSource = TestOcp{issuer, cap_table} <- setupTestOcp capTableReady <- addDefaultStakeholder issuer cap_table let securityId = "ESEC_BAL_TRANSFER_BALANCE_SECURITY" + let resultIssuance = (equityCompensationIssuance "TX_E_TRANSFER_BALANCE_RESULT" "TX_E_TRANSFER_BALANCE_SECURITY_RESULT" defaultStakeholderId 60.0) with date = jan02 + let balanceIssuance = (equityCompensationIssuance "TX_E_TRANSFER_BALANCE_REMAINDER" "ESEC_BALANCE_TRANSFER" defaultStakeholderId 40.0) with date = jan02 submitMustFail issuer do exerciseCmd capTableReady CT.UpdateCapTable with creates = [ CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_E_BAL_TRANSFER_BALANCE_SECURITY" securityId defaultStakeholderId 100.0) + , CT.OcfCreateEquityCompensationIssuance resultIssuance + , CT.OcfCreateEquityCompensationIssuance balanceIssuance , CT.OcfCreateEquityCompensationTransfer ((equityCompensationTransfer "TX_E_TRANSFER_BALANCE_SECURITY" securityId 60.0 jan02) with balance_security_id = Some "ESEC_BALANCE_TRANSFER") , CT.OcfCreateEquityCompensationCancellation (equityCompensationCancellation "TX_E_CANCEL_AFTER_BALANCE_TRANSFER" securityId 1.0 jan03) ] @@ -422,23 +482,27 @@ testEquityCompensationSecurityBalance_CancellationWithBalanceSecurityRetiresSour TestOcp{issuer, cap_table} <- setupTestOcp capTableReady <- addDefaultStakeholder issuer cap_table let securityId = "ESEC_BAL_CANCELLATION_BALANCE_SECURITY" + let balanceIssuance = (equityCompensationIssuance "TX_E_CANCEL_BALANCE_REMAINDER" "ESEC_BALANCE_CANCELLATION" defaultStakeholderId 40.0) with date = jan02 + let laterResult = (equityCompensationIssuance "TX_E_TRANSFER_AFTER_CANCEL_RESULT" "TX_E_TRANSFER_AFTER_BALANCE_CANCELLATION_RESULT" defaultStakeholderId 1.0) with date = jan03 submitMustFail issuer do exerciseCmd capTableReady CT.UpdateCapTable with creates = [ CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_E_BAL_CANCELLATION_BALANCE_SECURITY" securityId defaultStakeholderId 100.0) + , CT.OcfCreateEquityCompensationIssuance balanceIssuance + , CT.OcfCreateEquityCompensationIssuance laterResult , CT.OcfCreateEquityCompensationCancellation ((equityCompensationCancellation "TX_E_CANCEL_BALANCE_SECURITY" securityId 60.0 jan02) with balance_security_id = Some "ESEC_BALANCE_CANCELLATION") , CT.OcfCreateEquityCompensationTransfer (equityCompensationTransfer "TX_E_TRANSFER_AFTER_BALANCE_CANCELLATION" securityId 1.0 jan03) ] edits = [] deletes = [] -testEquityCompensationSecurityBalance_BalanceSecurityReductionCanConsumeRemainingSource = script do +testEquityCompensationSecurityBalance_BalanceSecurityRequiresPositiveRemainder = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableReady <- addDefaultStakeholder issuer cap_table let securityId = "ESEC_BAL_ZERO_REMAINDER" - _ <- submit issuer do + submitMustFail issuer do exerciseCmd capTableReady CT.UpdateCapTable with creates = [ CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_E_BAL_ZERO_REMAINDER" securityId defaultStakeholderId 100.0) @@ -447,8 +511,93 @@ testEquityCompensationSecurityBalance_BalanceSecurityReductionCanConsumeRemainin ] edits = [] deletes = [] + +testEquityCompensationSecurityBalance_ExactBalanceSuccessorSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let securityId = "ESEC_BAL_EXACT_SUCCESSOR" + let balanceSecurityId = "ESEC_BAL_EXACT_REMAINDER" + let balanceIssuance = (equityCompensationIssuance "TX_E_BAL_EXACT_REMAINDER" balanceSecurityId defaultStakeholderId 40.0) with date = jan02 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_E_BAL_EXACT_SOURCE" securityId defaultStakeholderId 100.0) + , CT.OcfCreateEquityCompensationIssuance balanceIssuance + , CT.OcfCreateEquityCompensationCancellation ((equityCompensationCancellation "TX_E_BAL_EXACT_CANCEL" securityId 60.0 jan02) with balance_security_id = Some balanceSecurityId) + ] + edits = [] + deletes = [] pure () +testEquityCompensationSecurityBalance_WrongBalanceSuccessorQuantityFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let securityId = "ESEC_BAL_WRONG_SUCCESSOR" + let balanceSecurityId = "ESEC_BAL_WRONG_REMAINDER" + let wrongBalance = (equityCompensationIssuance "TX_E_BAL_WRONG_REMAINDER" balanceSecurityId defaultStakeholderId 39.0) with date = jan02 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_E_BAL_WRONG_SOURCE" securityId defaultStakeholderId 100.0) + , CT.OcfCreateEquityCompensationIssuance wrongBalance + , CT.OcfCreateEquityCompensationCancellation ((equityCompensationCancellation "TX_E_BAL_WRONG_CANCEL" securityId 60.0 jan02) with balance_security_id = Some balanceSecurityId) + ] + edits = [] + deletes = [] + +testEquityCompensationSecurityBalance_MissingBalanceSuccessorFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let securityId = "ESEC_BAL_MISSING_SUCCESSOR" + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_E_BAL_MISSING_SOURCE" securityId defaultStakeholderId 100.0) + , CT.OcfCreateEquityCompensationCancellation ((equityCompensationCancellation "TX_E_BAL_MISSING_CANCEL" securityId 60.0 jan02) with balance_security_id = Some "ESEC_BAL_MISSING_REMAINDER") + ] + edits = [] + deletes = [] + +testEquityCompensationSecurityBalance_StockPlanTransferRetiresSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_TRANSFER_BALANCE" + let sourceSecurityId = "ESEC_PLAN_TRANSFER_SOURCE" + let successorSecurityId = "ESEC_PLAN_TRANSFER_SUCCESSOR" + let stockPlan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (equityCompensationIssuance "TX_E_PLAN_TRANSFER_SOURCE" sourceSecurityId stakeholderId 100.0) with + stock_class_id = Some stockClassId + stock_plan_id = Some stockPlanId + + sourceResult <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan stockPlan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let transfer = (equityCompensationTransfer "TX_E_PLAN_TRANSFER" sourceSecurityId 100.0 jan02) with + resulting_security_ids = [successorSecurityId] + let successorIssuance = (equityCompensationIssuance "TX_E_PLAN_TRANSFER_SUCCESSOR" successorSecurityId stakeholderId 100.0) with + date = jan02 + stock_class_id = Some stockClassId + let sourceExercise = (equityCompensationExercise "TX_E_PLAN_TRANSFER_SOURCE_EXERCISE" sourceSecurityId 1.0 jan03) with resulting_security_ids = [] + + submitMustFail issuer do + exerciseCmd sourceResult.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer transfer + , CT.OcfCreateEquityCompensationIssuance successorIssuance + , CT.OcfCreateEquityCompensationExercise sourceExercise + ] + edits = [] + deletes = [] + testEquityCompensationSecurityBalance_SplitAdjustedExerciseSucceeds = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/Test/daml/OpenCapTable/TestWarrantCancellation.daml b/Test/daml/OpenCapTable/TestWarrantCancellation.daml index 1e79b783..c9702d36 100644 --- a/Test/daml/OpenCapTable/TestWarrantCancellation.daml +++ b/Test/daml/OpenCapTable/TestWarrantCancellation.daml @@ -12,12 +12,12 @@ import qualified DA.Time as DT import Daml.Script -- Create a warrant issuance to cancel (follows real workflow) -createWarrantIssuance issuer capTableCid securityId quantity = +createWarrantIssuance issuer capTableCid securityId quantity issuanceDate = submit issuer do exerciseCmd capTableCid CT.UpdateCapTable with creates = [CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with - id = "TX_WARRANT_ISS_1" - date = DT.time (DA.date 2023 Jun 01) 0 0 0 + id = "TX_WARRANT_ISS_" <> securityId + date = issuanceDate security_id = securityId custom_id = "W-1" stakeholder_id = "SH-1" @@ -57,7 +57,7 @@ testWarrantCancellation_CreateAndArchiveByIssuer = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "WSEC-001" - result <- createWarrantIssuance issuer capTableCid securityId 50000.0 + result <- createWarrantIssuance issuer capTableCid securityId 50000.0 (DT.time (DA.date 2023 Jun 01) 0 0 0) _ <- createWarrantCancellation issuer result.updatedCapTableCid securityId 50000.0 pure () @@ -66,7 +66,7 @@ testWarrantCancellation_CreateAndArchiveBuiltIn = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "WSEC-002" - result <- createWarrantIssuance issuer capTableCid securityId 100000.0 + result <- createWarrantIssuance issuer capTableCid securityId 100000.0 (DT.time (DA.date 2023 Jun 01) 0 0 0) _ <- createWarrantCancellation issuer result.updatedCapTableCid securityId 100000.0 pure () @@ -75,12 +75,14 @@ testWarrantCancellation_PartialWithOptionals = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "WSEC-PARTIAL" - result <- createWarrantIssuance issuer capTableCid securityId 75000.0 + result <- createWarrantIssuance issuer capTableCid securityId 75000.0 (DT.time (DA.date 2023 Jun 01) 0 0 0) + let cancellationDate = DT.time (DA.date 2024 Jun 30) 0 0 0 + balanceResult <- createWarrantIssuance issuer result.updatedCapTableCid "WSEC-PARTIAL-BAL" 50000.0 cancellationDate _ <- submit issuer do - exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + exerciseCmd balanceResult.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateWarrantCancellation WarrantCancellationOcfData with id = "TX_WARRANT_CANCEL_PARTIAL" - date = DT.time (DA.date 2024 Jun 30) 0 0 0 + date = cancellationDate security_id = securityId quantity = 25000.0 reason_text = "Partial cancellation per warrant amendment agreement" diff --git a/Test/daml/OpenCapTable/TestWarrantTransfer.daml b/Test/daml/OpenCapTable/TestWarrantTransfer.daml index 579a669d..1a4414a1 100644 --- a/Test/daml/OpenCapTable/TestWarrantTransfer.daml +++ b/Test/daml/OpenCapTable/TestWarrantTransfer.daml @@ -13,12 +13,12 @@ import qualified DA.Time as DT import Daml.Script -- Create a warrant issuance to transfer (follows real workflow) -createWarrantIssuance issuer capTableCid securityId quantity = +createWarrantIssuance issuer capTableCid securityId quantity issuanceDate = submit issuer do exerciseCmd capTableCid CT.UpdateCapTable with creates = [CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with - id = "TX_WARRANT_ISS_1" - date = DT.time (DA.date 2023 Jun 01) 0 0 0 + id = "TX_WARRANT_ISS_" <> securityId + date = issuanceDate security_id = securityId custom_id = "W-1" stakeholder_id = "SH-1" @@ -45,7 +45,7 @@ testWarrantTransfer_SecondarySale = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "WSEC-001" - result <- createWarrantIssuance issuer capTableCid securityId 10000.0 + result <- createWarrantIssuance issuer capTableCid securityId 10000.0 (DT.time (DA.date 2023 Jun 01) 0 0 0) _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateWarrantTransfer WarrantTransferOcfData with @@ -66,12 +66,14 @@ testWarrantTransfer_PartialTransfer = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "WSEC-PARTIAL" - result <- createWarrantIssuance issuer capTableCid securityId 50000.0 + result <- createWarrantIssuance issuer capTableCid securityId 50000.0 (DT.time (DA.date 2023 Jun 01) 0 0 0) + let transferDate = DT.time (DA.date 2025 Jun 01) 0 0 0 + balanceResult <- createWarrantIssuance issuer result.updatedCapTableCid "WSEC-PARTIAL-REMAIN" 30000.0 transferDate _ <- submit issuer do - exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + exerciseCmd balanceResult.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateWarrantTransfer WarrantTransferOcfData with id = "TX_WARRANT_TRANSFER_PARTIAL" - date = DT.time (DA.date 2025 Jun 01) 0 0 0 + date = transferDate quantity = 20000.0 security_id = securityId resulting_security_ids = ["WSEC-NEW-HOLDER"] @@ -87,7 +89,7 @@ testWarrantTransfer_FullTransferNoConsideration = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "WSEC-FULL" - result <- createWarrantIssuance issuer capTableCid securityId 5000.0 + result <- createWarrantIssuance issuer capTableCid securityId 5000.0 (DT.time (DA.date 2023 Jun 01) 0 0 0) _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateWarrantTransfer WarrantTransferOcfData with diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index 1142a3b8..f051d7bd 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf38dc6929dc6daac7a7315d7fc421b3c7016b89253c0e9f23c2e3acd0a7855c -size 2939204 +oid sha256:0942fc30f005997c598611521bce8164cce650a5121bb889f594ab97d6d7cc70 +size 2961241 diff --git a/dars/dars.lock b/dars/dars.lock index 759c59e2..ab274621 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,8 +26,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "bf38dc6929dc6daac7a7315d7fc421b3c7016b89253c0e9f23c2e3acd0a7855c", - "size": 2939204, + "sha256": "0942fc30f005997c598611521bce8164cce650a5121bb889f594ab97d6d7cc70", + "size": 2961241, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-10T11:52:45.000Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 05835334..c21ceebe 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -1522,6 +1522,20 @@ validateSecurityQuantityBalance securityId initialQuantity events = do (sortOn securityQuantityEventSortKey events) pure () +securityQuantityBeforeEvent : + Text -> + Optional Decimal -> + [SecurityQuantityEvent] -> + (Time, Int, Int) -> + Update (Optional Decimal) +securityQuantityBeforeEvent securityId initialQuantity events targetSortKey = + foldlA + (validateSecurityQuantityBalanceEvent securityId) + initialQuantity + (filter + (\event -> securityQuantityEventSortKey event < targetSortKey) + (sortOn securityQuantityEventSortKey events)) + addSecurityQuantityEvent : Text -> SecurityQuantityEvent -> Map Text [SecurityQuantityEvent] -> Map Text [SecurityQuantityEvent] addSecurityQuantityEvent key event index = Map.insert key (event :: fromOptional [] (Map.lookup key index)) index @@ -1641,6 +1655,27 @@ warrantSecurityQuantityEvents : NonStockSecurityBalanceEventIndex -> WarrantIssu warrantSecurityQuantityEvents eventIndex issuanceData = fromOptional [] (Map.lookup issuanceData.security_id eventIndex.warrant_events_by_security_id) +warrantSecurityQuantityBeforeTerminal : + NonStockSecurityBalanceEventIndex -> + WarrantIssuanceOcfData -> + Time -> + Update (Optional Decimal) +warrantSecurityQuantityBeforeTerminal eventIndex issuanceData eventDate = + let events = warrantSecurityQuantityEvents eventIndex issuanceData + targetSortKey = (eventDate, 2, 1) + in case issuanceData.quantity of + Some quantity -> + securityQuantityBeforeEvent + issuanceData.security_id + (Some 0.0) + ((SecurityQuantityDelta with + event_date = issuanceData.date + event_priority = 0 + quantity_delta = quantity) + :: events) + targetSortKey + None -> securityQuantityBeforeEvent issuanceData.security_id None events targetSortKey + validateWarrantSecurityBalance : NonStockSecurityBalanceEventIndex -> WarrantIssuanceOcfData -> Update () validateWarrantSecurityBalance eventIndex issuanceData = do let events = warrantSecurityQuantityEvents eventIndex issuanceData @@ -1689,6 +1724,29 @@ equityCompensationSecurityQuantityEvents stockEventIndex balanceEventIndex issua None -> [] in securityEvents ++ splitEvents +equityCompensationSecurityQuantityBeforeTerminal : + StockClassShareEventIndex -> + NonStockSecurityBalanceEventIndex -> + EquityCompensationIssuanceOcfData -> + Time -> + Update Decimal +equityCompensationSecurityQuantityBeforeTerminal stockEventIndex balanceEventIndex issuanceData eventDate = do + let events = equityCompensationSecurityQuantityEvents stockEventIndex balanceEventIndex issuanceData + quantityOpt <- securityQuantityBeforeEvent + issuanceData.security_id + (Some 0.0) + ((SecurityQuantityDelta with + event_date = issuanceData.date + event_priority = 0 + quantity_delta = issuanceData.quantity) + :: events) + (eventDate, 2, 1) + case quantityOpt of + Some quantity -> pure quantity + None -> do + assertMsg ("Equity compensation quantity unexpectedly became unknown: " <> issuanceData.security_id) False + pure 0.0 + validateEquityCompensationSecurityBalance : StockClassShareEventIndex -> NonStockSecurityBalanceEventIndex -> @@ -1714,10 +1772,144 @@ validateEquityCompensationSecurityBalances stockEventIndex balanceEventIndex map mapA_ (\issuanceCid -> do issuance <- fetch issuanceCid - case issuance.issuance_data.stock_plan_id of - Some _ -> pure () - None -> validateEquityCompensationSecurityBalance stockEventIndex balanceEventIndex issuance.issuance_data) + validateEquityCompensationSecurityBalance stockEventIndex balanceEventIndex issuance.issuance_data) (Map.values maps.equity_compensation_issuances) + +validateWarrantBalanceSuccessorQuantity : + CapTableMaps -> + Text -> + Time -> + Decimal -> + Text -> + Update () +validateWarrantBalanceSuccessorQuantity maps label eventDate expectedQuantity securityId = + case Map.lookup securityId maps.warrant_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertMsg + (label <> " balance result date mismatch for " <> securityId <> ": expected " <> show eventDate <> ", found " <> show issuance.issuance_data.date) + (issuance.issuance_data.date == eventDate) + case issuance.issuance_data.quantity of + Some actualQuantity -> + assertMsg + (label <> " balance quantity mismatch for " <> securityId <> ": expected " <> show expectedQuantity <> ", found " <> show actualQuantity) + (actualQuantity == expectedQuantity) + None -> assertMsg (label <> " balance warrant quantity is missing for " <> securityId) False + None -> assertMsg (label <> " balance warrant issuance not found: " <> securityId) False + +validateWarrantTerminalBalanceSuccessor : + NonStockSecurityBalanceEventIndex -> + CapTableMaps -> + Text -> + Text -> + Time -> + Decimal -> + Optional Text -> + Update () +validateWarrantTerminalBalanceSuccessor eventIndex maps label sourceSecurityId eventDate reductionQuantity balanceSecurityId = + case balanceSecurityId of + Some securityId -> case Map.lookup sourceSecurityId maps.warrant_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + currentQuantityOpt <- warrantSecurityQuantityBeforeTerminal eventIndex sourceIssuance.issuance_data eventDate + case currentQuantityOpt of + Some currentQuantity -> do + let expectedQuantity = currentQuantity - reductionQuantity + assertMsg (label <> " balance successor requires a positive remainder") (expectedQuantity > 0.0) + validateWarrantBalanceSuccessorQuantity maps label eventDate expectedQuantity securityId + None -> assertMsg (label <> " balance successor cannot be validated because the source warrant quantity is unknown") False + None -> assertMsg (label <> " source warrant issuance not found: " <> sourceSecurityId) False + None -> pure () + +validateEquityCompensationBalanceSuccessorQuantity : + CapTableMaps -> + Text -> + EquityCompensationIssuanceOcfData -> + Time -> + Decimal -> + Text -> + Update () +validateEquityCompensationBalanceSuccessorQuantity maps label sourceIssuanceData eventDate expectedQuantity securityId = + case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertMsg + (label <> " balance result date mismatch for " <> securityId <> ": expected " <> show eventDate <> ", found " <> show issuance.issuance_data.date) + (issuance.issuance_data.date == eventDate) + assertMsg + (label <> " balance stock class mismatch for " <> securityId) + (issuance.issuance_data.stock_class_id == sourceIssuanceData.stock_class_id) + let targetSortKey = (eventDate, 2, 1) + actualQuantity <- case issuance.issuance_data.stock_class_id of + Some stockClassId -> stockClassQuantityAtSortKey maps stockClassId issuance.issuance_data.date issuance.issuance_data.quantity targetSortKey + None -> pure issuance.issuance_data.quantity + assertMsg + (label <> " balance quantity mismatch for " <> securityId <> ": expected " <> show expectedQuantity <> ", found " <> show actualQuantity) + (actualQuantity == expectedQuantity) + None -> assertMsg (label <> " balance equity compensation issuance not found: " <> securityId) False + +validateEquityCompensationTerminalBalanceSuccessor : + StockClassShareEventIndex -> + NonStockSecurityBalanceEventIndex -> + CapTableMaps -> + Text -> + Text -> + Time -> + Decimal -> + Optional Text -> + Update () +validateEquityCompensationTerminalBalanceSuccessor stockEventIndex balanceEventIndex maps label sourceSecurityId eventDate reductionQuantity balanceSecurityId = + case balanceSecurityId of + Some securityId -> case Map.lookup sourceSecurityId maps.equity_compensation_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + currentQuantity <- equityCompensationSecurityQuantityBeforeTerminal + stockEventIndex balanceEventIndex sourceIssuance.issuance_data eventDate + let expectedQuantity = currentQuantity - reductionQuantity + assertMsg (label <> " balance successor requires a positive remainder") (expectedQuantity > 0.0) + validateEquityCompensationBalanceSuccessorQuantity + maps label sourceIssuance.issuance_data eventDate expectedQuantity securityId + None -> assertMsg (label <> " source equity compensation issuance not found: " <> sourceSecurityId) False + None -> pure () + +validateNonStockBalanceSuccessorQuantities : + StockClassShareEventIndex -> + NonStockSecurityBalanceEventIndex -> + CapTableMaps -> + Update () +validateNonStockBalanceSuccessorQuantities stockEventIndex balanceEventIndex maps = do + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + validateWarrantTerminalBalanceSuccessor + balanceEventIndex maps ("Warrant transfer " <> transfer.transfer_data.id) + transfer.transfer_data.security_id transfer.transfer_data.date transfer.transfer_data.quantity + transfer.transfer_data.balance_security_id) + (Map.values maps.warrant_transfers) + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + validateWarrantTerminalBalanceSuccessor + balanceEventIndex maps ("Warrant cancellation " <> cancellation.cancellation_data.id) + cancellation.cancellation_data.security_id cancellation.cancellation_data.date cancellation.cancellation_data.quantity + cancellation.cancellation_data.balance_security_id) + (Map.values maps.warrant_cancellations) + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + validateEquityCompensationTerminalBalanceSuccessor + stockEventIndex balanceEventIndex maps ("Equity compensation transfer " <> transfer.transfer_data.id) + transfer.transfer_data.security_id transfer.transfer_data.date transfer.transfer_data.quantity + transfer.transfer_data.balance_security_id) + (Map.values maps.equity_compensation_transfers) + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + validateEquityCompensationTerminalBalanceSuccessor + stockEventIndex balanceEventIndex maps ("Equity compensation cancellation " <> cancellation.cancellation_data.id) + cancellation.cancellation_data.security_id cancellation.cancellation_data.date cancellation.cancellation_data.quantity + cancellation.cancellation_data.balance_security_id) + (Map.values maps.equity_compensation_cancellations) stockClassShareLotCurrentQuantity : StockClassShareEventIndex -> StockClassShareLot -> Optional Decimal stockClassShareLotCurrentQuantity eventIndex lot = foldl @@ -3247,6 +3439,18 @@ equityCompensationSecurityEvents maps issuanceData = do [] (Map.values maps.equity_compensation_cancellations) + transferEvents <- foldlA + (\events transferCid -> do + transfer <- fetch transferCid + if transfer.transfer_data.security_id == issuanceData.security_id + then pure (stockSecurityReductionEvents + transfer.transfer_data.date + transfer.transfer_data.quantity + transfer.transfer_data.balance_security_id ++ events) + else pure events) + cancellationEvents + (Map.values maps.equity_compensation_transfers) + exerciseEvents <- foldlA (\events exerciseCid -> do exercise <- fetch exerciseCid @@ -3256,7 +3460,7 @@ equityCompensationSecurityEvents maps issuanceData = do event_priority = 1 quantity_delta = 0.0 - exercise.exercise_data.quantity) :: events) else pure events) - cancellationEvents + transferEvents (Map.values maps.equity_compensation_exercises) releaseEvents <- foldlA @@ -4031,6 +4235,7 @@ template CapTable validateStockReissuanceSplitReferences finalMaps validateConfiguredReferences finalMaps validateStockBalanceSuccessorQuantities stockEventIndex finalMaps + validateNonStockBalanceSuccessorQuantities stockEventIndex nonStockBalanceEventIndex finalMaps validateStockSecurityBalances stockEventIndex finalMaps validateWarrantSecurityBalances nonStockBalanceEventIndex finalMaps validateEquityCompensationSecurityBalances stockEventIndex nonStockBalanceEventIndex finalMaps From 6a5b0d427d7eece9c2d6bc313657f7f18fb32c36 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 20:50:10 -0400 Subject: [PATCH 25/38] fix: split-adjust classless plan award balances --- .../TestNonStockSecurityBalances.daml | 23 +++++++++++++++ .../0.0.12/OpenCapTable-v34.dar | 4 +-- dars/dars.lock | 4 +-- .../codegen/templates/CapTable.daml.template | 28 +++++++++++-------- 4 files changed, 43 insertions(+), 16 deletions(-) diff --git a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml index 596d828e..06c25893 100644 --- a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml @@ -636,3 +636,26 @@ testEquityCompensationSecurityBalance_SplitAdjustedExerciseSucceeds = script do edits = [] deletes = [] pure () + +testEquityCompensationSecurityBalance_ClasslessSingleClassPlanSplitAdjustedExerciseSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_E_BAL_CLASSLESS_SPLIT" + let securityId = "ESEC_BAL_CLASSLESS_SPLIT" + let stockPlan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 200.0 + let issuance = (equityCompensationIssuance "TX_E_BAL_CLASSLESS_SPLIT" securityId stakeholderId 100.0) with + stock_plan_id = Some stockPlanId + let split = (stockClassSplit "TX_E_BAL_CLASSLESS_SPLIT" stockClassId 2.0 1.0) with date = jan02 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan stockPlan + , CT.OcfCreateEquityCompensationIssuance issuance + , CT.OcfCreateStockClassSplit split + , CT.OcfCreateEquityCompensationExercise + (equityCompensationExercise "TX_E_CLASSLESS_EXERCISE_AFTER_SPLIT" securityId 200.0 jan03) + ] + edits = [] + deletes = [] + pure () diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index 9d511603..aed6af1d 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:058c07790d64598f66b01f5dee2334833c16b88bfd125cb02dac61181e9b111f -size 3103026 +oid sha256:bb5600cf47390673537c2afa1ab2f3c8a1ef78952b0e689188f2c8a1ded4fa70 +size 3103741 diff --git a/dars/dars.lock b/dars/dars.lock index fdc2f7d1..2e30b545 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,8 +26,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "058c07790d64598f66b01f5dee2334833c16b88bfd125cb02dac61181e9b111f", - "size": 3103026, + "sha256": "bb5600cf47390673537c2afa1ab2f3c8a1ef78952b0e689188f2c8a1ded4fa70", + "size": 3103741, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-10T11:52:45.000Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 7ea88026..e6ce8967 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -2081,23 +2081,26 @@ stockClassSplitSecurityQuantityEvents eventIndex stockClassId earliest = equityCompensationSecurityQuantityEvents : StockClassShareEventIndex -> NonStockSecurityBalanceEventIndex -> + CapTableMaps -> EquityCompensationIssuanceOcfData -> - [SecurityQuantityEvent] -equityCompensationSecurityQuantityEvents stockEventIndex balanceEventIndex issuanceData = + Update [SecurityQuantityEvent] +equityCompensationSecurityQuantityEvents stockEventIndex balanceEventIndex maps issuanceData = do + stockClassIds <- equityCompensationPlanStockClassIds maps issuanceData let securityEvents = fromOptional [] (Map.lookup issuanceData.security_id balanceEventIndex.equity_compensation_events_by_security_id) - splitEvents = case issuanceData.stock_class_id of - Some stockClassId -> stockClassSplitSecurityQuantityEvents stockEventIndex stockClassId issuanceData.date - None -> [] - in securityEvents ++ splitEvents + splitEvents = case stockClassIds of + [stockClassId] -> stockClassSplitSecurityQuantityEvents stockEventIndex stockClassId issuanceData.date + _ -> [] + pure (securityEvents ++ splitEvents) equityCompensationSecurityQuantityBeforeTerminal : StockClassShareEventIndex -> NonStockSecurityBalanceEventIndex -> + CapTableMaps -> EquityCompensationIssuanceOcfData -> Time -> Update Decimal -equityCompensationSecurityQuantityBeforeTerminal stockEventIndex balanceEventIndex issuanceData eventDate = do - let events = equityCompensationSecurityQuantityEvents stockEventIndex balanceEventIndex issuanceData +equityCompensationSecurityQuantityBeforeTerminal stockEventIndex balanceEventIndex maps issuanceData eventDate = do + events <- equityCompensationSecurityQuantityEvents stockEventIndex balanceEventIndex maps issuanceData quantityOpt <- securityQuantityBeforeEvent issuanceData.security_id (Some 0.0) @@ -2116,10 +2119,11 @@ equityCompensationSecurityQuantityBeforeTerminal stockEventIndex balanceEventInd validateEquityCompensationSecurityBalance : StockClassShareEventIndex -> NonStockSecurityBalanceEventIndex -> + CapTableMaps -> EquityCompensationIssuanceOcfData -> Update () -validateEquityCompensationSecurityBalance stockEventIndex balanceEventIndex issuanceData = do - let events = equityCompensationSecurityQuantityEvents stockEventIndex balanceEventIndex issuanceData +validateEquityCompensationSecurityBalance stockEventIndex balanceEventIndex maps issuanceData = do + events <- equityCompensationSecurityQuantityEvents stockEventIndex balanceEventIndex maps issuanceData validateSecurityQuantityBalance issuanceData.security_id (Some 0.0) @@ -2138,7 +2142,7 @@ validateEquityCompensationSecurityBalances stockEventIndex balanceEventIndex map mapA_ (\issuanceCid -> do issuance <- fetch issuanceCid - validateEquityCompensationSecurityBalance stockEventIndex balanceEventIndex issuance.issuance_data) + validateEquityCompensationSecurityBalance stockEventIndex balanceEventIndex maps issuance.issuance_data) (Map.values maps.equity_compensation_issuances) validateWarrantBalanceSuccessorQuantity : @@ -2231,7 +2235,7 @@ validateEquityCompensationTerminalBalanceSuccessor stockEventIndex balanceEventI Some sourceIssuanceCid -> do sourceIssuance <- fetch sourceIssuanceCid currentQuantity <- equityCompensationSecurityQuantityBeforeTerminal - stockEventIndex balanceEventIndex sourceIssuance.issuance_data eventDate + stockEventIndex balanceEventIndex maps sourceIssuance.issuance_data eventDate let expectedQuantity = currentQuantity - reductionQuantity assertMsg (label <> " balance successor requires a positive remainder") (expectedQuantity > 0.0) validateEquityCompensationSecurityBalanceSuccessorQuantity From ef04b92d48b4e435dd2aa70f9f7034f5f808ae05 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 21:22:23 -0400 Subject: [PATCH 26/38] chore: refresh deterministic security balance DAR --- dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index aed6af1d..0b3dbac5 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb5600cf47390673537c2afa1ab2f3c8a1ef78952b0e689188f2c8a1ded4fa70 -size 3103741 +oid sha256:2ca7e2b1088c2873bb6c8037bdec8f5da2281cd1256c52209bd3ba70e68206bc +size 3114838 diff --git a/dars/dars.lock b/dars/dars.lock index a089522b..151a9dae 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,8 +26,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "bb5600cf47390673537c2afa1ab2f3c8a1ef78952b0e689188f2c8a1ded4fa70", - "size": 3103741, + "sha256": "2ca7e2b1088c2873bb6c8037bdec8f5da2281cd1256c52209bd3ba70e68206bc", + "size": 3114838, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-10T11:52:45.000Z", "networks": [] From 312eb3cbcba383a0393e6634e1b24bc507085ccb Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 22:21:21 -0400 Subject: [PATCH 27/38] chore: refresh deterministic security balance DAR --- dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index 0b3dbac5..5340a616 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ca7e2b1088c2873bb6c8037bdec8f5da2281cd1256c52209bd3ba70e68206bc -size 3114838 +oid sha256:31d7c34d75c1920aa8d60e3567c1e0addf28fa3f7f4638637686daeb35838a9e +size 3123591 diff --git a/dars/dars.lock b/dars/dars.lock index 12df109d..0337fb1c 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,10 +26,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "2ca7e2b1088c2873bb6c8037bdec8f5da2281cd1256c52209bd3ba70e68206bc", - "size": 3114838, + "sha256": "31d7c34d75c1920aa8d60e3567c1e0addf28fa3f7f4638637686daeb35838a9e", + "size": 3123591, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-10T11:52:45.000Z", + "uploadedAt": "2026-07-11T02:18:14Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { From 2e032c332ec341c91f05dd69b2fc18d7f22e8532 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 22:35:20 -0400 Subject: [PATCH 28/38] fix: preserve warrant transfer successor continuity --- .../TestNonStockSecurityBalances.daml | 112 ++++++++++++++++++ .../OpenCapTable/TestWarrantTransfer.daml | 30 ++++- .../codegen/templates/CapTable.daml.template | 110 ++++++++++++++--- 3 files changed, 230 insertions(+), 22 deletions(-) diff --git a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml index 06c25893..f750b2ab 100644 --- a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml @@ -374,6 +374,118 @@ testWarrantSecurityBalance_MissingBalanceSuccessorFails = script do edits = [] deletes = [] +testWarrantSecurityBalance_TransferResultsPreserveQuantityDateAndTerms = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourceSecurityId = "WSEC_TRANSFER_CONTINUITY_SOURCE" + let transferId = "TX_W_TRANSFER_CONTINUITY" + let resultSecurityId = transferId <> "_RESULT" + let sourceTrigger = warrantFixedExerciseTrigger "TRANSFER_TRIGGER" stockClassId 25.0 + let sourceIssuance = (warrantIssuance "TX_W_TRANSFER_CONTINUITY_SOURCE" sourceSecurityId stakeholderId 100.0) with + exercise_triggers = [sourceTrigger] + let transfer = warrantTransfer transferId sourceSecurityId 40.0 jan02 + let validResult = (warrantIssuance "TX_W_TRANSFER_CONTINUITY_RESULT" resultSecurityId stakeholderId 40.0) with + date = jan02 + exercise_triggers = [sourceTrigger] + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance sourceIssuance + , CT.OcfCreateWarrantIssuance (validResult with quantity = Some 41.0) + , CT.OcfCreateWarrantTransfer transfer + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance sourceIssuance + , CT.OcfCreateWarrantIssuance (validResult with date = jan03) + , CT.OcfCreateWarrantTransfer transfer + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance sourceIssuance + , CT.OcfCreateWarrantIssuance (validResult with + exercise_triggers = [warrantFixedExerciseTrigger "TRANSFER_TRIGGER" stockClassId 100.0]) + , CT.OcfCreateWarrantTransfer transfer + ] + edits = [] + deletes = [] + +testWarrantSecurityBalance_UnknownQuantityTransferPreservesUnknownBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let sourceSecurityId = "WSEC_UNKNOWN_TRANSFER_SOURCE" + let transferId = "TX_W_UNKNOWN_TRANSFER" + let resultSecurityId = transferId <> "_RESULT" + let balanceSecurityId = "WSEC_UNKNOWN_TRANSFER_BALANCE" + let sourceIssuance = (warrantIssuance "TX_W_UNKNOWN_TRANSFER_SOURCE" sourceSecurityId defaultStakeholderId 100.0) with + quantity = None + quantity_source = None + let resultIssuance = (warrantIssuance "TX_W_UNKNOWN_TRANSFER_RESULT" resultSecurityId defaultStakeholderId 40.0) with date = jan02 + let unknownBalance = (warrantIssuance "TX_W_UNKNOWN_TRANSFER_BALANCE" balanceSecurityId defaultStakeholderId 100.0) with + date = jan02 + quantity = None + quantity_source = None + let transfer = (warrantTransfer transferId sourceSecurityId 40.0 jan02) with + balance_security_id = Some balanceSecurityId + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance sourceIssuance + , CT.OcfCreateWarrantIssuance resultIssuance + , CT.OcfCreateWarrantIssuance (unknownBalance with quantity = Some 60.0) + , CT.OcfCreateWarrantTransfer transfer + ] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance sourceIssuance + , CT.OcfCreateWarrantIssuance resultIssuance + , CT.OcfCreateWarrantIssuance unknownBalance + , CT.OcfCreateWarrantTransfer transfer + ] + edits = [] + deletes = [] + pure () + +testWarrantSecurityBalance_UnknownQuantityCancellationPreservesUnknownBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let sourceSecurityId = "WSEC_UNKNOWN_CANCEL_SOURCE" + let balanceSecurityId = "WSEC_UNKNOWN_CANCEL_BALANCE" + let sourceIssuance = (warrantIssuance "TX_W_UNKNOWN_CANCEL_SOURCE" sourceSecurityId defaultStakeholderId 100.0) with + quantity = None + quantity_source = None + let unknownBalance = (warrantIssuance "TX_W_UNKNOWN_CANCEL_BALANCE" balanceSecurityId defaultStakeholderId 100.0) with + date = jan02 + quantity = None + quantity_source = None + let cancellation = (warrantCancellation "TX_W_UNKNOWN_CANCEL" sourceSecurityId 40.0 jan02) with + balance_security_id = Some balanceSecurityId + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance sourceIssuance + , CT.OcfCreateWarrantIssuance unknownBalance + , CT.OcfCreateWarrantCancellation cancellation + ] + edits = [] + deletes = [] + pure () + testWarrantSecurityBalance_UnknownQuantityExerciseBeforeIssuanceFails = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/Test/daml/OpenCapTable/TestWarrantTransfer.daml b/Test/daml/OpenCapTable/TestWarrantTransfer.daml index dc24cfa9..ae2427db 100644 --- a/Test/daml/OpenCapTable/TestWarrantTransfer.daml +++ b/Test/daml/OpenCapTable/TestWarrantTransfer.daml @@ -6,7 +6,7 @@ import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import OpenCapTable.HelpersTriggers (ocfTriggerElectiveAtWill) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addDefaultStakeholder, warrantSuccessorIssuance) +import OpenCapTable.TestHelpers (addDefaultStakeholder) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -38,6 +38,28 @@ createWarrantIssuance issuer capTableCid securityId quantity issuanceDate = edits = [] deletes = [] +warrantTransferSuccessorIssuance : Text -> Text -> Text -> Time -> Decimal -> CT.OcfCreateData +warrantTransferSuccessorIssuance issuanceId securityId stakeholderId date quantity = + CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with + id = issuanceId + date = date + security_id = securityId + custom_id = "WR-" <> issuanceId + stakeholder_id = stakeholderId + board_approval_date = None + stockholder_approval_date = None + consideration_text = None + security_law_exemptions = [] + quantity = Some quantity + quantity_source = None + exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + purchase_price = OcfMonetary with amount = 0.0, currency = "USD" + exercise_triggers = [ocfTriggerElectiveAtWill] + warrant_expiration_date = None + vesting_terms_id = None + vestings = [] + comments = [] + -- Test: Warrant transfer per secondary sale agreement -- No prod/dev examples (warrants usually exercised, not transferred) -- Extrapolated from stock transfer pattern @@ -48,7 +70,7 @@ testWarrantTransfer_SecondarySale = script do result <- createWarrantIssuance issuer capTableCid securityId 10000.0 (DT.time (DA.date 2023 Jun 01) 0 0 0) _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [warrantSuccessorIssuance "WR-ISS-TRANSFERRED-001" "WSEC-TRANSFERRED-001" "SH-1" (DT.time (DA.date 2025 Mar 15) 0 0 0) 10000.0 + creates = [warrantTransferSuccessorIssuance "WR-ISS-TRANSFERRED-001" "WSEC-TRANSFERRED-001" "SH-1" (DT.time (DA.date 2025 Mar 15) 0 0 0) 10000.0 , CT.OcfCreateWarrantTransfer WarrantTransferOcfData with id = "TX_WARRANT_TRANSFER_1" date = DT.time (DA.date 2025 Mar 15) 0 0 0 @@ -72,7 +94,7 @@ testWarrantTransfer_PartialTransfer = script do balanceResult <- createWarrantIssuance issuer result.updatedCapTableCid "WSEC-PARTIAL-REMAIN" 30000.0 transferDate _ <- submit issuer do exerciseCmd balanceResult.updatedCapTableCid CT.UpdateCapTable with - creates = [warrantSuccessorIssuance "WR-ISS-NEW-HOLDER" "WSEC-NEW-HOLDER" "SH-1" transferDate 20000.0 + creates = [warrantTransferSuccessorIssuance "WR-ISS-NEW-HOLDER" "WSEC-NEW-HOLDER" "SH-1" transferDate 20000.0 , CT.OcfCreateWarrantTransfer WarrantTransferOcfData with id = "TX_WARRANT_TRANSFER_PARTIAL" date = transferDate @@ -94,7 +116,7 @@ testWarrantTransfer_FullTransferNoConsideration = script do result <- createWarrantIssuance issuer capTableCid securityId 5000.0 (DT.time (DA.date 2023 Jun 01) 0 0 0) _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [warrantSuccessorIssuance "WR-ISS-NEWOWNER" "WSEC-NEWOWNER" "SH-1" (DT.time (DA.date 2025 Jul 20) 0 0 0) 5000.0 + creates = [warrantTransferSuccessorIssuance "WR-ISS-NEWOWNER" "WSEC-NEWOWNER" "SH-1" (DT.time (DA.date 2025 Jul 20) 0 0 0) 5000.0 , CT.OcfCreateWarrantTransfer WarrantTransferOcfData with id = "TX_WARRANT_TRANSFER_FULL" date = DT.time (DA.date 2025 Jul 20) 0 0 0 diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 6108546f..82883b26 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -2217,27 +2217,92 @@ validateEquityCompensationSecurityBalances stockEventIndex balanceEventIndex map validateEquityCompensationSecurityBalance stockEventIndex balanceEventIndex maps issuance.issuance_data) (Map.values maps.equity_compensation_issuances) -validateWarrantBalanceSuccessorQuantity : +validateWarrantSuccessorTerms : + Text -> + WarrantIssuanceOcfData -> + WarrantIssuanceOcfData -> + Update () +validateWarrantSuccessorTerms label sourceIssuanceData successorIssuanceData = do + assertMsg + (label <> " successor exercise price mismatch for " <> successorIssuanceData.security_id) + (successorIssuanceData.exercise_price == sourceIssuanceData.exercise_price) + assertMsg + (label <> " successor exercise triggers mismatch for " <> successorIssuanceData.security_id) + (successorIssuanceData.exercise_triggers == sourceIssuanceData.exercise_triggers) + assertMsg + (label <> " successor expiration date mismatch for " <> successorIssuanceData.security_id) + (successorIssuanceData.warrant_expiration_date == sourceIssuanceData.warrant_expiration_date) + assertMsg + (label <> " successor vesting terms mismatch for " <> successorIssuanceData.security_id) + (successorIssuanceData.vesting_terms_id == sourceIssuanceData.vesting_terms_id) + assertMsg + (label <> " successor vestings mismatch for " <> successorIssuanceData.security_id) + (successorIssuanceData.vestings == sourceIssuanceData.vestings) + +fetchValidatedWarrantSuccessor : CapTableMaps -> Text -> + WarrantIssuanceOcfData -> Time -> - Decimal -> Text -> - Update () -validateWarrantBalanceSuccessorQuantity maps label eventDate expectedQuantity securityId = + Update WarrantIssuanceOcfData +fetchValidatedWarrantSuccessor maps label sourceIssuanceData eventDate securityId = case Map.lookup securityId maps.warrant_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid assertMsg - (label <> " balance result date mismatch for " <> securityId <> ": expected " <> show eventDate <> ", found " <> show issuance.issuance_data.date) + (label <> " successor date mismatch for " <> securityId <> ": expected " <> show eventDate <> ", found " <> show issuance.issuance_data.date) (issuance.issuance_data.date == eventDate) - case issuance.issuance_data.quantity of - Some actualQuantity -> - assertMsg - (label <> " balance quantity mismatch for " <> securityId <> ": expected " <> show expectedQuantity <> ", found " <> show actualQuantity) - (actualQuantity == expectedQuantity) - None -> assertMsg (label <> " balance warrant quantity is missing for " <> securityId) False - None -> assertMsg (label <> " balance warrant issuance not found: " <> securityId) False + validateWarrantSuccessorTerms label sourceIssuanceData issuance.issuance_data + pure issuance.issuance_data + None -> do + assertMsg (label <> " successor warrant issuance not found: " <> securityId) False + pure sourceIssuanceData + +validateWarrantBalanceSuccessorQuantity : + CapTableMaps -> + Text -> + WarrantIssuanceOcfData -> + Time -> + Optional Decimal -> + Text -> + Update () +validateWarrantBalanceSuccessorQuantity maps label sourceIssuanceData eventDate expectedQuantityOpt securityId = do + successorIssuanceData <- fetchValidatedWarrantSuccessor maps label sourceIssuanceData eventDate securityId + case (expectedQuantityOpt, successorIssuanceData.quantity) of + (Some expectedQuantity, Some actualQuantity) -> + assertMsg + (label <> " balance quantity mismatch for " <> securityId <> ": expected " <> show expectedQuantity <> ", found " <> show actualQuantity) + (actualQuantity == expectedQuantity) + (Some _, None) -> assertMsg (label <> " balance warrant quantity is missing for " <> securityId) False + (None, Some actualQuantity) -> + assertMsg + (label <> " balance warrant quantity must remain unknown for " <> securityId <> ", found " <> show actualQuantity) + False + (None, None) -> pure () + +validateWarrantTransferResultIssuances : + CapTableMaps -> + Text -> + WarrantIssuanceOcfData -> + Time -> + Decimal -> + [Text] -> + Update () +validateWarrantTransferResultIssuances maps label sourceIssuanceData eventDate expectedQuantity securityIds = do + actualQuantity <- foldlA + (\total securityId -> do + successorIssuanceData <- fetchValidatedWarrantSuccessor maps label sourceIssuanceData eventDate securityId + case successorIssuanceData.quantity of + Some quantity -> pure (total + quantity) + None -> do + assertMsg (label <> " resulting warrant quantity is missing for " <> securityId) False + pure total) + 0.0 + securityIds + assertMsg + (label <> " resulting warrant quantity mismatch: expected " <> show expectedQuantity <> ", found " <> show actualQuantity) + (actualQuantity == expectedQuantity) validateWarrantTerminalBalanceSuccessor : NonStockSecurityBalanceEventIndex -> @@ -2258,8 +2323,9 @@ validateWarrantTerminalBalanceSuccessor eventIndex maps label sourceSecurityId e Some currentQuantity -> do let expectedQuantity = currentQuantity - reductionQuantity assertMsg (label <> " balance successor requires a positive remainder") (expectedQuantity > 0.0) - validateWarrantBalanceSuccessorQuantity maps label eventDate expectedQuantity securityId - None -> assertMsg (label <> " balance successor cannot be validated because the source warrant quantity is unknown") False + validateWarrantBalanceSuccessorQuantity maps label sourceIssuance.issuance_data eventDate (Some expectedQuantity) securityId + None -> + validateWarrantBalanceSuccessorQuantity maps label sourceIssuance.issuance_data eventDate None securityId None -> assertMsg (label <> " source warrant issuance not found: " <> sourceSecurityId) False None -> pure () @@ -2324,10 +2390,18 @@ validateNonStockBalanceSuccessorQuantities stockEventIndex balanceEventIndex map mapA_ (\transferCid -> do transfer <- fetch transferCid - validateWarrantTerminalBalanceSuccessor - balanceEventIndex maps ("Warrant transfer " <> transfer.transfer_data.id) - transfer.transfer_data.security_id transfer.transfer_data.date transfer.transfer_data.quantity - transfer.transfer_data.balance_security_id) + let label = "Warrant transfer " <> transfer.transfer_data.id + case Map.lookup transfer.transfer_data.security_id maps.warrant_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + validateWarrantTransferResultIssuances + maps label sourceIssuance.issuance_data transfer.transfer_data.date + transfer.transfer_data.quantity transfer.transfer_data.resulting_security_ids + validateWarrantTerminalBalanceSuccessor + balanceEventIndex maps label + transfer.transfer_data.security_id transfer.transfer_data.date transfer.transfer_data.quantity + transfer.transfer_data.balance_security_id + None -> assertMsg (label <> " source warrant issuance not found: " <> transfer.transfer_data.security_id) False) (Map.values maps.warrant_transfers) mapA_ (\cancellationCid -> do From 2d04334ffb6476416f080f4054f98ba858087d4d Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 23:00:56 -0400 Subject: [PATCH 29/38] chore: refresh security balance DAR after restack --- dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index 5340a616..f3abe731 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31d7c34d75c1920aa8d60e3567c1e0addf28fa3f7f4638637686daeb35838a9e -size 3123591 +oid sha256:c931cf442eed0e1405b416919704903cabc05e6478279a0281839558ef75a057 +size 3145265 diff --git a/dars/dars.lock b/dars/dars.lock index d2b35a2f..603d5793 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,10 +26,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "31d7c34d75c1920aa8d60e3567c1e0addf28fa3f7f4638637686daeb35838a9e", - "size": 3123591, + "sha256": "c931cf442eed0e1405b416919704903cabc05e6478279a0281839558ef75a057", + "size": 3145265, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T02:18:14Z", + "uploadedAt": "2026-07-11T02:58:47Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { From 3b4f0f69699eb418a1b831f5dd47664d9b3c1be0 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 23:21:38 -0400 Subject: [PATCH 30/38] fix: enforce nonnegative indexed equity exercises --- .../TestNonStockSecurityBalances.daml | 17 +++++++++++++++++ .../0.0.12/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- .../codegen/templates/CapTable.daml.template | 3 +++ 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml index f750b2ab..fd676ddd 100644 --- a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml @@ -517,6 +517,23 @@ testEquityCompensationSecurityBalance_CancellationCannotExceedIssuedQuantity = s edits = [] deletes = [] +testEquityCompensationSecurityBalance_NegativeExerciseFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let securityId = "ESEC_BAL_NEGATIVE_EXERCISE" + let issuance = (equityCompensationIssuance "TX_E_BAL_NEGATIVE_EXERCISE" securityId defaultStakeholderId 50.0) with + compensation_type = OcfCompensationTypeCSAR + base_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationIssuance issuance + , CT.OcfCreateEquityCompensationExercise (equityCompensationExercise "TX_E_NEGATIVE_EXERCISE" securityId (-1.0) jan02) + ] + edits = [] + deletes = [] + testEquityCompensationSecurityBalance_CumulativeReductionsCannotExceedIssuedQuantity = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index f3abe731..4fbec2b4 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c931cf442eed0e1405b416919704903cabc05e6478279a0281839558ef75a057 -size 3145265 +oid sha256:eccf3672395b35a056d29eee2ce8136bbcdc725d0df16d15c9b493fcda8bfc3e +size 3145975 diff --git a/dars/dars.lock b/dars/dars.lock index 1d2e0abd..aed6fd24 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,10 +26,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "c931cf442eed0e1405b416919704903cabc05e6478279a0281839558ef75a057", - "size": 3145265, + "sha256": "eccf3672395b35a056d29eee2ce8136bbcdc725d0df16d15c9b493fcda8bfc3e", + "size": 3145975, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T02:58:47Z", + "uploadedAt": "2026-07-11T03:18:44Z", "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 00a70b09..16899bdc 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -2098,6 +2098,9 @@ nonStockSecurityBalanceEventIndex maps = do equityExerciseEvents <- foldlA (\index exerciseCid -> do exercise <- fetch exerciseCid + assertNonNegativeQuantity + ("Equity compensation exercise quantity for " <> exercise.exercise_data.id) + exercise.exercise_data.quantity pure (addSecurityQuantityEvent exercise.exercise_data.security_id (SecurityQuantityDelta with From 447b5371ba33e22f762d38079b2bb6778989aac9 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 00:06:40 -0400 Subject: [PATCH 31/38] chore: refresh stock balance DAR after parent update --- dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index 4fbec2b4..a870de6b 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eccf3672395b35a056d29eee2ce8136bbcdc725d0df16d15c9b493fcda8bfc3e -size 3145975 +oid sha256:8bcdacccc0d3279bb243c04bb7b76e968d41d029f29a315c80c3ceed98e67870 +size 3146546 diff --git a/dars/dars.lock b/dars/dars.lock index c7df94c0..54b32730 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,10 +26,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "eccf3672395b35a056d29eee2ce8136bbcdc725d0df16d15c9b493fcda8bfc3e", - "size": 3145975, + "sha256": "8bcdacccc0d3279bb243c04bb7b76e968d41d029f29a315c80c3ceed98e67870", + "size": 3146546, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T03:18:44Z", + "uploadedAt": "2026-07-11T04:05:53Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { From 3475dd99b3f31935a28d271d2ec3feb822087f1b Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 00:20:09 -0400 Subject: [PATCH 32/38] fix: align equity balance event ordering --- .../TestNonStockSecurityBalances.daml | 29 +++++++++++++++++++ .../0.0.12/OpenCapTable-v34.dar | 4 +-- dars/dars.lock | 6 ++-- .../codegen/templates/CapTable.daml.template | 15 ++++++++-- 4 files changed, 46 insertions(+), 8 deletions(-) diff --git a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml index fd676ddd..40a13b9b 100644 --- a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml @@ -589,6 +589,35 @@ testEquityCompensationSecurityBalance_ReleaseBeforeRetractionUsesReleaseDate = s deletes = [] pure () +testEquityCompensationSecurityBalance_ReleaseBeforeBalanceCancellationUsesReleaseDate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let securityId = "ESEC_BAL_RELEASE_BEFORE_CANCELLATION" + let balanceSecurityId = "ESEC_BAL_RELEASE_CANCELLATION_REMAINDER" + let sourceIssuance = + (equityCompensationIssuance "TX_E_BAL_RELEASE_BEFORE_CANCELLATION" securityId stakeholderId 100.0) with + stock_class_id = Some stockClassId + let balanceIssuance = + (equityCompensationIssuance "TX_E_BAL_RELEASE_CANCELLATION_REMAINDER" balanceSecurityId stakeholderId 50.0) with + date = jan03 + stock_class_id = Some stockClassId + let cancellation = + (equityCompensationCancellation "TX_E_CANCEL_AFTER_RELEASE_BEFORE_SETTLEMENT" securityId 10.0 jan03) with + balance_security_id = Some balanceSecurityId + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationIssuance sourceIssuance + , CT.OcfCreateEquityCompensationRelease + ((equityCompensationRelease "TX_E_RELEASE_BEFORE_CANCELLATION" securityId jan02 jan04 40.0) with resulting_security_ids = []) + , CT.OcfCreateEquityCompensationIssuance balanceIssuance + , CT.OcfCreateEquityCompensationCancellation cancellation + ] + edits = [] + deletes = [] + pure () + testEquityCompensationSecurityBalance_RepeatedRetractionFails = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableReady <- addDefaultStakeholder issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index a870de6b..10d504ec 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8bcdacccc0d3279bb243c04bb7b76e968d41d029f29a315c80c3ceed98e67870 -size 3146546 +oid sha256:d31c86133ba70cb0ab92d1653c5066bc79d41dc0fb5eb6ef4745b55d7bc273dd +size 3147567 diff --git a/dars/dars.lock b/dars/dars.lock index 54b32730..f808e8eb 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,10 +26,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "8bcdacccc0d3279bb243c04bb7b76e968d41d029f29a315c80c3ceed98e67870", - "size": 3146546, + "sha256": "d31c86133ba70cb0ab92d1653c5066bc79d41dc0fb5eb6ef4745b55d7bc273dd", + "size": 3147567, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T04:05:53Z", + "uploadedAt": "2026-07-11T04:18:55Z", "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 16899bdc..32c733ed 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -2210,8 +2210,17 @@ equityCompensationSecurityQuantityEvents : equityCompensationSecurityQuantityEvents stockEventIndex balanceEventIndex maps issuanceData = do stockClassIds <- equityCompensationPlanStockClassIds maps issuanceData let securityEvents = fromOptional [] (Map.lookup issuanceData.security_id balanceEventIndex.equity_compensation_events_by_security_id) - splitEvents = case stockClassIds of - [stockClassId] -> stockClassSplitSecurityQuantityEvents stockEventIndex stockClassId issuanceData.date + planSplitEvents = concatMap + (\stockClassId -> stockClassSplitSecurityQuantityEvents stockEventIndex stockClassId issuanceData.date) + stockClassIds + case (issuanceData.stock_class_id, stockClassIds, planSplitEvents) of + (None, _ :: _ :: _, _ :: _) -> + assertMsg + ("Equity compensation issuance without stock_class_id cannot be split-adjusted across multiple stock-plan classes: " <> issuanceData.id) + False + _ -> pure () + let splitEvents = case stockClassIds of + [_] -> planSplitEvents _ -> [] pure (securityEvents ++ splitEvents) @@ -4518,7 +4527,7 @@ equityCompensationLifecycleQuantityEvents maps issuanceData = do release <- fetch releaseCid if release.release_data.security_id == issuanceData.security_id then pure ((SecurityQuantityDelta with - event_date = release.release_data.settlement_date + event_date = release.release_data.date event_priority = 1 quantity_delta = 0.0 - release.release_data.quantity) :: events) From c96ffcc4114214ac3358f25f2b3ee32506144e8e Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 01:16:58 -0400 Subject: [PATCH 33/38] fix: align stock balance event ordering --- .../TestStockSecurityBalances.daml | 38 +++++++++++++++++++ .../0.0.12/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 31 +++------------ 4 files changed, 47 insertions(+), 30 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestStockSecurityBalances.daml index bedafcfe..bd3a4e5f 100644 --- a/Test/daml/OpenCapTable/TestStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestStockSecurityBalances.daml @@ -423,6 +423,44 @@ testStockSecurityBalance_SplitAdjustedBalanceAllowsReduction = script do deletes = [] pure () +testStockSecurityBalance_ReissuanceSuccessorIgnoresSameDaySplit = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourceSecurityId = "SEC_STOCK_BAL_SAME_DAY_SPLIT_SOURCE" + reissuanceId = "TX_STOCK_BAL_SAME_DAY_SPLIT_REISSUANCE" + reissuedSecurityId = reissuanceId <> "_RESULT" + splitId = "TX_STOCK_BAL_SAME_DAY_SPLIT" + transferId = "TX_STOCK_BAL_AFTER_SAME_DAY_SPLIT" + transferResultSecurityId = transferId <> "_RESULT" + finalDate = DT.time (DA.date 2024 Jan 04) 0 0 0 + issuance = (defaultStockIssuance "TX_STOCK_BAL_SAME_DAY_SPLIT_SOURCE" sourceSecurityId stakeholderId stockClassId) with quantity = 25.0 + + reissued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockClassSplit (stockClassSplitOn splitId stockClassId 2.0 1.0 jan02) + , stockSuccessorIssuance "TX_STOCK_BAL_SAME_DAY_SPLIT_RESULT" reissuedSecurityId stakeholderId stockClassId jan02 50.0 + , CT.OcfCreateStockReissuance ((stockReissuance reissuanceId sourceSecurityId jan02) with split_transaction_id = Some splitId) + ] + edits = [] + deletes = [] + + transferred <- submit issuer do + exerciseCmd reissued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (stockTransfer transferId reissuedSecurityId 50.0 jan03) + , stockSuccessorIssuance "TX_STOCK_BAL_AFTER_SAME_DAY_SPLIT_RESULT" transferResultSecurityId stakeholderId stockClassId jan03 50.0 + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockCancellation (stockCancellationOn "TX_STOCK_BAL_REUSE_CONSUMED_REISSUANCE" reissuedSecurityId 1.0 finalDate)] + edits = [] + deletes = [] + testStockSecurityBalance_ReverseSplitReducesAvailableBalance = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index 10d504ec..5e78c72e 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d31c86133ba70cb0ab92d1653c5066bc79d41dc0fb5eb6ef4745b55d7bc273dd -size 3147567 +oid sha256:5450ab3e95db870058f95b4bd0c1ad2b425b984f633535d6c0660be570c39ce5 +size 3151573 diff --git a/dars/dars.lock b/dars/dars.lock index 934cb4fc..be5341f5 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,8 +26,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "d31c86133ba70cb0ab92d1653c5066bc79d41dc0fb5eb6ef4745b55d7bc273dd", - "size": 3147567, + "sha256": "5450ab3e95db870058f95b4bd0c1ad2b425b984f633535d6c0660be570c39ce5", + "size": 3151573, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-11T04:18:55Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index df0a85f3..5774ee80 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -960,22 +960,8 @@ stockClassShareLotsByStockClass maps stockClassIndex = do (\index transferCid -> do transfer <- fetch transferCid let label = "Stock transfer " <> transfer.transfer_data.id - assertNoPartialResultingStockIssuanceCoverage label maps transfer.transfer_data.resulting_security_ids - if allResultingStockIssuancesExist maps transfer.transfer_data.resulting_security_ids - then pure index - else case Map.lookup transfer.transfer_data.security_id maps.stock_issuances_by_security_id of - Some sourceIssuanceCid -> do - sourceIssuance <- fetch sourceIssuanceCid - pure (addStockClassShareLotByStockClass - (StockClassShareLot with - stock_class_id = sourceIssuance.issuance_data.stock_class_id - security_id = None - event_date = transfer.transfer_data.date - quantity = transfer.transfer_data.quantity) - index) - None -> do - assertMsg (label <> " references missing source stock issuance: " <> transfer.transfer_data.security_id) False - pure index) + assertAllResultingStockIssuancesExist label maps transfer.transfer_data.resulting_security_ids + pure index) stockIssuanceLots (Map.values maps.stock_transfers) @@ -1318,13 +1304,6 @@ stockClassShareEventDate event = case event of StockClassShareMultiplier with event_date -> event_date StockClassShareRetraction with event_date -> event_date -stockClassShareEventOnOrAfter : Time -> StockClassShareEvent -> Bool -stockClassShareEventOnOrAfter earliest event = case event of - StockClassShareDelta with event_date -> event_date >= earliest - StockClassShareTerminalDelta with event_date -> event_date >= earliest - StockClassShareMultiplier with event_date -> event_date >= earliest - StockClassShareRetraction with event_date -> event_date >= earliest - stockClassShareEventOnOrBefore : Time -> StockClassShareEvent -> Bool stockClassShareEventOnOrBefore latest event = case event of StockClassShareDelta with event_date -> event_date <= latest @@ -1493,8 +1472,7 @@ stockClassShareEventIndex maps = do assertNoPartialResultingStockIssuanceCoverage ("Stock transfer " <> transfer.transfer_data.id) maps transfer.transfer_data.resulting_security_ids let shareEvents = stockSecurityReductionEvents transfer.transfer_data.date transfer.transfer_data.quantity transfer.transfer_data.balance_security_id let nextShareIndex = - if allResultingStockIssuancesExist maps transfer.transfer_data.resulting_security_ids - && stockBalanceSecurityIssuanceExists maps transfer.transfer_data.balance_security_id + if stockBalanceSecurityIssuanceExists maps transfer.transfer_data.balance_security_id then addStockClassShareEvents transfer.transfer_data.security_id shareEvents shareIndex else shareIndex pure @@ -1706,9 +1684,10 @@ stockSecurityShareEvents : StockClassShareEventIndex -> StockIssuanceOcfData -> stockSecurityShareEvents eventIndex issuanceData = let balanceEventsBySecurityId = fromOptional Map.empty eventIndex.stock_security_balance_events_by_security_id securityEvents = fromOptional [] (Map.lookup issuanceData.security_id balanceEventsBySecurityId) + splitCutoffSortKey = stockClassShareLotSplitCutoffSortKey eventIndex (stockIssuanceShareLot issuanceData) splitEvents = filter - (stockClassShareEventOnOrAfter issuanceData.date) + (\event -> stockClassShareEventSortKey event >= splitCutoffSortKey) (fromOptional [] (Map.lookup issuanceData.stock_class_id eventIndex.stock_class_split_events_by_stock_class_id)) in (StockClassShareDelta with event_date = issuanceData.date From 3be8ce9a5bb0e7c29d5f961608b6caaca6dbeaa9 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 02:08:03 -0400 Subject: [PATCH 34/38] fix: preserve fixed warrant conversion ratios --- .../TestNonStockSecurityBalances.daml | 25 ++++++++++++-- .../0.0.12/OpenCapTable-v34.dar | 4 +-- dars/dars.lock | 6 ++-- .../codegen/templates/CapTable.daml.template | 33 ++++++++++++++++++- 4 files changed, 59 insertions(+), 9 deletions(-) diff --git a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml index 40a13b9b..64d04787 100644 --- a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml @@ -380,19 +380,25 @@ testWarrantSecurityBalance_TransferResultsPreserveQuantityDateAndTerms = script let sourceSecurityId = "WSEC_TRANSFER_CONTINUITY_SOURCE" let transferId = "TX_W_TRANSFER_CONTINUITY" let resultSecurityId = transferId <> "_RESULT" + let balanceSecurityId = "WSEC_TRANSFER_CONTINUITY_BALANCE" let sourceTrigger = warrantFixedExerciseTrigger "TRANSFER_TRIGGER" stockClassId 25.0 let sourceIssuance = (warrantIssuance "TX_W_TRANSFER_CONTINUITY_SOURCE" sourceSecurityId stakeholderId 100.0) with exercise_triggers = [sourceTrigger] - let transfer = warrantTransfer transferId sourceSecurityId 40.0 jan02 + let transfer = (warrantTransfer transferId sourceSecurityId 40.0 jan02) with + balance_security_id = Some balanceSecurityId let validResult = (warrantIssuance "TX_W_TRANSFER_CONTINUITY_RESULT" resultSecurityId stakeholderId 40.0) with date = jan02 - exercise_triggers = [sourceTrigger] + exercise_triggers = [warrantFixedExerciseTrigger "TRANSFER_TRIGGER" stockClassId 10.0] + let validBalance = (warrantIssuance "TX_W_TRANSFER_CONTINUITY_BALANCE" balanceSecurityId stakeholderId 60.0) with + date = jan02 + exercise_triggers = [warrantFixedExerciseTrigger "TRANSFER_TRIGGER" stockClassId 15.0] submitMustFail issuer do exerciseCmd capTableReady CT.UpdateCapTable with creates = [ CT.OcfCreateWarrantIssuance sourceIssuance , CT.OcfCreateWarrantIssuance (validResult with quantity = Some 41.0) + , CT.OcfCreateWarrantIssuance validBalance , CT.OcfCreateWarrantTransfer transfer ] edits = [] @@ -403,6 +409,7 @@ testWarrantSecurityBalance_TransferResultsPreserveQuantityDateAndTerms = script creates = [ CT.OcfCreateWarrantIssuance sourceIssuance , CT.OcfCreateWarrantIssuance (validResult with date = jan03) + , CT.OcfCreateWarrantIssuance validBalance , CT.OcfCreateWarrantTransfer transfer ] edits = [] @@ -413,7 +420,19 @@ testWarrantSecurityBalance_TransferResultsPreserveQuantityDateAndTerms = script creates = [ CT.OcfCreateWarrantIssuance sourceIssuance , CT.OcfCreateWarrantIssuance (validResult with - exercise_triggers = [warrantFixedExerciseTrigger "TRANSFER_TRIGGER" stockClassId 100.0]) + exercise_triggers = [sourceTrigger]) + , CT.OcfCreateWarrantIssuance validBalance + , CT.OcfCreateWarrantTransfer transfer + ] + edits = [] + deletes = [] + + submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance sourceIssuance + , CT.OcfCreateWarrantIssuance validResult + , CT.OcfCreateWarrantIssuance validBalance , CT.OcfCreateWarrantTransfer transfer ] edits = [] diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index 5e78c72e..e8d4d136 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5450ab3e95db870058f95b4bd0c1ad2b425b984f633535d6c0660be570c39ce5 -size 3151573 +oid sha256:ebc0e49841f274ff6c9881f1788aecd9362651b53326467db686714113b4591a +size 3154774 diff --git a/dars/dars.lock b/dars/dars.lock index be5341f5..3ba6402a 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,10 +26,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "5450ab3e95db870058f95b4bd0c1ad2b425b984f633535d6c0660be570c39ce5", - "size": 3151573, + "sha256": "ebc0e49841f274ff6c9881f1788aecd9362651b53326467db686714113b4591a", + "size": 3154774, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T04:18:55Z", + "uploadedAt": "2026-07-11T06:07:06.192Z", "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 5774ee80..19f116cb 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -2256,6 +2256,37 @@ validateEquityCompensationSecurityBalances stockEventIndex balanceEventIndex map validateEquityCompensationSecurityBalance stockEventIndex balanceEventIndex maps issuance.issuance_data) (Map.values maps.equity_compensation_issuances) +scaleWarrantSuccessorTrigger : + Decimal -> + Decimal -> + OcfConversionTrigger -> + OcfConversionTrigger +scaleWarrantSuccessorTrigger sourceQuantity successorQuantity trigger = + case trigger.conversion_right of + OcfRightWarrant conversionRight -> + case conversionRight.conversion_mechanism of + OcfWarrantMechanismFixedAmount with converts_to_quantity -> + trigger with + conversion_right = OcfRightWarrant (conversionRight with + conversion_mechanism = OcfWarrantMechanismFixedAmount with + converts_to_quantity = converts_to_quantity * successorQuantity / sourceQuantity) + _ -> trigger + _ -> trigger + +expectedWarrantSuccessorTriggers : + WarrantIssuanceOcfData -> + WarrantIssuanceOcfData -> + [OcfConversionTrigger] +expectedWarrantSuccessorTriggers sourceIssuanceData successorIssuanceData = + case (sourceIssuanceData.quantity, successorIssuanceData.quantity) of + (Some sourceQuantity, Some successorQuantity) -> + if sourceQuantity > 0.0 + then map + (scaleWarrantSuccessorTrigger sourceQuantity successorQuantity) + sourceIssuanceData.exercise_triggers + else sourceIssuanceData.exercise_triggers + _ -> sourceIssuanceData.exercise_triggers + validateWarrantSuccessorTerms : Text -> WarrantIssuanceOcfData -> @@ -2267,7 +2298,7 @@ validateWarrantSuccessorTerms label sourceIssuanceData successorIssuanceData = d (successorIssuanceData.exercise_price == sourceIssuanceData.exercise_price) assertMsg (label <> " successor exercise triggers mismatch for " <> successorIssuanceData.security_id) - (successorIssuanceData.exercise_triggers == sourceIssuanceData.exercise_triggers) + (successorIssuanceData.exercise_triggers == expectedWarrantSuccessorTriggers sourceIssuanceData successorIssuanceData) assertMsg (label <> " successor expiration date mismatch for " <> successorIssuanceData.security_id) (successorIssuanceData.warrant_expiration_date == sourceIssuanceData.warrant_expiration_date) From b5674f682f892bb15d0e1f5631a5de5d862fe826 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 02:55:22 -0400 Subject: [PATCH 35/38] fix: preserve partial warrant economics --- .../TestNonStockSecurityBalances.daml | 51 ++++++++++++- .../0.0.12/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 75 +++++++++++++++---- 4 files changed, 113 insertions(+), 21 deletions(-) diff --git a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml index 64d04787..e30f00b2 100644 --- a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml @@ -14,7 +14,7 @@ import Fairmint.OpenCapTable.OCF.WarrantTransfer (WarrantTransferOcfData(..)) import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfConversionTrigger(..), OcfConversionTriggerType(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Stock (OcfQuantitySourceType(..)) -import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) +import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..), OcfVesting(..)) import OpenCapTable.Setup import OpenCapTable.TestHelpers import qualified DA.Date as DA @@ -382,16 +382,20 @@ testWarrantSecurityBalance_TransferResultsPreserveQuantityDateAndTerms = script let resultSecurityId = transferId <> "_RESULT" let balanceSecurityId = "WSEC_TRANSFER_CONTINUITY_BALANCE" let sourceTrigger = warrantFixedExerciseTrigger "TRANSFER_TRIGGER" stockClassId 25.0 + let sourceVesting = OcfVesting with date = jan04, amount = 100.0 let sourceIssuance = (warrantIssuance "TX_W_TRANSFER_CONTINUITY_SOURCE" sourceSecurityId stakeholderId 100.0) with exercise_triggers = [sourceTrigger] + vestings = [sourceVesting] let transfer = (warrantTransfer transferId sourceSecurityId 40.0 jan02) with balance_security_id = Some balanceSecurityId let validResult = (warrantIssuance "TX_W_TRANSFER_CONTINUITY_RESULT" resultSecurityId stakeholderId 40.0) with date = jan02 exercise_triggers = [warrantFixedExerciseTrigger "TRANSFER_TRIGGER" stockClassId 10.0] + vestings = [sourceVesting with amount = 40.0] let validBalance = (warrantIssuance "TX_W_TRANSFER_CONTINUITY_BALANCE" balanceSecurityId stakeholderId 60.0) with date = jan02 exercise_triggers = [warrantFixedExerciseTrigger "TRANSFER_TRIGGER" stockClassId 15.0] + vestings = [sourceVesting with amount = 60.0] submitMustFail issuer do exerciseCmd capTableReady CT.UpdateCapTable with @@ -404,6 +408,17 @@ testWarrantSecurityBalance_TransferResultsPreserveQuantityDateAndTerms = script edits = [] deletes = [] + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance sourceIssuance + , CT.OcfCreateWarrantIssuance (validResult with vestings = [sourceVesting]) + , CT.OcfCreateWarrantIssuance validBalance + , CT.OcfCreateWarrantTransfer transfer + ] + edits = [] + deletes = [] + submitMustFail issuer do exerciseCmd capTableReady CT.UpdateCapTable with creates = @@ -479,6 +494,40 @@ testWarrantSecurityBalance_UnknownQuantityTransferPreservesUnknownBalance = scri deletes = [] pure () +testWarrantSecurityBalance_UnknownQuantityFixedAmountSuccessorsFail = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourceSecurityId = "WSEC_UNKNOWN_FIXED_TRANSFER_SOURCE" + let transferId = "TX_W_UNKNOWN_FIXED_TRANSFER" + let resultSecurityId = transferId <> "_RESULT" + let balanceSecurityId = "WSEC_UNKNOWN_FIXED_TRANSFER_BALANCE" + let sourceTrigger = warrantFixedExerciseTrigger "UNKNOWN_FIXED_TRIGGER" stockClassId 25.0 + let sourceIssuance = (warrantIssuance "TX_W_UNKNOWN_FIXED_TRANSFER_SOURCE" sourceSecurityId stakeholderId 100.0) with + quantity = None + quantity_source = None + exercise_triggers = [sourceTrigger] + let resultIssuance = (warrantIssuance "TX_W_UNKNOWN_FIXED_TRANSFER_RESULT" resultSecurityId stakeholderId 40.0) with + date = jan02 + exercise_triggers = [sourceTrigger] + let balanceIssuance = (warrantIssuance "TX_W_UNKNOWN_FIXED_TRANSFER_BALANCE" balanceSecurityId stakeholderId 100.0) with + date = jan02 + quantity = None + quantity_source = None + exercise_triggers = [sourceTrigger] + let transfer = (warrantTransfer transferId sourceSecurityId 40.0 jan02) with + balance_security_id = Some balanceSecurityId + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance sourceIssuance + , CT.OcfCreateWarrantIssuance resultIssuance + , CT.OcfCreateWarrantIssuance balanceIssuance + , CT.OcfCreateWarrantTransfer transfer + ] + edits = [] + deletes = [] + testWarrantSecurityBalance_UnknownQuantityCancellationPreservesUnknownBalance = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableReady <- addDefaultStakeholder issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index e8d4d136..35cfbd8c 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ebc0e49841f274ff6c9881f1788aecd9362651b53326467db686714113b4591a -size 3154774 +oid sha256:2723a9d489cd5fdcaac0d7a39b656c641cee118e9fabb48c44fdce10a69763bc +size 3159044 diff --git a/dars/dars.lock b/dars/dars.lock index 3ba6402a..fd400633 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,8 +26,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "ebc0e49841f274ff6c9881f1788aecd9362651b53326467db686714113b4591a", - "size": 3154774, + "sha256": "2723a9d489cd5fdcaac0d7a39b656c641cee118e9fabb48c44fdce10a69763bc", + "size": 3159044, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-11T06:07:06.192Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 19f116cb..426aa0d2 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -20,7 +20,7 @@ import DA.Optional (fromOptional, isNone, isSome) import Fairmint.OpenCapTable.Types.Core (Context) import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfCapitalizationDefinition, OcfConversionMechanism(..), OcfConversionTrigger(..), OcfConvertibleConversionMechanism(..), OcfConvertibleConversionRight(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..), OcfShareNumberRange(..), shareNumberRangeCoveredByRanges, shareNumberRangesOverlap) -import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) +import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..), OcfVesting(..)) import qualified Fairmint.OpenCapTable.OCF.ConvertibleIssuance as ConvertibleIssuance (OcfConvertibleConversionTrigger(..)) import Fairmint.OpenCapTable.OCF.Issuer (Issuer(..), IssuerOcfData) import Fairmint.OpenCapTable.OCF.Document (Document(..), DocumentOcfData, OcfObjectReference(..), OcfObjectType(..)) @@ -2273,19 +2273,50 @@ scaleWarrantSuccessorTrigger sourceQuantity successorQuantity trigger = _ -> trigger _ -> trigger +warrantTriggerRequiresQuantityScaling : OcfConversionTrigger -> Bool +warrantTriggerRequiresQuantityScaling trigger = + case trigger.conversion_right of + OcfRightWarrant conversionRight -> + case conversionRight.conversion_mechanism of + OcfWarrantMechanismFixedAmount {} -> True + _ -> False + _ -> False + expectedWarrantSuccessorTriggers : WarrantIssuanceOcfData -> WarrantIssuanceOcfData -> - [OcfConversionTrigger] + Optional [OcfConversionTrigger] expectedWarrantSuccessorTriggers sourceIssuanceData successorIssuanceData = - case (sourceIssuanceData.quantity, successorIssuanceData.quantity) of - (Some sourceQuantity, Some successorQuantity) -> - if sourceQuantity > 0.0 - then map - (scaleWarrantSuccessorTrigger sourceQuantity successorQuantity) - sourceIssuanceData.exercise_triggers - else sourceIssuanceData.exercise_triggers - _ -> sourceIssuanceData.exercise_triggers + if any warrantTriggerRequiresQuantityScaling sourceIssuanceData.exercise_triggers + then case (sourceIssuanceData.quantity, successorIssuanceData.quantity) of + (Some sourceQuantity, Some successorQuantity) -> + if sourceQuantity > 0.0 + then Some (map + (scaleWarrantSuccessorTrigger sourceQuantity successorQuantity) + sourceIssuanceData.exercise_triggers) + else None + _ -> None + else Some sourceIssuanceData.exercise_triggers + +scaleWarrantSuccessorVesting : Decimal -> Decimal -> OcfVesting -> OcfVesting +scaleWarrantSuccessorVesting sourceQuantity successorQuantity vesting = + vesting with amount = vesting.amount * successorQuantity / sourceQuantity + +expectedWarrantSuccessorVestings : + WarrantIssuanceOcfData -> + WarrantIssuanceOcfData -> + Optional [OcfVesting] +expectedWarrantSuccessorVestings sourceIssuanceData successorIssuanceData = + if null sourceIssuanceData.vestings + then Some [] + else case (sourceIssuanceData.quantity, successorIssuanceData.quantity) of + (Some sourceQuantity, Some successorQuantity) -> + if sourceQuantity > 0.0 + then Some (map + (scaleWarrantSuccessorVesting sourceQuantity successorQuantity) + sourceIssuanceData.vestings) + else None + _ -> None validateWarrantSuccessorTerms : Text -> @@ -2296,18 +2327,30 @@ validateWarrantSuccessorTerms label sourceIssuanceData successorIssuanceData = d assertMsg (label <> " successor exercise price mismatch for " <> successorIssuanceData.security_id) (successorIssuanceData.exercise_price == sourceIssuanceData.exercise_price) - assertMsg - (label <> " successor exercise triggers mismatch for " <> successorIssuanceData.security_id) - (successorIssuanceData.exercise_triggers == expectedWarrantSuccessorTriggers sourceIssuanceData successorIssuanceData) + case expectedWarrantSuccessorTriggers sourceIssuanceData successorIssuanceData of + Some expectedTriggers -> + assertMsg + (label <> " successor exercise triggers mismatch for " <> successorIssuanceData.security_id) + (successorIssuanceData.exercise_triggers == expectedTriggers) + None -> + assertMsg + (label <> " cannot scale fixed-amount exercise triggers for successor " <> successorIssuanceData.security_id <> " without known positive source and successor quantities") + False assertMsg (label <> " successor expiration date mismatch for " <> successorIssuanceData.security_id) (successorIssuanceData.warrant_expiration_date == sourceIssuanceData.warrant_expiration_date) assertMsg (label <> " successor vesting terms mismatch for " <> successorIssuanceData.security_id) (successorIssuanceData.vesting_terms_id == sourceIssuanceData.vesting_terms_id) - assertMsg - (label <> " successor vestings mismatch for " <> successorIssuanceData.security_id) - (successorIssuanceData.vestings == sourceIssuanceData.vestings) + case expectedWarrantSuccessorVestings sourceIssuanceData successorIssuanceData of + Some expectedVestings -> + assertMsg + (label <> " successor vestings mismatch for " <> successorIssuanceData.security_id) + (successorIssuanceData.vestings == expectedVestings) + None -> + assertMsg + (label <> " cannot scale inline vestings for successor " <> successorIssuanceData.security_id <> " without known positive source and successor quantities") + False fetchValidatedWarrantSuccessor : CapTableMaps -> From 6a70c2f0dde1374a1024590d8c1539850a73a100 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 03:20:54 -0400 Subject: [PATCH 36/38] fix: reject unscalable warrant vesting terms --- .../TestNonStockSecurityBalances.daml | 54 +++++++++++++++++++ .../0.0.12/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 40 +++++++++++++- 4 files changed, 96 insertions(+), 6 deletions(-) diff --git a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml index e30f00b2..9995a1ab 100644 --- a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml @@ -11,6 +11,7 @@ import Fairmint.OpenCapTable.OCF.WarrantExercise (WarrantExerciseOcfData(..)) import Fairmint.OpenCapTable.OCF.WarrantIssuance (WarrantIssuanceOcfData(..)) import Fairmint.OpenCapTable.OCF.WarrantRetraction (WarrantRetractionOcfData(..)) import Fairmint.OpenCapTable.OCF.WarrantTransfer (WarrantTransferOcfData(..)) +import Fairmint.OpenCapTable.OCF.VestingTerms (OcfVestingConditionPortion(..), OcfVestingTrigger(..)) import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfConversionTrigger(..), OcfConversionTriggerType(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Stock (OcfQuantitySourceType(..)) @@ -453,6 +454,59 @@ testWarrantSecurityBalance_TransferResultsPreserveQuantityDateAndTerms = script edits = [] deletes = [] +testWarrantSecurityBalance_FixedQuantityVestingTermsCannotBeReusedByPartialSuccessors = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let sourceSecurityId = "WSEC_FIXED_VESTING_TERMS_SOURCE" + let transferId = "TX_W_FIXED_VESTING_TERMS_TRANSFER" + let resultSecurityId = transferId <> "_RESULT" + let balanceSecurityId = "WSEC_FIXED_VESTING_TERMS_BALANCE" + let fixedTermsId = "VT_W_FIXED_QUANTITY" + let fixedSource = (warrantIssuance "TX_W_FIXED_VESTING_TERMS_SOURCE" sourceSecurityId defaultStakeholderId 100.0) with + vesting_terms_id = Some fixedTermsId + let fixedResult = (warrantIssuance "TX_W_FIXED_VESTING_TERMS_RESULT" resultSecurityId defaultStakeholderId 40.0) with + date = jan02 + vesting_terms_id = Some fixedTermsId + let fixedBalance = (warrantIssuance "TX_W_FIXED_VESTING_TERMS_BALANCE" balanceSecurityId defaultStakeholderId 60.0) with + date = jan02 + vesting_terms_id = Some fixedTermsId + let transfer = (warrantTransfer transferId sourceSecurityId 40.0 jan02) with + balance_security_id = Some balanceSecurityId + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateVestingTerms (defaultVestingTerms fixedTermsId) + , CT.OcfCreateWarrantIssuance fixedSource + , CT.OcfCreateWarrantIssuance fixedResult + , CT.OcfCreateWarrantIssuance fixedBalance + , CT.OcfCreateWarrantTransfer transfer + ] + edits = [] + deletes = [] + + let portionTermsId = "VT_W_PORTION" + let portionCondition = (defaultVestingCondition "start" OcfVestingStartTrigger []) with + portion = Some (OcfVestingConditionPortion with numerator = 1.0, denominator = 1.0, remainder = False) + quantity = None + let portionTerms = defaultVestingTermsWithConditions portionTermsId [portionCondition] + let portionSource = fixedSource with vesting_terms_id = Some portionTermsId + let portionResult = fixedResult with vesting_terms_id = Some portionTermsId + let portionBalance = fixedBalance with vesting_terms_id = Some portionTermsId + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateVestingTerms portionTerms + , CT.OcfCreateWarrantIssuance portionSource + , CT.OcfCreateWarrantIssuance portionResult + , CT.OcfCreateWarrantIssuance portionBalance + , CT.OcfCreateWarrantTransfer transfer + ] + edits = [] + deletes = [] + pure () + testWarrantSecurityBalance_UnknownQuantityTransferPreservesUnknownBalance = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableReady <- addDefaultStakeholder issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index 35cfbd8c..a294cdb3 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2723a9d489cd5fdcaac0d7a39b656c641cee118e9fabb48c44fdce10a69763bc -size 3159044 +oid sha256:a00b5d4c1d73114903437c2ceb932ef0f862ae5ee3a1b06cc1d660355869a487 +size 3162976 diff --git a/dars/dars.lock b/dars/dars.lock index fd400633..eae746d2 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,8 +26,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "2723a9d489cd5fdcaac0d7a39b656c641cee118e9fabb48c44fdce10a69763bc", - "size": 3159044, + "sha256": "a00b5d4c1d73114903437c2ceb932ef0f862ae5ee3a1b06cc1d660355869a487", + "size": 3162976, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-11T06:07:06.192Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 426aa0d2..b34fae1c 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -2318,12 +2318,47 @@ expectedWarrantSuccessorVestings sourceIssuanceData successorIssuanceData = else None _ -> None +vestingConditionUsesPositiveFixedQuantity : VT.OcfVestingCondition -> Bool +vestingConditionUsesPositiveFixedQuantity condition = + case condition.quantity of + Some quantity -> quantity > 0.0 + None -> False + +validateWarrantSuccessorVestingTerms : + CapTableMaps -> + Text -> + WarrantIssuanceOcfData -> + WarrantIssuanceOcfData -> + Update () +validateWarrantSuccessorVestingTerms maps label sourceIssuanceData successorIssuanceData = + case sourceIssuanceData.vesting_terms_id of + Some vestingTermsId -> case Map.lookup vestingTermsId maps.vesting_terms of + Some vestingTermsCid -> do + vestingTerms <- fetch vestingTermsCid + if any vestingConditionUsesPositiveFixedQuantity vestingTerms.vesting_terms_data.vesting_conditions + then case (sourceIssuanceData.quantity, successorIssuanceData.quantity) of + (Some sourceQuantity, Some successorQuantity) -> + assertMsg + (label <> " successor cannot reuse fixed-quantity vesting terms for " <> successorIssuanceData.security_id <> " unless source and successor quantities are equal") + (successorQuantity == sourceQuantity) + _ -> + assertMsg + (label <> " cannot prove fixed-quantity vesting terms are preserved for successor " <> successorIssuanceData.security_id <> " without known source and successor quantities") + False + else pure () + None -> + assertMsg + (label <> " source warrant references missing VestingTerms: " <> vestingTermsId) + False + None -> pure () + validateWarrantSuccessorTerms : + CapTableMaps -> Text -> WarrantIssuanceOcfData -> WarrantIssuanceOcfData -> Update () -validateWarrantSuccessorTerms label sourceIssuanceData successorIssuanceData = do +validateWarrantSuccessorTerms maps label sourceIssuanceData successorIssuanceData = do assertMsg (label <> " successor exercise price mismatch for " <> successorIssuanceData.security_id) (successorIssuanceData.exercise_price == sourceIssuanceData.exercise_price) @@ -2342,6 +2377,7 @@ validateWarrantSuccessorTerms label sourceIssuanceData successorIssuanceData = d assertMsg (label <> " successor vesting terms mismatch for " <> successorIssuanceData.security_id) (successorIssuanceData.vesting_terms_id == sourceIssuanceData.vesting_terms_id) + validateWarrantSuccessorVestingTerms maps label sourceIssuanceData successorIssuanceData case expectedWarrantSuccessorVestings sourceIssuanceData successorIssuanceData of Some expectedVestings -> assertMsg @@ -2366,7 +2402,7 @@ fetchValidatedWarrantSuccessor maps label sourceIssuanceData eventDate securityI assertMsg (label <> " successor date mismatch for " <> securityId <> ": expected " <> show eventDate <> ", found " <> show issuance.issuance_data.date) (issuance.issuance_data.date == eventDate) - validateWarrantSuccessorTerms label sourceIssuanceData issuance.issuance_data + validateWarrantSuccessorTerms maps label sourceIssuanceData issuance.issuance_data pure issuance.issuance_data None -> do assertMsg (label <> " successor warrant issuance not found: " <> securityId) False From a068d2b58c5b2583229de2f6db08bced93a168f1 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 03:54:23 -0400 Subject: [PATCH 37/38] fix: validate reduction and successor vesting terms --- Test/daml/OpenCapTable/TestHelpers.daml | 27 +++ .../TestNonStockSecurityBalances.daml | 126 ++++++++++++- .../TestStockSecurityBalances.daml | 29 +++ .../OpenCapTable/TestWarrantCancellation.daml | 31 +--- .../OpenCapTable/TestWarrantTransfer.daml | 28 +-- .../0.0.12/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 167 ++++++++++++------ 8 files changed, 299 insertions(+), 117 deletions(-) diff --git a/Test/daml/OpenCapTable/TestHelpers.daml b/Test/daml/OpenCapTable/TestHelpers.daml index 59d6d51f..6beb5688 100644 --- a/Test/daml/OpenCapTable/TestHelpers.daml +++ b/Test/daml/OpenCapTable/TestHelpers.daml @@ -19,6 +19,7 @@ import Fairmint.OpenCapTable.Types.Stock (OcfStockClassType(..), OcfInitialShare import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) import Fairmint.OpenCapTable.Types.Conversion (OcfConvertibleType(..), OcfConversionTriggerType(..), OcfConvertibleConversionRight(..), OcfConvertibleConversionMechanism(..), OcfSAFEConversionMechanism(..)) import qualified Fairmint.OpenCapTable.CapTable as CT +import OpenCapTable.HelpersTriggers (ocfTriggerElectiveAtWill) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -242,6 +243,32 @@ warrantSuccessorIssuance issuanceId securityId stakeholderId date quantity = vesting_terms_id = None warrant_expiration_date = None +createWarrantIssuance : Party -> ContractId CT.CapTable -> Text -> Decimal -> Time -> Script CT.UpdateCapTableResult +createWarrantIssuance issuer capTableCid securityId quantity issuanceDate = + submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with + id = "TX_WARRANT_ISS_" <> securityId + date = issuanceDate + security_id = securityId + custom_id = "W-1" + stakeholder_id = "SH-1" + board_approval_date = None + stockholder_approval_date = None + consideration_text = None + security_law_exemptions = [] + quantity = Some quantity + quantity_source = None + exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + purchase_price = OcfMonetary with amount = 0.0, currency = "USD" + exercise_triggers = [ocfTriggerElectiveAtWill] + warrant_expiration_date = None + vesting_terms_id = None + vestings = [] + comments = []] + edits = [] + deletes = [] + convertibleSuccessorIssuance : Text -> Text -> Text -> Time -> Decimal -> CT.OcfCreateData convertibleSuccessorIssuance issuanceId securityId stakeholderId date investmentAmount = CT.OcfCreateConvertibleIssuance ConvertibleIssuanceOcfData with diff --git a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml index 9995a1ab..623de5da 100644 --- a/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestNonStockSecurityBalances.daml @@ -462,6 +462,17 @@ testWarrantSecurityBalance_FixedQuantityVestingTermsCannotBeReusedByPartialSucce let resultSecurityId = transferId <> "_RESULT" let balanceSecurityId = "WSEC_FIXED_VESTING_TERMS_BALANCE" let fixedTermsId = "VT_W_FIXED_QUANTITY" + let resultTermsId = "VT_W_FIXED_QUANTITY_RESULT" + let balanceTermsId = "VT_W_FIXED_QUANTITY_BALANCE" + let sourceCondition = defaultVestingCondition "start" OcfVestingStartTrigger [] + let sourceTerms = (defaultVestingTerms fixedTermsId) with + vesting_conditions = [sourceCondition] + let resultTerms = sourceTerms with + id = resultTermsId + vesting_conditions = [sourceCondition with quantity = Some 40.0] + let balanceTerms = sourceTerms with + id = balanceTermsId + vesting_conditions = [sourceCondition with quantity = Some 60.0] let fixedSource = (warrantIssuance "TX_W_FIXED_VESTING_TERMS_SOURCE" sourceSecurityId defaultStakeholderId 100.0) with vesting_terms_id = Some fixedTermsId let fixedResult = (warrantIssuance "TX_W_FIXED_VESTING_TERMS_RESULT" resultSecurityId defaultStakeholderId 40.0) with @@ -476,7 +487,7 @@ testWarrantSecurityBalance_FixedQuantityVestingTermsCannotBeReusedByPartialSucce submitMustFail issuer do exerciseCmd capTableReady CT.UpdateCapTable with creates = - [ CT.OcfCreateVestingTerms (defaultVestingTerms fixedTermsId) + [ CT.OcfCreateVestingTerms sourceTerms , CT.OcfCreateWarrantIssuance fixedSource , CT.OcfCreateWarrantIssuance fixedResult , CT.OcfCreateWarrantIssuance fixedBalance @@ -485,14 +496,43 @@ testWarrantSecurityBalance_FixedQuantityVestingTermsCannotBeReusedByPartialSucce edits = [] deletes = [] + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateVestingTerms sourceTerms + , CT.OcfCreateVestingTerms resultTerms + , CT.OcfCreateVestingTerms balanceTerms + , CT.OcfCreateWarrantIssuance fixedSource + , CT.OcfCreateWarrantIssuance (fixedResult with vesting_terms_id = Some resultTermsId) + , CT.OcfCreateWarrantIssuance (fixedBalance with vesting_terms_id = Some balanceTermsId) + , CT.OcfCreateWarrantTransfer transfer + ] + edits = [] + deletes = [] + pure () + +testWarrantSecurityBalance_ProportionalVestingTermsCanBeReusedByPartialSuccessors = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let sourceSecurityId = "WSEC_PORTION_VESTING_TERMS_SOURCE" + let transferId = "TX_W_PORTION_VESTING_TERMS_TRANSFER" + let resultSecurityId = transferId <> "_RESULT" + let balanceSecurityId = "WSEC_PORTION_VESTING_TERMS_BALANCE" let portionTermsId = "VT_W_PORTION" let portionCondition = (defaultVestingCondition "start" OcfVestingStartTrigger []) with portion = Some (OcfVestingConditionPortion with numerator = 1.0, denominator = 1.0, remainder = False) quantity = None let portionTerms = defaultVestingTermsWithConditions portionTermsId [portionCondition] - let portionSource = fixedSource with vesting_terms_id = Some portionTermsId - let portionResult = fixedResult with vesting_terms_id = Some portionTermsId - let portionBalance = fixedBalance with vesting_terms_id = Some portionTermsId + let portionSource = (warrantIssuance "TX_W_PORTION_VESTING_TERMS_SOURCE" sourceSecurityId defaultStakeholderId 100.0) with + vesting_terms_id = Some portionTermsId + let portionResult = (warrantIssuance "TX_W_PORTION_VESTING_TERMS_RESULT" resultSecurityId defaultStakeholderId 40.0) with + date = jan02 + vesting_terms_id = Some portionTermsId + let portionBalance = (warrantIssuance "TX_W_PORTION_VESTING_TERMS_BALANCE" balanceSecurityId defaultStakeholderId 60.0) with + date = jan02 + vesting_terms_id = Some portionTermsId + let transfer = (warrantTransfer transferId sourceSecurityId 40.0 jan02) with + balance_security_id = Some balanceSecurityId _ <- submit issuer do exerciseCmd capTableReady CT.UpdateCapTable with @@ -507,6 +547,44 @@ testWarrantSecurityBalance_FixedQuantityVestingTermsCannotBeReusedByPartialSucce deletes = [] pure () +testWarrantSecurityBalance_InlineVestingsOverrideReferencedTermsForSuccessors = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let sourceSecurityId = "WSEC_INLINE_AND_TERMS_SOURCE" + let transferId = "TX_W_INLINE_AND_TERMS_TRANSFER" + let resultSecurityId = transferId <> "_RESULT" + let balanceSecurityId = "WSEC_INLINE_AND_TERMS_BALANCE" + let termsId = "VT_W_IGNORED_FIXED_QUANTITY" + let sourceVesting = OcfVesting with date = jan03, amount = 100.0 + let resultVesting = OcfVesting with date = jan03, amount = 40.0 + let balanceVesting = OcfVesting with date = jan03, amount = 60.0 + let sourceIssuance = (warrantIssuance "TX_W_INLINE_AND_TERMS_SOURCE" sourceSecurityId defaultStakeholderId 100.0) with + vesting_terms_id = Some termsId + vestings = [sourceVesting] + let resultIssuance = (warrantIssuance "TX_W_INLINE_AND_TERMS_RESULT" resultSecurityId defaultStakeholderId 40.0) with + date = jan02 + vesting_terms_id = None + vestings = [resultVesting] + let balanceIssuance = (warrantIssuance "TX_W_INLINE_AND_TERMS_BALANCE" balanceSecurityId defaultStakeholderId 60.0) with + date = jan02 + vesting_terms_id = None + vestings = [balanceVesting] + let transfer = (warrantTransfer transferId sourceSecurityId 40.0 jan02) with + balance_security_id = Some balanceSecurityId + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateVestingTerms (defaultVestingTerms termsId) + , CT.OcfCreateWarrantIssuance sourceIssuance + , CT.OcfCreateWarrantIssuance resultIssuance + , CT.OcfCreateWarrantIssuance balanceIssuance + , CT.OcfCreateWarrantTransfer transfer + ] + edits = [] + deletes = [] + pure () + testWarrantSecurityBalance_UnknownQuantityTransferPreservesUnknownBalance = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableReady <- addDefaultStakeholder issuer cap_table @@ -656,6 +734,46 @@ testEquityCompensationSecurityBalance_NegativeExerciseFails = script do edits = [] deletes = [] +testNonStockSecurityBalance_NegativeReductionsCannotIncreaseBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableReady <- addDefaultStakeholder issuer cap_table + let warrantSecurityId = "WSEC_BAL_NEGATIVE_CANCEL" + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantIssuance (warrantIssuance "TX_W_BAL_NEGATIVE_CANCEL" warrantSecurityId defaultStakeholderId 100.0) + , CT.OcfCreateWarrantCancellation (warrantCancellation "TX_W_CANCEL_NEGATIVE" warrantSecurityId (-1.0) jan02) + ] + edits = [] + deletes = [] + + let equityCancellationSecurityId = "ESEC_BAL_NEGATIVE_CANCEL" + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationIssuance + (equityCompensationIssuance "TX_E_BAL_NEGATIVE_CANCEL" equityCancellationSecurityId defaultStakeholderId 100.0) + , CT.OcfCreateEquityCompensationCancellation + (equityCompensationCancellation "TX_E_CANCEL_NEGATIVE" equityCancellationSecurityId (-1.0) jan02) + ] + edits = [] + deletes = [] + + let equityReleaseSecurityId = "ESEC_BAL_NEGATIVE_RELEASE" + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationIssuance + (equityCompensationIssuance "TX_E_BAL_NEGATIVE_RELEASE" equityReleaseSecurityId defaultStakeholderId 100.0) + , CT.OcfCreateEquityCompensationRelease + ((equityCompensationRelease "TX_E_RELEASE_NEGATIVE" equityReleaseSecurityId jan02 jan02 (-1.0)) with resulting_security_ids = []) + ] + edits = [] + deletes = [] + testEquityCompensationSecurityBalance_CumulativeReductionsCannotExceedIssuedQuantity = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/Test/daml/OpenCapTable/TestStockSecurityBalances.daml b/Test/daml/OpenCapTable/TestStockSecurityBalances.daml index bd3a4e5f..bd7ce17e 100644 --- a/Test/daml/OpenCapTable/TestStockSecurityBalances.daml +++ b/Test/daml/OpenCapTable/TestStockSecurityBalances.daml @@ -100,6 +100,35 @@ testStockSecurityBalance_CancellationCannotExceedIssuedShares = script do edits = [] deletes = [] +testStockSecurityBalance_NegativeReductionsCannotIncreaseBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let cancellationSecurityId = "SEC_STOCK_BAL_NEGATIVE_CANCEL" + let cancellationIssuance = + (defaultStockIssuance "TX_STOCK_BAL_NEGATIVE_CANCEL" cancellationSecurityId stakeholderId stockClassId) with quantity = 100.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance cancellationIssuance + , CT.OcfCreateStockCancellation (stockCancellationOn "TX_CANCEL_NEGATIVE" cancellationSecurityId (-1.0) jan02) + ] + edits = [] + deletes = [] + + let repurchaseSecurityId = "SEC_STOCK_BAL_NEGATIVE_REPURCHASE" + let repurchaseIssuance = + (defaultStockIssuance "TX_STOCK_BAL_NEGATIVE_REPURCHASE" repurchaseSecurityId stakeholderId stockClassId) with quantity = 100.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance repurchaseIssuance + , CT.OcfCreateStockRepurchase (stockRepurchase "TX_REPURCHASE_NEGATIVE" repurchaseSecurityId (-1.0) jan02) + ] + edits = [] + deletes = [] + testStockSecurityBalance_CumulativeReductionsCannotExceedIssuedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/Test/daml/OpenCapTable/TestWarrantCancellation.daml b/Test/daml/OpenCapTable/TestWarrantCancellation.daml index c9702d36..1db1ba2e 100644 --- a/Test/daml/OpenCapTable/TestWarrantCancellation.daml +++ b/Test/daml/OpenCapTable/TestWarrantCancellation.daml @@ -1,42 +1,13 @@ module OpenCapTable.TestWarrantCancellation where -import Fairmint.OpenCapTable.OCF.WarrantIssuance import Fairmint.OpenCapTable.OCF.WarrantCancellation -import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) -import OpenCapTable.HelpersTriggers (ocfTriggerElectiveAtWill) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addDefaultStakeholder) +import OpenCapTable.TestHelpers (addDefaultStakeholder, createWarrantIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT import Daml.Script --- Create a warrant issuance to cancel (follows real workflow) -createWarrantIssuance issuer capTableCid securityId quantity issuanceDate = - submit issuer do - exerciseCmd capTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with - id = "TX_WARRANT_ISS_" <> securityId - date = issuanceDate - security_id = securityId - custom_id = "W-1" - stakeholder_id = "SH-1" - board_approval_date = None - stockholder_approval_date = None - consideration_text = None - security_law_exemptions = [] - quantity = Some quantity - quantity_source = None - exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") - purchase_price = OcfMonetary with amount = 0.0, currency = "USD" - exercise_triggers = [ ocfTriggerElectiveAtWill ] - warrant_expiration_date = None - vesting_terms_id = None - vestings = [] - comments = []] - edits = [] - deletes = [] - -- Create cancellation based on realistic data pattern createWarrantCancellation issuer capTableCid securityId quantity = submit issuer do diff --git a/Test/daml/OpenCapTable/TestWarrantTransfer.daml b/Test/daml/OpenCapTable/TestWarrantTransfer.daml index ae2427db..59be4b64 100644 --- a/Test/daml/OpenCapTable/TestWarrantTransfer.daml +++ b/Test/daml/OpenCapTable/TestWarrantTransfer.daml @@ -6,38 +6,12 @@ import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import OpenCapTable.HelpersTriggers (ocfTriggerElectiveAtWill) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addDefaultStakeholder) +import OpenCapTable.TestHelpers (addDefaultStakeholder, createWarrantIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT import Daml.Script --- Create a warrant issuance to transfer (follows real workflow) -createWarrantIssuance issuer capTableCid securityId quantity issuanceDate = - submit issuer do - exerciseCmd capTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with - id = "TX_WARRANT_ISS_" <> securityId - date = issuanceDate - security_id = securityId - custom_id = "W-1" - stakeholder_id = "SH-1" - board_approval_date = None - stockholder_approval_date = None - consideration_text = None - security_law_exemptions = [] - quantity = Some quantity - quantity_source = None - exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") - purchase_price = OcfMonetary with amount = 0.0, currency = "USD" - exercise_triggers = [ ocfTriggerElectiveAtWill ] - warrant_expiration_date = None - vesting_terms_id = None - vestings = [] - comments = []] - edits = [] - deletes = [] - warrantTransferSuccessorIssuance : Text -> Text -> Text -> Time -> Decimal -> CT.OcfCreateData warrantTransferSuccessorIssuance issuanceId securityId stakeholderId date quantity = CT.OcfCreateWarrantIssuance WarrantIssuanceOcfData with diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index a294cdb3..fbc8d767 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a00b5d4c1d73114903437c2ceb932ef0f862ae5ee3a1b06cc1d660355869a487 -size 3162976 +oid sha256:8bb46a77ab9697b4446a2cfbabda29ba8b40b24bec073c428155e0d62271d5fb +size 3169059 diff --git a/dars/dars.lock b/dars/dars.lock index eae746d2..0bb74939 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,8 +26,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "a00b5d4c1d73114903437c2ceb932ef0f862ae5ee3a1b06cc1d660355869a487", - "size": 3162976, + "sha256": "8bb46a77ab9697b4446a2cfbabda29ba8b40b24bec073c428155e0d62271d5fb", + "size": 3169059, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-11T06:07:06.192Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index b34fae1c..ec9aae8b 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -1697,13 +1697,19 @@ stockSecurityShareEvents eventIndex issuanceData = validateStockSecurityBalanceEvent : Text -> Decimal -> StockClassShareEvent -> Update Decimal validateStockSecurityBalanceEvent securityId current event = case event of - StockClassShareDelta with quantity_delta -> do + StockClassShareDelta with event_priority; quantity_delta -> do + assertMsg + ("Stock security reduction quantity cannot be negative for " <> securityId <> ": derived delta is " <> show quantity_delta) + (event_priority == 0 || quantity_delta <= 0.0) let next = current + quantity_delta assertMsg ("Stock security balance cannot be negative for " <> securityId <> ": applying delta " <> show quantity_delta <> " leaves " <> show next) (next >= 0.0) pure next StockClassShareTerminalDelta with quantity_delta -> do + assertMsg + ("Stock security terminal reduction quantity cannot be negative for " <> securityId <> ": derived delta is " <> show quantity_delta) + (quantity_delta <= 0.0) let next = current + quantity_delta assertMsg ("Stock security terminal reduction requires a positive balance for " <> securityId <> ": current balance is " <> show current) @@ -1945,39 +1951,50 @@ assertSecurityQuantityEventsOnOrAfter securityId earliest events = events validateSecurityQuantityBalanceEvent : Text -> Optional Decimal -> SecurityQuantityEvent -> Update (Optional Decimal) -validateSecurityQuantityBalanceEvent securityId currentOpt event = case currentOpt of - Some current -> case event of - SecurityQuantityDelta with quantity_delta -> do - let next = current + quantity_delta - assertMsg - ("Security balance cannot be negative for " <> securityId <> ": applying delta " <> show quantity_delta <> " leaves " <> show next) - (next >= 0.0) - pure (Some next) - SecurityQuantityTerminalDelta with quantity_delta -> do - let next = current + quantity_delta +validateSecurityQuantityBalanceEvent securityId currentOpt event = do + case event of + SecurityQuantityDelta with event_priority; quantity_delta -> assertMsg - ("Security terminal reduction requires a positive balance for " <> securityId <> ": current balance is " <> show current) - (current > 0.0) + ("Security reduction quantity cannot be negative for " <> securityId <> ": derived delta is " <> show quantity_delta) + (event_priority == 0 || quantity_delta <= 0.0) + SecurityQuantityTerminalDelta with quantity_delta -> assertMsg - ("Security balance cannot be negative for " <> securityId <> ": applying delta " <> show quantity_delta <> " leaves " <> show next) - (next >= 0.0) - pure (Some 0.0) - SecurityQuantityMultiplier with ratio -> - pure (Some (current * ratio.numerator / ratio.denominator)) - SecurityQuantityRetraction _ _ -> do - assertMsg - ("Security retraction requires a positive balance for " <> securityId <> ": current balance is " <> show current) - (current > 0.0) - pure (Some 0.0) - None -> case event of - SecurityQuantityDelta {} -> - pure None - SecurityQuantityTerminalDelta {} -> - pure (Some 0.0) - SecurityQuantityMultiplier {} -> - pure None - SecurityQuantityRetraction _ _ -> - pure (Some 0.0) + ("Security terminal reduction quantity cannot be negative for " <> securityId <> ": derived delta is " <> show quantity_delta) + (quantity_delta <= 0.0) + _ -> pure () + case currentOpt of + Some current -> case event of + SecurityQuantityDelta with quantity_delta -> do + let next = current + quantity_delta + assertMsg + ("Security balance cannot be negative for " <> securityId <> ": applying delta " <> show quantity_delta <> " leaves " <> show next) + (next >= 0.0) + pure (Some next) + SecurityQuantityTerminalDelta with quantity_delta -> do + let next = current + quantity_delta + assertMsg + ("Security terminal reduction requires a positive balance for " <> securityId <> ": current balance is " <> show current) + (current > 0.0) + assertMsg + ("Security balance cannot be negative for " <> securityId <> ": applying delta " <> show quantity_delta <> " leaves " <> show next) + (next >= 0.0) + pure (Some 0.0) + SecurityQuantityMultiplier with ratio -> + pure (Some (current * ratio.numerator / ratio.denominator)) + SecurityQuantityRetraction _ _ -> do + assertMsg + ("Security retraction requires a positive balance for " <> securityId <> ": current balance is " <> show current) + (current > 0.0) + pure (Some 0.0) + None -> case event of + SecurityQuantityDelta {} -> + pure None + SecurityQuantityTerminalDelta {} -> + pure (Some 0.0) + SecurityQuantityMultiplier {} -> + pure None + SecurityQuantityRetraction _ _ -> + pure (Some 0.0) validateSecurityQuantityBalance : Text -> Optional Decimal -> [SecurityQuantityEvent] -> Update () validateSecurityQuantityBalance securityId initialQuantity events = do @@ -2324,6 +2341,30 @@ vestingConditionUsesPositiveFixedQuantity condition = Some quantity -> quantity > 0.0 None -> False +scaleWarrantSuccessorVestingCondition : + Decimal -> + Decimal -> + VT.OcfVestingCondition -> + VT.OcfVestingCondition +scaleWarrantSuccessorVestingCondition sourceQuantity successorQuantity condition = + let scaledQuantity = case condition.quantity of + Some quantity -> Some (quantity * successorQuantity / sourceQuantity) + None -> None + in condition with quantity = scaledQuantity + +expectedWarrantSuccessorVestingTerms : + Text -> + Decimal -> + Decimal -> + VT.VestingTermsOcfData -> + VT.VestingTermsOcfData +expectedWarrantSuccessorVestingTerms successorTermsId sourceQuantity successorQuantity sourceTerms = + sourceTerms with + id = successorTermsId + vesting_conditions = map + (scaleWarrantSuccessorVestingCondition sourceQuantity successorQuantity) + sourceTerms.vesting_conditions + validateWarrantSuccessorVestingTerms : CapTableMaps -> Text -> @@ -2331,26 +2372,51 @@ validateWarrantSuccessorVestingTerms : WarrantIssuanceOcfData -> Update () validateWarrantSuccessorVestingTerms maps label sourceIssuanceData successorIssuanceData = - case sourceIssuanceData.vesting_terms_id of - Some vestingTermsId -> case Map.lookup vestingTermsId maps.vesting_terms of - Some vestingTermsCid -> do - vestingTerms <- fetch vestingTermsCid - if any vestingConditionUsesPositiveFixedQuantity vestingTerms.vesting_terms_data.vesting_conditions - then case (sourceIssuanceData.quantity, successorIssuanceData.quantity) of - (Some sourceQuantity, Some successorQuantity) -> - assertMsg - (label <> " successor cannot reuse fixed-quantity vesting terms for " <> successorIssuanceData.security_id <> " unless source and successor quantities are equal") - (successorQuantity == sourceQuantity) - _ -> + if not (null sourceIssuanceData.vestings) + then pure () + else case sourceIssuanceData.vesting_terms_id of + Some sourceTermsId -> case Map.lookup sourceTermsId maps.vesting_terms of + Some sourceTermsCid -> do + sourceTerms <- fetch sourceTermsCid + if any vestingConditionUsesPositiveFixedQuantity sourceTerms.vesting_terms_data.vesting_conditions + then case (sourceIssuanceData.quantity, successorIssuanceData.quantity, successorIssuanceData.vesting_terms_id) of + (Some sourceQuantity, Some successorQuantity, Some successorTermsId) -> + if sourceQuantity > 0.0 + then case Map.lookup successorTermsId maps.vesting_terms of + Some successorTermsCid -> do + successorTerms <- fetch successorTermsCid + let expectedTerms = expectedWarrantSuccessorVestingTerms + successorTermsId + sourceQuantity + successorQuantity + sourceTerms.vesting_terms_data + assertMsg + (label <> " successor fixed-quantity vesting terms are not scaled for " <> successorIssuanceData.security_id) + (successorTerms.vesting_terms_data == expectedTerms) + None -> + assertMsg + (label <> " successor warrant references missing VestingTerms: " <> successorTermsId) + False + else + assertMsg + (label <> " cannot scale fixed-quantity vesting terms from a non-positive source quantity for " <> successorIssuanceData.security_id) + False + _ -> + assertMsg + (label <> " cannot scale fixed-quantity vesting terms for successor " <> successorIssuanceData.security_id <> " without known source and successor quantities and successor terms") + False + else assertMsg - (label <> " cannot prove fixed-quantity vesting terms are preserved for successor " <> successorIssuanceData.security_id <> " without known source and successor quantities") - False - else pure () + (label <> " successor vesting terms mismatch for " <> successorIssuanceData.security_id) + (successorIssuanceData.vesting_terms_id == sourceIssuanceData.vesting_terms_id) + None -> + assertMsg + (label <> " source warrant references missing VestingTerms: " <> sourceTermsId) + False None -> assertMsg - (label <> " source warrant references missing VestingTerms: " <> vestingTermsId) - False - None -> pure () + (label <> " successor adds vesting terms not present on source warrant " <> successorIssuanceData.security_id) + (isNone successorIssuanceData.vesting_terms_id) validateWarrantSuccessorTerms : CapTableMaps -> @@ -2374,9 +2440,6 @@ validateWarrantSuccessorTerms maps label sourceIssuanceData successorIssuanceDat assertMsg (label <> " successor expiration date mismatch for " <> successorIssuanceData.security_id) (successorIssuanceData.warrant_expiration_date == sourceIssuanceData.warrant_expiration_date) - assertMsg - (label <> " successor vesting terms mismatch for " <> successorIssuanceData.security_id) - (successorIssuanceData.vesting_terms_id == sourceIssuanceData.vesting_terms_id) validateWarrantSuccessorVestingTerms maps label sourceIssuanceData successorIssuanceData case expectedWarrantSuccessorVestings sourceIssuanceData successorIssuanceData of Some expectedVestings -> From c271839b043ff31f55e56448644eccd4b9765e53 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 11:08:54 -0400 Subject: [PATCH 38/38] chore: refresh security balance DAR --- dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar index fbc8d767..2840bf0c 100644 --- a/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8bb46a77ab9697b4446a2cfbabda29ba8b40b24bec073c428155e0d62271d5fb -size 3169059 +oid sha256:a760b56e3a5e50224a956fba2b0abfbcc84b75233178cf471c290ceced290ffc +size 3169473 diff --git a/dars/dars.lock b/dars/dars.lock index c88d6216..b74e2739 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -26,10 +26,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.12/OpenCapTable-v34.dar": { - "sha256": "8bb46a77ab9697b4446a2cfbabda29ba8b40b24bec073c428155e0d62271d5fb", - "size": 3169059, + "sha256": "a760b56e3a5e50224a956fba2b0abfbcc84b75233178cf471c290ceced290ffc", + "size": 3169473, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T06:07:06.192Z", + "uploadedAt": "2026-07-11T15:08:11.000Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": {