From 43b853fdab7f46d625e606e063937244848695c1 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 18:34:23 -0400 Subject: [PATCH 01/62] Enforce stock class authorized share ceiling --- .../TestStockClassAuthorizedShares.daml | 110 ++++++++++++++++++ scripts/codegen/generate-captable.ts | 6 + .../codegen/templates/CapTable.daml.template | 106 +++++++++++++++++ .../codegen/templates/loops/create-case.daml | 6 + .../codegen/templates/loops/delete-case.daml | 4 + .../codegen/templates/loops/edit-case.daml | 13 +++ 6 files changed, 245 insertions(+) create mode 100644 Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml new file mode 100644 index 00000000..65fb0073 --- /dev/null +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -0,0 +1,110 @@ +module OpenCapTable.TestStockClassAuthorizedShares where + +import Fairmint.OpenCapTable.OCF.StockClass +import Fairmint.OpenCapTable.OCF.StockClassAuthorizedSharesAdjustment +import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..)) +import qualified Fairmint.OpenCapTable.CapTable as CT +import OpenCapTable.Setup +import OpenCapTable.TestHelpers +import Daml.Script + +limitedStockClass : Text -> Decimal -> StockClassOcfData +limitedStockClass classId authorizedShares = + (defaultStockClass classId) with + initial_shares_authorized = OcfInitialSharesNumeric authorizedShares + +stockClassAuthorizedSharesAdjustment : Text -> Text -> Decimal -> StockClassAuthorizedSharesAdjustmentOcfData +stockClassAuthorizedSharesAdjustment adjustmentId stockClassId authorizedShares = + StockClassAuthorizedSharesAdjustmentOcfData with + id = adjustmentId + date = defaultTestDate + stock_class_id = stockClassId + new_shares_authorized = authorizedShares + board_approval_date = None + stockholder_approval_date = None + comments = [] + +testStockIssuanceCannotExceedStockClassInitialAuthorizedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_LIMITED" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_LIMITED" "Limited Holder" + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_TOO_MANY" "SEC_TOO_MANY" stakeholderId stockClassId) with quantity = 101.0)] + edits = [] + deletes = [] + +testStockClassAuthorizedSharesAdjustmentAllowsAdditionalIssuance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_ADJUSTABLE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_ADJUSTABLE" "Adjustable Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_INITIAL" "SEC_INITIAL" stakeholderId stockClassId) with quantity = 100.0) + + result <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_RAISE" stockClassId 150.0)] + edits = [] + deletes = [] + + _ <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_EXTRA" "SEC_EXTRA" stakeholderId stockClassId) with quantity = 50.0) + pure () + +testStockClassAuthorizedSharesAdjustmentCannotDropBelowIssuedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_NO_DROP" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_NO_DROP" "No Drop Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_ISSUED" "SEC_ISSUED" stakeholderId stockClassId) with quantity = 75.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_DROP" stockClassId 50.0)] + edits = [] + deletes = [] + +testStockClassEditCannotDropInitialAuthorizedSharesBelowIssuedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EDIT" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EDIT" "Edit Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_EDIT_ISSUED" "SEC_EDIT_ISSUED" stakeholderId stockClassId) with quantity = 75.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockClass (limitedStockClass stockClassId 50.0)] + deletes = [] + +testStockClassAuthorizedSharesAdjustmentDeleteCannotInvalidateIssuedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_DELETE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_DELETE" "Delete Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_DELETE" stockClassId 150.0)] + edits = [] + deletes = [] + + capTableIssued <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_DELETE_ISSUED" "SEC_DELETE_ISSUED" stakeholderId stockClassId) with quantity = 125.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockClassAuthorizedSharesAdjustment "TX_AUTH_DELETE"] + +testUnlimitedStockClassAuthorizedSharesAllowsLargeIssuance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + let unlimitedClass = (defaultStockClass "SC_UNLIMITED") with + initial_shares_authorized = OcfInitialSharesEnum OcfAuthorizedSharesUnlimited + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table unlimitedClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_UNLIMITED" "Unlimited Holder" + + _ <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_UNLIMITED" "SEC_UNLIMITED" stakeholderId stockClassId) with quantity = 2_000_000.0) + pure () + diff --git a/scripts/codegen/generate-captable.ts b/scripts/codegen/generate-captable.ts index ccd9d1b0..21c5f8ed 100644 --- a/scripts/codegen/generate-captable.ts +++ b/scripts/codegen/generate-captable.ts @@ -51,6 +51,9 @@ interface TypeDef { map_field: string; tier: number; validations: Validation[]; + validates_stock_class_initial_authorized_shares: boolean; + validates_stock_class_authorized_adjustment: boolean; + validates_stock_issuance_authorized_shares: boolean; // For issuance types, the name of the security_id index map (e.g., 'stock_issuances_by_security_id') security_id_index_map: string | null; } @@ -219,6 +222,9 @@ function discoverTypes(config: Config): TypeDef[] { error: `${toTitleCase(fieldSpec)} not found`, }; }), + validates_stock_class_initial_authorized_shares: name === 'StockClass', + validates_stock_class_authorized_adjustment: name === 'StockClassAuthorizedSharesAdjustment', + validates_stock_issuance_authorized_shares: name === 'StockIssuance', security_id_index_map: SECURITY_ID_INDEX_MAPS[name] ?? null, }; diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 6234f4b3..3619a830 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -18,6 +18,7 @@ import DA.Foldable (mapA_) import DA.Optional (isNone, isSome) import Fairmint.OpenCapTable.Types.Core (Context) +import Fairmint.OpenCapTable.Types.Stock (OcfInitialSharesAuthorized(..)) import Fairmint.OpenCapTable.OCF.Issuer (Issuer(..), IssuerOcfData) -- OCF Types @@ -98,6 +99,111 @@ toMaps ct = CapTableMaps with stock_issuances_by_security_id = ct.stock_issuances_by_security_id warrant_issuances_by_security_id = ct.warrant_issuances_by_security_id +authorizedSharesLimit : OcfInitialSharesAuthorized -> Optional Decimal +authorizedSharesLimit auth = case auth of + OcfInitialSharesNumeric n -> Some n + OcfInitialSharesEnum _ -> None + +latestStockClassAuthorizedAdjustment : [StockClassAuthorizedSharesAdjustmentOcfData] -> Optional StockClassAuthorizedSharesAdjustmentOcfData +latestStockClassAuthorizedAdjustment adjustments = + foldl + (\latest adjustment -> case latest of + None -> Some adjustment + Some current -> if adjustment.date >= current.date then Some adjustment else latest) + None + adjustments + +fetchStockClassData : CapTableMaps -> Text -> Optional StockClassOcfData -> Update StockClassOcfData +fetchStockClassData maps stockClassId stockClassOverride = case stockClassOverride of + Some stockClassData -> pure stockClassData + None -> do + let stockClassCidOpt = Map.lookup stockClassId maps.stock_classes + assertMsg ("Stock class not found: " <> stockClassId) (isSome stockClassCidOpt) + let Some stockClassCid = stockClassCidOpt + stockClass <- fetch stockClassCid + pure stockClass.stock_class_data + +fetchStockClassAuthorizedAdjustments : + CapTableMaps -> + Text -> + Optional Text -> + Optional StockClassAuthorizedSharesAdjustmentOcfData -> + Update [StockClassAuthorizedSharesAdjustmentOcfData] +fetchStockClassAuthorizedAdjustments maps stockClassId excludedAdjustmentId additionalAdjustment = do + baseAdjustments <- foldlA + (\adjustments adjustmentCid -> do + adjustment <- fetch adjustmentCid + let isExcluded = case excludedAdjustmentId of + Some excludedId -> adjustment.adjustment_data.id == excludedId + None -> False + if adjustment.adjustment_data.stock_class_id == stockClassId && not isExcluded + then pure (adjustment.adjustment_data :: adjustments) + else pure adjustments) + [] + (Map.values maps.stock_class_authorized_shares_adjustments) + + case additionalAdjustment of + Some adjustment -> + if adjustment.stock_class_id == stockClassId + then pure (adjustment :: baseAdjustments) + else pure baseAdjustments + None -> pure baseAdjustments + +currentStockClassAuthorizedLimit : + CapTableMaps -> + Text -> + Optional Text -> + Optional StockClassOcfData -> + Optional StockClassAuthorizedSharesAdjustmentOcfData -> + Update (Optional Decimal) +currentStockClassAuthorizedLimit maps stockClassId excludedAdjustmentId stockClassOverride additionalAdjustment = do + stockClassData <- fetchStockClassData maps stockClassId stockClassOverride + adjustments <- fetchStockClassAuthorizedAdjustments maps stockClassId excludedAdjustmentId additionalAdjustment + let latestAdjustment = latestStockClassAuthorizedAdjustment adjustments + pure $ case latestAdjustment of + Some adjustment -> Some adjustment.new_shares_authorized + None -> authorizedSharesLimit stockClassData.initial_shares_authorized + +stockClassIssuedQuantity : CapTableMaps -> Text -> Optional Text -> Update Decimal +stockClassIssuedQuantity maps stockClassId excludedIssuanceId = + foldlA + (\total issuanceCid -> do + issuance <- fetch issuanceCid + let isExcluded = case excludedIssuanceId of + Some excludedId -> issuance.issuance_data.id == excludedId + None -> False + if issuance.issuance_data.stock_class_id == stockClassId && not isExcluded + then pure (total + issuance.issuance_data.quantity) + else pure total) + 0.0 + (Map.values maps.stock_issuances) + +assertWithinStockClassAuthorizedShares : Text -> Decimal -> Optional Decimal -> Update () +assertWithinStockClassAuthorizedShares stockClassId issuedQuantity limitOpt = case limitOpt of + Some limit -> + assertMsg + ("Stock class authorized shares exceeded for " <> stockClassId <> ": issued " <> show issuedQuantity <> " exceeds authorized " <> show limit) + (issuedQuantity <= limit) + None -> pure () + +validateStockIssuanceAuthorizedShares : CapTableMaps -> StockIssuanceOcfData -> Optional Text -> Update () +validateStockIssuanceAuthorizedShares maps issuanceData excludedIssuanceId = do + currentIssued <- stockClassIssuedQuantity maps issuanceData.stock_class_id excludedIssuanceId + limitOpt <- currentStockClassAuthorizedLimit maps issuanceData.stock_class_id None None None + assertWithinStockClassAuthorizedShares issuanceData.stock_class_id (currentIssued + issuanceData.quantity) limitOpt + +validateCurrentStockClassAuthorizedShares : + CapTableMaps -> + Text -> + Optional Text -> + Optional StockClassOcfData -> + Optional StockClassAuthorizedSharesAdjustmentOcfData -> + Update () +validateCurrentStockClassAuthorizedShares maps stockClassId excludedAdjustmentId stockClassOverride additionalAdjustment = do + currentIssued <- stockClassIssuedQuantity maps stockClassId None + limitOpt <- currentStockClassAuthorizedLimit maps stockClassId excludedAdjustmentId stockClassOverride additionalAdjustment + assertWithinStockClassAuthorizedShares stockClassId currentIssued limitOpt + -- | Get the processing tier for a create operation getCreateTier : OcfCreateData -> Int getCreateTier createData = case createData of diff --git a/scripts/codegen/templates/loops/create-case.daml b/scripts/codegen/templates/loops/create-case.daml index d40a429c..2af254ac 100644 --- a/scripts/codegen/templates/loops/create-case.daml +++ b/scripts/codegen/templates/loops/create-case.daml @@ -17,6 +17,12 @@ OcfCreate{{name}} d -> do {{/if}} {{/if}} {{/each}} +{{#if validates_stock_issuance_authorized_shares}} + validateStockIssuanceAuthorizedShares maps d None +{{/if}} +{{#if validates_stock_class_authorized_adjustment}} + validateCurrentStockClassAuthorizedShares maps d.stock_class_id None None (Some d) +{{/if}} cid <- create {{name}} with context = ctx, {{data_param}} = d {{#if security_id_index_map}} let newMaps = maps with diff --git a/scripts/codegen/templates/loops/delete-case.daml b/scripts/codegen/templates/loops/delete-case.daml index 7d59431c..5f3327ea 100644 --- a/scripts/codegen/templates/loops/delete-case.daml +++ b/scripts/codegen/templates/loops/delete-case.daml @@ -13,6 +13,10 @@ OcfDelete{{name}} delId -> do {{map_field}} = Map.delete delId maps.{{map_field}} {{security_id_index_map}} = Map.delete securityId maps.{{security_id_index_map}}) {{else}} +{{#if validates_stock_class_authorized_adjustment}} + oldContract <- fetch oldCid + validateCurrentStockClassAuthorizedShares maps oldContract.{{data_param}}.stock_class_id (Some oldContract.{{data_param}}.id) None None +{{/if}} archive oldCid pure (maps with {{map_field}} = Map.delete delId maps.{{map_field}}) {{/if}} diff --git a/scripts/codegen/templates/loops/edit-case.daml b/scripts/codegen/templates/loops/edit-case.daml index 73321301..895b4cf1 100644 --- a/scripts/codegen/templates/loops/edit-case.daml +++ b/scripts/codegen/templates/loops/edit-case.daml @@ -4,6 +4,9 @@ OcfEdit{{name}} d -> do let oldCidOpt = Map.lookup d.id maps.{{map_field}} assertMsg "{{name}} not found" (isSome oldCidOpt) let Some oldCid = oldCidOpt +{{#if validates_stock_class_authorized_adjustment}} + oldAdjustment <- fetch oldCid +{{/if}} {{#each validations}} {{#if is_array}} -- Validate each element in array field {{field}} @@ -16,9 +19,19 @@ OcfEdit{{name}} d -> do {{/if}} {{/if}} {{/each}} +{{#if validates_stock_class_initial_authorized_shares}} + validateCurrentStockClassAuthorizedShares maps d.id None (Some d) None +{{/if}} +{{#if validates_stock_class_authorized_adjustment}} + validateCurrentStockClassAuthorizedShares maps oldAdjustment.{{data_param}}.stock_class_id (Some oldAdjustment.{{data_param}}.id) None (Some d) + validateCurrentStockClassAuthorizedShares maps d.stock_class_id (Some oldAdjustment.{{data_param}}.id) None (Some d) +{{/if}} {{#if security_id_index_map}} -- Fetch old contract to get old security_id for index map cleanup oldContract <- fetch oldCid +{{#if validates_stock_issuance_authorized_shares}} + validateStockIssuanceAuthorizedShares maps d (Some oldContract.{{data_param}}.id) +{{/if}} let oldSecurityId = oldContract.{{data_param}}.security_id -- Only check uniqueness if security_id is changing when (d.security_id /= oldSecurityId) do From 76fb636f4ece2756f7313649d645830afc0dddcd Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 19:24:16 -0400 Subject: [PATCH 02/62] Update DAR backup for stock class share ceiling --- dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar index 68074446..4e44774f 100644 --- a/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ce462b8afcb1bcdbe351c56b0a47de478477f3c655da00e41cb397f63e997d6 -size 2446238 +oid sha256:5a83b3fdecb2a6796ff63f353c06c9c13d171062a2a9afa3894f789f18a4fa72 +size 2462750 diff --git a/dars/dars.lock b/dars/dars.lock index 9bf4ac54..9dfea42f 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -12,8 +12,8 @@ ] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { - "sha256": "2ce462b8afcb1bcdbe351c56b0a47de478477f3c655da00e41cb397f63e997d6", - "size": 2446238, + "sha256": "5a83b3fdecb2a6796ff63f353c06c9c13d171062a2a9afa3894f789f18a4fa72", + "size": 2462750, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-07T21:50:59.634Z", "networks": [] From 8d13eadf573a1628bb7e987feee8ee6ed14d9eaf Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 20:45:53 -0400 Subject: [PATCH 03/62] Combine authorized share ceiling enforcement --- OpenCapTable-v34/daml.yaml | 2 +- Test/daml.yaml | 2 +- Test/daml/OpenCapTable/Setup.daml | 2 +- Test/daml/OpenCapTable/TestHelpers.daml | 18 + .../TestIssuerAuthorizedShares.daml | 143 ++++++++ .../TestStockClassAuthorizedShares.daml | 184 +++++++++- .../0.0.2/OpenCapTable-v34.dar | 4 +- .../0.0.3/OpenCapTable-v34.dar | 3 + dars/dars.lock | 11 +- .../codegen/templates/CapTable.daml.template | 334 +++++++++++++----- .../codegen/templates/loops/create-case.daml | 6 - .../codegen/templates/loops/delete-case.daml | 4 - .../codegen/templates/loops/edit-case.daml | 13 - 13 files changed, 609 insertions(+), 117 deletions(-) create mode 100644 Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml create mode 100644 dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar diff --git a/OpenCapTable-v34/daml.yaml b/OpenCapTable-v34/daml.yaml index 6d6d6797..2403aff1 100644 --- a/OpenCapTable-v34/daml.yaml +++ b/OpenCapTable-v34/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.4.10 name: OpenCapTable-v34 source: daml -version: 0.0.2 +version: 0.0.3 build-options: - -Wno-crypto-text-is-alpha - --typecheck-upgrades=no diff --git a/Test/daml.yaml b/Test/daml.yaml index cdf173c0..151e762a 100644 --- a/Test/daml.yaml +++ b/Test/daml.yaml @@ -7,6 +7,6 @@ dependencies: - daml-stdlib - daml-script data-dependencies: - - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.2.dar + - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.3.dar build-options: - -Wno-template-interface-depends-on-daml-script diff --git a/Test/daml/OpenCapTable/Setup.daml b/Test/daml/OpenCapTable/Setup.daml index 764e6754..246c5146 100644 --- a/Test/daml/OpenCapTable/Setup.daml +++ b/Test/daml/OpenCapTable/Setup.daml @@ -54,7 +54,7 @@ setupTestOcp = script do email = None phone = None address = None - initial_shares_authorized = Some (OcfInitialSharesNumeric 1000.0) + initial_shares_authorized = Some (OcfInitialSharesNumeric 100_000_000.0) comments = [] return TestOcp diff --git a/Test/daml/OpenCapTable/TestHelpers.daml b/Test/daml/OpenCapTable/TestHelpers.daml index 1e4db3a0..a882b9bc 100644 --- a/Test/daml/OpenCapTable/TestHelpers.daml +++ b/Test/daml/OpenCapTable/TestHelpers.daml @@ -1,6 +1,7 @@ module OpenCapTable.TestHelpers where import DA.Assert (assertMsg) +import Fairmint.OpenCapTable.OCF.Issuer import Fairmint.OpenCapTable.OCF.Stakeholder import Fairmint.OpenCapTable.OCF.StockClass import Fairmint.OpenCapTable.OCF.StockIssuance (StockIssuanceOcfData(..)) @@ -24,6 +25,23 @@ import DA.Time (time) -- Functions that return valid default OCF data with sensible defaults. -- Use these to reduce boilerplate in tests that don't care about specific values. +-- | Returns valid IssuerOcfData with sensible defaults. +defaultIssuerWithAuthorizedShares : Text -> Optional OcfInitialSharesAuthorized -> IssuerOcfData +defaultIssuerWithAuthorizedShares issuerId initialSharesAuthorized = IssuerOcfData with + id = issuerId + legal_name = "TestIssuer Inc." + formation_date = DT.time (DA.date 2020 Jan 15) 12 0 0 + country_of_formation = "US" + dba = None + country_subdivision_of_formation = None + country_subdivision_name_of_formation = None + tax_ids = [] + email = None + phone = None + address = None + initial_shares_authorized = initialSharesAuthorized + comments = [] + -- | Default initial shares authorized for test stock classes. -- Configurable constant to avoid magic numbers. defaultInitialSharesAuthorized : Decimal diff --git a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml new file mode 100644 index 00000000..5198974b --- /dev/null +++ b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml @@ -0,0 +1,143 @@ +module OpenCapTable.TestIssuerAuthorizedShares where + +import Fairmint.OpenCapTable.OCF.IssuerAuthorizedSharesAdjustment +import Fairmint.OpenCapTable.OCF.StockClass +import Fairmint.OpenCapTable.OCF.StockClassAuthorizedSharesAdjustment +import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..)) +import qualified Fairmint.OpenCapTable.CapTable as CT +import OpenCapTable.Setup +import OpenCapTable.TestHelpers +import Daml.Script + +limitedStockClass : Text -> Decimal -> StockClassOcfData +limitedStockClass classId authorizedShares = + (defaultStockClass classId) with + initial_shares_authorized = OcfInitialSharesNumeric authorizedShares + +issuerAuthorizedSharesAdjustment : Text -> Decimal -> IssuerAuthorizedSharesAdjustmentOcfData +issuerAuthorizedSharesAdjustment adjustmentId sharesAuthorized = + IssuerAuthorizedSharesAdjustmentOcfData with + id = adjustmentId + date = defaultTestDate + issuer_id = "ISSUER_1" + new_shares_authorized = sharesAuthorized + board_approval_date = None + stockholder_approval_date = None + comments = [] + +stockClassAuthorizedSharesAdjustment : Text -> Text -> Decimal -> StockClassAuthorizedSharesAdjustmentOcfData +stockClassAuthorizedSharesAdjustment adjustmentId stockClassId authorizedShares = + StockClassAuthorizedSharesAdjustmentOcfData with + id = adjustmentId + date = defaultTestDate + stock_class_id = stockClassId + new_shares_authorized = authorizedShares + board_approval_date = None + stockholder_approval_date = None + comments = [] + +setIssuerAuthorizedShares : Party -> ContractId CT.CapTable -> Decimal -> Script (ContractId CT.CapTable) +setIssuerAuthorizedShares issuer capTable sharesAuthorized = do + result <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditIssuer (defaultIssuerWithAuthorizedShares "ISSUER_1" (Some (OcfInitialSharesNumeric sharesAuthorized)))] + deletes = [] + pure result.updatedCapTableCid + +testStockClassCannotExceedIssuerInitialAuthorizedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + + submitMustFail issuer do + exerciseCmd capTableLimited CT.UpdateCapTable with + creates = [CT.OcfCreateStockClass (limitedStockClass "SC_TOO_MANY_FOR_ISSUER" 101.0)] + edits = [] + deletes = [] + +testIssuerAuthorizedSharesAdjustmentAllowsSameBatchStockClassIncrease = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + + _ <- submit issuer do + exerciseCmd capTableLimited CT.UpdateCapTable with + creates = + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_FOR_CLASS" 150.0) + , CT.OcfCreateStockClass (limitedStockClass "SC_ALLOWED_BY_ISSUER_RAISE" 150.0) + ] + edits = [] + deletes = [] + pure () + +testIssuerAuthorizedSharesAdjustmentCannotDropBelowStockClasses = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + (capTableWithClass, _) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_ISSUER_DROP" 100.0) + + submitMustFail issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with + creates = [CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_DROP_TOO_LOW" 99.0)] + edits = [] + deletes = [] + +testIssuerEditCannotDropBelowStockClasses = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + (capTableWithClass, _) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_ISSUER_EDIT_DROP" 100.0) + + submitMustFail issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditIssuer (defaultIssuerWithAuthorizedShares "ISSUER_1" (Some (OcfInitialSharesNumeric 99.0)))] + deletes = [] + +testStockClassAuthorizedSharesAdjustmentCannotExceedIssuerAuthorizedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + (capTableWithClass, stockClassId) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_CLASS_RAISE_TOO_HIGH" 100.0) + + submitMustFail issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_CLASS_RAISE_TOO_HIGH" stockClassId 101.0)] + edits = [] + deletes = [] + +testIssuerAuthorizedSharesAdjustmentDeleteCannotInvalidateStockClasses = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + (capTableWithClass, stockClassId) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_ISSUER_DELETE_DROP" 100.0) + result <- submit issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with + creates = + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_DELETE" 150.0) + , CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_CLASS_RAISE_FOR_DELETE" stockClassId 150.0) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteIssuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_DELETE"] + +testFiniteIssuerRejectsUnlimitedStockClassAuthorization = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + let unlimitedClass = (defaultStockClass "SC_UNLIMITED_ISSUER_REJECT") with + initial_shares_authorized = OcfInitialSharesEnum OcfAuthorizedSharesUnlimited + + submitMustFail issuer do + exerciseCmd capTableLimited CT.UpdateCapTable with + creates = [CT.OcfCreateStockClass unlimitedClass] + edits = [] + deletes = [] + +testFiniteIssuerAllowsNotApplicableStockClassAuthorization = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + let notApplicableClass = (defaultStockClass "SC_NOT_APPLICABLE_ISSUER") with + initial_shares_authorized = OcfInitialSharesEnum OcfAuthorizedSharesNotApplicable + + _ <- setupStockClassWith issuer capTableLimited notApplicableClass + pure () diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 65fb0073..06e52080 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -2,7 +2,12 @@ module OpenCapTable.TestStockClassAuthorizedShares where import Fairmint.OpenCapTable.OCF.StockClass import Fairmint.OpenCapTable.OCF.StockClassAuthorizedSharesAdjustment -import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..)) +import Fairmint.OpenCapTable.OCF.StockCancellation +import Fairmint.OpenCapTable.OCF.StockClassSplit +import Fairmint.OpenCapTable.OCF.StockRepurchase +import Fairmint.OpenCapTable.OCF.StockRetraction +import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup import OpenCapTable.TestHelpers @@ -24,6 +29,47 @@ stockClassAuthorizedSharesAdjustment adjustmentId stockClassId authorizedShares stockholder_approval_date = None comments = [] +stockCancellation : Text -> Text -> Decimal -> StockCancellationOcfData +stockCancellation cancellationId securityId quantity = + StockCancellationOcfData with + id = cancellationId + date = defaultTestDate + security_id = securityId + quantity = quantity + balance_security_id = None + reason_text = "Cancelled shares" + comments = [] + +stockRepurchase : Text -> Text -> Decimal -> StockRepurchaseOcfData +stockRepurchase repurchaseId securityId quantity = + StockRepurchaseOcfData with + id = repurchaseId + date = defaultTestDate + security_id = securityId + price = OcfMonetary with amount = 1.0, currency = "USD" + quantity = quantity + balance_security_id = None + consideration_text = None + comments = [] + +stockRetraction : Text -> Text -> StockRetractionOcfData +stockRetraction retractionId securityId = + StockRetractionOcfData with + id = retractionId + date = defaultTestDate + security_id = securityId + reason_text = "Retracted stock issuance" + comments = [] + +stockClassSplit : Text -> Text -> Decimal -> Decimal -> StockClassSplitOcfData +stockClassSplit splitId stockClassId numerator denominator = + StockClassSplitOcfData with + id = splitId + date = defaultTestDate + stock_class_id = stockClassId + split_ratio = OcfRatio with numerator = numerator, denominator = denominator + comments = [] + testStockIssuanceCannotExceedStockClassInitialAuthorizedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -36,6 +82,20 @@ testStockIssuanceCannotExceedStockClassInitialAuthorizedShares = script do edits = [] deletes = [] +testStockIssuanceEditCannotExceedStockClassAuthorizedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EDIT_ISSUANCE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EDIT_ISSUANCE" "Edit Issuance Holder" + let originalIssuance = (defaultStockIssuance "TX_EDIT_ISSUANCE" "SEC_EDIT_ISSUANCE" stakeholderId stockClassId) with quantity = 75.0 + capTableIssued <- createStockIssuanceWith issuer capTableReady originalIssuance + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockIssuance (originalIssuance with quantity = 101.0)] + deletes = [] + testStockClassAuthorizedSharesAdjustmentAllowsAdditionalIssuance = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -97,14 +157,132 @@ testStockClassAuthorizedSharesAdjustmentDeleteCannotInvalidateIssuedShares = scr edits = [] deletes = [CT.OcfDeleteStockClassAuthorizedSharesAdjustment "TX_AUTH_DELETE"] +testStockClassAuthorizedSharesReplacementAllowedInSameBatch = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REPLACE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_REPLACE" "Replacement Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_REPLACE_OLD" stockClassId 150.0)] + edits = [] + deletes = [] + + capTableIssued <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_REPLACE_ISSUED" "SEC_REPLACE_ISSUED" stakeholderId stockClassId) with quantity = 125.0) + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_REPLACE_NEW" stockClassId 200.0)] + edits = [] + deletes = [CT.OcfDeleteStockClassAuthorizedSharesAdjustment "TX_AUTH_REPLACE_OLD"] + pure () + +testSameDateLowerStockClassAuthorizedSharesAdjustmentWins = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_SAME_DATE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_SAME_DATE" "Same Date Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_SAME_DATE_HIGH" stockClassId 200.0) + , CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_SAME_DATE_LOW" stockClassId 100.0) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_SAME_DATE_ISSUED" "SEC_SAME_DATE_ISSUED" stakeholderId stockClassId) with quantity = 150.0)] + edits = [] + deletes = [] + +testStockClassSplitCannotLeaveIssuedSharesAboveAuthorizedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_SPLIT" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_SPLIT" "Split Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_SPLIT_ISSUED" "SEC_SPLIT_ISSUED" stakeholderId stockClassId) with quantity = 100.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassSplit (stockClassSplit "TX_SPLIT_TOO_MANY" stockClassId 2.0 1.0)] + edits = [] + deletes = [] + +testStockCancellationReducesIssuedSharesForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CANCEL" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CANCEL" "Cancellation Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CANCEL_ISSUED" "SEC_CANCEL_ISSUED" stakeholderId stockClassId) with quantity = 100.0) + result <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockCancellation (stockCancellation "TX_CANCEL_REDUCE" "SEC_CANCEL_ISSUED" 50.0)] + edits = [] + deletes = [] + + _ <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_CANCEL_REISSUE" "SEC_CANCEL_REISSUE" stakeholderId stockClassId) with quantity = 50.0) + pure () + +testStockRepurchaseReducesIssuedSharesForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REPURCHASE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_REPURCHASE" "Repurchase Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_REPURCHASE_ISSUED" "SEC_REPURCHASE_ISSUED" stakeholderId stockClassId) with quantity = 100.0) + result <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockRepurchase (stockRepurchase "TX_REPURCHASE_REDUCE" "SEC_REPURCHASE_ISSUED" 50.0)] + edits = [] + deletes = [] + + _ <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_REPURCHASE_REISSUE" "SEC_REPURCHASE_REISSUE" stakeholderId stockClassId) with quantity = 50.0) + pure () + +testStockRetractionReducesIssuedSharesForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_RETRACTION" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_RETRACTION" "Retraction Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_RETRACTION_ISSUED" "SEC_RETRACTION_ISSUED" stakeholderId stockClassId) with quantity = 100.0) + result <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockRetraction (stockRetraction "TX_RETRACTION_REDUCE" "SEC_RETRACTION_ISSUED")] + edits = [] + deletes = [] + + _ <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_RETRACTION_REISSUE" "SEC_RETRACTION_REISSUE" stakeholderId stockClassId) with quantity = 100.0) + pure () + +testStockClassAuthorizedSharesAdjustmentCreatedWithIssuanceEditInSameBatchSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EDIT_AND_ADJUST" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EDIT_AND_ADJUST" "Edit And Adjust Holder" + let originalIssuance = (defaultStockIssuance "TX_EDIT_AND_ADJUST" "SEC_EDIT_AND_ADJUST" stakeholderId stockClassId) with quantity = 100.0 + capTableIssued <- createStockIssuanceWith issuer capTableReady originalIssuance + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_FOR_EDIT" stockClassId 150.0)] + edits = [CT.OcfEditStockIssuance (originalIssuance with quantity = 125.0)] + deletes = [] + pure () + testUnlimitedStockClassAuthorizedSharesAllowsLargeIssuance = script do TestOcp{issuer, cap_table} <- setupTestOcp + issuerResult <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditIssuer (defaultIssuerWithAuthorizedShares "ISSUER_1" None)] + deletes = [] + let unlimitedClass = (defaultStockClass "SC_UNLIMITED") with initial_shares_authorized = OcfInitialSharesEnum OcfAuthorizedSharesUnlimited - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table unlimitedClass + (capTableWithClass, stockClassId) <- setupStockClassWith issuer issuerResult.updatedCapTableCid unlimitedClass (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_UNLIMITED" "Unlimited Holder" _ <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_UNLIMITED" "SEC_UNLIMITED" stakeholderId stockClassId) with quantity = 2_000_000.0) pure () - diff --git a/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar index 4e44774f..68074446 100644 --- a/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a83b3fdecb2a6796ff63f353c06c9c13d171062a2a9afa3894f789f18a4fa72 -size 2462750 +oid sha256:2ce462b8afcb1bcdbe351c56b0a47de478477f3c655da00e41cb397f63e997d6 +size 2446238 diff --git a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar new file mode 100644 index 00000000..aa9e8b20 --- /dev/null +++ b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce8a0a1860edb50f39bb29377fb0ecf4afa3c2bfc91a60962315bdc7e443e942 +size 2485461 diff --git a/dars/dars.lock b/dars/dars.lock index 9dfea42f..da382610 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -12,11 +12,18 @@ ] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { - "sha256": "5a83b3fdecb2a6796ff63f353c06c9c13d171062a2a9afa3894f789f18a4fa72", - "size": 2462750, + "sha256": "2ce462b8afcb1bcdbe351c56b0a47de478477f3c655da00e41cb397f63e997d6", + "size": 2446238, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-07T21:50:59.634Z", "networks": [] + }, + "OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar": { + "sha256": "ce8a0a1860edb50f39bb29377fb0ecf4afa3c2bfc91a60962315bdc7e443e942", + "size": 2485461, + "sdkVersion": "3.4.10", + "uploadedAt": "2026-07-09T00:41:37.578Z", + "networks": [] } } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 3619a830..6cb1c065 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -18,7 +18,7 @@ import DA.Foldable (mapA_) import DA.Optional (isNone, isSome) import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Stock (OcfInitialSharesAuthorized(..)) +import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..)) import Fairmint.OpenCapTable.OCF.Issuer (Issuer(..), IssuerOcfData) -- OCF Types @@ -99,84 +99,208 @@ toMaps ct = CapTableMaps with stock_issuances_by_security_id = ct.stock_issuances_by_security_id warrant_issuances_by_security_id = ct.warrant_issuances_by_security_id +data AuthorizedSharesCeiling + = AuthorizedSharesFinite Decimal + | AuthorizedSharesUnlimited + | AuthorizedSharesNotApplicable + deriving (Eq, Show) + +data StockClassShareEvent + = StockClassShareDelta with + event_date: Time + event_priority: Int + quantity_delta: Decimal + | StockClassShareMultiplier with + event_date: Time + event_priority: Int + ratio: OcfRatio + | StockClassShareRetraction with + event_date: Time + event_priority: Int + deriving (Eq, Show) + +authorizedSharesCeiling : OcfInitialSharesAuthorized -> AuthorizedSharesCeiling +authorizedSharesCeiling auth = case auth of + OcfInitialSharesNumeric n -> AuthorizedSharesFinite n + OcfInitialSharesEnum OcfAuthorizedSharesUnlimited -> AuthorizedSharesUnlimited + OcfInitialSharesEnum OcfAuthorizedSharesNotApplicable -> AuthorizedSharesNotApplicable + authorizedSharesLimit : OcfInitialSharesAuthorized -> Optional Decimal -authorizedSharesLimit auth = case auth of - OcfInitialSharesNumeric n -> Some n - OcfInitialSharesEnum _ -> None - -latestStockClassAuthorizedAdjustment : [StockClassAuthorizedSharesAdjustmentOcfData] -> Optional StockClassAuthorizedSharesAdjustmentOcfData -latestStockClassAuthorizedAdjustment adjustments = - foldl - (\latest adjustment -> case latest of - None -> Some adjustment - Some current -> if adjustment.date >= current.date then Some adjustment else latest) - None - adjustments - -fetchStockClassData : CapTableMaps -> Text -> Optional StockClassOcfData -> Update StockClassOcfData -fetchStockClassData maps stockClassId stockClassOverride = case stockClassOverride of - Some stockClassData -> pure stockClassData - None -> do - let stockClassCidOpt = Map.lookup stockClassId maps.stock_classes - assertMsg ("Stock class not found: " <> stockClassId) (isSome stockClassCidOpt) - let Some stockClassCid = stockClassCidOpt - stockClass <- fetch stockClassCid - pure stockClass.stock_class_data - -fetchStockClassAuthorizedAdjustments : - CapTableMaps -> - Text -> - Optional Text -> - Optional StockClassAuthorizedSharesAdjustmentOcfData -> - Update [StockClassAuthorizedSharesAdjustmentOcfData] -fetchStockClassAuthorizedAdjustments maps stockClassId excludedAdjustmentId additionalAdjustment = do - baseAdjustments <- foldlA - (\adjustments adjustmentCid -> do +authorizedSharesLimit auth = case authorizedSharesCeiling auth of + AuthorizedSharesFinite n -> Some n + _ -> None + +issuerAuthorizedSharesLimit : Optional OcfInitialSharesAuthorized -> Optional Decimal +issuerAuthorizedSharesLimit initialSharesAuthorized = + authorizedSharesLimit =<< initialSharesAuthorized + +stockClassAuthorizedAdjustmentTakesPrecedence : + StockClassAuthorizedSharesAdjustmentOcfData -> + StockClassAuthorizedSharesAdjustmentOcfData -> + Bool +stockClassAuthorizedAdjustmentTakesPrecedence candidate current = + candidate.date > current.date || + (candidate.date == current.date && candidate.new_shares_authorized < current.new_shares_authorized) + +issuerAuthorizedAdjustmentTakesPrecedence : + IssuerAuthorizedSharesAdjustmentOcfData -> + IssuerAuthorizedSharesAdjustmentOcfData -> + Bool +issuerAuthorizedAdjustmentTakesPrecedence candidate current = + candidate.date > current.date || + (candidate.date == current.date && candidate.new_shares_authorized < current.new_shares_authorized) + +stockClassAuthorizedAdjustmentIndex : CapTableMaps -> Update (Map Text StockClassAuthorizedSharesAdjustmentOcfData) +stockClassAuthorizedAdjustmentIndex maps = + foldlA + (\index adjustmentCid -> do adjustment <- fetch adjustmentCid - let isExcluded = case excludedAdjustmentId of - Some excludedId -> adjustment.adjustment_data.id == excludedId - None -> False - if adjustment.adjustment_data.stock_class_id == stockClassId && not isExcluded - then pure (adjustment.adjustment_data :: adjustments) - else pure adjustments) - [] + let stockClassId = adjustment.adjustment_data.stock_class_id + let nextIndex = case Map.lookup stockClassId index of + Some current -> + if stockClassAuthorizedAdjustmentTakesPrecedence adjustment.adjustment_data current + then Map.insert stockClassId adjustment.adjustment_data index + else index + None -> Map.insert stockClassId adjustment.adjustment_data index + pure nextIndex) + Map.empty (Map.values maps.stock_class_authorized_shares_adjustments) - case additionalAdjustment of - Some adjustment -> - if adjustment.stock_class_id == stockClassId - then pure (adjustment :: baseAdjustments) - else pure baseAdjustments - None -> pure baseAdjustments +currentStockClassAuthorizedCeiling : + Map Text StockClassAuthorizedSharesAdjustmentOcfData -> + StockClassOcfData -> + AuthorizedSharesCeiling +currentStockClassAuthorizedCeiling adjustmentIndex stockClassData = + case Map.lookup stockClassData.id adjustmentIndex of + Some adjustment -> AuthorizedSharesFinite adjustment.new_shares_authorized + None -> authorizedSharesCeiling stockClassData.initial_shares_authorized currentStockClassAuthorizedLimit : - CapTableMaps -> - Text -> - Optional Text -> - Optional StockClassOcfData -> - Optional StockClassAuthorizedSharesAdjustmentOcfData -> - Update (Optional Decimal) -currentStockClassAuthorizedLimit maps stockClassId excludedAdjustmentId stockClassOverride additionalAdjustment = do - stockClassData <- fetchStockClassData maps stockClassId stockClassOverride - adjustments <- fetchStockClassAuthorizedAdjustments maps stockClassId excludedAdjustmentId additionalAdjustment - let latestAdjustment = latestStockClassAuthorizedAdjustment adjustments + Map Text StockClassAuthorizedSharesAdjustmentOcfData -> + StockClassOcfData -> + Optional Decimal +currentStockClassAuthorizedLimit adjustmentIndex stockClassData = + case currentStockClassAuthorizedCeiling adjustmentIndex stockClassData of + AuthorizedSharesFinite n -> Some n + _ -> None + +latestIssuerAuthorizedAdjustment : CapTableMaps -> Text -> Update (Optional IssuerAuthorizedSharesAdjustmentOcfData) +latestIssuerAuthorizedAdjustment maps issuerId = + foldlA + (\latest adjustmentCid -> do + adjustment <- fetch adjustmentCid + if adjustment.adjustment_data.issuer_id == issuerId + then pure $ case latest of + Some current -> + if issuerAuthorizedAdjustmentTakesPrecedence adjustment.adjustment_data current + then Some adjustment.adjustment_data + else latest + None -> Some adjustment.adjustment_data + else pure latest) + None + (Map.values maps.issuer_authorized_shares_adjustments) + +currentIssuerAuthorizedLimit : CapTableMaps -> ContractId Issuer -> Update (Optional Decimal) +currentIssuerAuthorizedLimit maps issuerCid = do + issuerContract <- fetch issuerCid + latestAdjustment <- latestIssuerAuthorizedAdjustment maps issuerContract.issuer_data.id pure $ case latestAdjustment of Some adjustment -> Some adjustment.new_shares_authorized - None -> authorizedSharesLimit stockClassData.initial_shares_authorized - -stockClassIssuedQuantity : CapTableMaps -> Text -> Optional Text -> Update Decimal -stockClassIssuedQuantity maps stockClassId excludedIssuanceId = - foldlA - (\total issuanceCid -> do + None -> issuerAuthorizedSharesLimit issuerContract.issuer_data.initial_shares_authorized + +nonNegativeDecimal : Decimal -> Decimal +nonNegativeDecimal n = + if n < 0.0 then 0.0 else n + +stockClassShareEventSortKey : StockClassShareEvent -> (Time, Int) +stockClassShareEventSortKey event = case event of + StockClassShareDelta 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) + +applyStockClassShareEvent : Decimal -> StockClassShareEvent -> Decimal +applyStockClassShareEvent current event = case event of + StockClassShareDelta with quantity_delta -> nonNegativeDecimal (current + quantity_delta) + StockClassShareMultiplier with ratio -> current * ratio.numerator / ratio.denominator + StockClassShareRetraction with event_date; event_priority -> 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 ((StockClassShareDelta with + event_date = cancellation.cancellation_data.date + event_priority = 2 + quantity_delta = 0.0 - cancellation.cancellation_data.quantity) + :: 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 ((StockClassShareDelta with + event_date = repurchase.repurchase_data.date + event_priority = 2 + quantity_delta = 0.0 - repurchase.repurchase_data.quantity) + :: events) + else pure events) + [] + (Map.values maps.stock_repurchases) + + 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 + then pure ((StockClassShareMultiplier with + event_date = split.split_data.date + event_priority = 1 + 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 ++ retractionEvents ++ splitEvents)) + +stockIssuanceCurrentQuantity : CapTableMaps -> StockIssuanceOcfData -> Update Decimal +stockIssuanceCurrentQuantity maps issuanceData = do + events <- stockSecurityShareEvents maps issuanceData + pure $ foldl applyStockClassShareEvent 0.0 (sortOn stockClassShareEventSortKey events) + +stockClassIssuedQuantity : CapTableMaps -> Text -> Update Decimal +stockClassIssuedQuantity maps stockClassId = do + quantities <- foldlA + (\acc issuanceCid -> do issuance <- fetch issuanceCid - let isExcluded = case excludedIssuanceId of - Some excludedId -> issuance.issuance_data.id == excludedId - None -> False - if issuance.issuance_data.stock_class_id == stockClassId && not isExcluded - then pure (total + issuance.issuance_data.quantity) - else pure total) - 0.0 + if issuance.issuance_data.stock_class_id == stockClassId + then do + quantity <- stockIssuanceCurrentQuantity maps issuance.issuance_data + pure (quantity :: acc) + else pure acc) + [] (Map.values maps.stock_issuances) + pure (foldl (\total quantity -> total + quantity) 0.0 quantities) assertWithinStockClassAuthorizedShares : Text -> Decimal -> Optional Decimal -> Update () assertWithinStockClassAuthorizedShares stockClassId issuedQuantity limitOpt = case limitOpt of @@ -186,23 +310,63 @@ assertWithinStockClassAuthorizedShares stockClassId issuedQuantity limitOpt = ca (issuedQuantity <= limit) None -> pure () -validateStockIssuanceAuthorizedShares : CapTableMaps -> StockIssuanceOcfData -> Optional Text -> Update () -validateStockIssuanceAuthorizedShares maps issuanceData excludedIssuanceId = do - currentIssued <- stockClassIssuedQuantity maps issuanceData.stock_class_id excludedIssuanceId - limitOpt <- currentStockClassAuthorizedLimit maps issuanceData.stock_class_id None None None - assertWithinStockClassAuthorizedShares issuanceData.stock_class_id (currentIssued + issuanceData.quantity) limitOpt - -validateCurrentStockClassAuthorizedShares : +validateAllStockClassAuthorizedShares : CapTableMaps -> - Text -> - Optional Text -> - Optional StockClassOcfData -> - Optional StockClassAuthorizedSharesAdjustmentOcfData -> + Map Text StockClassAuthorizedSharesAdjustmentOcfData -> Update () -validateCurrentStockClassAuthorizedShares maps stockClassId excludedAdjustmentId stockClassOverride additionalAdjustment = do - currentIssued <- stockClassIssuedQuantity maps stockClassId None - limitOpt <- currentStockClassAuthorizedLimit maps stockClassId excludedAdjustmentId stockClassOverride additionalAdjustment - assertWithinStockClassAuthorizedShares stockClassId currentIssued limitOpt +validateAllStockClassAuthorizedShares maps adjustmentIndex = + mapA_ + (\stockClassCid -> do + stockClass <- fetch stockClassCid + currentIssued <- stockClassIssuedQuantity maps stockClass.stock_class_data.id + let limitOpt = currentStockClassAuthorizedLimit adjustmentIndex stockClass.stock_class_data + assertWithinStockClassAuthorizedShares stockClass.stock_class_data.id currentIssued limitOpt) + (Map.values maps.stock_classes) + +addStockClassAuthorizedContribution : Optional Decimal -> AuthorizedSharesCeiling -> Optional Decimal +addStockClassAuthorizedContribution totalOpt ceiling = case (totalOpt, ceiling) of + (Some total, AuthorizedSharesFinite shares) -> Some (total + shares) + (Some total, AuthorizedSharesNotApplicable) -> Some total + (_, AuthorizedSharesUnlimited) -> None + (None, _) -> None + +currentStockClassAuthorizedTotal : + CapTableMaps -> + Map Text StockClassAuthorizedSharesAdjustmentOcfData -> + Update (Optional Decimal) +currentStockClassAuthorizedTotal maps adjustmentIndex = + foldlA + (\totalOpt stockClassCid -> do + stockClass <- fetch stockClassCid + let ceiling = currentStockClassAuthorizedCeiling adjustmentIndex stockClass.stock_class_data + pure (addStockClassAuthorizedContribution totalOpt ceiling)) + (Some 0.0) + (Map.values maps.stock_classes) + +validateIssuerAuthorizedShares : + CapTableMaps -> + Map Text StockClassAuthorizedSharesAdjustmentOcfData -> + ContractId Issuer -> + Update () +validateIssuerAuthorizedShares maps adjustmentIndex issuerCid = do + issuerLimitOpt <- currentIssuerAuthorizedLimit maps issuerCid + case issuerLimitOpt of + None -> pure () + Some issuerLimit -> do + stockClassTotalOpt <- currentStockClassAuthorizedTotal maps adjustmentIndex + case stockClassTotalOpt of + Some stockClassTotal -> + assertMsg + ("Issuer authorized shares exceeded: stock classes authorize " <> show stockClassTotal <> " shares but issuer authorizes " <> show issuerLimit) + (stockClassTotal <= issuerLimit) + None -> + assertMsg "Issuer authorized shares exceeded: stock class authorization is unlimited but issuer authorization is finite" False + +validateAuthorizedShareCeilings : CapTableMaps -> ContractId Issuer -> Update () +validateAuthorizedShareCeilings maps issuerCid = do + adjustmentIndex <- stockClassAuthorizedAdjustmentIndex maps + validateAllStockClassAuthorizedShares maps adjustmentIndex + validateIssuerAuthorizedShares maps adjustmentIndex issuerCid -- | Get the processing tier for a create operation getCreateTier : OcfCreateData -> Int @@ -337,6 +501,8 @@ template CapTable (mapsAfterEdits, []) sortedCreates + validateAuthorizedShareCeilings finalMaps newIssuerCid + -- Create new CapTable with updated maps and potentially new issuer newCapTableCid <- create this with issuer = newIssuerCid diff --git a/scripts/codegen/templates/loops/create-case.daml b/scripts/codegen/templates/loops/create-case.daml index 2af254ac..d40a429c 100644 --- a/scripts/codegen/templates/loops/create-case.daml +++ b/scripts/codegen/templates/loops/create-case.daml @@ -17,12 +17,6 @@ OcfCreate{{name}} d -> do {{/if}} {{/if}} {{/each}} -{{#if validates_stock_issuance_authorized_shares}} - validateStockIssuanceAuthorizedShares maps d None -{{/if}} -{{#if validates_stock_class_authorized_adjustment}} - validateCurrentStockClassAuthorizedShares maps d.stock_class_id None None (Some d) -{{/if}} cid <- create {{name}} with context = ctx, {{data_param}} = d {{#if security_id_index_map}} let newMaps = maps with diff --git a/scripts/codegen/templates/loops/delete-case.daml b/scripts/codegen/templates/loops/delete-case.daml index 5f3327ea..7d59431c 100644 --- a/scripts/codegen/templates/loops/delete-case.daml +++ b/scripts/codegen/templates/loops/delete-case.daml @@ -13,10 +13,6 @@ OcfDelete{{name}} delId -> do {{map_field}} = Map.delete delId maps.{{map_field}} {{security_id_index_map}} = Map.delete securityId maps.{{security_id_index_map}}) {{else}} -{{#if validates_stock_class_authorized_adjustment}} - oldContract <- fetch oldCid - validateCurrentStockClassAuthorizedShares maps oldContract.{{data_param}}.stock_class_id (Some oldContract.{{data_param}}.id) None None -{{/if}} archive oldCid pure (maps with {{map_field}} = Map.delete delId maps.{{map_field}}) {{/if}} diff --git a/scripts/codegen/templates/loops/edit-case.daml b/scripts/codegen/templates/loops/edit-case.daml index 895b4cf1..73321301 100644 --- a/scripts/codegen/templates/loops/edit-case.daml +++ b/scripts/codegen/templates/loops/edit-case.daml @@ -4,9 +4,6 @@ OcfEdit{{name}} d -> do let oldCidOpt = Map.lookup d.id maps.{{map_field}} assertMsg "{{name}} not found" (isSome oldCidOpt) let Some oldCid = oldCidOpt -{{#if validates_stock_class_authorized_adjustment}} - oldAdjustment <- fetch oldCid -{{/if}} {{#each validations}} {{#if is_array}} -- Validate each element in array field {{field}} @@ -19,19 +16,9 @@ OcfEdit{{name}} d -> do {{/if}} {{/if}} {{/each}} -{{#if validates_stock_class_initial_authorized_shares}} - validateCurrentStockClassAuthorizedShares maps d.id None (Some d) None -{{/if}} -{{#if validates_stock_class_authorized_adjustment}} - validateCurrentStockClassAuthorizedShares maps oldAdjustment.{{data_param}}.stock_class_id (Some oldAdjustment.{{data_param}}.id) None (Some d) - validateCurrentStockClassAuthorizedShares maps d.stock_class_id (Some oldAdjustment.{{data_param}}.id) None (Some d) -{{/if}} {{#if security_id_index_map}} -- Fetch old contract to get old security_id for index map cleanup oldContract <- fetch oldCid -{{#if validates_stock_issuance_authorized_shares}} - validateStockIssuanceAuthorizedShares maps d (Some oldContract.{{data_param}}.id) -{{/if}} let oldSecurityId = oldContract.{{data_param}}.security_id -- Only check uniqueness if security_id is changing when (d.security_id /= oldSecurityId) do From 664cb1dfce673f3513c462132d334688d78fb278 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 21:25:32 -0400 Subject: [PATCH 04/62] Tighten authorized share edit validation --- .../TestIssuerAuthorizedShares.daml | 18 ++++++++ .../TestStockClassAuthorizedShares.daml | 19 +++++++++ .../0.0.3/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 42 ++++++++++++++++++- 5 files changed, 82 insertions(+), 5 deletions(-) diff --git a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml index 5198974b..440e9bc6 100644 --- a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml @@ -91,6 +91,24 @@ testIssuerEditCannotDropBelowStockClasses = script do edits = [CT.OcfEditIssuer (defaultIssuerWithAuthorizedShares "ISSUER_1" (Some (OcfInitialSharesNumeric 99.0)))] deletes = [] +testIssuerEditCannotDropInitialBelowStockClassesWhenAdjustmentExists = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + result <- submit issuer do + exerciseCmd capTableLimited CT.UpdateCapTable with + creates = + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_MASK" 150.0) + , CT.OcfCreateStockClass (limitedStockClass "SC_ISSUER_EDIT_WITH_ADJUSTMENT" 125.0) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditIssuer (defaultIssuerWithAuthorizedShares "ISSUER_1" (Some (OcfInitialSharesNumeric 99.0)))] + deletes = [] + testStockClassAuthorizedSharesAdjustmentCannotExceedIssuerAuthorizedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 06e52080..e59ec07a 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -138,6 +138,25 @@ testStockClassEditCannotDropInitialAuthorizedSharesBelowIssuedShares = script do edits = [CT.OcfEditStockClass (limitedStockClass stockClassId 50.0)] deletes = [] +testStockClassEditCannotDropInitialAuthorizedSharesBelowIssuedSharesWhenAdjustmentExists = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EDIT_WITH_ADJUSTMENT" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EDIT_WITH_ADJUSTMENT" "Edit With Adjustment Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_EDIT_MASK" stockClassId 150.0)] + edits = [] + deletes = [] + + capTableIssued <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_EDIT_WITH_ADJUSTMENT_ISSUED" "SEC_EDIT_WITH_ADJUSTMENT_ISSUED" stakeholderId stockClassId) with quantity = 125.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockClass (limitedStockClass stockClassId 50.0)] + deletes = [] + testStockClassAuthorizedSharesAdjustmentDeleteCannotInvalidateIssuedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar index aa9e8b20..901498c2 100644 --- a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce8a0a1860edb50f39bb29377fb0ecf4afa3c2bfc91a60962315bdc7e443e942 -size 2485461 +oid sha256:db981c4ac3db717dd8ceb8dfe6e429b5f2e6c38235afd09da3f20eea608435b6 +size 2489389 diff --git a/dars/dars.lock b/dars/dars.lock index da382610..37c99094 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar": { - "sha256": "ce8a0a1860edb50f39bb29377fb0ecf4afa3c2bfc91a60962315bdc7e443e942", - "size": 2485461, + "sha256": "db981c4ac3db717dd8ceb8dfe6e429b5f2e6c38235afd09da3f20eea608435b6", + "size": 2489389, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T00:41:37.578Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 6cb1c065..3a7a4047 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -222,7 +222,7 @@ applyStockClassShareEvent : Decimal -> StockClassShareEvent -> Decimal applyStockClassShareEvent current event = case event of StockClassShareDelta with quantity_delta -> nonNegativeDecimal (current + quantity_delta) StockClassShareMultiplier with ratio -> current * ratio.numerator / ratio.denominator - StockClassShareRetraction with event_date; event_priority -> 0.0 + StockClassShareRetraction _ _ -> 0.0 stockSecurityShareEvents : CapTableMaps -> StockIssuanceOcfData -> Update [StockClassShareEvent] stockSecurityShareEvents maps issuanceData = do @@ -323,6 +323,17 @@ validateAllStockClassAuthorizedShares maps adjustmentIndex = assertWithinStockClassAuthorizedShares stockClass.stock_class_data.id currentIssued limitOpt) (Map.values maps.stock_classes) +validateEditedStockClassInitialAuthorizedShares : CapTableMaps -> [OcfEditData] -> Update () +validateEditedStockClassInitialAuthorizedShares maps edits = + mapA_ + (\editData -> case editData of + OcfEditStockClass stockClassData -> do + currentIssued <- stockClassIssuedQuantity maps stockClassData.id + let limitOpt = authorizedSharesLimit stockClassData.initial_shares_authorized + assertWithinStockClassAuthorizedShares stockClassData.id currentIssued limitOpt + _ -> pure ()) + edits + addStockClassAuthorizedContribution : Optional Decimal -> AuthorizedSharesCeiling -> Optional Decimal addStockClassAuthorizedContribution totalOpt ceiling = case (totalOpt, ceiling) of (Some total, AuthorizedSharesFinite shares) -> Some (total + shares) @@ -362,12 +373,40 @@ validateIssuerAuthorizedShares maps adjustmentIndex issuerCid = do None -> assertMsg "Issuer authorized shares exceeded: stock class authorization is unlimited but issuer authorization is finite" False +validateEditedIssuerInitialAuthorizedShares : + CapTableMaps -> + Map Text StockClassAuthorizedSharesAdjustmentOcfData -> + [OcfEditData] -> + Update () +validateEditedIssuerInitialAuthorizedShares maps adjustmentIndex edits = + mapA_ + (\editData -> case editData of + OcfEditIssuer issuerData -> case issuerAuthorizedSharesLimit issuerData.initial_shares_authorized of + None -> pure () + Some issuerLimit -> do + stockClassTotalOpt <- currentStockClassAuthorizedTotal maps adjustmentIndex + case stockClassTotalOpt of + Some stockClassTotal -> + assertMsg + ("Issuer initial authorized shares exceeded: stock classes authorize " <> show stockClassTotal <> " shares but issuer initially authorizes " <> show issuerLimit) + (stockClassTotal <= issuerLimit) + None -> + assertMsg "Issuer initial authorized shares exceeded: stock class authorization is unlimited but issuer initial authorization is finite" False + _ -> pure ()) + edits + validateAuthorizedShareCeilings : CapTableMaps -> ContractId Issuer -> Update () validateAuthorizedShareCeilings maps issuerCid = do adjustmentIndex <- stockClassAuthorizedAdjustmentIndex maps validateAllStockClassAuthorizedShares maps adjustmentIndex validateIssuerAuthorizedShares maps adjustmentIndex issuerCid +validateAuthorizedShareCeilingsForEdits : CapTableMaps -> [OcfEditData] -> Update () +validateAuthorizedShareCeilingsForEdits maps edits = do + adjustmentIndex <- stockClassAuthorizedAdjustmentIndex maps + validateEditedStockClassInitialAuthorizedShares maps edits + validateEditedIssuerInitialAuthorizedShares maps adjustmentIndex edits + -- | Get the processing tier for a create operation getCreateTier : OcfCreateData -> Int getCreateTier createData = case createData of @@ -502,6 +541,7 @@ template CapTable sortedCreates validateAuthorizedShareCeilings finalMaps newIssuerCid + validateAuthorizedShareCeilingsForEdits finalMaps edits -- Create new CapTable with updated maps and potentially new issuer newCapTableCid <- create this with From c72b8ba1a4f34ad2d4ad3b04be307621f33f8531 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 21:38:31 -0400 Subject: [PATCH 05/62] Track authorized share initial edits --- .../TestIssuerAuthorizedShares.daml | 19 +++++++ .../TestStockClassAuthorizedShares.daml | 20 ++++++++ .../0.0.3/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 51 ++++++++++++------- .../codegen/templates/loops/edit-case.daml | 11 +++- 6 files changed, 86 insertions(+), 23 deletions(-) diff --git a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml index 440e9bc6..33850a06 100644 --- a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml @@ -109,6 +109,25 @@ testIssuerEditCannotDropInitialBelowStockClassesWhenAdjustmentExists = script do edits = [CT.OcfEditIssuer (defaultIssuerWithAuthorizedShares "ISSUER_1" (Some (OcfInitialSharesNumeric 99.0)))] deletes = [] +testIssuerMetadataEditAllowedWhenAdjustmentRaisesAuthorizedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + result <- submit issuer do + exerciseCmd capTableLimited CT.UpdateCapTable with + creates = + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_METADATA" 150.0) + , CT.OcfCreateStockClass (limitedStockClass "SC_ISSUER_METADATA_WITH_ADJUSTMENT" 125.0) + ] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditIssuer ((defaultIssuerWithAuthorizedShares "ISSUER_1" (Some (OcfInitialSharesNumeric 100.0))) with legal_name = "Renamed TestIssuer Inc.")] + deletes = [] + pure () + testStockClassAuthorizedSharesAdjustmentCannotExceedIssuerAuthorizedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index e59ec07a..9d9e927c 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -157,6 +157,26 @@ testStockClassEditCannotDropInitialAuthorizedSharesBelowIssuedSharesWhenAdjustme edits = [CT.OcfEditStockClass (limitedStockClass stockClassId 50.0)] deletes = [] +testStockClassMetadataEditAllowedWhenAdjustmentRaisesAuthorizedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_METADATA_WITH_ADJUSTMENT" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_METADATA_WITH_ADJUSTMENT" "Metadata With Adjustment Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_METADATA_EDIT" stockClassId 150.0)] + edits = [] + deletes = [] + + capTableIssued <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_METADATA_WITH_ADJUSTMENT_ISSUED" "SEC_METADATA_WITH_ADJUSTMENT_ISSUED" stakeholderId stockClassId) with quantity = 125.0) + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockClass ((limitedStockClass stockClassId 100.0) with name = "Edited Common Stock")] + deletes = [] + pure () + testStockClassAuthorizedSharesAdjustmentDeleteCannotInvalidateIssuedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar index 901498c2..2097b70f 100644 --- a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db981c4ac3db717dd8ceb8dfe6e429b5f2e6c38235afd09da3f20eea608435b6 -size 2489389 +oid sha256:b818b26430f9f36f1846cc515832e9f695182a05175dfad0c6713b0650c0b87c +size 2497253 diff --git a/dars/dars.lock b/dars/dars.lock index 37c99094..a381ac1d 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar": { - "sha256": "db981c4ac3db717dd8ceb8dfe6e429b5f2e6c38235afd09da3f20eea608435b6", - "size": 2489389, + "sha256": "b818b26430f9f36f1846cc515832e9f695182a05175dfad0c6713b0650c0b87c", + "size": 2497253, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T00:41:37.578Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 3a7a4047..859bae16 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -395,15 +395,21 @@ validateEditedIssuerInitialAuthorizedShares maps adjustmentIndex edits = _ -> pure ()) edits -validateAuthorizedShareCeilings : CapTableMaps -> ContractId Issuer -> Update () -validateAuthorizedShareCeilings maps issuerCid = do - adjustmentIndex <- stockClassAuthorizedAdjustmentIndex maps +validateAuthorizedShareCeilings : + CapTableMaps -> + Map Text StockClassAuthorizedSharesAdjustmentOcfData -> + ContractId Issuer -> + Update () +validateAuthorizedShareCeilings maps adjustmentIndex issuerCid = do validateAllStockClassAuthorizedShares maps adjustmentIndex validateIssuerAuthorizedShares maps adjustmentIndex issuerCid -validateAuthorizedShareCeilingsForEdits : CapTableMaps -> [OcfEditData] -> Update () -validateAuthorizedShareCeilingsForEdits maps edits = do - adjustmentIndex <- stockClassAuthorizedAdjustmentIndex maps +validateAuthorizedShareCeilingsForEdits : + CapTableMaps -> + Map Text StockClassAuthorizedSharesAdjustmentOcfData -> + [OcfEditData] -> + Update () +validateAuthorizedShareCeilingsForEdits maps adjustmentIndex edits = do validateEditedStockClassInitialAuthorizedShares maps edits validateEditedIssuerInitialAuthorizedShares maps adjustmentIndex edits @@ -421,9 +427,10 @@ processCreate ctx maps createData = case createData of {{> create-case}} {{/each}} --- | Process a single edit operation, returning updated maps and the edited ContractId +-- | Process a single edit operation, returning updated maps, the edited ContractId, +-- and any edit that changed initial authorized shares. -- Note: OcfEditIssuer is handled separately in UpdateCapTable since issuer is not a map -processEdit : Context -> CapTableMaps -> OcfEditData -> Update (CapTableMaps, OcfContractId) +processEdit : Context -> CapTableMaps -> OcfEditData -> Update (CapTableMaps, OcfContractId, [OcfEditData]) processEdit ctx maps editData = case editData of OcfEditIssuer _ -> error "OcfEditIssuer should be handled separately in UpdateCapTable" {{#each types_alpha}} @@ -506,27 +513,31 @@ template CapTable assertMsg "Cannot edit issuer more than once per batch" (length issuerEdits <= 1) -- Process regular edits (map-based entities) - (mapsAfterEdits, regularEditedCids) <- foldlA - (\(maps, cids) editData -> do - (newMaps, cid) <- processEdit context maps editData - pure (newMaps, cids ++ [cid])) - (mapsAfterDeletes, []) + (mapsAfterEdits, regularEditedCids, regularInitialAuthorizedShareEdits) <- foldlA + (\(maps, cids, initialAuthorizedShareEdits) editData -> do + (newMaps, cid, editInitialAuthorizedShareEdits) <- processEdit context maps editData + pure (newMaps, cids ++ [cid], initialAuthorizedShareEdits ++ editInitialAuthorizedShareEdits)) + (mapsAfterDeletes, [], []) regularEdits -- Process issuer edit (if any) - special handling since it's a single reference -- Note: In editedCids output, issuer edits appear first regardless of input position - (newIssuerCid, issuerEditedCids) <- case issuerEdits of - [] -> pure (issuer, []) -- No issuer edit, keep current + (newIssuerCid, issuerEditedCids, issuerInitialAuthorizedShareEdits) <- case issuerEdits of + [] -> pure (issuer, [], []) -- No issuer edit, keep current [OcfEditIssuer newIssuerData] -> do -- Validate issuer ID matches (cannot change ID during edit) currentIssuer <- fetch issuer assertMsg "Issuer ID mismatch: cannot change issuer ID during edit" (newIssuerData.id == currentIssuer.issuer_data.id) + let initialAuthorizedShareEdits = + if newIssuerData.initial_shares_authorized /= currentIssuer.issuer_data.initial_shares_authorized + then [OcfEditIssuer newIssuerData] + else [] archive issuer newCid <- create Issuer with context = context issuer_data = newIssuerData - pure (newCid, [CidIssuer newCid]) + pure (newCid, [CidIssuer newCid], initialAuthorizedShareEdits) _ -> error "Multiple issuer edits not allowed" -- Defensive: assertMsg above prevents this -- Sort creates by tier for dependency ordering @@ -540,8 +551,12 @@ template CapTable (mapsAfterEdits, []) sortedCreates - validateAuthorizedShareCeilings finalMaps newIssuerCid - validateAuthorizedShareCeilingsForEdits finalMaps edits + authorizedShareAdjustmentIndex <- stockClassAuthorizedAdjustmentIndex finalMaps + validateAuthorizedShareCeilings finalMaps authorizedShareAdjustmentIndex newIssuerCid + validateAuthorizedShareCeilingsForEdits + finalMaps + authorizedShareAdjustmentIndex + (issuerInitialAuthorizedShareEdits ++ regularInitialAuthorizedShareEdits) -- Create new CapTable with updated maps and potentially new issuer newCapTableCid <- create this with diff --git a/scripts/codegen/templates/loops/edit-case.daml b/scripts/codegen/templates/loops/edit-case.daml index 73321301..e5c7eac9 100644 --- a/scripts/codegen/templates/loops/edit-case.daml +++ b/scripts/codegen/templates/loops/edit-case.daml @@ -16,6 +16,15 @@ OcfEdit{{name}} d -> do {{/if}} {{/if}} {{/each}} +{{#if validates_stock_class_initial_authorized_shares}} + oldContract <- fetch oldCid + let authorizedShareInitialEdits = + if d.initial_shares_authorized /= oldContract.{{data_param}}.initial_shares_authorized + then [OcfEdit{{name}} d] + else [] +{{else}} + let authorizedShareInitialEdits = [] +{{/if}} {{#if security_id_index_map}} -- Fetch old contract to get old security_id for index map cleanup oldContract <- fetch oldCid @@ -33,4 +42,4 @@ OcfEdit{{name}} d -> do newCid <- create {{name}} with context = ctx, {{data_param}} = d let newMaps = maps with {{map_field}} = Map.insert d.id newCid maps.{{map_field}} {{/if}} - pure (newMaps, Cid{{name}} newCid) + pure (newMaps, Cid{{name}} newCid, authorizedShareInitialEdits) From c7e67c45a116a00663809edbc6a971de22fa4761 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 21:52:02 -0400 Subject: [PATCH 06/62] Share authorized ceiling test helpers --- Test/daml/OpenCapTable/TestHelpers.daml | 18 +++++++++++++++++- .../TestIssuerAuthorizedShares.daml | 18 ------------------ .../TestStockClassAuthorizedShares.daml | 18 ------------------ 3 files changed, 17 insertions(+), 37 deletions(-) diff --git a/Test/daml/OpenCapTable/TestHelpers.daml b/Test/daml/OpenCapTable/TestHelpers.daml index a882b9bc..c71cfc49 100644 --- a/Test/daml/OpenCapTable/TestHelpers.daml +++ b/Test/daml/OpenCapTable/TestHelpers.daml @@ -1,9 +1,9 @@ module OpenCapTable.TestHelpers where -import DA.Assert (assertMsg) import Fairmint.OpenCapTable.OCF.Issuer import Fairmint.OpenCapTable.OCF.Stakeholder import Fairmint.OpenCapTable.OCF.StockClass +import Fairmint.OpenCapTable.OCF.StockClassAuthorizedSharesAdjustment import Fairmint.OpenCapTable.OCF.StockIssuance (StockIssuanceOcfData(..)) import Fairmint.OpenCapTable.OCF.WarrantIssuance (WarrantIssuanceOcfData(..)) import Fairmint.OpenCapTable.OCF.ConvertibleIssuance (ConvertibleIssuanceOcfData(..), OcfConvertibleConversionTrigger(..)) @@ -71,6 +71,22 @@ defaultStockClass classId = StockClassOcfData with conversion_rights = [] comments = [] +limitedStockClass : Text -> Decimal -> StockClassOcfData +limitedStockClass classId authorizedShares = + (defaultStockClass classId) with + initial_shares_authorized = OcfInitialSharesNumeric authorizedShares + +stockClassAuthorizedSharesAdjustment : Text -> Text -> Decimal -> StockClassAuthorizedSharesAdjustmentOcfData +stockClassAuthorizedSharesAdjustment adjustmentId stockClassId authorizedShares = + StockClassAuthorizedSharesAdjustmentOcfData with + id = adjustmentId + date = defaultTestDate + stock_class_id = stockClassId + new_shares_authorized = authorizedShares + board_approval_date = None + stockholder_approval_date = 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/TestIssuerAuthorizedShares.daml b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml index 33850a06..55a555e2 100644 --- a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml @@ -1,19 +1,12 @@ module OpenCapTable.TestIssuerAuthorizedShares where import Fairmint.OpenCapTable.OCF.IssuerAuthorizedSharesAdjustment -import Fairmint.OpenCapTable.OCF.StockClass -import Fairmint.OpenCapTable.OCF.StockClassAuthorizedSharesAdjustment import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup import OpenCapTable.TestHelpers import Daml.Script -limitedStockClass : Text -> Decimal -> StockClassOcfData -limitedStockClass classId authorizedShares = - (defaultStockClass classId) with - initial_shares_authorized = OcfInitialSharesNumeric authorizedShares - issuerAuthorizedSharesAdjustment : Text -> Decimal -> IssuerAuthorizedSharesAdjustmentOcfData issuerAuthorizedSharesAdjustment adjustmentId sharesAuthorized = IssuerAuthorizedSharesAdjustmentOcfData with @@ -25,17 +18,6 @@ issuerAuthorizedSharesAdjustment adjustmentId sharesAuthorized = stockholder_approval_date = None comments = [] -stockClassAuthorizedSharesAdjustment : Text -> Text -> Decimal -> StockClassAuthorizedSharesAdjustmentOcfData -stockClassAuthorizedSharesAdjustment adjustmentId stockClassId authorizedShares = - StockClassAuthorizedSharesAdjustmentOcfData with - id = adjustmentId - date = defaultTestDate - stock_class_id = stockClassId - new_shares_authorized = authorizedShares - board_approval_date = None - stockholder_approval_date = None - comments = [] - setIssuerAuthorizedShares : Party -> ContractId CT.CapTable -> Decimal -> Script (ContractId CT.CapTable) setIssuerAuthorizedShares issuer capTable sharesAuthorized = do result <- submit issuer do diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 9d9e927c..82feb8d5 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -1,7 +1,5 @@ module OpenCapTable.TestStockClassAuthorizedShares where -import Fairmint.OpenCapTable.OCF.StockClass -import Fairmint.OpenCapTable.OCF.StockClassAuthorizedSharesAdjustment import Fairmint.OpenCapTable.OCF.StockCancellation import Fairmint.OpenCapTable.OCF.StockClassSplit import Fairmint.OpenCapTable.OCF.StockRepurchase @@ -13,22 +11,6 @@ import OpenCapTable.Setup import OpenCapTable.TestHelpers import Daml.Script -limitedStockClass : Text -> Decimal -> StockClassOcfData -limitedStockClass classId authorizedShares = - (defaultStockClass classId) with - initial_shares_authorized = OcfInitialSharesNumeric authorizedShares - -stockClassAuthorizedSharesAdjustment : Text -> Text -> Decimal -> StockClassAuthorizedSharesAdjustmentOcfData -stockClassAuthorizedSharesAdjustment adjustmentId stockClassId authorizedShares = - StockClassAuthorizedSharesAdjustmentOcfData with - id = adjustmentId - date = defaultTestDate - stock_class_id = stockClassId - new_shares_authorized = authorizedShares - board_approval_date = None - stockholder_approval_date = None - comments = [] - stockCancellation : Text -> Text -> Decimal -> StockCancellationOcfData stockCancellation cancellationId securityId quantity = StockCancellationOcfData with From ce1af67c7d502b8c1136bbaebeac7827cc7282d6 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 22:02:23 -0400 Subject: [PATCH 07/62] Fix authorized ceiling split timing --- .../TestStockClassAuthorizedShares.daml | 25 +++++++++++++++++++ .../0.0.3/OpenCapTable-v34.dar | 4 +-- dars/dars.lock | 4 +-- .../codegen/templates/CapTable.daml.template | 2 +- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 82feb8d5..9385ba24 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -9,6 +9,9 @@ import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSha import qualified Fairmint.OpenCapTable.CapTable as CT 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 -> StockCancellationOcfData @@ -231,6 +234,28 @@ testStockClassSplitCannotLeaveIssuedSharesAboveAuthorizedShares = script do edits = [] deletes = [] +testPostSplitIssuanceUsesPostSplitQuantityForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_POST_SPLIT" 150.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_POST_SPLIT" "Post Split Holder" + splitResult <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassSplit ((stockClassSplit "TX_POST_SPLIT" stockClassId 2.0 1.0) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0) + ] + edits = [] + deletes = [] + + _ <- createStockIssuanceWith + issuer + splitResult.updatedCapTableCid + ((defaultStockIssuance "TX_AFTER_SPLIT" "SEC_AFTER_SPLIT" stakeholderId stockClassId) with + date = DT.time (DA.date 2024 Jan 03) 0 0 0 + quantity = 125.0) + pure () + testStockCancellationReducesIssuedSharesForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar index 2097b70f..7f5baf66 100644 --- a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b818b26430f9f36f1846cc515832e9f695182a05175dfad0c6713b0650c0b87c -size 2497253 +oid sha256:7f8dae9407c4bbbb8cf182ab0cdbfb2dae95e2a6711c0f85e352f87a0948f0d8 +size 2497511 diff --git a/dars/dars.lock b/dars/dars.lock index a381ac1d..0403fab7 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar": { - "sha256": "b818b26430f9f36f1846cc515832e9f695182a05175dfad0c6713b0650c0b87c", - "size": 2497253, + "sha256": "7f8dae9407c4bbbb8cf182ab0cdbfb2dae95e2a6711c0f85e352f87a0948f0d8", + "size": 2497511, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T00:41:37.578Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 859bae16..46e95303 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -267,7 +267,7 @@ stockSecurityShareEvents maps issuanceData = do splitEvents <- foldlA (\events splitCid -> do split <- fetch splitCid - if split.split_data.stock_class_id == issuanceData.stock_class_id + 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 = 1 From 01f34afa723f830b153c585a9e78f5ec6b49e9d8 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 22:08:44 -0400 Subject: [PATCH 08/62] Order same-day share reductions before splits --- .../TestStockClassAuthorizedShares.daml | 17 +++++++++++++++++ .../OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- .../codegen/templates/CapTable.daml.template | 6 +++--- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 9385ba24..4d6ac05d 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -256,6 +256,23 @@ testPostSplitIssuanceUsesPostSplitQuantityForCeiling = script do quantity = 125.0) pure () +testSameDateCancellationAppliesBeforeSplitForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CANCEL_SPLIT" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CANCEL_SPLIT" "Cancel Split Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CANCEL_SPLIT_ISSUED" "SEC_CANCEL_SPLIT_ISSUED" stakeholderId stockClassId) with quantity = 100.0) + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockCancellation (stockCancellation "TX_CANCEL_BEFORE_SPLIT" "SEC_CANCEL_SPLIT_ISSUED" 50.0) + , CT.OcfCreateStockClassSplit (stockClassSplit "TX_SPLIT_AFTER_CANCEL" stockClassId 2.0 1.0) + ] + edits = [] + deletes = [] + pure () + testStockCancellationReducesIssuedSharesForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar index 7f5baf66..f0d4ec4f 100644 --- a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f8dae9407c4bbbb8cf182ab0cdbfb2dae95e2a6711c0f85e352f87a0948f0d8 -size 2497511 +oid sha256:096a3f37ca9107abdf97c11fef75169084f74cc3a55249a6cf5cda15ba4c2a86 +size 2497513 diff --git a/dars/dars.lock b/dars/dars.lock index 0403fab7..a9aceb2b 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar": { - "sha256": "7f8dae9407c4bbbb8cf182ab0cdbfb2dae95e2a6711c0f85e352f87a0948f0d8", - "size": 2497511, + "sha256": "096a3f37ca9107abdf97c11fef75169084f74cc3a55249a6cf5cda15ba4c2a86", + "size": 2497513, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T00:41:37.578Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 46e95303..f706fba1 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -232,7 +232,7 @@ stockSecurityShareEvents maps issuanceData = do if cancellation.cancellation_data.security_id == issuanceData.security_id then pure ((StockClassShareDelta with event_date = cancellation.cancellation_data.date - event_priority = 2 + event_priority = 1 quantity_delta = 0.0 - cancellation.cancellation_data.quantity) :: events) else pure events) @@ -245,7 +245,7 @@ stockSecurityShareEvents maps issuanceData = do if repurchase.repurchase_data.security_id == issuanceData.security_id then pure ((StockClassShareDelta with event_date = repurchase.repurchase_data.date - event_priority = 2 + event_priority = 1 quantity_delta = 0.0 - repurchase.repurchase_data.quantity) :: events) else pure events) @@ -270,7 +270,7 @@ stockSecurityShareEvents maps issuanceData = do 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 = 1 + event_priority = 2 ratio = split.split_data.split_ratio) :: events) else pure events) From 01aade2968b02f551d88f5902a3f7074a862feae Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 22:52:40 -0400 Subject: [PATCH 09/62] Remove unused authorized share codegen flags --- scripts/codegen/generate-captable.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/scripts/codegen/generate-captable.ts b/scripts/codegen/generate-captable.ts index 21c5f8ed..ed84e12e 100644 --- a/scripts/codegen/generate-captable.ts +++ b/scripts/codegen/generate-captable.ts @@ -52,8 +52,6 @@ interface TypeDef { tier: number; validations: Validation[]; validates_stock_class_initial_authorized_shares: boolean; - validates_stock_class_authorized_adjustment: boolean; - validates_stock_issuance_authorized_shares: boolean; // For issuance types, the name of the security_id index map (e.g., 'stock_issuances_by_security_id') security_id_index_map: string | null; } @@ -223,8 +221,6 @@ function discoverTypes(config: Config): TypeDef[] { }; }), validates_stock_class_initial_authorized_shares: name === 'StockClass', - validates_stock_class_authorized_adjustment: name === 'StockClassAuthorizedSharesAdjustment', - validates_stock_issuance_authorized_shares: name === 'StockIssuance', security_id_index_map: SECURITY_ID_INDEX_MAPS[name] ?? null, }; From c4e2fb955331c42a459d2d34fb3186e8f3ffdbda Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 23:15:36 -0400 Subject: [PATCH 10/62] Account for stock conversions in share ceilings --- .../TestStockClassAuthorizedShares.daml | 27 +++++++++++++++++++ .../0.0.3/OpenCapTable-v34.dar | 4 +-- dars/dars.lock | 4 +-- .../codegen/templates/CapTable.daml.template | 15 ++++++++++- 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 4d6ac05d..417b0766 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -2,6 +2,7 @@ module OpenCapTable.TestStockClassAuthorizedShares where import Fairmint.OpenCapTable.OCF.StockCancellation import Fairmint.OpenCapTable.OCF.StockClassSplit +import Fairmint.OpenCapTable.OCF.StockConversion import Fairmint.OpenCapTable.OCF.StockRepurchase import Fairmint.OpenCapTable.OCF.StockRetraction import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) @@ -37,6 +38,17 @@ stockRepurchase repurchaseId securityId quantity = consideration_text = None comments = [] +stockConversion : Text -> Text -> Decimal -> StockConversionOcfData +stockConversion conversionId securityId quantity = + StockConversionOcfData with + id = conversionId + date = defaultTestDate + security_id = securityId + quantity_converted = quantity + resulting_security_ids = [conversionId <> "_RESULT"] + balance_security_id = None + comments = [] + stockRetraction : Text -> Text -> StockRetractionOcfData stockRetraction retractionId securityId = StockRetractionOcfData with @@ -303,6 +315,21 @@ testStockRepurchaseReducesIssuedSharesForCeiling = script do _ <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_REPURCHASE_REISSUE" "SEC_REPURCHASE_REISSUE" stakeholderId stockClassId) with quantity = 50.0) pure () +testStockConversionReducesIssuedSharesForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERSION" "Conversion Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_ISSUED" "SEC_CONVERSION_ISSUED" stakeholderId stockClassId) with quantity = 100.0) + result <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_REDUCE" "SEC_CONVERSION_ISSUED" 50.0)] + edits = [] + deletes = [] + + _ <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_CONVERSION_REISSUE" "SEC_CONVERSION_REISSUE" stakeholderId stockClassId) with quantity = 50.0) + pure () + testStockRetractionReducesIssuedSharesForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar index f0d4ec4f..3aa7bc8c 100644 --- a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:096a3f37ca9107abdf97c11fef75169084f74cc3a55249a6cf5cda15ba4c2a86 -size 2497513 +oid sha256:f027dd4ad6d5a5ccebcbf1dd44d1f57d40ae601a5f98351f2f0a5c2562b1e174 +size 2498459 diff --git a/dars/dars.lock b/dars/dars.lock index a9aceb2b..625a0bee 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar": { - "sha256": "096a3f37ca9107abdf97c11fef75169084f74cc3a55249a6cf5cda15ba4c2a86", - "size": 2497513, + "sha256": "f027dd4ad6d5a5ccebcbf1dd44d1f57d40ae601a5f98351f2f0a5c2562b1e174", + "size": 2498459, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T00:41:37.578Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index f706fba1..eb7886fb 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -252,6 +252,19 @@ 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 @@ -281,7 +294,7 @@ stockSecurityShareEvents maps issuanceData = do event_date = issuanceData.date event_priority = 0 quantity_delta = issuanceData.quantity) - :: (cancellationEvents ++ repurchaseEvents ++ retractionEvents ++ splitEvents)) + :: (cancellationEvents ++ repurchaseEvents ++ conversionEvents ++ retractionEvents ++ splitEvents)) stockIssuanceCurrentQuantity : CapTableMaps -> StockIssuanceOcfData -> Update Decimal stockIssuanceCurrentQuantity maps issuanceData = do From 1df0ee4561babacd9f85de7a3425156fe0470122 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 23:26:06 -0400 Subject: [PATCH 11/62] Index stock share events for ceiling validation --- .../0.0.3/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 153 ++++++++++-------- 3 files changed, 90 insertions(+), 71 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar index 3aa7bc8c..e28aa5d3 100644 --- a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f027dd4ad6d5a5ccebcbf1dd44d1f57d40ae601a5f98351f2f0a5c2562b1e174 -size 2498459 +oid sha256:61832a7866fa462b9fec773e4e251a5938e27ca0c52fa52003adc5aad80e407c +size 2503696 diff --git a/dars/dars.lock b/dars/dars.lock index 625a0bee..5bd1c6fb 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar": { - "sha256": "f027dd4ad6d5a5ccebcbf1dd44d1f57d40ae601a5f98351f2f0a5c2562b1e174", - "size": 2498459, + "sha256": "61832a7866fa462b9fec773e4e251a5938e27ca0c52fa52003adc5aad80e407c", + "size": 2503696, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T00:41:37.578Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index eb7886fb..6ef210a1 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -15,7 +15,7 @@ import qualified DA.Map as Map import DA.List (sortOn) import DA.Action (foldlA, when) import DA.Foldable (mapA_) -import DA.Optional (isNone, isSome) +import DA.Optional (fromOptional, isNone, isSome) import Fairmint.OpenCapTable.Types.Core (Context) import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..)) @@ -119,6 +119,11 @@ data StockClassShareEvent event_priority: Int deriving (Eq, Show) +data StockClassShareEventIndex = StockClassShareEventIndex with + stock_security_events_by_security_id: Map Text [StockClassShareEvent] + stock_class_split_events_by_stock_class_id: Map Text [StockClassShareEvent] + deriving (Eq, Show) + authorizedSharesCeiling : OcfInitialSharesAuthorized -> AuthorizedSharesCeiling authorizedSharesCeiling auth = case auth of OcfInitialSharesNumeric n -> AuthorizedSharesFinite n @@ -218,98 +223,116 @@ stockClassShareEventSortKey event = case event of 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 + StockClassShareMultiplier with event_date -> event_date >= earliest + StockClassShareRetraction with event_date -> event_date >= earliest + applyStockClassShareEvent : Decimal -> StockClassShareEvent -> Decimal applyStockClassShareEvent current event = case event of StockClassShareDelta with quantity_delta -> nonNegativeDecimal (current + quantity_delta) StockClassShareMultiplier with ratio -> current * ratio.numerator / ratio.denominator StockClassShareRetraction _ _ -> 0.0 -stockSecurityShareEvents : CapTableMaps -> StockIssuanceOcfData -> Update [StockClassShareEvent] -stockSecurityShareEvents maps issuanceData = do - cancellationEvents <- foldlA - (\events cancellationCid -> do +addStockClassShareEvent : Text -> StockClassShareEvent -> Map Text [StockClassShareEvent] -> Map Text [StockClassShareEvent] +addStockClassShareEvent key event index = + Map.insert key (event :: fromOptional [] (Map.lookup key index)) index + +stockClassShareEventIndex : CapTableMaps -> Update StockClassShareEventIndex +stockClassShareEventIndex maps = do + cancellationEventsBySecurityId <- foldlA + (\index cancellationCid -> do cancellation <- fetch cancellationCid - if cancellation.cancellation_data.security_id == issuanceData.security_id - then pure ((StockClassShareDelta with + pure (addStockClassShareEvent + cancellation.cancellation_data.security_id + (StockClassShareDelta with event_date = cancellation.cancellation_data.date event_priority = 1 quantity_delta = 0.0 - cancellation.cancellation_data.quantity) - :: events) - else pure events) - [] + index)) + Map.empty (Map.values maps.stock_cancellations) - repurchaseEvents <- foldlA - (\events repurchaseCid -> do + repurchaseEventsBySecurityId <- foldlA + (\index repurchaseCid -> do repurchase <- fetch repurchaseCid - if repurchase.repurchase_data.security_id == issuanceData.security_id - then pure ((StockClassShareDelta with + pure (addStockClassShareEvent + repurchase.repurchase_data.security_id + (StockClassShareDelta with event_date = repurchase.repurchase_data.date event_priority = 1 quantity_delta = 0.0 - repurchase.repurchase_data.quantity) - :: events) - else pure events) - [] + index)) + cancellationEventsBySecurityId (Map.values maps.stock_repurchases) - conversionEvents <- foldlA - (\events conversionCid -> do + conversionEventsBySecurityId <- foldlA + (\index conversionCid -> do conversion <- fetch conversionCid - if conversion.conversion_data.security_id == issuanceData.security_id - then pure ((StockClassShareDelta with + pure (addStockClassShareEvent + 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) - :: events) - else pure events) - [] + index)) + repurchaseEventsBySecurityId (Map.values maps.stock_conversions) - retractionEvents <- foldlA - (\events retractionCid -> do + retractionEventsBySecurityId <- foldlA + (\index retractionCid -> do retraction <- fetch retractionCid - if retraction.retraction_data.security_id == issuanceData.security_id - then pure ((StockClassShareRetraction with + pure (addStockClassShareEvent + retraction.retraction_data.security_id + (StockClassShareRetraction with event_date = retraction.retraction_data.date event_priority = 3) - :: events) - else pure events) - [] + index)) + conversionEventsBySecurityId (Map.values maps.stock_retractions) - splitEvents <- foldlA - (\events splitCid -> do + splitEventsByStockClassId <- foldlA + (\index 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 + pure (addStockClassShareEvent + split.split_data.stock_class_id + (StockClassShareMultiplier with event_date = split.split_data.date event_priority = 2 ratio = split.split_data.split_ratio) - :: events) - else pure events) - [] + index)) + Map.empty (Map.values maps.stock_class_splits) - pure ((StockClassShareDelta with + pure StockClassShareEventIndex with + stock_security_events_by_security_id = retractionEventsBySecurityId + stock_class_split_events_by_stock_class_id = splitEventsByStockClassId + +stockIssuedShareEvents : StockClassShareEventIndex -> StockIssuanceOcfData -> [StockClassShareEvent] +stockIssuedShareEvents eventIndex issuanceData = + let securityEvents = fromOptional [] (Map.lookup issuanceData.security_id eventIndex.stock_security_events_by_security_id) + 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 ++ conversionEvents ++ retractionEvents ++ splitEvents)) + :: (securityEvents ++ splitEvents) -stockIssuanceCurrentQuantity : CapTableMaps -> StockIssuanceOcfData -> Update Decimal -stockIssuanceCurrentQuantity maps issuanceData = do - events <- stockSecurityShareEvents maps issuanceData - pure $ foldl applyStockClassShareEvent 0.0 (sortOn stockClassShareEventSortKey events) +stockIssuanceCurrentQuantity : StockClassShareEventIndex -> StockIssuanceOcfData -> Decimal +stockIssuanceCurrentQuantity eventIndex issuanceData = + foldl applyStockClassShareEvent 0.0 (sortOn stockClassShareEventSortKey (stockIssuedShareEvents eventIndex issuanceData)) -stockClassIssuedQuantity : CapTableMaps -> Text -> Update Decimal -stockClassIssuedQuantity maps stockClassId = do +stockClassIssuedQuantity : StockClassShareEventIndex -> CapTableMaps -> Text -> Update Decimal +stockClassIssuedQuantity eventIndex maps stockClassId = do quantities <- foldlA (\acc issuanceCid -> do issuance <- fetch issuanceCid if issuance.issuance_data.stock_class_id == stockClassId - then do - quantity <- stockIssuanceCurrentQuantity maps issuance.issuance_data - pure (quantity :: acc) + then pure (stockIssuanceCurrentQuantity eventIndex issuance.issuance_data :: acc) else pure acc) [] (Map.values maps.stock_issuances) @@ -324,24 +347,25 @@ assertWithinStockClassAuthorizedShares stockClassId issuedQuantity limitOpt = ca None -> pure () validateAllStockClassAuthorizedShares : + StockClassShareEventIndex -> CapTableMaps -> Map Text StockClassAuthorizedSharesAdjustmentOcfData -> Update () -validateAllStockClassAuthorizedShares maps adjustmentIndex = +validateAllStockClassAuthorizedShares eventIndex maps adjustmentIndex = mapA_ (\stockClassCid -> do stockClass <- fetch stockClassCid - currentIssued <- stockClassIssuedQuantity maps stockClass.stock_class_data.id + currentIssued <- stockClassIssuedQuantity eventIndex maps stockClass.stock_class_data.id let limitOpt = currentStockClassAuthorizedLimit adjustmentIndex stockClass.stock_class_data assertWithinStockClassAuthorizedShares stockClass.stock_class_data.id currentIssued limitOpt) (Map.values maps.stock_classes) -validateEditedStockClassInitialAuthorizedShares : CapTableMaps -> [OcfEditData] -> Update () -validateEditedStockClassInitialAuthorizedShares maps edits = +validateEditedStockClassInitialAuthorizedShares : StockClassShareEventIndex -> CapTableMaps -> [OcfEditData] -> Update () +validateEditedStockClassInitialAuthorizedShares eventIndex maps edits = mapA_ (\editData -> case editData of OcfEditStockClass stockClassData -> do - currentIssued <- stockClassIssuedQuantity maps stockClassData.id + currentIssued <- stockClassIssuedQuantity eventIndex maps stockClassData.id let limitOpt = authorizedSharesLimit stockClassData.initial_shares_authorized assertWithinStockClassAuthorizedShares stockClassData.id currentIssued limitOpt _ -> pure ()) @@ -412,19 +436,14 @@ validateAuthorizedShareCeilings : CapTableMaps -> Map Text StockClassAuthorizedSharesAdjustmentOcfData -> ContractId Issuer -> - Update () -validateAuthorizedShareCeilings maps adjustmentIndex issuerCid = do - validateAllStockClassAuthorizedShares maps adjustmentIndex - validateIssuerAuthorizedShares maps adjustmentIndex issuerCid - -validateAuthorizedShareCeilingsForEdits : - CapTableMaps -> - Map Text StockClassAuthorizedSharesAdjustmentOcfData -> [OcfEditData] -> Update () -validateAuthorizedShareCeilingsForEdits maps adjustmentIndex edits = do - validateEditedStockClassInitialAuthorizedShares maps edits - validateEditedIssuerInitialAuthorizedShares maps adjustmentIndex edits +validateAuthorizedShareCeilings maps adjustmentIndex issuerCid initialAuthorizedShareEdits = do + eventIndex <- stockClassShareEventIndex maps + validateAllStockClassAuthorizedShares eventIndex maps adjustmentIndex + validateIssuerAuthorizedShares maps adjustmentIndex issuerCid + validateEditedStockClassInitialAuthorizedShares eventIndex maps initialAuthorizedShareEdits + validateEditedIssuerInitialAuthorizedShares maps adjustmentIndex initialAuthorizedShareEdits -- | Get the processing tier for a create operation getCreateTier : OcfCreateData -> Int @@ -565,10 +584,10 @@ template CapTable sortedCreates authorizedShareAdjustmentIndex <- stockClassAuthorizedAdjustmentIndex finalMaps - validateAuthorizedShareCeilings finalMaps authorizedShareAdjustmentIndex newIssuerCid - validateAuthorizedShareCeilingsForEdits + validateAuthorizedShareCeilings finalMaps authorizedShareAdjustmentIndex + newIssuerCid (issuerInitialAuthorizedShareEdits ++ regularInitialAuthorizedShareEdits) -- Create new CapTable with updated maps and potentially new issuer From 790d5ce12186e4f707efb2860bdff34bb6a356aa Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Wed, 8 Jul 2026 23:47:01 -0400 Subject: [PATCH 12/62] Add authorized share same-date tie-break tests --- .../TestIssuerAuthorizedShares.daml | 19 ++++++++++++++++++ .../TestStockClassAuthorizedShares.daml | 20 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml index 55a555e2..0c38bbc5 100644 --- a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml @@ -62,6 +62,25 @@ testIssuerAuthorizedSharesAdjustmentCannotDropBelowStockClasses = script do edits = [] deletes = [] +testSameDateLowerIssuerAuthorizedSharesAdjustmentWins = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + (capTableWithClass, _) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_ISSUER_SAME_DATE" 100.0) + result <- submit issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with + creates = + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_SAME_DATE_LOW" 100.0) + , CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_SAME_DATE_HIGH" 150.0) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockClass (limitedStockClass "SC_ISSUER_SAME_DATE_EXTRA" 50.0)] + edits = [] + deletes = [] + testIssuerEditCannotDropBelowStockClasses = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 417b0766..b44892d7 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -233,6 +233,26 @@ testSameDateLowerStockClassAuthorizedSharesAdjustmentWins = script do edits = [] deletes = [] +testSameDateLowerStockClassAuthorizedSharesAdjustmentWinsWithReversedCreateOrder = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_SAME_DATE_REVERSED" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_SAME_DATE_REVERSED" "Same Date Reversed Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_SAME_DATE_LOW_FIRST" stockClassId 100.0) + , CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_SAME_DATE_HIGH_SECOND" stockClassId 200.0) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_SAME_DATE_REVERSED_ISSUED" "SEC_SAME_DATE_REVERSED_ISSUED" stakeholderId stockClassId) with quantity = 150.0)] + edits = [] + deletes = [] + testStockClassSplitCannotLeaveIssuedSharesAboveAuthorizedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp From 0820f92f2888caa8899f0877715741f4e21fbf8a Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 02:51:53 -0400 Subject: [PATCH 13/62] Add issuer same-date ceiling tie-break test --- .../TestIssuerAuthorizedShares.daml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml index 0c38bbc5..9aec56fa 100644 --- a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml @@ -81,6 +81,25 @@ testSameDateLowerIssuerAuthorizedSharesAdjustmentWins = script do edits = [] deletes = [] +testSameDateLowerIssuerAuthorizedSharesAdjustmentWinsWithReversedCreateOrder = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + (capTableWithClass, _) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_ISSUER_SAME_DATE_REVERSED" 100.0) + result <- submit issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with + creates = + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_SAME_DATE_HIGH_FIRST" 150.0) + , CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_SAME_DATE_LOW_SECOND" 100.0) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockClass (limitedStockClass "SC_ISSUER_SAME_DATE_REVERSED_EXTRA" 50.0)] + edits = [] + deletes = [] + testIssuerEditCannotDropBelowStockClasses = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 From 22972de238542b2982bc2688dc9563f3e3674f52 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 03:49:50 -0400 Subject: [PATCH 14/62] Enforce authorized share ceiling dates --- .../TestIssuerAuthorizedShares.daml | 20 ++ .../TestStockClassAuthorizedShares.daml | 37 +++ .../0.0.3/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 244 ++++++++++++++++++ 5 files changed, 305 insertions(+), 4 deletions(-) diff --git a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml index 9aec56fa..0d2eed6e 100644 --- a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml @@ -5,6 +5,9 @@ import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSha import qualified Fairmint.OpenCapTable.CapTable as CT 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 issuerAuthorizedSharesAdjustment : Text -> Decimal -> IssuerAuthorizedSharesAdjustmentOcfData @@ -159,6 +162,23 @@ testStockClassAuthorizedSharesAdjustmentCannotExceedIssuerAuthorizedShares = scr edits = [] deletes = [] +testStockClassAuthorizedSharesAdjustmentCannotUseLaterIssuerAuthorizedSharesAdjustment = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + (capTableWithClass, stockClassId) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_CLASS_BEFORE_ISSUER_RAISE" 100.0) + let classAdjustment = stockClassAuthorizedSharesAdjustment "TX_CLASS_BEFORE_ISSUER_RAISE" stockClassId 150.0 + let laterIssuerAdjustment = (issuerAuthorizedSharesAdjustment "TX_ISSUER_AFTER_CLASS_RAISE" 150.0) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + + submitMustFail issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassAuthorizedSharesAdjustment classAdjustment + , CT.OcfCreateIssuerAuthorizedSharesAdjustment laterIssuerAdjustment + ] + edits = [] + deletes = [] + testIssuerAuthorizedSharesAdjustmentDeleteCannotInvalidateStockClasses = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index b44892d7..913339cf 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -109,6 +109,23 @@ testStockClassAuthorizedSharesAdjustmentAllowsAdditionalIssuance = script do _ <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_EXTRA" "SEC_EXTRA" stakeholderId stockClassId) with quantity = 50.0) pure () +testStockIssuanceCannotUseLaterStockClassAuthorizedSharesAdjustment = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_BACKDATED_ISSUANCE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_BACKDATED_ISSUANCE" "Backdated Holder" + let laterAdjustment = (stockClassAuthorizedSharesAdjustment "TX_AUTH_AFTER_ISSUANCE" stockClassId 150.0) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassAuthorizedSharesAdjustment laterAdjustment + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_BACKDATED_TOO_MANY" "SEC_BACKDATED_TOO_MANY" stakeholderId stockClassId) with quantity = 125.0) + ] + edits = [] + deletes = [] + testStockClassAuthorizedSharesAdjustmentCannotDropBelowIssuedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -320,6 +337,26 @@ testStockCancellationReducesIssuedSharesForCeiling = script do _ <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_CANCEL_REISSUE" "SEC_CANCEL_REISSUE" stakeholderId stockClassId) with quantity = 50.0) pure () +testStockClassSplitCannotUseLaterAuthorizedSharesAdjustment = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_SPLIT_LATER_AUTH" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_SPLIT_LATER_AUTH" "Split Later Auth Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_SPLIT_LATER_AUTH_ISSUED" "SEC_SPLIT_LATER_AUTH_ISSUED" stakeholderId stockClassId) with quantity = 100.0) + let laterAdjustment = (stockClassAuthorizedSharesAdjustment "TX_AUTH_AFTER_SPLIT" stockClassId 200.0) with + date = DT.time (DA.date 2024 Jan 03) 0 0 0 + let earlierSplit = (stockClassSplit "TX_SPLIT_BEFORE_AUTH" stockClassId 2.0 1.0) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassAuthorizedSharesAdjustment laterAdjustment + , CT.OcfCreateStockClassSplit earlierSplit + ] + edits = [] + deletes = [] + testStockRepurchaseReducesIssuedSharesForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar index e28aa5d3..f0ed49f8 100644 --- a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:61832a7866fa462b9fec773e4e251a5938e27ca0c52fa52003adc5aad80e407c -size 2503696 +oid sha256:b375002c60e494a38af0844088140f24879b04c1e77a7b7d00ff8d5f49917ce5 +size 2527616 diff --git a/dars/dars.lock b/dars/dars.lock index 5bd1c6fb..944948b8 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar": { - "sha256": "61832a7866fa462b9fec773e4e251a5938e27ca0c52fa52003adc5aad80e407c", - "size": 2503696, + "sha256": "b375002c60e494a38af0844088140f24879b04c1e77a7b7d00ff8d5f49917ce5", + "size": 2527616, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T00:41:37.578Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 6ef210a1..cb534970 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -155,6 +155,26 @@ issuerAuthorizedAdjustmentTakesPrecedence candidate current = candidate.date > current.date || (candidate.date == current.date && candidate.new_shares_authorized < current.new_shares_authorized) +addDateById : Text -> Time -> Map Text [Time] -> Map Text [Time] +addDateById key date index = + Map.insert key (date :: fromOptional [] (Map.lookup key index)) index + +addIssuerAuthorizedAdjustmentById : + Text -> + IssuerAuthorizedSharesAdjustmentOcfData -> + Map Text [IssuerAuthorizedSharesAdjustmentOcfData] -> + Map Text [IssuerAuthorizedSharesAdjustmentOcfData] +addIssuerAuthorizedAdjustmentById key adjustment index = + Map.insert key (adjustment :: fromOptional [] (Map.lookup key index)) index + +addStockClassAuthorizedAdjustmentById : + Text -> + StockClassAuthorizedSharesAdjustmentOcfData -> + Map Text [StockClassAuthorizedSharesAdjustmentOcfData] -> + Map Text [StockClassAuthorizedSharesAdjustmentOcfData] +addStockClassAuthorizedAdjustmentById key adjustment index = + Map.insert key (adjustment :: fromOptional [] (Map.lookup key index)) index + stockClassAuthorizedAdjustmentIndex : CapTableMaps -> Update (Map Text StockClassAuthorizedSharesAdjustmentOcfData) stockClassAuthorizedAdjustmentIndex maps = foldlA @@ -171,6 +191,34 @@ stockClassAuthorizedAdjustmentIndex maps = Map.empty (Map.values maps.stock_class_authorized_shares_adjustments) +stockClassAuthorizedAdjustmentHistoryIndex : CapTableMaps -> Update (Map Text [StockClassAuthorizedSharesAdjustmentOcfData]) +stockClassAuthorizedAdjustmentHistoryIndex maps = + foldlA + (\index adjustmentCid -> do + adjustment <- fetch adjustmentCid + pure (addStockClassAuthorizedAdjustmentById adjustment.adjustment_data.stock_class_id adjustment.adjustment_data index)) + Map.empty + (Map.values maps.stock_class_authorized_shares_adjustments) + +stockClassAuthorizedAdjustmentAsOf : + Map Text [StockClassAuthorizedSharesAdjustmentOcfData] -> + Text -> + Time -> + Optional StockClassAuthorizedSharesAdjustmentOcfData +stockClassAuthorizedAdjustmentAsOf adjustmentHistory stockClassId asOf = + foldl + (\latest candidate -> + if candidate.date <= asOf + then case latest of + Some current -> + if stockClassAuthorizedAdjustmentTakesPrecedence candidate current + then Some candidate + else latest + None -> Some candidate + else latest) + None + (fromOptional [] (Map.lookup stockClassId adjustmentHistory)) + currentStockClassAuthorizedCeiling : Map Text StockClassAuthorizedSharesAdjustmentOcfData -> StockClassOcfData -> @@ -189,6 +237,26 @@ currentStockClassAuthorizedLimit adjustmentIndex stockClassData = AuthorizedSharesFinite n -> Some n _ -> None +stockClassAuthorizedCeilingAt : + Map Text [StockClassAuthorizedSharesAdjustmentOcfData] -> + StockClassOcfData -> + Time -> + AuthorizedSharesCeiling +stockClassAuthorizedCeilingAt adjustmentHistory stockClassData asOf = + case stockClassAuthorizedAdjustmentAsOf adjustmentHistory stockClassData.id asOf of + Some adjustment -> AuthorizedSharesFinite adjustment.new_shares_authorized + None -> authorizedSharesCeiling stockClassData.initial_shares_authorized + +stockClassAuthorizedLimitAt : + Map Text [StockClassAuthorizedSharesAdjustmentOcfData] -> + StockClassOcfData -> + Time -> + Optional Decimal +stockClassAuthorizedLimitAt adjustmentHistory stockClassData asOf = + case stockClassAuthorizedCeilingAt adjustmentHistory stockClassData asOf of + AuthorizedSharesFinite n -> Some n + _ -> None + latestIssuerAuthorizedAdjustment : CapTableMaps -> Text -> Update (Optional IssuerAuthorizedSharesAdjustmentOcfData) latestIssuerAuthorizedAdjustment maps issuerId = foldlA @@ -205,6 +273,34 @@ latestIssuerAuthorizedAdjustment maps issuerId = None (Map.values maps.issuer_authorized_shares_adjustments) +issuerAuthorizedAdjustmentHistoryIndex : CapTableMaps -> Update (Map Text [IssuerAuthorizedSharesAdjustmentOcfData]) +issuerAuthorizedAdjustmentHistoryIndex maps = + foldlA + (\index adjustmentCid -> do + adjustment <- fetch adjustmentCid + pure (addIssuerAuthorizedAdjustmentById adjustment.adjustment_data.issuer_id adjustment.adjustment_data index)) + Map.empty + (Map.values maps.issuer_authorized_shares_adjustments) + +issuerAuthorizedAdjustmentAsOf : + Map Text [IssuerAuthorizedSharesAdjustmentOcfData] -> + Text -> + Time -> + Optional IssuerAuthorizedSharesAdjustmentOcfData +issuerAuthorizedAdjustmentAsOf adjustmentHistory issuerId asOf = + foldl + (\latest candidate -> + if candidate.date <= asOf + then case latest of + Some current -> + if issuerAuthorizedAdjustmentTakesPrecedence candidate current + then Some candidate + else latest + None -> Some candidate + else latest) + None + (fromOptional [] (Map.lookup issuerId adjustmentHistory)) + currentIssuerAuthorizedLimit : CapTableMaps -> ContractId Issuer -> Update (Optional Decimal) currentIssuerAuthorizedLimit maps issuerCid = do issuerContract <- fetch issuerCid @@ -213,6 +309,16 @@ currentIssuerAuthorizedLimit maps issuerCid = do Some adjustment -> Some adjustment.new_shares_authorized None -> issuerAuthorizedSharesLimit issuerContract.issuer_data.initial_shares_authorized +issuerAuthorizedLimitAt : + Map Text [IssuerAuthorizedSharesAdjustmentOcfData] -> + IssuerOcfData -> + Time -> + Optional Decimal +issuerAuthorizedLimitAt adjustmentHistory issuerData asOf = + case issuerAuthorizedAdjustmentAsOf adjustmentHistory issuerData.id asOf of + Some adjustment -> Some adjustment.new_shares_authorized + None -> issuerAuthorizedSharesLimit issuerData.initial_shares_authorized + nonNegativeDecimal : Decimal -> Decimal nonNegativeDecimal n = if n < 0.0 then 0.0 else n @@ -229,6 +335,12 @@ stockClassShareEventOnOrAfter earliest event = case event of 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 + StockClassShareMultiplier with event_date -> event_date <= latest + StockClassShareRetraction with event_date -> event_date <= latest + applyStockClassShareEvent : Decimal -> StockClassShareEvent -> Decimal applyStockClassShareEvent current event = case event of StockClassShareDelta with quantity_delta -> nonNegativeDecimal (current + quantity_delta) @@ -326,6 +438,18 @@ stockIssuanceCurrentQuantity : StockClassShareEventIndex -> StockIssuanceOcfData stockIssuanceCurrentQuantity eventIndex issuanceData = foldl applyStockClassShareEvent 0.0 (sortOn stockClassShareEventSortKey (stockIssuedShareEvents eventIndex issuanceData)) +stockIssuanceQuantityAt : StockClassShareEventIndex -> StockIssuanceOcfData -> Time -> Decimal +stockIssuanceQuantityAt eventIndex issuanceData asOf = + if issuanceData.date <= asOf + then + foldl + applyStockClassShareEvent + 0.0 + (sortOn + stockClassShareEventSortKey + (filter (stockClassShareEventOnOrBefore asOf) (stockIssuedShareEvents eventIndex issuanceData))) + else 0.0 + stockClassIssuedQuantity : StockClassShareEventIndex -> CapTableMaps -> Text -> Update Decimal stockClassIssuedQuantity eventIndex maps stockClassId = do quantities <- foldlA @@ -338,6 +462,18 @@ stockClassIssuedQuantity eventIndex maps stockClassId = do (Map.values maps.stock_issuances) pure (foldl (\total quantity -> total + quantity) 0.0 quantities) +stockClassIssuedQuantityAt : StockClassShareEventIndex -> CapTableMaps -> Text -> Time -> Update Decimal +stockClassIssuedQuantityAt eventIndex maps stockClassId asOf = do + quantities <- foldlA + (\acc issuanceCid -> do + issuance <- fetch issuanceCid + if issuance.issuance_data.stock_class_id == stockClassId + then pure (stockIssuanceQuantityAt eventIndex issuance.issuance_data asOf :: acc) + else pure acc) + [] + (Map.values maps.stock_issuances) + pure (foldl (\total quantity -> total + quantity) 0.0 quantities) + assertWithinStockClassAuthorizedShares : Text -> Decimal -> Optional Decimal -> Update () assertWithinStockClassAuthorizedShares stockClassId issuedQuantity limitOpt = case limitOpt of Some limit -> @@ -346,6 +482,37 @@ assertWithinStockClassAuthorizedShares stockClassId issuedQuantity limitOpt = ca (issuedQuantity <= limit) None -> pure () +assertWithinStockClassAuthorizedSharesAtDate : Text -> Time -> Decimal -> Optional Decimal -> Update () +assertWithinStockClassAuthorizedSharesAtDate stockClassId asOf issuedQuantity limitOpt = case limitOpt of + Some limit -> + assertMsg + ("Stock class authorized shares exceeded for " <> stockClassId <> " on " <> show asOf <> ": issued " <> show issuedQuantity <> " exceeds authorized " <> show limit) + (issuedQuantity <= limit) + None -> pure () + +stockClassAuthorizedValidationDateIndex : CapTableMaps -> Update (Map Text [Time]) +stockClassAuthorizedValidationDateIndex maps = do + issuanceDates <- foldlA + (\index issuanceCid -> do + issuance <- fetch issuanceCid + pure (addDateById issuance.issuance_data.stock_class_id issuance.issuance_data.date index)) + Map.empty + (Map.values maps.stock_issuances) + + splitDates <- foldlA + (\index splitCid -> do + split <- fetch splitCid + pure (addDateById split.split_data.stock_class_id split.split_data.date index)) + issuanceDates + (Map.values maps.stock_class_splits) + + foldlA + (\index adjustmentCid -> do + adjustment <- fetch adjustmentCid + pure (addDateById adjustment.adjustment_data.stock_class_id adjustment.adjustment_data.date index)) + splitDates + (Map.values maps.stock_class_authorized_shares_adjustments) + validateAllStockClassAuthorizedShares : StockClassShareEventIndex -> CapTableMaps -> @@ -360,6 +527,25 @@ validateAllStockClassAuthorizedShares eventIndex maps adjustmentIndex = assertWithinStockClassAuthorizedShares stockClass.stock_class_data.id currentIssued limitOpt) (Map.values maps.stock_classes) +validateStockClassAuthorizedSharesAtDates : + StockClassShareEventIndex -> + CapTableMaps -> + Map Text [StockClassAuthorizedSharesAdjustmentOcfData] -> + Map Text [Time] -> + Update () +validateStockClassAuthorizedSharesAtDates eventIndex maps adjustmentHistory validationDates = + mapA_ + (\stockClassCid -> do + stockClass <- fetch stockClassCid + let stockClassId = stockClass.stock_class_data.id + mapA_ + (\asOf -> do + issuedAtDate <- stockClassIssuedQuantityAt eventIndex maps stockClassId asOf + let limitOpt = stockClassAuthorizedLimitAt adjustmentHistory stockClass.stock_class_data asOf + assertWithinStockClassAuthorizedSharesAtDate stockClassId asOf issuedAtDate limitOpt) + (fromOptional [] (Map.lookup stockClassId validationDates))) + (Map.values maps.stock_classes) + validateEditedStockClassInitialAuthorizedShares : StockClassShareEventIndex -> CapTableMaps -> [OcfEditData] -> Update () validateEditedStockClassInitialAuthorizedShares eventIndex maps edits = mapA_ @@ -378,6 +564,20 @@ addStockClassAuthorizedContribution totalOpt ceiling = case (totalOpt, ceiling) (_, AuthorizedSharesUnlimited) -> None (None, _) -> None +currentStockClassAuthorizedTotalAt : + CapTableMaps -> + Map Text [StockClassAuthorizedSharesAdjustmentOcfData] -> + Time -> + Update (Optional Decimal) +currentStockClassAuthorizedTotalAt maps adjustmentHistory asOf = + foldlA + (\totalOpt stockClassCid -> do + stockClass <- fetch stockClassCid + let ceiling = stockClassAuthorizedCeilingAt adjustmentHistory stockClass.stock_class_data asOf + pure (addStockClassAuthorizedContribution totalOpt ceiling)) + (Some 0.0) + (Map.values maps.stock_classes) + currentStockClassAuthorizedTotal : CapTableMaps -> Map Text StockClassAuthorizedSharesAdjustmentOcfData -> @@ -410,6 +610,45 @@ validateIssuerAuthorizedShares maps adjustmentIndex issuerCid = do None -> assertMsg "Issuer authorized shares exceeded: stock class authorization is unlimited but issuer authorization is finite" False +issuerAuthorizedValidationDates : CapTableMaps -> Update [Time] +issuerAuthorizedValidationDates maps = do + issuerAdjustmentDates <- foldlA + (\dates adjustmentCid -> do + adjustment <- fetch adjustmentCid + pure (adjustment.adjustment_data.date :: dates)) + [] + (Map.values maps.issuer_authorized_shares_adjustments) + + foldlA + (\dates adjustmentCid -> do + adjustment <- fetch adjustmentCid + pure (adjustment.adjustment_data.date :: dates)) + issuerAdjustmentDates + (Map.values maps.stock_class_authorized_shares_adjustments) + +validateIssuerAuthorizedSharesAtDates : + CapTableMaps -> + Map Text [StockClassAuthorizedSharesAdjustmentOcfData] -> + Map Text [IssuerAuthorizedSharesAdjustmentOcfData] -> + ContractId Issuer -> + Update () +validateIssuerAuthorizedSharesAtDates maps stockClassAdjustmentHistory issuerAdjustmentHistory issuerCid = do + issuerContract <- fetch issuerCid + validationDates <- issuerAuthorizedValidationDates maps + mapA_ + (\asOf -> case issuerAuthorizedLimitAt issuerAdjustmentHistory issuerContract.issuer_data asOf of + None -> pure () + Some issuerLimit -> do + stockClassTotalOpt <- currentStockClassAuthorizedTotalAt maps stockClassAdjustmentHistory asOf + case stockClassTotalOpt of + Some stockClassTotal -> + assertMsg + ("Issuer authorized shares exceeded on " <> show asOf <> ": stock classes authorize " <> show stockClassTotal <> " shares but issuer authorizes " <> show issuerLimit) + (stockClassTotal <= issuerLimit) + None -> + assertMsg ("Issuer authorized shares exceeded on " <> show asOf <> ": stock class authorization is unlimited but issuer authorization is finite") False) + validationDates + validateEditedIssuerInitialAuthorizedShares : CapTableMaps -> Map Text StockClassAuthorizedSharesAdjustmentOcfData -> @@ -440,8 +679,13 @@ validateAuthorizedShareCeilings : Update () validateAuthorizedShareCeilings maps adjustmentIndex issuerCid initialAuthorizedShareEdits = do eventIndex <- stockClassShareEventIndex maps + stockClassAdjustmentHistory <- stockClassAuthorizedAdjustmentHistoryIndex maps + stockClassValidationDates <- stockClassAuthorizedValidationDateIndex maps + issuerAdjustmentHistory <- issuerAuthorizedAdjustmentHistoryIndex maps validateAllStockClassAuthorizedShares eventIndex maps adjustmentIndex + validateStockClassAuthorizedSharesAtDates eventIndex maps stockClassAdjustmentHistory stockClassValidationDates validateIssuerAuthorizedShares maps adjustmentIndex issuerCid + validateIssuerAuthorizedSharesAtDates maps stockClassAdjustmentHistory issuerAdjustmentHistory issuerCid validateEditedStockClassInitialAuthorizedShares eventIndex maps initialAuthorizedShareEdits validateEditedIssuerInitialAuthorizedShares maps adjustmentIndex initialAuthorizedShareEdits From 2e6b4ffa74de5e81456c0dd50cc751cc8628144e Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 04:11:23 -0400 Subject: [PATCH 15/62] Reuse authorized share ceiling validation inputs --- .../0.0.3/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 175 +++++++++++------- 3 files changed, 107 insertions(+), 76 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar index f0ed49f8..3cc930cf 100644 --- a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b375002c60e494a38af0844088140f24879b04c1e77a7b7d00ff8d5f49917ce5 -size 2527616 +oid sha256:5110557d64360e4b50fc981fea53c9aa68c2cf6669bfa680217ac95726f009e9 +size 2529166 diff --git a/dars/dars.lock b/dars/dars.lock index 944948b8..b08b1624 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar": { - "sha256": "b375002c60e494a38af0844088140f24879b04c1e77a7b7d00ff8d5f49917ce5", - "size": 2527616, + "sha256": "5110557d64360e4b50fc981fea53c9aa68c2cf6669bfa680217ac95726f009e9", + "size": 2529166, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T00:41:37.578Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index cb534970..dc1b78f3 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -175,6 +175,32 @@ addStockClassAuthorizedAdjustmentById : addStockClassAuthorizedAdjustmentById key adjustment index = Map.insert key (adjustment :: fromOptional [] (Map.lookup key index)) index +addStockIssuanceDataByStockClass : + Text -> + StockIssuanceOcfData -> + Map Text [StockIssuanceOcfData] -> + Map Text [StockIssuanceOcfData] +addStockIssuanceDataByStockClass key issuanceData index = + Map.insert key (issuanceData :: fromOptional [] (Map.lookup key index)) index + +stockIssuanceDataByStockClass : CapTableMaps -> Update (Map Text [StockIssuanceOcfData]) +stockIssuanceDataByStockClass maps = + foldlA + (\index issuanceCid -> do + issuance <- fetch issuanceCid + pure (addStockIssuanceDataByStockClass issuance.issuance_data.stock_class_id issuance.issuance_data index)) + Map.empty + (Map.values maps.stock_issuances) + +stockClassDataList : CapTableMaps -> Update [StockClassOcfData] +stockClassDataList maps = + foldlA + (\stockClasses stockClassCid -> do + stockClass <- fetch stockClassCid + pure (stockClass.stock_class_data :: stockClasses)) + [] + (Map.values maps.stock_classes) + stockClassAuthorizedAdjustmentIndex : CapTableMaps -> Update (Map Text StockClassAuthorizedSharesAdjustmentOcfData) stockClassAuthorizedAdjustmentIndex maps = foldlA @@ -450,29 +476,28 @@ stockIssuanceQuantityAt eventIndex issuanceData asOf = (filter (stockClassShareEventOnOrBefore asOf) (stockIssuedShareEvents eventIndex issuanceData))) else 0.0 -stockClassIssuedQuantity : StockClassShareEventIndex -> CapTableMaps -> Text -> Update Decimal -stockClassIssuedQuantity eventIndex maps stockClassId = do - quantities <- foldlA - (\acc issuanceCid -> do - issuance <- fetch issuanceCid - if issuance.issuance_data.stock_class_id == stockClassId - then pure (stockIssuanceCurrentQuantity eventIndex issuance.issuance_data :: acc) - else pure acc) - [] - (Map.values maps.stock_issuances) - pure (foldl (\total quantity -> total + quantity) 0.0 quantities) +stockClassIssuedQuantity : + StockClassShareEventIndex -> + Map Text [StockIssuanceOcfData] -> + Text -> + Decimal +stockClassIssuedQuantity eventIndex issuancesByStockClass stockClassId = + foldl + (\total issuanceData -> total + stockIssuanceCurrentQuantity eventIndex issuanceData) + 0.0 + (fromOptional [] (Map.lookup stockClassId issuancesByStockClass)) -stockClassIssuedQuantityAt : StockClassShareEventIndex -> CapTableMaps -> Text -> Time -> Update Decimal -stockClassIssuedQuantityAt eventIndex maps stockClassId asOf = do - quantities <- foldlA - (\acc issuanceCid -> do - issuance <- fetch issuanceCid - if issuance.issuance_data.stock_class_id == stockClassId - then pure (stockIssuanceQuantityAt eventIndex issuance.issuance_data asOf :: acc) - else pure acc) - [] - (Map.values maps.stock_issuances) - pure (foldl (\total quantity -> total + quantity) 0.0 quantities) +stockClassIssuedQuantityAt : + StockClassShareEventIndex -> + Map Text [StockIssuanceOcfData] -> + Text -> + Time -> + Decimal +stockClassIssuedQuantityAt eventIndex issuancesByStockClass stockClassId asOf = + foldl + (\total issuanceData -> total + stockIssuanceQuantityAt eventIndex issuanceData asOf) + 0.0 + (fromOptional [] (Map.lookup stockClassId issuancesByStockClass)) assertWithinStockClassAuthorizedShares : Text -> Decimal -> Optional Decimal -> Update () assertWithinStockClassAuthorizedShares stockClassId issuedQuantity limitOpt = case limitOpt of @@ -515,43 +540,47 @@ stockClassAuthorizedValidationDateIndex maps = do validateAllStockClassAuthorizedShares : StockClassShareEventIndex -> - CapTableMaps -> + [StockClassOcfData] -> + Map Text [StockIssuanceOcfData] -> Map Text StockClassAuthorizedSharesAdjustmentOcfData -> Update () -validateAllStockClassAuthorizedShares eventIndex maps adjustmentIndex = +validateAllStockClassAuthorizedShares eventIndex stockClasses issuancesByStockClass adjustmentIndex = mapA_ - (\stockClassCid -> do - stockClass <- fetch stockClassCid - currentIssued <- stockClassIssuedQuantity eventIndex maps stockClass.stock_class_data.id - let limitOpt = currentStockClassAuthorizedLimit adjustmentIndex stockClass.stock_class_data - assertWithinStockClassAuthorizedShares stockClass.stock_class_data.id currentIssued limitOpt) - (Map.values maps.stock_classes) + (\stockClassData -> do + let currentIssued = stockClassIssuedQuantity eventIndex issuancesByStockClass stockClassData.id + let limitOpt = currentStockClassAuthorizedLimit adjustmentIndex stockClassData + assertWithinStockClassAuthorizedShares stockClassData.id currentIssued limitOpt) + stockClasses validateStockClassAuthorizedSharesAtDates : StockClassShareEventIndex -> - CapTableMaps -> + [StockClassOcfData] -> + Map Text [StockIssuanceOcfData] -> Map Text [StockClassAuthorizedSharesAdjustmentOcfData] -> Map Text [Time] -> Update () -validateStockClassAuthorizedSharesAtDates eventIndex maps adjustmentHistory validationDates = +validateStockClassAuthorizedSharesAtDates eventIndex stockClasses issuancesByStockClass adjustmentHistory validationDates = mapA_ - (\stockClassCid -> do - stockClass <- fetch stockClassCid - let stockClassId = stockClass.stock_class_data.id + (\stockClassData -> do + let stockClassId = stockClassData.id mapA_ (\asOf -> do - issuedAtDate <- stockClassIssuedQuantityAt eventIndex maps stockClassId asOf - let limitOpt = stockClassAuthorizedLimitAt adjustmentHistory stockClass.stock_class_data asOf + let issuedAtDate = stockClassIssuedQuantityAt eventIndex issuancesByStockClass stockClassId asOf + let limitOpt = stockClassAuthorizedLimitAt adjustmentHistory stockClassData asOf assertWithinStockClassAuthorizedSharesAtDate stockClassId asOf issuedAtDate limitOpt) (fromOptional [] (Map.lookup stockClassId validationDates))) - (Map.values maps.stock_classes) + stockClasses -validateEditedStockClassInitialAuthorizedShares : StockClassShareEventIndex -> CapTableMaps -> [OcfEditData] -> Update () -validateEditedStockClassInitialAuthorizedShares eventIndex maps edits = +validateEditedStockClassInitialAuthorizedShares : + StockClassShareEventIndex -> + Map Text [StockIssuanceOcfData] -> + [OcfEditData] -> + Update () +validateEditedStockClassInitialAuthorizedShares eventIndex issuancesByStockClass edits = mapA_ (\editData -> case editData of OcfEditStockClass stockClassData -> do - currentIssued <- stockClassIssuedQuantity eventIndex maps stockClassData.id + let currentIssued = stockClassIssuedQuantity eventIndex issuancesByStockClass stockClassData.id let limitOpt = authorizedSharesLimit stockClassData.initial_shares_authorized assertWithinStockClassAuthorizedShares stockClassData.id currentIssued limitOpt _ -> pure ()) @@ -565,43 +594,42 @@ addStockClassAuthorizedContribution totalOpt ceiling = case (totalOpt, ceiling) (None, _) -> None currentStockClassAuthorizedTotalAt : - CapTableMaps -> + [StockClassOcfData] -> Map Text [StockClassAuthorizedSharesAdjustmentOcfData] -> Time -> - Update (Optional Decimal) -currentStockClassAuthorizedTotalAt maps adjustmentHistory asOf = - foldlA - (\totalOpt stockClassCid -> do - stockClass <- fetch stockClassCid - let ceiling = stockClassAuthorizedCeilingAt adjustmentHistory stockClass.stock_class_data asOf - pure (addStockClassAuthorizedContribution totalOpt ceiling)) + Optional Decimal +currentStockClassAuthorizedTotalAt stockClasses adjustmentHistory asOf = + foldl + (\totalOpt stockClassData -> + let ceiling = stockClassAuthorizedCeilingAt adjustmentHistory stockClassData asOf + in addStockClassAuthorizedContribution totalOpt ceiling) (Some 0.0) - (Map.values maps.stock_classes) + stockClasses currentStockClassAuthorizedTotal : - CapTableMaps -> + [StockClassOcfData] -> Map Text StockClassAuthorizedSharesAdjustmentOcfData -> - Update (Optional Decimal) -currentStockClassAuthorizedTotal maps adjustmentIndex = - foldlA - (\totalOpt stockClassCid -> do - stockClass <- fetch stockClassCid - let ceiling = currentStockClassAuthorizedCeiling adjustmentIndex stockClass.stock_class_data - pure (addStockClassAuthorizedContribution totalOpt ceiling)) + Optional Decimal +currentStockClassAuthorizedTotal stockClasses adjustmentIndex = + foldl + (\totalOpt stockClassData -> + let ceiling = currentStockClassAuthorizedCeiling adjustmentIndex stockClassData + in addStockClassAuthorizedContribution totalOpt ceiling) (Some 0.0) - (Map.values maps.stock_classes) + stockClasses validateIssuerAuthorizedShares : + [StockClassOcfData] -> CapTableMaps -> Map Text StockClassAuthorizedSharesAdjustmentOcfData -> ContractId Issuer -> Update () -validateIssuerAuthorizedShares maps adjustmentIndex issuerCid = do +validateIssuerAuthorizedShares stockClasses maps adjustmentIndex issuerCid = do issuerLimitOpt <- currentIssuerAuthorizedLimit maps issuerCid case issuerLimitOpt of None -> pure () Some issuerLimit -> do - stockClassTotalOpt <- currentStockClassAuthorizedTotal maps adjustmentIndex + let stockClassTotalOpt = currentStockClassAuthorizedTotal stockClasses adjustmentIndex case stockClassTotalOpt of Some stockClassTotal -> assertMsg @@ -627,19 +655,20 @@ issuerAuthorizedValidationDates maps = do (Map.values maps.stock_class_authorized_shares_adjustments) validateIssuerAuthorizedSharesAtDates : + [StockClassOcfData] -> CapTableMaps -> Map Text [StockClassAuthorizedSharesAdjustmentOcfData] -> Map Text [IssuerAuthorizedSharesAdjustmentOcfData] -> ContractId Issuer -> Update () -validateIssuerAuthorizedSharesAtDates maps stockClassAdjustmentHistory issuerAdjustmentHistory issuerCid = do +validateIssuerAuthorizedSharesAtDates stockClasses maps stockClassAdjustmentHistory issuerAdjustmentHistory issuerCid = do issuerContract <- fetch issuerCid validationDates <- issuerAuthorizedValidationDates maps mapA_ (\asOf -> case issuerAuthorizedLimitAt issuerAdjustmentHistory issuerContract.issuer_data asOf of None -> pure () Some issuerLimit -> do - stockClassTotalOpt <- currentStockClassAuthorizedTotalAt maps stockClassAdjustmentHistory asOf + let stockClassTotalOpt = currentStockClassAuthorizedTotalAt stockClasses stockClassAdjustmentHistory asOf case stockClassTotalOpt of Some stockClassTotal -> assertMsg @@ -650,17 +679,17 @@ validateIssuerAuthorizedSharesAtDates maps stockClassAdjustmentHistory issuerAdj validationDates validateEditedIssuerInitialAuthorizedShares : - CapTableMaps -> + [StockClassOcfData] -> Map Text StockClassAuthorizedSharesAdjustmentOcfData -> [OcfEditData] -> Update () -validateEditedIssuerInitialAuthorizedShares maps adjustmentIndex edits = +validateEditedIssuerInitialAuthorizedShares stockClasses adjustmentIndex edits = mapA_ (\editData -> case editData of OcfEditIssuer issuerData -> case issuerAuthorizedSharesLimit issuerData.initial_shares_authorized of None -> pure () Some issuerLimit -> do - stockClassTotalOpt <- currentStockClassAuthorizedTotal maps adjustmentIndex + let stockClassTotalOpt = currentStockClassAuthorizedTotal stockClasses adjustmentIndex case stockClassTotalOpt of Some stockClassTotal -> assertMsg @@ -679,15 +708,17 @@ validateAuthorizedShareCeilings : Update () validateAuthorizedShareCeilings maps adjustmentIndex issuerCid initialAuthorizedShareEdits = do eventIndex <- stockClassShareEventIndex maps + stockClasses <- stockClassDataList maps + issuancesByStockClass <- stockIssuanceDataByStockClass maps stockClassAdjustmentHistory <- stockClassAuthorizedAdjustmentHistoryIndex maps stockClassValidationDates <- stockClassAuthorizedValidationDateIndex maps issuerAdjustmentHistory <- issuerAuthorizedAdjustmentHistoryIndex maps - validateAllStockClassAuthorizedShares eventIndex maps adjustmentIndex - validateStockClassAuthorizedSharesAtDates eventIndex maps stockClassAdjustmentHistory stockClassValidationDates - validateIssuerAuthorizedShares maps adjustmentIndex issuerCid - validateIssuerAuthorizedSharesAtDates maps stockClassAdjustmentHistory issuerAdjustmentHistory issuerCid - validateEditedStockClassInitialAuthorizedShares eventIndex maps initialAuthorizedShareEdits - validateEditedIssuerInitialAuthorizedShares maps adjustmentIndex initialAuthorizedShareEdits + validateAllStockClassAuthorizedShares eventIndex stockClasses issuancesByStockClass adjustmentIndex + validateStockClassAuthorizedSharesAtDates eventIndex stockClasses issuancesByStockClass stockClassAdjustmentHistory stockClassValidationDates + validateIssuerAuthorizedShares stockClasses maps adjustmentIndex issuerCid + validateIssuerAuthorizedSharesAtDates stockClasses maps stockClassAdjustmentHistory issuerAdjustmentHistory issuerCid + validateEditedStockClassInitialAuthorizedShares eventIndex issuancesByStockClass initialAuthorizedShareEdits + validateEditedIssuerInitialAuthorizedShares stockClasses adjustmentIndex initialAuthorizedShareEdits -- | Get the processing tier for a create operation getCreateTier : OcfCreateData -> Int From e072f631660d355dabd813cf96f9692bfc2a8f44 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 04:31:27 -0400 Subject: [PATCH 16/62] Validate authorized share edits historically --- Test/daml/OpenCapTable/Setup.daml | 19 +----- .../TestIssuerAuthorizedShares.daml | 31 +++++++-- .../TestStockClassAuthorizedShares.daml | 32 +++++++-- .../0.0.3/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 68 +++---------------- .../codegen/templates/loops/edit-case.daml | 11 +-- 7 files changed, 73 insertions(+), 96 deletions(-) diff --git a/Test/daml/OpenCapTable/Setup.daml b/Test/daml/OpenCapTable/Setup.daml index 246c5146..65ac7d33 100644 --- a/Test/daml/OpenCapTable/Setup.daml +++ b/Test/daml/OpenCapTable/Setup.daml @@ -1,13 +1,11 @@ module OpenCapTable.Setup where -import Fairmint.OpenCapTable.OCF.Issuer import Fairmint.OpenCapTable.CapTable import Fairmint.OpenCapTable.IssuerAuthorization import Fairmint.OpenCapTable.Types.Core (Context(..)) import Fairmint.OpenCapTable.Types.Stock (OcfInitialSharesAuthorized(..)) +import OpenCapTable.TestHelpers (defaultIssuerWithAuthorizedShares) import Daml.Script -import DA.Date -import DA.Time data TestUsers = TestUsers{ tu_system_operator: Party, tu_issuer: Party, tu_alice: Party, tu_bob: Party, tu_charlie: Party, tu_rando: Party } @@ -42,20 +40,7 @@ setupTestOcp = script do capTableCid <- submit issuer do exerciseCmd authCid CreateCapTable with - issuer_data = IssuerOcfData with - id = "ISSUER_1" - legal_name = "TestIssuer Inc." - formation_date = time (DA.Date.date 2020 Jan 15) 12 0 0 - country_of_formation = "US" - dba = None - country_subdivision_of_formation = None - country_subdivision_name_of_formation = None - tax_ids = [] - email = None - phone = None - address = None - initial_shares_authorized = Some (OcfInitialSharesNumeric 100_000_000.0) - comments = [] + issuer_data = defaultIssuerWithAuthorizedShares "ISSUER_1" (Some (OcfInitialSharesNumeric 100_000_000.0)) return TestOcp { system_operator = system_operator diff --git a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml index 0d2eed6e..95a78ed7 100644 --- a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml @@ -115,22 +115,45 @@ testIssuerEditCannotDropBelowStockClasses = script do deletes = [] testIssuerEditCannotDropInitialBelowStockClassesWhenAdjustmentExists = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 125.0 + (capTableWithClass, stockClassId) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_ISSUER_EDIT_HISTORICAL_DROP" 100.0) + let classAdjustment = stockClassAuthorizedSharesAdjustment "TX_CLASS_RAISE_BEFORE_ISSUER_EDIT" stockClassId 125.0 + let laterIssuerAdjustment = (issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_AFTER_CLASS_EDIT" 150.0) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + result <- submit issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassAuthorizedSharesAdjustment classAdjustment + , CT.OcfCreateIssuerAuthorizedSharesAdjustment laterIssuerAdjustment + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditIssuer (defaultIssuerWithAuthorizedShares "ISSUER_1" (Some (OcfInitialSharesNumeric 100.0)))] + deletes = [] + +testIssuerEditAllowsInitialBelowCurrentTotalWhenAdjustmentCoversHistory = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 result <- submit issuer do exerciseCmd capTableLimited CT.UpdateCapTable with creates = - [ CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_MASK" 150.0) - , CT.OcfCreateStockClass (limitedStockClass "SC_ISSUER_EDIT_WITH_ADJUSTMENT" 125.0) + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_HISTORY_OK" 150.0) + , CT.OcfCreateStockClass (limitedStockClass "SC_ISSUER_EDIT_HISTORY_OK" 125.0) ] edits = [] deletes = [] - submitMustFail issuer do + _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with creates = [] - edits = [CT.OcfEditIssuer (defaultIssuerWithAuthorizedShares "ISSUER_1" (Some (OcfInitialSharesNumeric 99.0)))] + edits = [CT.OcfEditIssuer (defaultIssuerWithAuthorizedShares "ISSUER_1" (Some (OcfInitialSharesNumeric 110.0)))] deletes = [] + pure () testIssuerMetadataEditAllowedWhenAdjustmentRaisesAuthorizedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 913339cf..1cd88824 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -157,19 +157,43 @@ testStockClassEditCannotDropInitialAuthorizedSharesBelowIssuedSharesWhenAdjustme (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EDIT_WITH_ADJUSTMENT" 100.0) (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EDIT_WITH_ADJUSTMENT" "Edit With Adjustment Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_EDIT_WITH_ADJUSTMENT_EARLY_ISSUED" "SEC_EDIT_WITH_ADJUSTMENT_EARLY_ISSUED" stakeholderId stockClassId) with quantity = 75.0) + let laterAdjustment = (stockClassAuthorizedSharesAdjustment "TX_AUTH_EDIT_MASK" stockClassId 150.0) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + result <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassAuthorizedSharesAdjustment laterAdjustment] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockClass (limitedStockClass stockClassId 50.0)] + deletes = [] + +testStockClassEditAllowsInitialBelowCurrentSharesWhenAdjustmentCoversHistory = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EDIT_HISTORY_OK" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EDIT_HISTORY_OK" "Edit History Holder" result <- submit issuer do exerciseCmd capTableReady CT.UpdateCapTable with - creates = [CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_EDIT_MASK" stockClassId 150.0)] + creates = [CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_EDIT_HISTORY_OK" stockClassId 150.0)] edits = [] deletes = [] - capTableIssued <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_EDIT_WITH_ADJUSTMENT_ISSUED" "SEC_EDIT_WITH_ADJUSTMENT_ISSUED" stakeholderId stockClassId) with quantity = 125.0) + let laterIssuance = ((defaultStockIssuance "TX_EDIT_HISTORY_ISSUED" "SEC_EDIT_HISTORY_ISSUED" stakeholderId stockClassId) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + quantity = 125.0) + capTableIssued <- createStockIssuanceWith issuer result.updatedCapTableCid laterIssuance - submitMustFail issuer do + _ <- submit issuer do exerciseCmd capTableIssued CT.UpdateCapTable with creates = [] - edits = [CT.OcfEditStockClass (limitedStockClass stockClassId 50.0)] + edits = [CT.OcfEditStockClass (limitedStockClass stockClassId 110.0)] deletes = [] + pure () testStockClassMetadataEditAllowedWhenAdjustmentRaisesAuthorizedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar index 3cc930cf..56e83482 100644 --- a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5110557d64360e4b50fc981fea53c9aa68c2cf6669bfa680217ac95726f009e9 -size 2529166 +oid sha256:99da6c90d5641a17f3b4ddb80136a0ed62362030e466330b3c01d06f734ca1df +size 2517748 diff --git a/dars/dars.lock b/dars/dars.lock index b08b1624..7dddab1f 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar": { - "sha256": "5110557d64360e4b50fc981fea53c9aa68c2cf6669bfa680217ac95726f009e9", - "size": 2529166, + "sha256": "99da6c90d5641a17f3b4ddb80136a0ed62362030e466330b3c01d06f734ca1df", + "size": 2517748, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T00:41:37.578Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index dc1b78f3..143303f2 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -571,21 +571,6 @@ validateStockClassAuthorizedSharesAtDates eventIndex stockClasses issuancesBySto (fromOptional [] (Map.lookup stockClassId validationDates))) stockClasses -validateEditedStockClassInitialAuthorizedShares : - StockClassShareEventIndex -> - Map Text [StockIssuanceOcfData] -> - [OcfEditData] -> - Update () -validateEditedStockClassInitialAuthorizedShares eventIndex issuancesByStockClass edits = - mapA_ - (\editData -> case editData of - OcfEditStockClass stockClassData -> do - let currentIssued = stockClassIssuedQuantity eventIndex issuancesByStockClass stockClassData.id - let limitOpt = authorizedSharesLimit stockClassData.initial_shares_authorized - assertWithinStockClassAuthorizedShares stockClassData.id currentIssued limitOpt - _ -> pure ()) - edits - addStockClassAuthorizedContribution : Optional Decimal -> AuthorizedSharesCeiling -> Optional Decimal addStockClassAuthorizedContribution totalOpt ceiling = case (totalOpt, ceiling) of (Some total, AuthorizedSharesFinite shares) -> Some (total + shares) @@ -678,35 +663,12 @@ validateIssuerAuthorizedSharesAtDates stockClasses maps stockClassAdjustmentHist assertMsg ("Issuer authorized shares exceeded on " <> show asOf <> ": stock class authorization is unlimited but issuer authorization is finite") False) validationDates -validateEditedIssuerInitialAuthorizedShares : - [StockClassOcfData] -> - Map Text StockClassAuthorizedSharesAdjustmentOcfData -> - [OcfEditData] -> - Update () -validateEditedIssuerInitialAuthorizedShares stockClasses adjustmentIndex edits = - mapA_ - (\editData -> case editData of - OcfEditIssuer issuerData -> case issuerAuthorizedSharesLimit issuerData.initial_shares_authorized of - None -> pure () - Some issuerLimit -> do - let stockClassTotalOpt = currentStockClassAuthorizedTotal stockClasses adjustmentIndex - case stockClassTotalOpt of - Some stockClassTotal -> - assertMsg - ("Issuer initial authorized shares exceeded: stock classes authorize " <> show stockClassTotal <> " shares but issuer initially authorizes " <> show issuerLimit) - (stockClassTotal <= issuerLimit) - None -> - assertMsg "Issuer initial authorized shares exceeded: stock class authorization is unlimited but issuer initial authorization is finite" False - _ -> pure ()) - edits - validateAuthorizedShareCeilings : CapTableMaps -> Map Text StockClassAuthorizedSharesAdjustmentOcfData -> ContractId Issuer -> - [OcfEditData] -> Update () -validateAuthorizedShareCeilings maps adjustmentIndex issuerCid initialAuthorizedShareEdits = do +validateAuthorizedShareCeilings maps adjustmentIndex issuerCid = do eventIndex <- stockClassShareEventIndex maps stockClasses <- stockClassDataList maps issuancesByStockClass <- stockIssuanceDataByStockClass maps @@ -717,8 +679,6 @@ validateAuthorizedShareCeilings maps adjustmentIndex issuerCid initialAuthorized validateStockClassAuthorizedSharesAtDates eventIndex stockClasses issuancesByStockClass stockClassAdjustmentHistory stockClassValidationDates validateIssuerAuthorizedShares stockClasses maps adjustmentIndex issuerCid validateIssuerAuthorizedSharesAtDates stockClasses maps stockClassAdjustmentHistory issuerAdjustmentHistory issuerCid - validateEditedStockClassInitialAuthorizedShares eventIndex issuancesByStockClass initialAuthorizedShareEdits - validateEditedIssuerInitialAuthorizedShares stockClasses adjustmentIndex initialAuthorizedShareEdits -- | Get the processing tier for a create operation getCreateTier : OcfCreateData -> Int @@ -734,10 +694,9 @@ processCreate ctx maps createData = case createData of {{> create-case}} {{/each}} --- | Process a single edit operation, returning updated maps, the edited ContractId, --- and any edit that changed initial authorized shares. +-- | Process a single edit operation, returning updated maps and the edited ContractId. -- Note: OcfEditIssuer is handled separately in UpdateCapTable since issuer is not a map -processEdit : Context -> CapTableMaps -> OcfEditData -> Update (CapTableMaps, OcfContractId, [OcfEditData]) +processEdit : Context -> CapTableMaps -> OcfEditData -> Update (CapTableMaps, OcfContractId) processEdit ctx maps editData = case editData of OcfEditIssuer _ -> error "OcfEditIssuer should be handled separately in UpdateCapTable" {{#each types_alpha}} @@ -820,31 +779,27 @@ template CapTable assertMsg "Cannot edit issuer more than once per batch" (length issuerEdits <= 1) -- Process regular edits (map-based entities) - (mapsAfterEdits, regularEditedCids, regularInitialAuthorizedShareEdits) <- foldlA - (\(maps, cids, initialAuthorizedShareEdits) editData -> do - (newMaps, cid, editInitialAuthorizedShareEdits) <- processEdit context maps editData - pure (newMaps, cids ++ [cid], initialAuthorizedShareEdits ++ editInitialAuthorizedShareEdits)) - (mapsAfterDeletes, [], []) + (mapsAfterEdits, regularEditedCids) <- foldlA + (\(maps, cids) editData -> do + (newMaps, cid) <- processEdit context maps editData + pure (newMaps, cids ++ [cid])) + (mapsAfterDeletes, []) regularEdits -- Process issuer edit (if any) - special handling since it's a single reference -- Note: In editedCids output, issuer edits appear first regardless of input position - (newIssuerCid, issuerEditedCids, issuerInitialAuthorizedShareEdits) <- case issuerEdits of - [] -> pure (issuer, [], []) -- No issuer edit, keep current + (newIssuerCid, issuerEditedCids) <- case issuerEdits of + [] -> pure (issuer, []) -- No issuer edit, keep current [OcfEditIssuer newIssuerData] -> do -- Validate issuer ID matches (cannot change ID during edit) currentIssuer <- fetch issuer assertMsg "Issuer ID mismatch: cannot change issuer ID during edit" (newIssuerData.id == currentIssuer.issuer_data.id) - let initialAuthorizedShareEdits = - if newIssuerData.initial_shares_authorized /= currentIssuer.issuer_data.initial_shares_authorized - then [OcfEditIssuer newIssuerData] - else [] archive issuer newCid <- create Issuer with context = context issuer_data = newIssuerData - pure (newCid, [CidIssuer newCid], initialAuthorizedShareEdits) + pure (newCid, [CidIssuer newCid]) _ -> error "Multiple issuer edits not allowed" -- Defensive: assertMsg above prevents this -- Sort creates by tier for dependency ordering @@ -863,7 +818,6 @@ template CapTable finalMaps authorizedShareAdjustmentIndex newIssuerCid - (issuerInitialAuthorizedShareEdits ++ regularInitialAuthorizedShareEdits) -- Create new CapTable with updated maps and potentially new issuer newCapTableCid <- create this with diff --git a/scripts/codegen/templates/loops/edit-case.daml b/scripts/codegen/templates/loops/edit-case.daml index e5c7eac9..73321301 100644 --- a/scripts/codegen/templates/loops/edit-case.daml +++ b/scripts/codegen/templates/loops/edit-case.daml @@ -16,15 +16,6 @@ OcfEdit{{name}} d -> do {{/if}} {{/if}} {{/each}} -{{#if validates_stock_class_initial_authorized_shares}} - oldContract <- fetch oldCid - let authorizedShareInitialEdits = - if d.initial_shares_authorized /= oldContract.{{data_param}}.initial_shares_authorized - then [OcfEdit{{name}} d] - else [] -{{else}} - let authorizedShareInitialEdits = [] -{{/if}} {{#if security_id_index_map}} -- Fetch old contract to get old security_id for index map cleanup oldContract <- fetch oldCid @@ -42,4 +33,4 @@ OcfEdit{{name}} d -> do newCid <- create {{name}} with context = ctx, {{data_param}} = d let newMaps = maps with {{map_field}} = Map.insert d.id newCid maps.{{map_field}} {{/if}} - pure (newMaps, Cid{{name}} newCid, authorizedShareInitialEdits) + pure (newMaps, Cid{{name}} newCid) From e0e5f786670c073855d11153792949844992abd9 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 04:51:21 -0400 Subject: [PATCH 17/62] Harden authorized share ceiling outputs --- .../TestIssuerAuthorizedShares.daml | 20 + .../TestStockClassAuthorizedShares.daml | 193 ++++++++- .../0.0.3/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 379 ++++++++++++++---- 5 files changed, 519 insertions(+), 81 deletions(-) diff --git a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml index 95a78ed7..d02b918f 100644 --- a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml @@ -54,6 +54,26 @@ testIssuerAuthorizedSharesAdjustmentAllowsSameBatchStockClassIncrease = script d deletes = [] pure () +testIssuerHistoricalTotalsExcludeFutureEffectiveStockClasses = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + (capTableWithExistingClass, _) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_ISSUER_EXISTING_EFFECTIVE" 100.0) + let jan1 = DT.time (DA.date 2024 Jan 01) 0 0 0 + let jan2 = DT.time (DA.date 2024 Jan 02) 0 0 0 + let futureClass = (limitedStockClass "SC_ISSUER_FUTURE_EFFECTIVE" 100.0) with + board_approval_date = Some jan2 + + _ <- submit issuer do + exerciseCmd capTableWithExistingClass CT.UpdateCapTable with + creates = + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment ((issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_BEFORE_FUTURE_CLASS" 150.0) with date = jan1) + , CT.OcfCreateStockClass futureClass + , CT.OcfCreateIssuerAuthorizedSharesAdjustment ((issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_WITH_FUTURE_CLASS" 200.0) with date = jan2) + ] + edits = [] + deletes = [] + pure () + testIssuerAuthorizedSharesAdjustmentCannotDropBelowStockClasses = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 1cd88824..09af1cea 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -3,11 +3,18 @@ module OpenCapTable.TestStockClassAuthorizedShares where import Fairmint.OpenCapTable.OCF.StockCancellation import Fairmint.OpenCapTable.OCF.StockClassSplit import Fairmint.OpenCapTable.OCF.StockConversion +import Fairmint.OpenCapTable.OCF.EquityCompensationExercise +import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance import Fairmint.OpenCapTable.OCF.StockRepurchase import Fairmint.OpenCapTable.OCF.StockRetraction +import Fairmint.OpenCapTable.OCF.WarrantExercise +import Fairmint.OpenCapTable.OCF.WarrantIssuance +import Fairmint.OpenCapTable.Types.Conversion import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) -import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..)) +import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfQuantitySourceType(..), OcfRatio(..)) +import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) import qualified Fairmint.OpenCapTable.CapTable as CT +import OpenCapTable.HelpersTriggers (ocfTriggerElectiveAtWill) import OpenCapTable.Setup import OpenCapTable.TestHelpers import qualified DA.Date as DA @@ -67,6 +74,113 @@ stockClassSplit splitId stockClassId numerator denominator = split_ratio = OcfRatio with numerator = numerator, denominator = denominator comments = [] +stockClassConversionRightTo : Text -> OcfStockClassConversionRight +stockClassConversionRightTo stockClassId = + OcfStockClassConversionRight with + ceiling_price_per_share = None + conversion_mechanism = OcfConversionMechanismRatioConversion + conversion_price = None + conversion_trigger = ocfTriggerElectiveAtWill + converts_to_future_round = None + converts_to_stock_class_id = stockClassId + custom_description = None + discount_rate = None + expires_at = None + floor_price_per_share = None + percent_of_capitalization = None + ratio = Some (OcfRatio with numerator = 1.0, denominator = 1.0) + reference_share_price = None + reference_valuation_price_per_share = None + type_ = "STOCK_CLASS_CONVERSION_RIGHT" + valuation_cap = None + +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 -> Text -> Decimal -> WarrantIssuanceOcfData +warrantIssuance issuanceId securityId stakeholderId stockClassId convertsToQuantity = + WarrantIssuanceOcfData with + id = issuanceId + custom_id = "WR-" <> issuanceId + date = defaultTestDate + purchase_price = OcfMonetary with amount = 1.0, currency = "USD" + security_id = securityId + stakeholder_id = stakeholderId + comments = [] + exercise_triggers = [warrantFixedExerciseTrigger "TRIGGER_FIXED" stockClassId convertsToQuantity] + security_law_exemptions = [] + vestings = [] + board_approval_date = None + consideration_text = None + exercise_price = None + quantity = Some convertsToQuantity + quantity_source = Some OcfQuantityUnspecified + stockholder_approval_date = None + vesting_terms_id = None + warrant_expiration_date = None + +warrantExercise : Text -> Text -> Decimal -> WarrantExerciseOcfData +warrantExercise exerciseId securityId quantity = + WarrantExerciseOcfData with + id = exerciseId + date = defaultTestDate + security_id = securityId + trigger_id = "TRIGGER_FIXED" + comments = [] + resulting_security_ids = [exerciseId <> "_RESULT"] + consideration_text = None + quantity = Some quantity + +equityCompensationIssuance : Text -> Text -> Text -> Text -> Decimal -> EquityCompensationIssuanceOcfData +equityCompensationIssuance issuanceId securityId stakeholderId stockClassId quantity = + EquityCompensationIssuanceOcfData with + id = issuanceId + custom_id = "EC-" <> issuanceId + date = defaultTestDate + compensation_type = OcfCompensationTypeOptionNSO + quantity = quantity + security_id = securityId + stakeholder_id = stakeholderId + comments = [] + security_law_exemptions = [] + vestings = [] + termination_exercise_windows = [] + base_price = None + board_approval_date = None + consideration_text = None + early_exercisable = None + exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + expiration_date = None + stock_class_id = Some stockClassId + stock_plan_id = None + stockholder_approval_date = None + vesting_terms_id = None + +equityCompensationExercise : Text -> Text -> Decimal -> EquityCompensationExerciseOcfData +equityCompensationExercise exerciseId securityId quantity = + EquityCompensationExerciseOcfData with + id = exerciseId + date = defaultTestDate + quantity = quantity + security_id = securityId + comments = [] + resulting_security_ids = [exerciseId <> "_RESULT"] + consideration_text = None + testStockIssuanceCannotExceedStockClassInitialAuthorizedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -361,6 +475,19 @@ testStockCancellationReducesIssuedSharesForCeiling = script do _ <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_CANCEL_REISSUE" "SEC_CANCEL_REISSUE" stakeholderId stockClassId) with quantity = 50.0) pure () +testStockCancellationCannotReduceSecurityBelowZero = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CANCEL_OVER_REDUCE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CANCEL_OVER_REDUCE" "Cancel Over Reduce Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CANCEL_OVER_REDUCE_ISSUED" "SEC_CANCEL_OVER_REDUCE" stakeholderId stockClassId) with quantity = 100.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockCancellation (stockCancellation "TX_CANCEL_OVER_REDUCE" "SEC_CANCEL_OVER_REDUCE" 150.0)] + edits = [] + deletes = [] + testStockClassSplitCannotUseLaterAuthorizedSharesAdjustment = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -411,6 +538,70 @@ testStockConversionReducesIssuedSharesForCeiling = script do _ <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_CONVERSION_REISSUE" "SEC_CONVERSION_REISSUE" stakeholderId stockClassId) with quantity = 50.0) pure () +testStockConversionCreditsDestinationClassForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_COMMON" 100.0) + let preferredClassId = "SC_CONVERSION_PREFERRED" + let preferredClass = (limitedStockClass preferredClassId 200.0) with + conversion_rights = [stockClassConversionRightTo commonClassId] + (capTableWithPreferred, _) <- setupStockClassWith issuer capTableWithCommon preferredClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_DESTINATION" "Conversion Destination Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_DESTINATION_SOURCE" "SEC_CONVERSION_DESTINATION_SOURCE" stakeholderId preferredClassId) with quantity = 150.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_DESTINATION" "SEC_CONVERSION_DESTINATION_SOURCE" 150.0)] + edits = [] + deletes = [] + +testStockConversionCannotReduceSecurityBelowZero = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_OVER_REDUCE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERSION_OVER_REDUCE" "Conversion Over Reduce Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_OVER_REDUCE_ISSUED" "SEC_CONVERSION_OVER_REDUCE" stakeholderId stockClassId) with quantity = 100.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_OVER_REDUCE" "SEC_CONVERSION_OVER_REDUCE" 150.0)] + edits = [] + deletes = [] + +testWarrantExerciseCannotExceedStockClassAuthorizedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_OUTPUT" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_OUTPUT" "Warrant Output Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (warrantIssuance "TX_WARRANT_OUTPUT_ISSUED" "WSEC_OUTPUT" stakeholderId stockClassId 150.0)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantExercise (warrantExercise "TX_WARRANT_OUTPUT_EXERCISE" "WSEC_OUTPUT" 150.0)] + edits = [] + deletes = [] + +testEquityCompensationExerciseCannotExceedStockClassAuthorizedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_COMP_OUTPUT" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_COMP_OUTPUT" "Equity Comp Output Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_EQUITY_COMP_GRANTED" "ECSEC_OUTPUT" stakeholderId stockClassId 150.0)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise (equityCompensationExercise "TX_EQUITY_COMP_EXERCISE" "ECSEC_OUTPUT" 150.0)] + edits = [] + deletes = [] + testStockRetractionReducesIssuedSharesForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar index 56e83482..85e4cd4c 100644 --- a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99da6c90d5641a17f3b4ddb80136a0ed62362030e466330b3c01d06f734ca1df -size 2517748 +oid sha256:5d414c8acd4a9ebcea64d3738ae22131081d7f449081242ab005cf8f403eb625 +size 2544062 diff --git a/dars/dars.lock b/dars/dars.lock index 7dddab1f..c1f4baaa 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar": { - "sha256": "99da6c90d5641a17f3b4ddb80136a0ed62362030e466330b3c01d06f734ca1df", - "size": 2517748, + "sha256": "5d414c8acd4a9ebcea64d3738ae22131081d7f449081242ab005cf8f403eb625", + "size": 2544062, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T00:41:37.578Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 143303f2..099efafa 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -18,6 +18,7 @@ import DA.Foldable (mapA_) import DA.Optional (fromOptional, isNone, isSome) import Fairmint.OpenCapTable.Types.Core (Context) +import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfConversionTrigger(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..)) import Fairmint.OpenCapTable.OCF.Issuer (Issuer(..), IssuerOcfData) @@ -119,6 +120,13 @@ data StockClassShareEvent event_priority: Int deriving (Eq, Show) +data StockClassShareLot = StockClassShareLot with + stock_class_id: Text + security_id: Optional Text + event_date: Time + quantity: Decimal + deriving (Eq, Show) + data StockClassShareEventIndex = StockClassShareEventIndex with stock_security_events_by_security_id: Map Text [StockClassShareEvent] stock_class_split_events_by_stock_class_id: Map Text [StockClassShareEvent] @@ -175,22 +183,29 @@ addStockClassAuthorizedAdjustmentById : addStockClassAuthorizedAdjustmentById key adjustment index = Map.insert key (adjustment :: fromOptional [] (Map.lookup key index)) index -addStockIssuanceDataByStockClass : - Text -> - StockIssuanceOcfData -> - Map Text [StockIssuanceOcfData] -> - Map Text [StockIssuanceOcfData] -addStockIssuanceDataByStockClass key issuanceData index = - Map.insert key (issuanceData :: fromOptional [] (Map.lookup key index)) index - -stockIssuanceDataByStockClass : CapTableMaps -> Update (Map Text [StockIssuanceOcfData]) -stockIssuanceDataByStockClass maps = +addStockClassShareLotByStockClass : + StockClassShareLot -> + Map Text [StockClassShareLot] -> + Map Text [StockClassShareLot] +addStockClassShareLotByStockClass lot index = + Map.insert lot.stock_class_id (lot :: fromOptional [] (Map.lookup lot.stock_class_id index)) index + +stockIssuanceShareLot : StockIssuanceOcfData -> StockClassShareLot +stockIssuanceShareLot issuanceData = + StockClassShareLot with + stock_class_id = issuanceData.stock_class_id + security_id = Some issuanceData.security_id + event_date = issuanceData.date + quantity = issuanceData.quantity + +stockClassDataIndex : CapTableMaps -> Update (Map Text StockClassOcfData) +stockClassDataIndex maps = foldlA - (\index issuanceCid -> do - issuance <- fetch issuanceCid - pure (addStockIssuanceDataByStockClass issuance.issuance_data.stock_class_id issuance.issuance_data index)) + (\index stockClassCid -> do + stockClass <- fetch stockClassCid + pure (Map.insert stockClass.stock_class_data.id stockClass.stock_class_data index)) Map.empty - (Map.values maps.stock_issuances) + (Map.values maps.stock_classes) stockClassDataList : CapTableMaps -> Update [StockClassOcfData] stockClassDataList maps = @@ -201,6 +216,165 @@ stockClassDataList maps = [] (Map.values maps.stock_classes) +singleStockClassConversionRight : StockClassOcfData -> Optional OcfStockClassConversionRight +singleStockClassConversionRight stockClassData = + case stockClassData.conversion_rights of + [conversionRight] -> Some conversionRight + _ -> None + +stockClassConversionOutputQuantity : OcfStockClassConversionRight -> Decimal -> Decimal +stockClassConversionOutputQuantity conversionRight quantityConverted = + case conversionRight.ratio of + Some ratio -> quantityConverted * ratio.numerator / ratio.denominator + None -> quantityConverted + +stockConversionOutputShareLot : + Map Text StockClassOcfData -> + StockIssuanceOcfData -> + StockConversionOcfData -> + Optional StockClassShareLot +stockConversionOutputShareLot stockClassIndex sourceIssuanceData conversionData = + case Map.lookup sourceIssuanceData.stock_class_id stockClassIndex of + Some sourceStockClass -> + case singleStockClassConversionRight sourceStockClass of + Some conversionRight -> + Some (StockClassShareLot with + stock_class_id = conversionRight.converts_to_stock_class_id + security_id = None + event_date = conversionData.date + quantity = stockClassConversionOutputQuantity conversionRight conversionData.quantity_converted) + None -> None + None -> None + +warrantExerciseTrigger : WarrantIssuanceOcfData -> Text -> Optional OcfConversionTrigger +warrantExerciseTrigger issuanceData triggerId = + foldl + (\matched trigger -> + case matched of + Some _ -> matched + None -> if trigger.trigger_id == triggerId then Some trigger else None) + None + issuanceData.exercise_triggers + +warrantExerciseQuantity : WarrantIssuanceOcfData -> WarrantExerciseOcfData -> OcfWarrantConversionRight -> Decimal +warrantExerciseQuantity issuanceData exerciseData conversionRight = + case conversionRight.conversion_mechanism of + OcfWarrantMechanismFixedAmount with converts_to_quantity -> converts_to_quantity + _ -> fromOptional (fromOptional 0.0 issuanceData.quantity) exerciseData.quantity + +warrantExerciseOutputShareLot : + WarrantIssuanceOcfData -> + WarrantExerciseOcfData -> + Optional StockClassShareLot +warrantExerciseOutputShareLot issuanceData exerciseData = + case warrantExerciseTrigger issuanceData exerciseData.trigger_id of + Some trigger -> + case trigger.conversion_right of + OcfRightWarrant conversionRight -> + case conversionRight.converts_to_stock_class_id of + Some stockClassId -> + Some (StockClassShareLot with + stock_class_id = stockClassId + security_id = None + event_date = exerciseData.date + quantity = warrantExerciseQuantity issuanceData exerciseData conversionRight) + None -> None + _ -> None + None -> None + +equityCompensationExerciseOutputShareLot : + EquityCompensationIssuanceOcfData -> + EquityCompensationExerciseOcfData -> + Optional StockClassShareLot +equityCompensationExerciseOutputShareLot issuanceData exerciseData = + case issuanceData.stock_class_id of + Some stockClassId -> + Some (StockClassShareLot with + stock_class_id = stockClassId + security_id = None + event_date = exerciseData.date + quantity = exerciseData.quantity) + None -> None + +equityCompensationReleaseOutputShareLot : + EquityCompensationIssuanceOcfData -> + EquityCompensationReleaseOcfData -> + Optional StockClassShareLot +equityCompensationReleaseOutputShareLot issuanceData releaseData = + case issuanceData.stock_class_id of + Some stockClassId -> + Some (StockClassShareLot with + stock_class_id = stockClassId + security_id = None + event_date = releaseData.date + quantity = releaseData.quantity) + None -> None + +addOptionalStockClassShareLot : + Optional StockClassShareLot -> + Map Text [StockClassShareLot] -> + Map Text [StockClassShareLot] +addOptionalStockClassShareLot lotOpt index = + case lotOpt of + Some lot -> addStockClassShareLotByStockClass lot index + None -> index + +stockClassShareLotsByStockClass : + CapTableMaps -> + Map Text StockClassOcfData -> + Update (Map Text [StockClassShareLot]) +stockClassShareLotsByStockClass maps stockClassIndex = do + stockIssuanceLots <- foldlA + (\index issuanceCid -> do + issuance <- fetch issuanceCid + pure (addStockClassShareLotByStockClass (stockIssuanceShareLot issuance.issuance_data) index)) + Map.empty + (Map.values maps.stock_issuances) + + stockConversionLots <- foldlA + (\index conversionCid -> do + conversion <- fetch conversionCid + case Map.lookup conversion.conversion_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + pure (addOptionalStockClassShareLot (stockConversionOutputShareLot stockClassIndex sourceIssuance.issuance_data conversion.conversion_data) index) + None -> pure index) + stockIssuanceLots + (Map.values maps.stock_conversions) + + warrantExerciseLots <- foldlA + (\index exerciseCid -> do + exercise <- fetch exerciseCid + case Map.lookup exercise.exercise_data.security_id maps.warrant_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + pure (addOptionalStockClassShareLot (warrantExerciseOutputShareLot issuance.issuance_data exercise.exercise_data) index) + None -> pure index) + stockConversionLots + (Map.values maps.warrant_exercises) + + equityExerciseLots <- foldlA + (\index exerciseCid -> do + exercise <- fetch exerciseCid + case Map.lookup exercise.exercise_data.security_id maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + pure (addOptionalStockClassShareLot (equityCompensationExerciseOutputShareLot issuance.issuance_data exercise.exercise_data) index) + None -> pure index) + warrantExerciseLots + (Map.values maps.equity_compensation_exercises) + + foldlA + (\index releaseCid -> do + release <- fetch releaseCid + case Map.lookup release.release_data.security_id maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + pure (addOptionalStockClassShareLot (equityCompensationReleaseOutputShareLot issuance.issuance_data release.release_data) index) + None -> pure index) + equityExerciseLots + (Map.values maps.equity_compensation_releases) + stockClassAuthorizedAdjustmentIndex : CapTableMaps -> Update (Map Text StockClassAuthorizedSharesAdjustmentOcfData) stockClassAuthorizedAdjustmentIndex maps = foldlA @@ -345,10 +519,6 @@ issuerAuthorizedLimitAt adjustmentHistory issuerData asOf = Some adjustment -> Some adjustment.new_shares_authorized None -> issuerAuthorizedSharesLimit issuerData.initial_shares_authorized -nonNegativeDecimal : Decimal -> Decimal -nonNegativeDecimal n = - if n < 0.0 then 0.0 else n - stockClassShareEventSortKey : StockClassShareEvent -> (Time, Int) stockClassShareEventSortKey event = case event of StockClassShareDelta with event_date; event_priority -> (event_date, event_priority) @@ -367,11 +537,19 @@ stockClassShareEventOnOrBefore latest event = case event of StockClassShareMultiplier with event_date -> event_date <= latest StockClassShareRetraction with event_date -> event_date <= latest -applyStockClassShareEvent : Decimal -> StockClassShareEvent -> Decimal +applyStockClassShareEvent : Decimal -> StockClassShareEvent -> Optional Decimal applyStockClassShareEvent current event = case event of - StockClassShareDelta with quantity_delta -> nonNegativeDecimal (current + quantity_delta) - StockClassShareMultiplier with ratio -> current * ratio.numerator / ratio.denominator - StockClassShareRetraction _ _ -> 0.0 + StockClassShareDelta with quantity_delta -> + let next = current + quantity_delta + in if next < 0.0 then None else Some next + StockClassShareMultiplier with ratio -> Some (current * ratio.numerator / ratio.denominator) + StockClassShareRetraction _ _ -> Some 0.0 + +applyStockClassShareEventOptional : Optional Decimal -> StockClassShareEvent -> Optional Decimal +applyStockClassShareEventOptional currentOpt event = + case currentOpt of + Some current -> applyStockClassShareEvent current event + None -> None addStockClassShareEvent : Text -> StockClassShareEvent -> Map Text [StockClassShareEvent] -> Map Text [StockClassShareEvent] addStockClassShareEvent key event index = @@ -447,57 +625,68 @@ stockClassShareEventIndex maps = do stock_security_events_by_security_id = retractionEventsBySecurityId stock_class_split_events_by_stock_class_id = splitEventsByStockClassId -stockIssuedShareEvents : StockClassShareEventIndex -> StockIssuanceOcfData -> [StockClassShareEvent] -stockIssuedShareEvents eventIndex issuanceData = - let securityEvents = fromOptional [] (Map.lookup issuanceData.security_id eventIndex.stock_security_events_by_security_id) +stockClassShareLotEvents : StockClassShareEventIndex -> StockClassShareLot -> [StockClassShareEvent] +stockClassShareLotEvents eventIndex lot = + let securityEvents = case lot.security_id of + Some securityId -> fromOptional [] (Map.lookup securityId eventIndex.stock_security_events_by_security_id) + None -> [] splitEvents = filter - (stockClassShareEventOnOrAfter issuanceData.date) - (fromOptional [] (Map.lookup issuanceData.stock_class_id eventIndex.stock_class_split_events_by_stock_class_id)) + (stockClassShareEventOnOrAfter lot.event_date) + (fromOptional [] (Map.lookup lot.stock_class_id eventIndex.stock_class_split_events_by_stock_class_id)) in (StockClassShareDelta with - event_date = issuanceData.date + event_date = lot.event_date event_priority = 0 - quantity_delta = issuanceData.quantity) + quantity_delta = lot.quantity) :: (securityEvents ++ splitEvents) -stockIssuanceCurrentQuantity : StockClassShareEventIndex -> StockIssuanceOcfData -> Decimal -stockIssuanceCurrentQuantity eventIndex issuanceData = - foldl applyStockClassShareEvent 0.0 (sortOn stockClassShareEventSortKey (stockIssuedShareEvents eventIndex issuanceData)) +stockClassShareLotCurrentQuantity : StockClassShareEventIndex -> StockClassShareLot -> Optional Decimal +stockClassShareLotCurrentQuantity eventIndex lot = + foldl + applyStockClassShareEventOptional + (Some 0.0) + (sortOn stockClassShareEventSortKey (stockClassShareLotEvents eventIndex lot)) -stockIssuanceQuantityAt : StockClassShareEventIndex -> StockIssuanceOcfData -> Time -> Decimal -stockIssuanceQuantityAt eventIndex issuanceData asOf = - if issuanceData.date <= asOf +stockClassShareLotQuantityAt : StockClassShareEventIndex -> StockClassShareLot -> Time -> Optional Decimal +stockClassShareLotQuantityAt eventIndex lot asOf = + if lot.event_date <= asOf then foldl - applyStockClassShareEvent - 0.0 + applyStockClassShareEventOptional + (Some 0.0) (sortOn stockClassShareEventSortKey - (filter (stockClassShareEventOnOrBefore asOf) (stockIssuedShareEvents eventIndex issuanceData))) - else 0.0 + (filter (stockClassShareEventOnOrBefore asOf) (stockClassShareLotEvents eventIndex lot))) + else Some 0.0 + +addOptionalDecimal : Optional Decimal -> Optional Decimal -> Optional Decimal +addOptionalDecimal leftOpt rightOpt = + case (leftOpt, rightOpt) of + (Some left, Some right) -> Some (left + right) + _ -> None stockClassIssuedQuantity : StockClassShareEventIndex -> - Map Text [StockIssuanceOcfData] -> + Map Text [StockClassShareLot] -> Text -> - Decimal -stockClassIssuedQuantity eventIndex issuancesByStockClass stockClassId = + Optional Decimal +stockClassIssuedQuantity eventIndex lotsByStockClass stockClassId = foldl - (\total issuanceData -> total + stockIssuanceCurrentQuantity eventIndex issuanceData) - 0.0 - (fromOptional [] (Map.lookup stockClassId issuancesByStockClass)) + (\totalOpt lot -> addOptionalDecimal totalOpt (stockClassShareLotCurrentQuantity eventIndex lot)) + (Some 0.0) + (fromOptional [] (Map.lookup stockClassId lotsByStockClass)) stockClassIssuedQuantityAt : StockClassShareEventIndex -> - Map Text [StockIssuanceOcfData] -> + Map Text [StockClassShareLot] -> Text -> Time -> - Decimal -stockClassIssuedQuantityAt eventIndex issuancesByStockClass stockClassId asOf = + Optional Decimal +stockClassIssuedQuantityAt eventIndex lotsByStockClass stockClassId asOf = foldl - (\total issuanceData -> total + stockIssuanceQuantityAt eventIndex issuanceData asOf) - 0.0 - (fromOptional [] (Map.lookup stockClassId issuancesByStockClass)) + (\totalOpt lot -> addOptionalDecimal totalOpt (stockClassShareLotQuantityAt eventIndex lot asOf)) + (Some 0.0) + (fromOptional [] (Map.lookup stockClassId lotsByStockClass)) assertWithinStockClassAuthorizedShares : Text -> Decimal -> Optional Decimal -> Update () assertWithinStockClassAuthorizedShares stockClassId issuedQuantity limitOpt = case limitOpt of @@ -507,6 +696,14 @@ assertWithinStockClassAuthorizedShares stockClassId issuedQuantity limitOpt = ca (issuedQuantity <= limit) None -> pure () +assertValidStockClassIssuedQuantity : Text -> Optional Decimal -> Update Decimal +assertValidStockClassIssuedQuantity stockClassId issuedQuantityOpt = + case issuedQuantityOpt of + Some issuedQuantity -> pure issuedQuantity + None -> do + assertMsg ("Stock class issued shares cannot be computed because a stock security was reduced below zero for " <> stockClassId) False + pure 0.0 + assertWithinStockClassAuthorizedSharesAtDate : Text -> Time -> Decimal -> Optional Decimal -> Update () assertWithinStockClassAuthorizedSharesAtDate stockClassId asOf issuedQuantity limitOpt = case limitOpt of Some limit -> @@ -515,20 +712,19 @@ assertWithinStockClassAuthorizedSharesAtDate stockClassId asOf issuedQuantity li (issuedQuantity <= limit) None -> pure () -stockClassAuthorizedValidationDateIndex : CapTableMaps -> Update (Map Text [Time]) -stockClassAuthorizedValidationDateIndex maps = do - issuanceDates <- foldlA - (\index issuanceCid -> do - issuance <- fetch issuanceCid - pure (addDateById issuance.issuance_data.stock_class_id issuance.issuance_data.date index)) - Map.empty - (Map.values maps.stock_issuances) +stockClassAuthorizedValidationDateIndex : CapTableMaps -> Map Text [StockClassShareLot] -> Update (Map Text [Time]) +stockClassAuthorizedValidationDateIndex maps lotsByStockClass = do + let lotDates = + foldl + (\index lot -> addDateById lot.stock_class_id lot.event_date index) + Map.empty + (concat (Map.values lotsByStockClass)) splitDates <- foldlA (\index splitCid -> do split <- fetch splitCid pure (addDateById split.split_data.stock_class_id split.split_data.date index)) - issuanceDates + lotDates (Map.values maps.stock_class_splits) foldlA @@ -541,13 +737,13 @@ stockClassAuthorizedValidationDateIndex maps = do validateAllStockClassAuthorizedShares : StockClassShareEventIndex -> [StockClassOcfData] -> - Map Text [StockIssuanceOcfData] -> + Map Text [StockClassShareLot] -> Map Text StockClassAuthorizedSharesAdjustmentOcfData -> Update () -validateAllStockClassAuthorizedShares eventIndex stockClasses issuancesByStockClass adjustmentIndex = +validateAllStockClassAuthorizedShares eventIndex stockClasses lotsByStockClass adjustmentIndex = mapA_ (\stockClassData -> do - let currentIssued = stockClassIssuedQuantity eventIndex issuancesByStockClass stockClassData.id + currentIssued <- assertValidStockClassIssuedQuantity stockClassData.id (stockClassIssuedQuantity eventIndex lotsByStockClass stockClassData.id) let limitOpt = currentStockClassAuthorizedLimit adjustmentIndex stockClassData assertWithinStockClassAuthorizedShares stockClassData.id currentIssued limitOpt) stockClasses @@ -555,17 +751,17 @@ validateAllStockClassAuthorizedShares eventIndex stockClasses issuancesByStockCl validateStockClassAuthorizedSharesAtDates : StockClassShareEventIndex -> [StockClassOcfData] -> - Map Text [StockIssuanceOcfData] -> + Map Text [StockClassShareLot] -> Map Text [StockClassAuthorizedSharesAdjustmentOcfData] -> Map Text [Time] -> Update () -validateStockClassAuthorizedSharesAtDates eventIndex stockClasses issuancesByStockClass adjustmentHistory validationDates = +validateStockClassAuthorizedSharesAtDates eventIndex stockClasses lotsByStockClass adjustmentHistory validationDates = mapA_ (\stockClassData -> do let stockClassId = stockClassData.id mapA_ (\asOf -> do - let issuedAtDate = stockClassIssuedQuantityAt eventIndex issuancesByStockClass stockClassId asOf + issuedAtDate <- assertValidStockClassIssuedQuantity stockClassId (stockClassIssuedQuantityAt eventIndex lotsByStockClass stockClassId asOf) let limitOpt = stockClassAuthorizedLimitAt adjustmentHistory stockClassData asOf assertWithinStockClassAuthorizedSharesAtDate stockClassId asOf issuedAtDate limitOpt) (fromOptional [] (Map.lookup stockClassId validationDates))) @@ -578,6 +774,24 @@ addStockClassAuthorizedContribution totalOpt ceiling = case (totalOpt, ceiling) (_, AuthorizedSharesUnlimited) -> None (None, _) -> None +earliestOptionalTime : Optional Time -> Optional Time -> Optional Time +earliestOptionalTime leftOpt rightOpt = + case (leftOpt, rightOpt) of + (Some left, Some right) -> Some (if left <= right then left else right) + (Some left, None) -> Some left + (None, Some right) -> Some right + (None, None) -> None + +stockClassEffectiveDate : StockClassOcfData -> Optional Time +stockClassEffectiveDate stockClassData = + earliestOptionalTime stockClassData.board_approval_date stockClassData.stockholder_approval_date + +stockClassEffectiveOnOrBefore : Time -> StockClassOcfData -> Bool +stockClassEffectiveOnOrBefore asOf stockClassData = + case stockClassEffectiveDate stockClassData of + Some effectiveDate -> effectiveDate <= asOf + None -> True + currentStockClassAuthorizedTotalAt : [StockClassOcfData] -> Map Text [StockClassAuthorizedSharesAdjustmentOcfData] -> @@ -586,8 +800,11 @@ currentStockClassAuthorizedTotalAt : currentStockClassAuthorizedTotalAt stockClasses adjustmentHistory asOf = foldl (\totalOpt stockClassData -> - let ceiling = stockClassAuthorizedCeilingAt adjustmentHistory stockClassData asOf - in addStockClassAuthorizedContribution totalOpt ceiling) + if stockClassEffectiveOnOrBefore asOf stockClassData + then + let ceiling = stockClassAuthorizedCeilingAt adjustmentHistory stockClassData asOf + in addStockClassAuthorizedContribution totalOpt ceiling + else totalOpt) (Some 0.0) stockClasses @@ -623,8 +840,8 @@ validateIssuerAuthorizedShares stockClasses maps adjustmentIndex issuerCid = do None -> assertMsg "Issuer authorized shares exceeded: stock class authorization is unlimited but issuer authorization is finite" False -issuerAuthorizedValidationDates : CapTableMaps -> Update [Time] -issuerAuthorizedValidationDates maps = do +issuerAuthorizedValidationDates : CapTableMaps -> [StockClassOcfData] -> Update [Time] +issuerAuthorizedValidationDates maps stockClasses = do issuerAdjustmentDates <- foldlA (\dates adjustmentCid -> do adjustment <- fetch adjustmentCid @@ -632,13 +849,22 @@ issuerAuthorizedValidationDates maps = do [] (Map.values maps.issuer_authorized_shares_adjustments) - foldlA + stockClassAdjustmentDates <- foldlA (\dates adjustmentCid -> do adjustment <- fetch adjustmentCid pure (adjustment.adjustment_data.date :: dates)) issuerAdjustmentDates (Map.values maps.stock_class_authorized_shares_adjustments) + pure + (foldl + (\dates stockClassData -> + case stockClassEffectiveDate stockClassData of + Some effectiveDate -> effectiveDate :: dates + None -> dates) + stockClassAdjustmentDates + stockClasses) + validateIssuerAuthorizedSharesAtDates : [StockClassOcfData] -> CapTableMaps -> @@ -648,7 +874,7 @@ validateIssuerAuthorizedSharesAtDates : Update () validateIssuerAuthorizedSharesAtDates stockClasses maps stockClassAdjustmentHistory issuerAdjustmentHistory issuerCid = do issuerContract <- fetch issuerCid - validationDates <- issuerAuthorizedValidationDates maps + validationDates <- issuerAuthorizedValidationDates maps stockClasses mapA_ (\asOf -> case issuerAuthorizedLimitAt issuerAdjustmentHistory issuerContract.issuer_data asOf of None -> pure () @@ -670,13 +896,14 @@ validateAuthorizedShareCeilings : Update () validateAuthorizedShareCeilings maps adjustmentIndex issuerCid = do eventIndex <- stockClassShareEventIndex maps + stockClassIndex <- stockClassDataIndex maps stockClasses <- stockClassDataList maps - issuancesByStockClass <- stockIssuanceDataByStockClass maps + lotsByStockClass <- stockClassShareLotsByStockClass maps stockClassIndex stockClassAdjustmentHistory <- stockClassAuthorizedAdjustmentHistoryIndex maps - stockClassValidationDates <- stockClassAuthorizedValidationDateIndex maps + stockClassValidationDates <- stockClassAuthorizedValidationDateIndex maps lotsByStockClass issuerAdjustmentHistory <- issuerAuthorizedAdjustmentHistoryIndex maps - validateAllStockClassAuthorizedShares eventIndex stockClasses issuancesByStockClass adjustmentIndex - validateStockClassAuthorizedSharesAtDates eventIndex stockClasses issuancesByStockClass stockClassAdjustmentHistory stockClassValidationDates + validateAllStockClassAuthorizedShares eventIndex stockClasses lotsByStockClass adjustmentIndex + validateStockClassAuthorizedSharesAtDates eventIndex stockClasses lotsByStockClass stockClassAdjustmentHistory stockClassValidationDates validateIssuerAuthorizedShares stockClasses maps adjustmentIndex issuerCid validateIssuerAuthorizedSharesAtDates stockClasses maps stockClassAdjustmentHistory issuerAdjustmentHistory issuerCid From a96122b5ccfa7a39afb493f1d43ec94f2bbb5b4e Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 05:11:00 -0400 Subject: [PATCH 18/62] Handle ambiguous stock conversion outputs --- .../TestStockClassAuthorizedShares.daml | 32 +++++++++++++++++-- .../0.0.3/OpenCapTable-v34.dar | 4 +-- dars/dars.lock | 4 +-- .../codegen/templates/CapTable.daml.template | 26 +++++++-------- 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 09af1cea..5d9799b6 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -526,7 +526,11 @@ testStockRepurchaseReducesIssuedSharesForCeiling = script do testStockConversionReducesIssuedSharesForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION" 100.0) + (capTableWithDestination, destinationClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_DEST" 100.0) + let sourceClassId = "SC_CONVERSION" + let sourceClass = (limitedStockClass sourceClassId 100.0) with + conversion_rights = [stockClassConversionRightTo destinationClassId] + (capTableWithClass, stockClassId) <- setupStockClassWith issuer capTableWithDestination sourceClass (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERSION" "Conversion Holder" capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_ISSUED" "SEC_CONVERSION_ISSUED" stakeholderId stockClassId) with quantity = 100.0) result <- submit issuer do @@ -558,7 +562,10 @@ testStockConversionCreditsDestinationClassForCeiling = script do testStockConversionCannotReduceSecurityBelowZero = script do TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_OVER_REDUCE" 100.0) + (capTableWithDestination, destinationClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_OVER_REDUCE_DEST" 200.0) + let stockClassData = (limitedStockClass "SC_CONVERSION_OVER_REDUCE" 100.0) with + conversion_rights = [stockClassConversionRightTo destinationClassId] + (capTableWithClass, stockClassId) <- setupStockClassWith issuer capTableWithDestination stockClassData (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERSION_OVER_REDUCE" "Conversion Over Reduce Holder" capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_OVER_REDUCE_ISSUED" "SEC_CONVERSION_OVER_REDUCE" stakeholderId stockClassId) with quantity = 100.0) @@ -568,6 +575,27 @@ testStockConversionCannotReduceSecurityBelowZero = script do edits = [] deletes = [] +testStockConversionWithAmbiguousConversionRightsFailsForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_AMBIG_COMMON" 100.0) + (capTableWithAlternative, alternativeClassId) <- setupStockClassWith issuer capTableWithCommon (limitedStockClass "SC_CONVERSION_AMBIG_ALT" 100.0) + let preferredClassId = "SC_CONVERSION_AMBIG_SOURCE" + let preferredClass = (limitedStockClass preferredClassId 100.0) with + conversion_rights = + [ stockClassConversionRightTo commonClassId + , stockClassConversionRightTo alternativeClassId + ] + (capTableWithPreferred, _) <- setupStockClassWith issuer capTableWithAlternative preferredClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_AMBIG" "Ambiguous Conversion Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_AMBIG_ISSUED" "SEC_CONVERSION_AMBIG" stakeholderId preferredClassId) with quantity = 50.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_AMBIG" "SEC_CONVERSION_AMBIG" 50.0)] + edits = [] + deletes = [] + testWarrantExerciseCannotExceedStockClassAuthorizedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar index 85e4cd4c..0702feba 100644 --- a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d414c8acd4a9ebcea64d3738ae22131081d7f449081242ab005cf8f403eb625 -size 2544062 +oid sha256:204c5415ea474f19c4996f8919624a2f28b12d184c7c04aece11726fe89629e5 +size 2544116 diff --git a/dars/dars.lock b/dars/dars.lock index c1f4baaa..7113b881 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar": { - "sha256": "5d414c8acd4a9ebcea64d3738ae22131081d7f449081242ab005cf8f403eb625", - "size": 2544062, + "sha256": "204c5415ea474f19c4996f8919624a2f28b12d184c7c04aece11726fe89629e5", + "size": 2544116, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T00:41:37.578Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 099efafa..27039ccb 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -216,12 +216,6 @@ stockClassDataList maps = [] (Map.values maps.stock_classes) -singleStockClassConversionRight : StockClassOcfData -> Optional OcfStockClassConversionRight -singleStockClassConversionRight stockClassData = - case stockClassData.conversion_rights of - [conversionRight] -> Some conversionRight - _ -> None - stockClassConversionOutputQuantity : OcfStockClassConversionRight -> Decimal -> Decimal stockClassConversionOutputQuantity conversionRight quantityConverted = case conversionRight.ratio of @@ -232,19 +226,22 @@ stockConversionOutputShareLot : Map Text StockClassOcfData -> StockIssuanceOcfData -> StockConversionOcfData -> - Optional StockClassShareLot + Update (Optional StockClassShareLot) stockConversionOutputShareLot stockClassIndex sourceIssuanceData conversionData = case Map.lookup sourceIssuanceData.stock_class_id stockClassIndex of Some sourceStockClass -> - case singleStockClassConversionRight sourceStockClass of - Some conversionRight -> - Some (StockClassShareLot with + case sourceStockClass.conversion_rights of + [conversionRight] -> + pure (Some (StockClassShareLot with stock_class_id = conversionRight.converts_to_stock_class_id security_id = None event_date = conversionData.date - quantity = stockClassConversionOutputQuantity conversionRight conversionData.quantity_converted) - None -> None - None -> None + quantity = stockClassConversionOutputQuantity conversionRight conversionData.quantity_converted)) + [] -> pure None + _ -> do + assertMsg ("Stock conversion output cannot be reconstructed because stock class has multiple conversion rights: " <> sourceStockClass.id) False + pure None + None -> pure None warrantExerciseTrigger : WarrantIssuanceOcfData -> Text -> Optional OcfConversionTrigger warrantExerciseTrigger issuanceData triggerId = @@ -337,7 +334,8 @@ stockClassShareLotsByStockClass maps stockClassIndex = do case Map.lookup conversion.conversion_data.security_id maps.stock_issuances_by_security_id of Some sourceIssuanceCid -> do sourceIssuance <- fetch sourceIssuanceCid - pure (addOptionalStockClassShareLot (stockConversionOutputShareLot stockClassIndex sourceIssuance.issuance_data conversion.conversion_data) index) + lotOpt <- stockConversionOutputShareLot stockClassIndex sourceIssuance.issuance_data conversion.conversion_data + pure (addOptionalStockClassShareLot lotOpt index) None -> pure index) stockIssuanceLots (Map.values maps.stock_conversions) From 1ef3e9c1e2ec09ada43d6b5f215d858d33e18f4a Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 05:32:55 -0400 Subject: [PATCH 19/62] Harden authorized share output accounting --- .../TestStockClassAuthorizedShares.daml | 168 ++++++++++++++++++ .../0.0.3/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 166 ++++++++++++----- 4 files changed, 293 insertions(+), 49 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 5d9799b6..7e4c2d26 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -5,8 +5,10 @@ import Fairmint.OpenCapTable.OCF.StockClassSplit 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.StockClassConversionRatioAdjustment import Fairmint.OpenCapTable.OCF.WarrantExercise import Fairmint.OpenCapTable.OCF.WarrantIssuance import Fairmint.OpenCapTable.Types.Conversion @@ -94,6 +96,18 @@ stockClassConversionRightTo stockClassId = type_ = "STOCK_CLASS_CONVERSION_RIGHT" valuation_cap = None +stockClassConversionRatioAdjustment : Text -> Text -> Decimal -> StockClassConversionRatioAdjustmentOcfData +stockClassConversionRatioAdjustment adjustmentId stockClassId numerator = + StockClassConversionRatioAdjustmentOcfData with + id = adjustmentId + date = defaultTestDate + stock_class_id = stockClassId + new_ratio_conversion_mechanism = OcfRatioConversionMechanism with + conversion_price = OcfMonetary with amount = 1.0, currency = "USD" + ratio = OcfRatio with numerator = numerator, denominator = 1.0 + rounding_type = OcfRoundingNormal + comments = [] + warrantFixedExerciseTrigger : Text -> Text -> Decimal -> OcfConversionTrigger warrantFixedExerciseTrigger triggerId stockClassId convertsToQuantity = OcfConversionTrigger with @@ -111,6 +125,23 @@ warrantFixedExerciseTrigger triggerId stockClassId convertsToQuantity = trigger_id = triggerId type_ = OcfTriggerTypeTypeElectiveAtWill +warrantCustomExerciseTrigger : Text -> Text -> OcfConversionTrigger +warrantCustomExerciseTrigger triggerId stockClassId = + OcfConversionTrigger with + conversion_right = OcfRightWarrant (OcfWarrantConversionRight with + conversion_mechanism = OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant output" + 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 -> Text -> Decimal -> WarrantIssuanceOcfData warrantIssuance issuanceId securityId stakeholderId stockClassId convertsToQuantity = WarrantIssuanceOcfData with @@ -181,6 +212,19 @@ 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 @@ -596,6 +640,47 @@ testStockConversionWithAmbiguousConversionRightsFailsForCeiling = script do edits = [] deletes = [] +testStockConversionUsesRatioAdjustmentForDestinationCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_RATIO_COMMON" 100.0) + let preferredClassId = "SC_CONVERSION_RATIO_SOURCE" + let preferredClass = (limitedStockClass preferredClassId 200.0) with + conversion_rights = [stockClassConversionRightTo commonClassId] + (capTableWithPreferred, _) <- setupStockClassWith issuer capTableWithCommon preferredClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_RATIO" "Conversion Ratio Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_RATIO_ISSUED" "SEC_CONVERSION_RATIO" stakeholderId preferredClassId) with quantity = 75.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassConversionRatioAdjustment (stockClassConversionRatioAdjustment "TX_CONVERSION_RATIO_ADJUST" preferredClassId 2.0) + , CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_RATIO" "SEC_CONVERSION_RATIO" 75.0) + ] + edits = [] + deletes = [] + +testStockConversionWithNonRatioRightFailsForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_NON_RATIO_COMMON" 100.0) + let preferredClassId = "SC_CONVERSION_NON_RATIO_SOURCE" + let ppsRight = (stockClassConversionRightTo commonClassId) with + conversion_mechanism = OcfConversionMechanismPpsBasedConversion + ratio = None + reference_share_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + let preferredClass = (limitedStockClass preferredClassId 200.0) with + conversion_rights = [ppsRight] + (capTableWithPreferred, _) <- setupStockClassWith issuer capTableWithCommon preferredClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_NON_RATIO" "Conversion Non Ratio Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_NON_RATIO_ISSUED" "SEC_CONVERSION_NON_RATIO" stakeholderId preferredClassId) with quantity = 50.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_NON_RATIO" "SEC_CONVERSION_NON_RATIO" 50.0)] + edits = [] + deletes = [] + testWarrantExerciseCannotExceedStockClassAuthorizedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -613,6 +698,44 @@ testWarrantExerciseCannotExceedStockClassAuthorizedShares = script do edits = [] deletes = [] +testFixedWarrantPartialExerciseUsesExerciseQuantityForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_PARTIAL" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_PARTIAL" "Warrant Partial Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (warrantIssuance "TX_WARRANT_PARTIAL_ISSUED" "WSEC_PARTIAL" stakeholderId stockClassId 150.0)] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantExercise (warrantExercise "TX_WARRANT_PARTIAL_EXERCISE" "WSEC_PARTIAL" 50.0)] + edits = [] + deletes = [] + pure () + +testWarrantExerciseWithUnknownOutputQuantityFailsForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_UNKNOWN" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_UNKNOWN" "Warrant Unknown Holder" + let issuance = (warrantIssuance "TX_WARRANT_UNKNOWN_ISSUED" "WSEC_UNKNOWN" stakeholderId stockClassId 100.0) with + quantity = None + exercise_triggers = [warrantCustomExerciseTrigger "TRIGGER_CUSTOM" stockClassId] + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantExercise ((warrantExercise "TX_WARRANT_UNKNOWN_EXERCISE" "WSEC_UNKNOWN" 50.0) with trigger_id = "TRIGGER_CUSTOM", quantity = None)] + edits = [] + deletes = [] + testEquityCompensationExerciseCannotExceedStockClassAuthorizedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -630,6 +753,51 @@ testEquityCompensationExerciseCannotExceedStockClassAuthorizedShares = script do edits = [] deletes = [] +testEquityCompensationExerciseWithResultingStockIssuanceDoesNotDoubleCount = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_COMP_RESULTING" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_COMP_RESULTING" "Equity Comp Resulting Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_EQUITY_COMP_RESULTING_GRANTED" "ECSEC_RESULTING" stakeholderId stockClassId 100.0)] + edits = [] + deletes = [] + + let exercise = equityCompensationExercise "TX_EQUITY_COMP_RESULTING_EXERCISE" "ECSEC_RESULTING" 100.0 + _ <- submit issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationExercise exercise + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_EQUITY_COMP_RESULTING_STOCK" "TX_EQUITY_COMP_RESULTING_EXERCISE_RESULT" stakeholderId stockClassId) with quantity = 100.0) + ] + edits = [] + deletes = [] + pure () + +testEquityCompensationReleaseUsesSettlementDateForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_COMP_RELEASE_DATE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_COMP_RELEASE_DATE" "Equity Comp Release Date Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_EQUITY_COMP_RELEASE_GRANTED" "ECSEC_RELEASE_DATE" stakeholderId stockClassId 150.0)] + edits = [] + deletes = [] + + let releaseDate = DT.time (DA.date 2024 Jan 01) 0 0 0 + let settlementDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + _ <- submit issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassAuthorizedSharesAdjustment ((stockClassAuthorizedSharesAdjustment "TX_EQUITY_COMP_RELEASE_AUTH" stockClassId 150.0) with date = settlementDate) + , CT.OcfCreateEquityCompensationRelease (equityCompensationRelease "TX_EQUITY_COMP_RELEASE" "ECSEC_RELEASE_DATE" releaseDate settlementDate 150.0) + ] + edits = [] + deletes = [] + pure () + testStockRetractionReducesIssuedSharesForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar index 0702feba..5d654338 100644 --- a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:204c5415ea474f19c4996f8919624a2f28b12d184c7c04aece11726fe89629e5 -size 2544116 +oid sha256:90b2c10dd8f004e6f5e504ca16d931ff9fdfe672afb95e1acf8fe43a6a74e596 +size 2552210 diff --git a/dars/dars.lock b/dars/dars.lock index 7113b881..e29f9ee6 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar": { - "sha256": "204c5415ea474f19c4996f8919624a2f28b12d184c7c04aece11726fe89629e5", - "size": 2544116, + "sha256": "90b2c10dd8f004e6f5e504ca16d931ff9fdfe672afb95e1acf8fe43a6a74e596", + "size": 2552210, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T00:41:37.578Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 27039ccb..c20d207e 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -18,7 +18,7 @@ import DA.Foldable (mapA_) import DA.Optional (fromOptional, isNone, isSome) import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfConversionTrigger(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) +import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfConversionMechanism(..), OcfConversionTrigger(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..)) import Fairmint.OpenCapTable.OCF.Issuer (Issuer(..), IssuerOcfData) @@ -163,6 +163,13 @@ issuerAuthorizedAdjustmentTakesPrecedence candidate current = candidate.date > current.date || (candidate.date == current.date && candidate.new_shares_authorized < current.new_shares_authorized) +stockClassConversionRatioAdjustmentTakesPrecedence : + StockClassConversionRatioAdjustmentOcfData -> + StockClassConversionRatioAdjustmentOcfData -> + Bool +stockClassConversionRatioAdjustmentTakesPrecedence candidate current = + candidate.date > current.date + addDateById : Text -> Time -> Map Text [Time] -> Map Text [Time] addDateById key date index = Map.insert key (date :: fromOptional [] (Map.lookup key index)) index @@ -190,6 +197,10 @@ addStockClassShareLotByStockClass : addStockClassShareLotByStockClass lot index = Map.insert lot.stock_class_id (lot :: fromOptional [] (Map.lookup lot.stock_class_id index)) index +resultingStockIssuanceExists : CapTableMaps -> [Text] -> Bool +resultingStockIssuanceExists maps securityIds = + any (\securityId -> isSome (Map.lookup securityId maps.stock_issuances_by_security_id)) securityIds + stockIssuanceShareLot : StockIssuanceOcfData -> StockClassShareLot stockIssuanceShareLot issuanceData = StockClassShareLot with @@ -216,27 +227,66 @@ stockClassDataList maps = [] (Map.values maps.stock_classes) -stockClassConversionOutputQuantity : OcfStockClassConversionRight -> Decimal -> Decimal -stockClassConversionOutputQuantity conversionRight quantityConverted = - case conversionRight.ratio of - Some ratio -> quantityConverted * ratio.numerator / ratio.denominator - None -> quantityConverted +stockClassConversionRatioAdjustmentAsOf : + CapTableMaps -> + Text -> + Time -> + Update (Optional StockClassConversionRatioAdjustmentOcfData) +stockClassConversionRatioAdjustmentAsOf maps stockClassId asOf = + foldlA + (\latest adjustmentCid -> do + adjustment <- fetch adjustmentCid + let candidate = adjustment.adjustment_data + if candidate.stock_class_id == stockClassId && candidate.date <= asOf + then case latest of + Some current -> + if stockClassConversionRatioAdjustmentTakesPrecedence candidate current + then pure (Some candidate) + else pure latest + None -> pure (Some candidate) + else pure latest) + None + (Map.values maps.stock_class_conversion_ratio_adjustments) + +stockClassConversionOutputQuantity : + CapTableMaps -> + StockClassOcfData -> + OcfStockClassConversionRight -> + StockConversionOcfData -> + Update Decimal +stockClassConversionOutputQuantity maps sourceStockClass conversionRight conversionData = + case conversionRight.conversion_mechanism of + OcfConversionMechanismRatioConversion -> do + adjustmentOpt <- stockClassConversionRatioAdjustmentAsOf maps sourceStockClass.id conversionData.date + let ratioOpt = case adjustmentOpt of + Some adjustment -> Some adjustment.new_ratio_conversion_mechanism.ratio + None -> conversionRight.ratio + case ratioOpt of + Some ratio -> pure (conversionData.quantity_converted * ratio.numerator / ratio.denominator) + None -> do + assertMsg ("Stock conversion output cannot be reconstructed because ratio conversion has no ratio: " <> sourceStockClass.id) False + pure 0.0 + _ -> do + assertMsg ("Stock conversion output cannot be reconstructed for non-ratio conversion mechanism: " <> sourceStockClass.id) False + pure 0.0 stockConversionOutputShareLot : + CapTableMaps -> Map Text StockClassOcfData -> StockIssuanceOcfData -> StockConversionOcfData -> Update (Optional StockClassShareLot) -stockConversionOutputShareLot stockClassIndex sourceIssuanceData conversionData = +stockConversionOutputShareLot maps stockClassIndex sourceIssuanceData conversionData = case Map.lookup sourceIssuanceData.stock_class_id stockClassIndex of Some sourceStockClass -> case sourceStockClass.conversion_rights of - [conversionRight] -> + [conversionRight] -> do + outputQuantity <- stockClassConversionOutputQuantity maps sourceStockClass conversionRight conversionData pure (Some (StockClassShareLot with stock_class_id = conversionRight.converts_to_stock_class_id security_id = None event_date = conversionData.date - quantity = stockClassConversionOutputQuantity conversionRight conversionData.quantity_converted)) + quantity = outputQuantity)) [] -> pure None _ -> do assertMsg ("Stock conversion output cannot be reconstructed because stock class has multiple conversion rights: " <> sourceStockClass.id) False @@ -256,13 +306,13 @@ warrantExerciseTrigger issuanceData triggerId = warrantExerciseQuantity : WarrantIssuanceOcfData -> WarrantExerciseOcfData -> OcfWarrantConversionRight -> Decimal warrantExerciseQuantity issuanceData exerciseData conversionRight = case conversionRight.conversion_mechanism of - OcfWarrantMechanismFixedAmount with converts_to_quantity -> converts_to_quantity + OcfWarrantMechanismFixedAmount with converts_to_quantity -> fromOptional converts_to_quantity exerciseData.quantity _ -> fromOptional (fromOptional 0.0 issuanceData.quantity) exerciseData.quantity warrantExerciseOutputShareLot : WarrantIssuanceOcfData -> WarrantExerciseOcfData -> - Optional StockClassShareLot + Update (Optional StockClassShareLot) warrantExerciseOutputShareLot issuanceData exerciseData = case warrantExerciseTrigger issuanceData exerciseData.trigger_id of Some trigger -> @@ -270,14 +320,31 @@ warrantExerciseOutputShareLot issuanceData exerciseData = OcfRightWarrant conversionRight -> case conversionRight.converts_to_stock_class_id of Some stockClassId -> - Some (StockClassShareLot with - stock_class_id = stockClassId - security_id = None - event_date = exerciseData.date - quantity = warrantExerciseQuantity issuanceData exerciseData conversionRight) - None -> None - _ -> None - None -> None + case (conversionRight.conversion_mechanism, exerciseData.quantity, issuanceData.quantity) of + (OcfWarrantMechanismFixedAmount _, _, _) -> + pure (Some (StockClassShareLot with + stock_class_id = stockClassId + security_id = None + event_date = exerciseData.date + quantity = warrantExerciseQuantity issuanceData exerciseData conversionRight)) + (_, Some _, _) -> + pure (Some (StockClassShareLot with + stock_class_id = stockClassId + security_id = None + event_date = exerciseData.date + quantity = warrantExerciseQuantity issuanceData exerciseData conversionRight)) + (_, None, Some _) -> + pure (Some (StockClassShareLot with + stock_class_id = stockClassId + security_id = None + event_date = exerciseData.date + quantity = warrantExerciseQuantity issuanceData exerciseData conversionRight)) + (_, None, None) -> do + assertMsg ("Warrant exercise output cannot be reconstructed because exercise and issuance quantities are missing: " <> exerciseData.id) False + pure None + None -> pure None + _ -> pure None + None -> pure None equityCompensationExerciseOutputShareLot : EquityCompensationIssuanceOcfData -> @@ -287,10 +354,10 @@ equityCompensationExerciseOutputShareLot issuanceData exerciseData = case issuanceData.stock_class_id of Some stockClassId -> Some (StockClassShareLot with - stock_class_id = stockClassId - security_id = None - event_date = exerciseData.date - quantity = exerciseData.quantity) + stock_class_id = stockClassId + security_id = None + event_date = exerciseData.date + quantity = exerciseData.quantity) None -> None equityCompensationReleaseOutputShareLot : @@ -303,7 +370,7 @@ equityCompensationReleaseOutputShareLot issuanceData releaseData = Some (StockClassShareLot with stock_class_id = stockClassId security_id = None - event_date = releaseData.date + event_date = releaseData.settlement_date quantity = releaseData.quantity) None -> None @@ -331,45 +398,54 @@ stockClassShareLotsByStockClass maps stockClassIndex = do stockConversionLots <- foldlA (\index conversionCid -> do conversion <- fetch conversionCid - case Map.lookup conversion.conversion_data.security_id maps.stock_issuances_by_security_id of - Some sourceIssuanceCid -> do - sourceIssuance <- fetch sourceIssuanceCid - lotOpt <- stockConversionOutputShareLot stockClassIndex sourceIssuance.issuance_data conversion.conversion_data - pure (addOptionalStockClassShareLot lotOpt index) - None -> pure index) + if resultingStockIssuanceExists maps conversion.conversion_data.resulting_security_ids + then pure index + else case Map.lookup conversion.conversion_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + lotOpt <- stockConversionOutputShareLot maps stockClassIndex sourceIssuance.issuance_data conversion.conversion_data + pure (addOptionalStockClassShareLot lotOpt index) + None -> pure index) stockIssuanceLots (Map.values maps.stock_conversions) warrantExerciseLots <- foldlA (\index exerciseCid -> do exercise <- fetch exerciseCid - case Map.lookup exercise.exercise_data.security_id maps.warrant_issuances_by_security_id of - Some issuanceCid -> do - issuance <- fetch issuanceCid - pure (addOptionalStockClassShareLot (warrantExerciseOutputShareLot issuance.issuance_data exercise.exercise_data) index) - None -> pure index) + if resultingStockIssuanceExists maps exercise.exercise_data.resulting_security_ids + then pure index + else case Map.lookup exercise.exercise_data.security_id maps.warrant_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + lotOpt <- warrantExerciseOutputShareLot issuance.issuance_data exercise.exercise_data + pure (addOptionalStockClassShareLot lotOpt index) + None -> pure index) stockConversionLots (Map.values maps.warrant_exercises) equityExerciseLots <- foldlA (\index exerciseCid -> do exercise <- fetch exerciseCid - case Map.lookup exercise.exercise_data.security_id maps.equity_compensation_issuances_by_security_id of - Some issuanceCid -> do - issuance <- fetch issuanceCid - pure (addOptionalStockClassShareLot (equityCompensationExerciseOutputShareLot issuance.issuance_data exercise.exercise_data) index) - None -> pure index) + if resultingStockIssuanceExists maps exercise.exercise_data.resulting_security_ids + then pure index + else case Map.lookup exercise.exercise_data.security_id maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + pure (addOptionalStockClassShareLot (equityCompensationExerciseOutputShareLot issuance.issuance_data exercise.exercise_data) index) + None -> pure index) warrantExerciseLots (Map.values maps.equity_compensation_exercises) foldlA (\index releaseCid -> do release <- fetch releaseCid - case Map.lookup release.release_data.security_id maps.equity_compensation_issuances_by_security_id of - Some issuanceCid -> do - issuance <- fetch issuanceCid - pure (addOptionalStockClassShareLot (equityCompensationReleaseOutputShareLot issuance.issuance_data release.release_data) index) - None -> pure index) + if resultingStockIssuanceExists maps release.release_data.resulting_security_ids + then pure index + else case Map.lookup release.release_data.security_id maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + pure (addOptionalStockClassShareLot (equityCompensationReleaseOutputShareLot issuance.issuance_data release.release_data) index) + None -> pure index) equityExerciseLots (Map.values maps.equity_compensation_releases) From b3ca8da78e82088ae04a0181a49b9b694a9103cb Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 05:44:58 -0400 Subject: [PATCH 20/62] Harden stock conversion ceiling reconstruction --- .../TestStockClassAuthorizedShares.daml | 34 ++++++++++++++ .../OpenCapTable/TestStockConversion.daml | 45 ++++++++++++++++++- .../0.0.3/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 16 ++++++- 5 files changed, 95 insertions(+), 8 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 7e4c2d26..160951b3 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -640,6 +640,19 @@ testStockConversionWithAmbiguousConversionRightsFailsForCeiling = script do edits = [] deletes = [] +testStockConversionWithoutConversionRightsFailsForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_NO_RIGHTS" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERSION_NO_RIGHTS" "Conversion No Rights Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_NO_RIGHTS_ISSUED" "SEC_CONVERSION_NO_RIGHTS" stakeholderId stockClassId) with quantity = 50.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_NO_RIGHTS" "SEC_CONVERSION_NO_RIGHTS" 50.0)] + edits = [] + deletes = [] + testStockConversionUsesRatioAdjustmentForDestinationCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -660,6 +673,27 @@ testStockConversionUsesRatioAdjustmentForDestinationCeiling = script do edits = [] deletes = [] +testSameDateHigherStockConversionRatioAdjustmentWinsForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_RATIO_TIE_COMMON" 100.0) + let preferredClassId = "SC_CONVERSION_RATIO_TIE_SOURCE" + let preferredClass = (limitedStockClass preferredClassId 200.0) with + conversion_rights = [stockClassConversionRightTo commonClassId] + (capTableWithPreferred, _) <- setupStockClassWith issuer capTableWithCommon preferredClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_RATIO_TIE" "Conversion Ratio Tie Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_RATIO_TIE_ISSUED" "SEC_CONVERSION_RATIO_TIE" stakeholderId preferredClassId) with quantity = 75.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassConversionRatioAdjustment (stockClassConversionRatioAdjustment "TX_CONVERSION_RATIO_TIE_LOW" preferredClassId 1.0) + , CT.OcfCreateStockClassConversionRatioAdjustment (stockClassConversionRatioAdjustment "TX_CONVERSION_RATIO_TIE_HIGH" preferredClassId 2.0) + , CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_RATIO_TIE" "SEC_CONVERSION_RATIO_TIE" 75.0) + ] + edits = [] + deletes = [] + testStockConversionWithNonRatioRightFailsForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestStockConversion.daml b/Test/daml/OpenCapTable/TestStockConversion.daml index ec29ea17..ecae377e 100644 --- a/Test/daml/OpenCapTable/TestStockConversion.daml +++ b/Test/daml/OpenCapTable/TestStockConversion.daml @@ -3,14 +3,52 @@ module OpenCapTable.TestStockConversion where import Fairmint.OpenCapTable.OCF.StockIssuance import Fairmint.OpenCapTable.OCF.StockConversion import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import Fairmint.OpenCapTable.Types.Conversion +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) +import OpenCapTable.TestHelpers (addPrerequisites, defaultStockClass) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT import Daml.Script +stockClassConversionRightTo : Text -> OcfStockClassConversionRight +stockClassConversionRightTo stockClassId = + OcfStockClassConversionRight with + ceiling_price_per_share = None + conversion_mechanism = OcfConversionMechanismRatioConversion + conversion_price = None + conversion_trigger = ocfTriggerElectiveAtWill + converts_to_future_round = None + converts_to_stock_class_id = stockClassId + custom_description = None + discount_rate = None + expires_at = None + floor_price_per_share = None + percent_of_capitalization = None + ratio = Some (OcfRatio with numerator = 1.0, denominator = 1.0) + reference_share_price = None + reference_valuation_price_per_share = None + type_ = "STOCK_CLASS_CONVERSION_RIGHT" + valuation_cap = None + +addPreferredStockClass : Party -> ContractId CT.CapTable -> Script (ContractId CT.CapTable) +addPreferredStockClass issuer capTableCid = do + result <- submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass ((defaultStockClass "SC_PREF") with + class_type = OcfStockClassTypePreferred + default_id_prefix = "PS-" + seniority = 2.0 + conversion_rights = [stockClassConversionRightTo "SC_COMMON"]) + ] + edits = [] + deletes = [] + pure result.updatedCapTableCid + -- Create a stock issuance to convert (follows real workflow) createStockIssuance issuer capTableCid securityId quantity = submit issuer do @@ -25,7 +63,7 @@ createStockIssuance issuer capTableCid securityId quantity = stockholder_approval_date = None consideration_text = None security_law_exemptions = [] - stock_class_id = "SC_COMMON" + stock_class_id = "SC_PREF" share_price = OcfMonetary with amount = 1.0, currency = "USD" quantity = quantity cost_basis = None @@ -44,6 +82,7 @@ createStockIssuance issuer capTableCid securityId quantity = testStockConversion_PreferredToCommon = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addPrerequisites issuer cap_table + capTableCid <- addPreferredStockClass issuer capTableCid let securityId = "SSEC-PREF-001" result <- createStockIssuance issuer capTableCid securityId 100000.0 _ <- submit issuer do @@ -64,6 +103,7 @@ testStockConversion_PreferredToCommon = script do testStockConversion_PartialConversion = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addPrerequisites issuer cap_table + capTableCid <- addPreferredStockClass issuer capTableCid let securityId = "SSEC-PREF-002" result <- createStockIssuance issuer capTableCid securityId 50000.0 _ <- submit issuer do @@ -84,6 +124,7 @@ testStockConversion_PartialConversion = script do testStockConversion_MultipleResulting = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addPrerequisites issuer cap_table + capTableCid <- addPreferredStockClass issuer capTableCid let securityId = "SSEC-PREF-003" result <- createStockIssuance issuer capTableCid securityId 200000.0 _ <- submit issuer do diff --git a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar index 5d654338..5c23792a 100644 --- a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90b2c10dd8f004e6f5e504ca16d931ff9fdfe672afb95e1acf8fe43a6a74e596 -size 2552210 +oid sha256:c969f3a6c09c811439b4064037291eb35634f8256b544465b2b1487907c94d63 +size 2553795 diff --git a/dars/dars.lock b/dars/dars.lock index e29f9ee6..526297e4 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar": { - "sha256": "90b2c10dd8f004e6f5e504ca16d931ff9fdfe672afb95e1acf8fe43a6a74e596", - "size": 2552210, + "sha256": "c969f3a6c09c811439b4064037291eb35634f8256b544465b2b1487907c94d63", + "size": 2553795, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T00:41:37.578Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index c20d207e..d886b48d 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -168,7 +168,17 @@ stockClassConversionRatioAdjustmentTakesPrecedence : StockClassConversionRatioAdjustmentOcfData -> Bool stockClassConversionRatioAdjustmentTakesPrecedence candidate current = - candidate.date > current.date + candidate.date > current.date || + (candidate.date == current.date && stockClassConversionRatioAdjustmentIncreasesShares candidate current) + +stockClassConversionRatioAdjustmentIncreasesShares : + StockClassConversionRatioAdjustmentOcfData -> + StockClassConversionRatioAdjustmentOcfData -> + Bool +stockClassConversionRatioAdjustmentIncreasesShares candidate current = + let candidateRatio = candidate.new_ratio_conversion_mechanism.ratio in + let currentRatio = current.new_ratio_conversion_mechanism.ratio in + candidateRatio.numerator * currentRatio.denominator > currentRatio.numerator * candidateRatio.denominator addDateById : Text -> Time -> Map Text [Time] -> Map Text [Time] addDateById key date index = @@ -287,7 +297,9 @@ stockConversionOutputShareLot maps stockClassIndex sourceIssuanceData conversion security_id = None event_date = conversionData.date quantity = outputQuantity)) - [] -> pure None + [] -> do + assertMsg ("Stock conversion output cannot be reconstructed because stock class has no conversion rights: " <> sourceStockClass.id) False + pure None _ -> do assertMsg ("Stock conversion output cannot be reconstructed because stock class has multiple conversion rights: " <> sourceStockClass.id) False pure None From a97c04218225c5dbf7026f900803edc0a3e6d9e3 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 05:52:06 -0400 Subject: [PATCH 21/62] Reject unreconstructable warrant exercise outputs --- .../TestStockClassAuthorizedShares.daml | 17 +++++++++++ .../OpenCapTable/TestWarrantExercise.daml | 29 +++++++++++++++---- .../0.0.3/OpenCapTable-v34.dar | 4 +-- dars/dars.lock | 4 +-- .../codegen/templates/CapTable.daml.template | 12 ++++++-- 5 files changed, 53 insertions(+), 13 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 160951b3..f231cbae 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -770,6 +770,23 @@ testWarrantExerciseWithUnknownOutputQuantityFailsForCeiling = script do edits = [] deletes = [] +testWarrantExerciseWithUnknownTriggerFailsForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_MISSING_TRIGGER" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_MISSING_TRIGGER" "Warrant Missing Trigger Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (warrantIssuance "TX_WARRANT_MISSING_TRIGGER_ISSUED" "WSEC_MISSING_TRIGGER" stakeholderId stockClassId 50.0)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantExercise ((warrantExercise "TX_WARRANT_MISSING_TRIGGER_EXERCISE" "WSEC_MISSING_TRIGGER" 50.0) with trigger_id = "TRIGGER_DOES_NOT_EXIST")] + edits = [] + deletes = [] + testEquityCompensationExerciseCannotExceedStockClassAuthorizedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestWarrantExercise.daml b/Test/daml/OpenCapTable/TestWarrantExercise.daml index 226709c5..809017b8 100644 --- a/Test/daml/OpenCapTable/TestWarrantExercise.daml +++ b/Test/daml/OpenCapTable/TestWarrantExercise.daml @@ -2,17 +2,34 @@ module OpenCapTable.TestWarrantExercise where import Fairmint.OpenCapTable.OCF.WarrantIssuance import Fairmint.OpenCapTable.OCF.WarrantExercise +import Fairmint.OpenCapTable.Types.Conversion import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Stock (OcfQuantitySourceType(..)) -import OpenCapTable.HelpersTriggers (ocfTriggerElectiveAtWill) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addDefaultStakeholder) +import OpenCapTable.TestHelpers (addPrerequisites) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT import Daml.Script +warrantExerciseTrigger : Decimal -> OcfConversionTrigger +warrantExerciseTrigger 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 "SC_COMMON" + type_ = "WARRANT_CONVERSION_RIGHT") + end_date = None + nickname = None + start_date = None + trigger_condition = None + trigger_date = None + trigger_description = None + trigger_id = "ELECTIVE_AT_WILL" + type_ = OcfTriggerTypeTypeElectiveAtWill + -- Create a warrant issuance to exercise (follows real workflow) createWarrantIssuance issuer capTableCid securityId qty = submit issuer do @@ -30,7 +47,7 @@ createWarrantIssuance issuer capTableCid securityId qty = quantity = Some qty quantity_source = Some OcfQuantityUnspecified exercise_price = Some (OcfMonetary with amount = 0.01, currency = "USD") - exercise_triggers = [ocfTriggerElectiveAtWill] + exercise_triggers = [warrantExerciseTrigger qty] purchase_price = OcfMonetary with amount = 0.01, currency = "USD" vestings = [] vesting_terms_id = None @@ -43,7 +60,7 @@ createWarrantIssuance issuer capTableCid securityId qty = -- Warrants exercised into common stock testWarrantExercise_BasicExercise = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp - capTableCid <- addDefaultStakeholder issuer cap_table + capTableCid <- addPrerequisites issuer cap_table let securityId = "WSEC-001" result <- createWarrantIssuance issuer capTableCid securityId 10000.0 _ <- submit issuer do @@ -64,7 +81,7 @@ testWarrantExercise_BasicExercise = script do -- Test 2: Warrant exercise with comments (partial exercise scenario) testWarrantExercise_PartialWithComments = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp - capTableCid <- addDefaultStakeholder issuer cap_table + capTableCid <- addPrerequisites issuer cap_table let securityId = "WSEC-002" result <- createWarrantIssuance issuer capTableCid securityId 50000.0 _ <- submit issuer do @@ -85,7 +102,7 @@ testWarrantExercise_PartialWithComments = script do -- Test 3: Simple exercise with minimal fields testWarrantExercise_MinimalFields = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp - capTableCid <- addDefaultStakeholder issuer cap_table + capTableCid <- addPrerequisites issuer cap_table let securityId = "WSEC-003" result <- createWarrantIssuance issuer capTableCid securityId 5000.0 _ <- submit issuer do diff --git a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar index 5c23792a..5286be37 100644 --- a/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c969f3a6c09c811439b4064037291eb35634f8256b544465b2b1487907c94d63 -size 2553795 +oid sha256:97ff8995277f708334f31219aa06c7a742ecb30fe3aaa23fb046583561b053c5 +size 2554649 diff --git a/dars/dars.lock b/dars/dars.lock index 526297e4..b3eb875b 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -19,8 +19,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar": { - "sha256": "c969f3a6c09c811439b4064037291eb35634f8256b544465b2b1487907c94d63", - "size": 2553795, + "sha256": "97ff8995277f708334f31219aa06c7a742ecb30fe3aaa23fb046583561b053c5", + "size": 2554649, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T00:41:37.578Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index d886b48d..ec675768 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -354,9 +354,15 @@ warrantExerciseOutputShareLot issuanceData exerciseData = (_, None, None) -> do assertMsg ("Warrant exercise output cannot be reconstructed because exercise and issuance quantities are missing: " <> exerciseData.id) False pure None - None -> pure None - _ -> pure None - None -> pure None + None -> do + assertMsg ("Warrant exercise output cannot be reconstructed because trigger has no destination stock class: " <> exerciseData.trigger_id) False + pure None + _ -> do + assertMsg ("Warrant exercise output cannot be reconstructed because trigger is not a warrant conversion right: " <> exerciseData.trigger_id) False + pure None + None -> do + assertMsg ("Warrant exercise output cannot be reconstructed because trigger does not exist on warrant issuance: " <> exerciseData.trigger_id) False + pure None equityCompensationExerciseOutputShareLot : EquityCompensationIssuanceOcfData -> From f3df0bc55bb523c7a6d6467cb2cc4c43d96d6992 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 08:35:02 -0400 Subject: [PATCH 22/62] Group authorized share ceiling enforcement --- OpenCapTable-v34/daml.yaml | 2 +- Test/daml.yaml | 2 +- .../TestEquityCompensationAcceptance.daml | 2 +- .../TestEquityCompensationCancellation.daml | 2 +- .../TestEquityCompensationIssuance.daml | 2 +- .../TestEquityCompensationRetraction.daml | 2 +- .../TestEquityCompensationTransfer.daml | 2 +- Test/daml/OpenCapTable/TestHelpers.daml | 13 + .../TestIssuerAuthorizedShares.daml | 9 +- Test/daml/OpenCapTable/TestStockIssuance.daml | 2 +- .../TestStockPlanReservedShares.daml | 273 ++++++++++++++++++ .../TestStockPlanReturnToPool.daml | 59 ++-- .../OpenCapTable/TestVestingAcceleration.daml | 2 +- Test/daml/OpenCapTable/TestVestingEvent.daml | 2 +- Test/daml/OpenCapTable/TestVestingStart.daml | 4 +- .../0.0.4/OpenCapTable-v34.dar | 3 + dars/dars.lock | 7 + .../codegen/templates/CapTable.daml.template | 177 +++++++++++- 18 files changed, 527 insertions(+), 38 deletions(-) create mode 100644 Test/daml/OpenCapTable/TestStockPlanReservedShares.daml create mode 100644 dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar diff --git a/OpenCapTable-v34/daml.yaml b/OpenCapTable-v34/daml.yaml index 2403aff1..4d300d29 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.3 +version: 0.0.4 build-options: - -Wno-crypto-text-is-alpha - --typecheck-upgrades=no diff --git a/Test/daml.yaml b/Test/daml.yaml index 151e762a..c6e7f2d6 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.3.dar + - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.4.dar build-options: - -Wno-template-interface-depends-on-daml-script diff --git a/Test/daml/OpenCapTable/TestEquityCompensationAcceptance.daml b/Test/daml/OpenCapTable/TestEquityCompensationAcceptance.daml index 93196948..7b914598 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationAcceptance.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationAcceptance.daml @@ -25,7 +25,7 @@ createEquityCompensationIssuance issuer capTableCid = stockholder_approval_date = None consideration_text = Some "Services rendered" security_law_exemptions = [] - stock_plan_id = Some "SP_2024" + stock_plan_id = None stock_class_id = Some "SC_COMMON" compensation_type = OcfCompensationTypeOptionISO quantity = 10000.0 diff --git a/Test/daml/OpenCapTable/TestEquityCompensationCancellation.daml b/Test/daml/OpenCapTable/TestEquityCompensationCancellation.daml index 7e0aaf6e..555578cd 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationCancellation.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationCancellation.daml @@ -25,7 +25,7 @@ createEquityCompensationIssuance issuer capTableCid securityId quantity = stockholder_approval_date = None consideration_text = None security_law_exemptions = [] - stock_plan_id = Some "PLAN-2020" + stock_plan_id = None stock_class_id = Some "COMMON" compensation_type = OcfCompensationTypeOptionNSO quantity = quantity diff --git a/Test/daml/OpenCapTable/TestEquityCompensationIssuance.daml b/Test/daml/OpenCapTable/TestEquityCompensationIssuance.daml index 197b697f..19ea5815 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationIssuance.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationIssuance.daml @@ -122,7 +122,7 @@ testEquityCompensationIssuance_OptionalsSome = script do exercise_price = Some (OcfMonetary with amount = 2.5, currency = "USD") expiration_date = Some (DT.time (DA.date 2034 Feb 01) 0 0 0) stock_class_id = Some "SC-1" - stock_plan_id = Some "PLAN-1" + stock_plan_id = None stockholder_approval_date = Some (DT.time (DA.date 2024 Jan 20) 0 0 0) vesting_terms_id = Some "VT-1" security_law_exemptions = [OcfSecurityExemption with description = "ISO", jurisdiction = "US"] diff --git a/Test/daml/OpenCapTable/TestEquityCompensationRetraction.daml b/Test/daml/OpenCapTable/TestEquityCompensationRetraction.daml index 2103da62..95f38f7e 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationRetraction.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationRetraction.daml @@ -25,7 +25,7 @@ createEquityCompensationIssuance issuer capTableCid = stockholder_approval_date = None consideration_text = Some "Services rendered" security_law_exemptions = [] - stock_plan_id = Some "SP_2024" + stock_plan_id = None stock_class_id = Some "SC_COMMON" compensation_type = OcfCompensationTypeOptionISO quantity = 10000.0 diff --git a/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml b/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml index a3055eac..6aa27394 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml @@ -35,7 +35,7 @@ createEquityCompensationIssuance issuer capTableCid securityId quantity = early_exercisable = None expiration_date = Some (DT.time (DA.date 2032 Jan 15) 0 0 0) stock_class_id = Some "COMMON" - stock_plan_id = Some "PLAN-2020" + stock_plan_id = None vesting_terms_id = None stockholder_approval_date = None] edits = [] diff --git a/Test/daml/OpenCapTable/TestHelpers.daml b/Test/daml/OpenCapTable/TestHelpers.daml index c71cfc49..954832db 100644 --- a/Test/daml/OpenCapTable/TestHelpers.daml +++ b/Test/daml/OpenCapTable/TestHelpers.daml @@ -5,6 +5,7 @@ import Fairmint.OpenCapTable.OCF.Stakeholder import Fairmint.OpenCapTable.OCF.StockClass import Fairmint.OpenCapTable.OCF.StockClassAuthorizedSharesAdjustment import Fairmint.OpenCapTable.OCF.StockIssuance (StockIssuanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockPlan (StockPlanOcfData(..)) import Fairmint.OpenCapTable.OCF.WarrantIssuance (WarrantIssuanceOcfData(..)) import Fairmint.OpenCapTable.OCF.ConvertibleIssuance (ConvertibleIssuanceOcfData(..), OcfConvertibleConversionTrigger(..)) import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance (EquityCompensationIssuanceOcfData(..)) @@ -146,6 +147,18 @@ defaultStockIssuance issuanceId securityId stakeholderId stockClassId = StockIss issuance_type = None comments = [] +-- | Returns valid StockPlanOcfData with sensible defaults for a stock class. +defaultStockPlan : Text -> Text -> StockPlanOcfData +defaultStockPlan planId stockClassId = StockPlanOcfData with + id = planId + initial_shares_reserved = 1000.0 + plan_name = "Equity Incentive Plan" + comments = [] + stock_class_ids = [stockClassId] + board_approval_date = None + default_cancellation_behavior = None + stockholder_approval_date = None + -- ============================================================================= -- SETUP COMBINATORS -- ============================================================================= diff --git a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml index d02b918f..d32ccae0 100644 --- a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml @@ -253,11 +253,14 @@ testFiniteIssuerRejectsUnlimitedStockClassAuthorization = script do edits = [] deletes = [] -testFiniteIssuerAllowsNotApplicableStockClassAuthorization = script do +testFiniteIssuerRejectsNotApplicableStockClassAuthorization = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 let notApplicableClass = (defaultStockClass "SC_NOT_APPLICABLE_ISSUER") with initial_shares_authorized = OcfInitialSharesEnum OcfAuthorizedSharesNotApplicable - _ <- setupStockClassWith issuer capTableLimited notApplicableClass - pure () + submitMustFail issuer do + exerciseCmd capTableLimited CT.UpdateCapTable with + creates = [CT.OcfCreateStockClass notApplicableClass] + edits = [] + deletes = [] diff --git a/Test/daml/OpenCapTable/TestStockIssuance.daml b/Test/daml/OpenCapTable/TestStockIssuance.daml index cfa5f86a..5a66cb93 100644 --- a/Test/daml/OpenCapTable/TestStockIssuance.daml +++ b/Test/daml/OpenCapTable/TestStockIssuance.daml @@ -58,7 +58,7 @@ testStockIssuance_OptionalsSome = script do stockholder_approval_date = Some (DT.time (DA.date 2024 Jan 16) 0 0 0) consideration_text = Some "Cash consideration" security_law_exemptions = [OcfSecurityExemption with description = "Rule 506(b)", jurisdiction = "US-CA"] - stock_plan_id = Some "PLAN_1" + stock_plan_id = None share_numbers_issued = [OcfShareNumberRange with starting_share_number = 1.0, ending_share_number = 10.0] share_price = OcfMonetary with amount = 2.5, currency = "USD" quantity = 10.0 diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml new file mode 100644 index 00000000..76eb2125 --- /dev/null +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -0,0 +1,273 @@ +module OpenCapTable.TestStockPlanReservedShares where + +import qualified Fairmint.OpenCapTable.CapTable as CT +import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance (EquityCompensationIssuanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockPlanPoolAdjustment (StockPlanPoolAdjustmentOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockPlanReturnToPool (StockPlanReturnToPoolOcfData(..)) +import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) +import OpenCapTable.Setup +import OpenCapTable.TestHelpers +import Daml.Script + +stockPlanPoolAdjustment : Text -> Text -> Decimal -> StockPlanPoolAdjustmentOcfData +stockPlanPoolAdjustment adjustmentId stockPlanId sharesReserved = StockPlanPoolAdjustmentOcfData with + id = adjustmentId + date = defaultTestDate + stock_plan_id = stockPlanId + shares_reserved = sharesReserved + board_approval_date = None + stockholder_approval_date = None + comments = [] + +stockPlanReturnToPool : Text -> Text -> Text -> Decimal -> StockPlanReturnToPoolOcfData +stockPlanReturnToPool returnId securityId stockPlanId quantity = StockPlanReturnToPoolOcfData with + id = returnId + date = defaultTestDate + security_id = securityId + stock_plan_id = stockPlanId + quantity = quantity + reason_text = "Returned to pool" + comments = [] + +equityCompensationIssuance : Text -> Text -> Text -> Text -> Text -> Decimal -> EquityCompensationIssuanceOcfData +equityCompensationIssuance issuanceId securityId stakeholderId stockClassId stockPlanId quantity = EquityCompensationIssuanceOcfData with + id = issuanceId + date = defaultTestDate + security_id = securityId + custom_id = "EC-" <> issuanceId + stakeholder_id = stakeholderId + base_price = None + board_approval_date = None + consideration_text = None + early_exercisable = Some False + exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + expiration_date = None + stock_class_id = Some stockClassId + stock_plan_id = Some stockPlanId + stockholder_approval_date = None + vesting_terms_id = None + security_law_exemptions = [] + vestings = [] + termination_exercise_windows = [] + compensation_type = OcfCompensationTypeOptionNSO + quantity = quantity + comments = [] + +createPlanAndStockIssuance : + Party -> + ContractId CT.CapTable -> + Text -> + Decimal -> + Decimal -> + Script (ContractId CT.CapTable, Text, Text) +createPlanAndStockIssuance issuer capTableCid stockPlanId reservedShares issuedShares = do + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer capTableCid + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = reservedShares + let issuance = (defaultStockIssuance ("TX_" <> stockPlanId) ("SEC-" <> stockPlanId) stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = issuedShares + + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + pure (result.updatedCapTableCid, stockClassId, stakeholderId) + +testStockPlanReservedShares_StockIssuanceCannotExceedInitialPool = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_STOCK_LIMIT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = (defaultStockIssuance "TX_STOCK_OVER_POOL" "SEC-STOCK-OVER-POOL" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 101.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityCompensationCannotExceedInitialPool = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EC_LIMIT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = equityCompensationIssuance "TX_EC_OVER_POOL" "SEC-EC-OVER-POOL" stakeholderId stockClassId stockPlanId 101.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_AdjustmentAllowsAdditionalIssuanceSameBatch = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_ADJUSTED" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let adjustment = stockPlanPoolAdjustment "TX_POOL_UP" stockPlanId 200.0 + let issuance = (defaultStockIssuance "TX_STOCK_ADJUSTED_POOL" "SEC-STOCK-ADJUSTED-POOL" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 150.0 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockPlanPoolAdjustment adjustment + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_SameDateLowerAdjustmentWins = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_SAME_DATE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let higherAdjustment = stockPlanPoolAdjustment "TX_POOL_HIGH" stockPlanId 200.0 + let lowerAdjustment = stockPlanPoolAdjustment "TX_POOL_LOW" stockPlanId 120.0 + let issuance = (defaultStockIssuance "TX_STOCK_SAME_DATE_POOL" "SEC-STOCK-SAME-DATE-POOL" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 150.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockPlanPoolAdjustment higherAdjustment + , CT.OcfCreateStockPlanPoolAdjustment lowerAdjustment + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ReturnToPoolAllowsReuse = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_RETURN_REUSE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let firstIssuance = equityCompensationIssuance "TX_EC_ORIGINAL_POOL" "SEC-EC-ORIGINAL-POOL" stakeholderId stockClassId stockPlanId 100.0 + let returnToPool = stockPlanReturnToPool "TX_RETURN_TO_POOL" firstIssuance.security_id stockPlanId 40.0 + let secondIssuance = equityCompensationIssuance "TX_EC_REUSED_POOL" "SEC-EC-REUSED-POOL" stakeholderId stockClassId stockPlanId 40.0 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance firstIssuance + , CT.OcfCreateStockPlanReturnToPool returnToPool + , CT.OcfCreateEquityCompensationIssuance secondIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_ReturnToPoolCannotExceedSecurityAllocation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_RETURN_OVER" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = equityCompensationIssuance "TX_EC_RETURN_OVER" "SEC-EC-RETURN-OVER" stakeholderId stockClassId stockPlanId 100.0 + let returnToPool = stockPlanReturnToPool "TX_RETURN_OVER" issuance.security_id stockPlanId 101.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + , CT.OcfCreateStockPlanReturnToPool returnToPool + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ReturnToPoolMustMatchSecurityPlan = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourcePlanId = "PLAN_RETURN_SOURCE" + let targetPlanId = "PLAN_RETURN_TARGET" + let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 100.0 + let targetPlan = (defaultStockPlan targetPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = equityCompensationIssuance "TX_EC_RETURN_WRONG_PLAN" "SEC-EC-RETURN-WRONG-PLAN" stakeholderId stockClassId sourcePlanId 50.0 + let returnToPool = stockPlanReturnToPool "TX_RETURN_WRONG_PLAN" issuance.security_id targetPlanId 50.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan sourcePlan + , CT.OcfCreateStockPlan targetPlan + , CT.OcfCreateEquityCompensationIssuance issuance + , CT.OcfCreateStockPlanReturnToPool returnToPool + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_AdjustmentCannotDropBelowAllocatedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithIssuance, _, _) <- createPlanAndStockIssuance issuer cap_table "PLAN_ADJ_DROP" 200.0 150.0 + + submitMustFail issuer do + exerciseCmd capTableWithIssuance CT.UpdateCapTable with + creates = [CT.OcfCreateStockPlanPoolAdjustment (stockPlanPoolAdjustment "TX_POOL_DOWN" "PLAN_ADJ_DROP" 100.0)] + edits = [] + deletes = [] + +testStockPlanReservedShares_DeleteAdjustmentCannotInvalidateAllocatedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_DELETE_ADJ" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let adjustment = stockPlanPoolAdjustment "TX_POOL_DELETE" stockPlanId 200.0 + let issuance = (defaultStockIssuance "TX_STOCK_DELETE_ADJ" "SEC-STOCK-DELETE-ADJ" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 150.0 + + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockPlanPoolAdjustment adjustment + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockPlanPoolAdjustment adjustment.id] + +testStockPlanReservedShares_DeletePlanCannotOrphanStockIssuance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithIssuance, _, _) <- createPlanAndStockIssuance issuer cap_table "PLAN_DELETE_ORPHAN" 200.0 150.0 + + submitMustFail issuer do + exerciseCmd capTableWithIssuance CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockPlan "PLAN_DELETE_ORPHAN"] + +testStockPlanReservedShares_StockPlanEditCannotDropBelowAllocatedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithIssuance, stockClassId, _) <- createPlanAndStockIssuance issuer cap_table "PLAN_EDIT_DROP" 200.0 150.0 + let editedPlan = (defaultStockPlan "PLAN_EDIT_DROP" stockClassId) with initial_shares_reserved = 100.0 + + submitMustFail issuer do + exerciseCmd capTableWithIssuance CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockPlan editedPlan] + deletes = [] diff --git a/Test/daml/OpenCapTable/TestStockPlanReturnToPool.daml b/Test/daml/OpenCapTable/TestStockPlanReturnToPool.daml index 3dc9004d..019e8539 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReturnToPool.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReturnToPool.daml @@ -4,6 +4,7 @@ import Fairmint.OpenCapTable.OCF.StockPlan import Fairmint.OpenCapTable.OCF.StockPlanReturnToPool import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup +import OpenCapTable.TestHelpers (defaultStockIssuance, setupForStockIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -13,17 +14,24 @@ import Daml.Script -- Test: Create and archive return to pool (termination scenario) testCreateAndArchiveStockPlanReturnToPool = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let securityId = "SEC_OPT_001" result <- submit issuer do - exerciseCmd cap_table CT.UpdateCapTable with - creates = [CT.OcfCreateStockPlan StockPlanOcfData with - id = "PLAN_2021" - plan_name = "2021 Equity Incentive Plan" - initial_shares_reserved = 1_000_000.0 - board_approval_date = Some (DT.time (DA.date 2021 Jan 01) 0 0 0) - stockholder_approval_date = None - default_cancellation_behavior = Some OcfPlanCancelReturnToPool - stock_class_ids = ["SC_COMMON"] - comments = []] + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan StockPlanOcfData with + id = "PLAN_2021" + plan_name = "2021 Equity Incentive Plan" + initial_shares_reserved = 1_000_000.0 + board_approval_date = Some (DT.time (DA.date 2021 Jan 01) 0 0 0) + stockholder_approval_date = None + default_cancellation_behavior = Some OcfPlanCancelReturnToPool + stock_class_ids = [stockClassId] + comments = [] + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_OPT_001" securityId stakeholderId stockClassId) with + stock_plan_id = Some "PLAN_2021" + quantity = 25000.0) + ] edits = [] deletes = [] -- Employee termination - unvested options return to pool @@ -32,7 +40,7 @@ testCreateAndArchiveStockPlanReturnToPool = script do creates = [CT.OcfCreateStockPlanReturnToPool StockPlanReturnToPoolOcfData with id = "TX_RETURN_TO_POOL_1" date = DT.time (DA.date 2024 Jun 30) 0 0 0 - security_id = "SEC_OPT_001" + security_id = securityId stock_plan_id = "PLAN_2021" quantity = 25000.0 reason_text = "Employee termination - unvested options cancelled and returned to pool" @@ -45,17 +53,24 @@ testCreateAndArchiveStockPlanReturnToPool = script do -- Test: Create and archive with Archive choice (expiration scenario) testCreateAndArchiveStockPlanReturnToPoolExpiration = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let securityId = "SEC_OPT_002" result <- submit issuer do - exerciseCmd cap_table CT.UpdateCapTable with - creates = [CT.OcfCreateStockPlan StockPlanOcfData with - id = "PLAN_2015" - plan_name = "2015 Stock Option Plan" - initial_shares_reserved = 500_000.0 - board_approval_date = Some (DT.time (DA.date 2015 Mar 15) 0 0 0) - stockholder_approval_date = Some (DT.time (DA.date 2015 Apr 01) 0 0 0) - default_cancellation_behavior = Some OcfPlanCancelReturnToPool - stock_class_ids = ["SC_COMMON"] - comments = []] + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan StockPlanOcfData with + id = "PLAN_2015" + plan_name = "2015 Stock Option Plan" + initial_shares_reserved = 500_000.0 + board_approval_date = Some (DT.time (DA.date 2015 Mar 15) 0 0 0) + stockholder_approval_date = Some (DT.time (DA.date 2015 Apr 01) 0 0 0) + default_cancellation_behavior = Some OcfPlanCancelReturnToPool + stock_class_ids = [stockClassId] + comments = [] + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_OPT_002" securityId stakeholderId stockClassId) with + stock_plan_id = Some "PLAN_2015" + quantity = 10000.0) + ] edits = [] deletes = [] -- Expired unexercised options return to pool @@ -64,7 +79,7 @@ testCreateAndArchiveStockPlanReturnToPoolExpiration = script do creates = [CT.OcfCreateStockPlanReturnToPool StockPlanReturnToPoolOcfData with id = "TX_RETURN_TO_POOL_2" date = DT.time (DA.date 2025 Mar 15) 0 0 0 - security_id = "SEC_OPT_002" + security_id = securityId stock_plan_id = "PLAN_2015" quantity = 10000.0 reason_text = "Options expired unexercised after 10-year term" diff --git a/Test/daml/OpenCapTable/TestVestingAcceleration.daml b/Test/daml/OpenCapTable/TestVestingAcceleration.daml index 75248cc7..177c424b 100644 --- a/Test/daml/OpenCapTable/TestVestingAcceleration.daml +++ b/Test/daml/OpenCapTable/TestVestingAcceleration.daml @@ -26,7 +26,7 @@ createEquityCompensationIssuance issuer capTableCid securityId quantity = stockholder_approval_date = None consideration_text = None security_law_exemptions = [] - stock_plan_id = Some "ESOP-2024" + stock_plan_id = None stock_class_id = Some "COMMON" compensation_type = OcfCompensationTypeOptionISO quantity = quantity diff --git a/Test/daml/OpenCapTable/TestVestingEvent.daml b/Test/daml/OpenCapTable/TestVestingEvent.daml index 77acc903..f5cbcd49 100644 --- a/Test/daml/OpenCapTable/TestVestingEvent.daml +++ b/Test/daml/OpenCapTable/TestVestingEvent.daml @@ -26,7 +26,7 @@ createEquityCompensationIssuance issuer capTableCid securityId quantity = stockholder_approval_date = None consideration_text = None security_law_exemptions = [] - stock_plan_id = Some "ESOP-2024" + stock_plan_id = None stock_class_id = Some "COMMON" compensation_type = OcfCompensationTypeOptionISO quantity = quantity diff --git a/Test/daml/OpenCapTable/TestVestingStart.daml b/Test/daml/OpenCapTable/TestVestingStart.daml index 20f23760..f4678b13 100644 --- a/Test/daml/OpenCapTable/TestVestingStart.daml +++ b/Test/daml/OpenCapTable/TestVestingStart.daml @@ -26,7 +26,7 @@ createEquityCompensationIssuance issuer capTableCid securityId quantity = stockholder_approval_date = None consideration_text = None security_law_exemptions = [] - stock_plan_id = Some "ESOP-2024" + stock_plan_id = None stock_class_id = Some "COMMON" compensation_type = OcfCompensationTypeOptionISO quantity = quantity @@ -95,7 +95,7 @@ testVestingStart_RSUGrant = script do stockholder_approval_date = None consideration_text = None security_law_exemptions = [] - stock_plan_id = Some "ESOP-2024" + stock_plan_id = None stock_class_id = Some "COMMON" compensation_type = OcfCompensationTypeRSU quantity = 25000.0 diff --git a/dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar new file mode 100644 index 00000000..f36ce95c --- /dev/null +++ b/dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ced34adbf61393475b2e00e51d8f112f5c423f3bbf0bb544c10febd2058c80ad +size 2576174 diff --git a/dars/dars.lock b/dars/dars.lock index b3eb875b..3761b9ce 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -24,6 +24,13 @@ "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T00:41:37.578Z", "networks": [] + }, + "OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar": { + "sha256": "ced34adbf61393475b2e00e51d8f112f5c423f3bbf0bb544c10febd2058c80ad", + "size": 2576174, + "sdkVersion": "3.4.10", + "uploadedAt": "2026-07-09T12:34:07.425Z", + "networks": [] } } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index ec675768..63faf63c 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -163,6 +163,14 @@ issuerAuthorizedAdjustmentTakesPrecedence candidate current = candidate.date > current.date || (candidate.date == current.date && candidate.new_shares_authorized < current.new_shares_authorized) +stockPlanPoolAdjustmentTakesPrecedence : + StockPlanPoolAdjustmentOcfData -> + StockPlanPoolAdjustmentOcfData -> + Bool +stockPlanPoolAdjustmentTakesPrecedence candidate current = + candidate.date > current.date || + (candidate.date == current.date && candidate.shares_reserved < current.shares_reserved) + stockClassConversionRatioAdjustmentTakesPrecedence : StockClassConversionRatioAdjustmentOcfData -> StockClassConversionRatioAdjustmentOcfData -> @@ -601,6 +609,31 @@ currentIssuerAuthorizedLimit maps issuerCid = do Some adjustment -> Some adjustment.new_shares_authorized None -> issuerAuthorizedSharesLimit issuerContract.issuer_data.initial_shares_authorized +stockPlanPoolAdjustmentIndex : CapTableMaps -> Update (Map Text StockPlanPoolAdjustmentOcfData) +stockPlanPoolAdjustmentIndex maps = + foldlA + (\index adjustmentCid -> do + adjustment <- fetch adjustmentCid + let stockPlanId = adjustment.adjustment_data.stock_plan_id + let nextIndex = case Map.lookup stockPlanId index of + Some current -> + if stockPlanPoolAdjustmentTakesPrecedence adjustment.adjustment_data current + then Map.insert stockPlanId adjustment.adjustment_data index + else index + None -> Map.insert stockPlanId adjustment.adjustment_data index + pure nextIndex) + Map.empty + (Map.values maps.stock_plan_pool_adjustments) + +currentStockPlanReservedShares : + Map Text StockPlanPoolAdjustmentOcfData -> + StockPlanOcfData -> + Decimal +currentStockPlanReservedShares adjustmentIndex stockPlanData = + case Map.lookup stockPlanData.id adjustmentIndex of + Some adjustment -> adjustment.shares_reserved + None -> stockPlanData.initial_shares_reserved + issuerAuthorizedLimitAt : Map Text [IssuerAuthorizedSharesAdjustmentOcfData] -> IssuerOcfData -> @@ -862,7 +895,7 @@ validateStockClassAuthorizedSharesAtDates eventIndex stockClasses lotsByStockCla addStockClassAuthorizedContribution : Optional Decimal -> AuthorizedSharesCeiling -> Optional Decimal addStockClassAuthorizedContribution totalOpt ceiling = case (totalOpt, ceiling) of (Some total, AuthorizedSharesFinite shares) -> Some (total + shares) - (Some total, AuthorizedSharesNotApplicable) -> Some total + (_, AuthorizedSharesNotApplicable) -> None (_, AuthorizedSharesUnlimited) -> None (None, _) -> None @@ -981,6 +1014,147 @@ validateIssuerAuthorizedSharesAtDates stockClasses maps stockClassAdjustmentHist assertMsg ("Issuer authorized shares exceeded on " <> show asOf <> ": stock class authorization is unlimited but issuer authorization is finite") False) validationDates +addQuantityById : Text -> Decimal -> Map Text Decimal -> Map Text Decimal +addQuantityById key quantity totals = + let current = fromOptional 0.0 (Map.lookup key totals) + in Map.insert key (current + quantity) totals + +mergeQuantityMaps : Map Text Decimal -> Map Text Decimal -> Map Text Decimal +mergeQuantityMaps base delta = + foldl (\totals (key, quantity) -> addQuantityById key quantity totals) base (Map.toList delta) + +negateQuantityMap : Map Text Decimal -> Map Text Decimal +negateQuantityMap quantities = + foldl (\totals (key, quantity) -> Map.insert key (-quantity) totals) Map.empty (Map.toList quantities) + +stockIssuancePlanAllocationIndex : CapTableMaps -> Update (Map Text Decimal) +stockIssuancePlanAllocationIndex maps = + foldlA + (\totals issuanceCid -> do + issuance <- fetch issuanceCid + case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> pure (addQuantityById stockPlanId issuance.issuance_data.quantity totals) + None -> pure totals) + Map.empty + (Map.values maps.stock_issuances) + +equityCompensationPlanAllocationIndex : CapTableMaps -> Update (Map Text Decimal) +equityCompensationPlanAllocationIndex maps = + foldlA + (\totals issuanceCid -> do + issuance <- fetch issuanceCid + case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> pure (addQuantityById stockPlanId issuance.issuance_data.quantity totals) + None -> pure totals) + Map.empty + (Map.values maps.equity_compensation_issuances) + +stockPlanReturnedQuantityIndex : CapTableMaps -> Update (Map Text Decimal) +stockPlanReturnedQuantityIndex maps = + foldlA + (\totals returnCid -> do + returnToPool <- fetch returnCid + pure (addQuantityById returnToPool.return_data.stock_plan_id returnToPool.return_data.quantity totals)) + Map.empty + (Map.values maps.stock_plan_return_to_pools) + +stockPlanReturnedQuantityBySecurityIndex : CapTableMaps -> Update (Map Text Decimal) +stockPlanReturnedQuantityBySecurityIndex maps = + foldlA + (\totals returnCid -> do + returnToPool <- fetch returnCid + pure (addQuantityById returnToPool.return_data.security_id returnToPool.return_data.quantity totals)) + Map.empty + (Map.values maps.stock_plan_return_to_pools) + +stockPlanSecurityAllocation : CapTableMaps -> StockPlanReturnToPoolOcfData -> Update Decimal +stockPlanSecurityAllocation maps returnData = do + let stockCidOpt = Map.lookup returnData.security_id maps.stock_issuances_by_security_id + let equityCompensationCidOpt = Map.lookup returnData.security_id maps.equity_compensation_issuances_by_security_id + case (stockCidOpt, equityCompensationCidOpt) of + (Some stockCid, None) -> do + issuance <- fetch stockCid + case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> do + assertMsg + ("Stock plan return to pool references security " <> returnData.security_id <> " from stock plan " <> stockPlanId <> " but returns to " <> returnData.stock_plan_id) + (stockPlanId == returnData.stock_plan_id) + pure issuance.issuance_data.quantity + None -> do + assertMsg ("Stock plan return to pool references non-plan stock security: " <> returnData.security_id) False + pure 0.0 + (None, Some equityCompensationCid) -> do + issuance <- fetch equityCompensationCid + case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> do + assertMsg + ("Stock plan return to pool references security " <> returnData.security_id <> " from stock plan " <> stockPlanId <> " but returns to " <> returnData.stock_plan_id) + (stockPlanId == returnData.stock_plan_id) + pure issuance.issuance_data.quantity + None -> do + assertMsg ("Stock plan return to pool references non-plan equity compensation security: " <> returnData.security_id) False + pure 0.0 + (Some _, Some _) -> do + assertMsg ("Stock plan return to pool security id is ambiguous: " <> returnData.security_id) False + pure 0.0 + (None, None) -> do + assertMsg ("Stock plan return to pool security not found: " <> returnData.security_id) False + pure 0.0 + +validateStockPlanReturnToPoolQuantities : CapTableMaps -> Update () +validateStockPlanReturnToPoolQuantities maps = do + returnedQuantityBySecurity <- stockPlanReturnedQuantityBySecurityIndex maps + mapA_ + (\returnCid -> do + returnToPool <- fetch returnCid + securityAllocation <- stockPlanSecurityAllocation maps returnToPool.return_data + let returnedQuantity = fromOptional 0.0 (Map.lookup returnToPool.return_data.security_id returnedQuantityBySecurity) + assertMsg + ("Stock plan return to pool exceeds security allocation for " <> returnToPool.return_data.security_id <> ": returned " <> show returnedQuantity <> " exceeds allocated " <> show securityAllocation) + (returnedQuantity <= securityAllocation)) + (Map.values maps.stock_plan_return_to_pools) + +stockPlanNetAllocatedQuantityIndex : CapTableMaps -> Update (Map Text Decimal) +stockPlanNetAllocatedQuantityIndex maps = do + stockAllocation <- stockIssuancePlanAllocationIndex maps + equityCompensationAllocation <- equityCompensationPlanAllocationIndex maps + returnedQuantity <- stockPlanReturnedQuantityIndex maps + let combinedAllocation = mergeQuantityMaps stockAllocation equityCompensationAllocation + let netAllocatedByPlan = mergeQuantityMaps combinedAllocation (negateQuantityMap returnedQuantity) + pure netAllocatedByPlan + +validateStockPlanAllocationReferences : CapTableMaps -> Map Text Decimal -> Update () +validateStockPlanAllocationReferences maps netAllocatedQuantityIndex = + mapA_ + (\(stockPlanId, _) -> + assertMsg + ("Stock plan allocation references missing stock plan: " <> stockPlanId) + (isSome (Map.lookup stockPlanId maps.stock_plans))) + (Map.toList netAllocatedQuantityIndex) + +validateStockPlanReservedShares : + CapTableMaps -> + Map Text StockPlanPoolAdjustmentOcfData -> + Update () +validateStockPlanReservedShares maps adjustmentIndex = do + netAllocatedQuantityIndex <- stockPlanNetAllocatedQuantityIndex maps + validateStockPlanAllocationReferences maps netAllocatedQuantityIndex + mapA_ + (\stockPlanCid -> do + stockPlan <- fetch stockPlanCid + let reservedShares = currentStockPlanReservedShares adjustmentIndex stockPlan.plan_data + let allocatedQuantity = fromOptional 0.0 (Map.lookup stockPlan.plan_data.id netAllocatedQuantityIndex) + assertMsg + ("Stock plan reserved shares exceeded for " <> stockPlan.plan_data.id <> ": allocated " <> show allocatedQuantity <> " exceeds reserved " <> show reservedShares) + (allocatedQuantity <= reservedShares)) + (Map.values maps.stock_plans) + +validateStockPlanPoolCeilings : CapTableMaps -> Update () +validateStockPlanPoolCeilings maps = do + adjustmentIndex <- stockPlanPoolAdjustmentIndex maps + validateStockPlanReturnToPoolQuantities maps + validateStockPlanReservedShares maps adjustmentIndex + validateAuthorizedShareCeilings : CapTableMaps -> Map Text StockClassAuthorizedSharesAdjustmentOcfData -> @@ -998,6 +1172,7 @@ validateAuthorizedShareCeilings maps adjustmentIndex issuerCid = do validateStockClassAuthorizedSharesAtDates eventIndex stockClasses lotsByStockClass stockClassAdjustmentHistory stockClassValidationDates validateIssuerAuthorizedShares stockClasses maps adjustmentIndex issuerCid validateIssuerAuthorizedSharesAtDates stockClasses maps stockClassAdjustmentHistory issuerAdjustmentHistory issuerCid + validateStockPlanPoolCeilings maps -- | Get the processing tier for a create operation getCreateTier : OcfCreateData -> Int From 517e2e260eff85e5f9df3e1390f75f2dceb8c7fb Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 08:45:32 -0400 Subject: [PATCH 23/62] Enforce stock plan pool edge cases --- OpenCapTable-v34/daml.yaml | 2 +- Test/daml.yaml | 2 +- .../TestStockPlanReservedShares.daml | 108 ++++++++++++++++++ .../TestStockPlanReturnToPool.daml | 53 ++++++--- .../0.0.5/OpenCapTable-v34.dar | 3 + dars/dars.lock | 7 ++ .../codegen/templates/CapTable.daml.template | 91 +++++++++++---- 7 files changed, 227 insertions(+), 39 deletions(-) create mode 100644 dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar diff --git a/OpenCapTable-v34/daml.yaml b/OpenCapTable-v34/daml.yaml index 4d300d29..f16d912a 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.4 +version: 0.0.5 build-options: - -Wno-crypto-text-is-alpha - --typecheck-upgrades=no diff --git a/Test/daml.yaml b/Test/daml.yaml index c6e7f2d6..64cd009c 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.4.dar + - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.5.dar build-options: - -Wno-template-interface-depends-on-daml-script diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index 76eb2125..807b1c99 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -2,9 +2,12 @@ module OpenCapTable.TestStockPlanReservedShares where import qualified Fairmint.OpenCapTable.CapTable as CT import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance (EquityCompensationIssuanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockCancellation (StockCancellationOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockClassSplit (StockClassSplitOcfData(..)) import Fairmint.OpenCapTable.OCF.StockPlanPoolAdjustment (StockPlanPoolAdjustmentOcfData(..)) import Fairmint.OpenCapTable.OCF.StockPlanReturnToPool (StockPlanReturnToPoolOcfData(..)) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import Fairmint.OpenCapTable.Types.Stock (OcfRatio(..)) import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) import OpenCapTable.Setup import OpenCapTable.TestHelpers @@ -30,6 +33,24 @@ stockPlanReturnToPool returnId securityId stockPlanId quantity = StockPlanReturn reason_text = "Returned to pool" comments = [] +stockCancellation : Text -> Text -> Decimal -> StockCancellationOcfData +stockCancellation cancellationId securityId quantity = StockCancellationOcfData with + id = cancellationId + date = defaultTestDate + 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 + id = splitId + date = defaultTestDate + stock_class_id = stockClassId + split_ratio = OcfRatio with numerator = numerator, denominator = denominator + comments = [] + equityCompensationIssuance : Text -> Text -> Text -> Text -> Text -> Decimal -> EquityCompensationIssuanceOcfData equityCompensationIssuance issuanceId securityId stakeholderId stockClassId stockPlanId quantity = EquityCompensationIssuanceOcfData with id = issuanceId @@ -112,6 +133,46 @@ testStockPlanReservedShares_EquityCompensationCannotExceedInitialPool = script d edits = [] deletes = [] +testStockPlanReservedShares_NegativeEquityCompensationCannotOffsetOverallocatedPool = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_NEGATIVE_OFFSET" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let stockIssuance = (defaultStockIssuance "TX_STOCK_NEGATIVE_OFFSET" "SEC-STOCK-NEGATIVE-OFFSET" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 150.0 + let negativeEquityCompensation = equityCompensationIssuance "TX_EC_NEGATIVE_OFFSET" "SEC-EC-NEGATIVE-OFFSET" stakeholderId stockClassId stockPlanId (-50.0) + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance stockIssuance + , CT.OcfCreateEquityCompensationIssuance negativeEquityCompensation + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_StockSplitCountsAgainstPlanPool = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_SPLIT_POOL" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = (defaultStockIssuance "TX_STOCK_SPLIT_POOL" "SEC-STOCK-SPLIT-POOL" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + let split = stockClassSplit "TX_SPLIT_POOL" stockClassId 2.0 1.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockClassSplit split + ] + edits = [] + deletes = [] + testStockPlanReservedShares_AdjustmentAllowsAdditionalIssuanceSameBatch = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table @@ -176,6 +237,53 @@ testStockPlanReservedShares_ReturnToPoolAllowsReuse = script do deletes = [] pure () +testStockPlanReservedShares_StockReturnRequiresReturnedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_STOCK_RETURN_LIVE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = (defaultStockIssuance "TX_STOCK_RETURN_LIVE" "SEC-STOCK-RETURN-LIVE" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + let returnToPool = stockPlanReturnToPool "TX_RETURN_LIVE_STOCK" issuance.security_id stockPlanId 100.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockPlanReturnToPool returnToPool + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_StockCancellationReturnAllowsReuse = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_STOCK_RETURN_REUSE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let firstIssuance = (defaultStockIssuance "TX_STOCK_RETURN_REUSE_FIRST" "SEC-STOCK-RETURN-REUSE-FIRST" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + let cancellation = stockCancellation "TX_CANCEL_STOCK_RETURN_REUSE" firstIssuance.security_id 40.0 + let returnToPool = stockPlanReturnToPool "TX_RETURN_STOCK_REUSE" firstIssuance.security_id stockPlanId 40.0 + let secondIssuance = (defaultStockIssuance "TX_STOCK_RETURN_REUSE_SECOND" "SEC-STOCK-RETURN-REUSE-SECOND" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 40.0 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance firstIssuance + , CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockPlanReturnToPool returnToPool + , CT.OcfCreateStockIssuance secondIssuance + ] + edits = [] + deletes = [] + pure () + testStockPlanReservedShares_ReturnToPoolCannotExceedSecurityAllocation = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/Test/daml/OpenCapTable/TestStockPlanReturnToPool.daml b/Test/daml/OpenCapTable/TestStockPlanReturnToPool.daml index 019e8539..ef2f9690 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReturnToPool.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReturnToPool.daml @@ -2,6 +2,7 @@ module OpenCapTable.TestStockPlanReturnToPool where import Fairmint.OpenCapTable.OCF.StockPlan import Fairmint.OpenCapTable.OCF.StockPlanReturnToPool +import Fairmint.OpenCapTable.OCF.StockCancellation import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup import OpenCapTable.TestHelpers (defaultStockIssuance, setupForStockIssuance) @@ -37,14 +38,24 @@ testCreateAndArchiveStockPlanReturnToPool = script do -- Employee termination - unvested options return to pool _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockPlanReturnToPool StockPlanReturnToPoolOcfData with - id = "TX_RETURN_TO_POOL_1" - date = DT.time (DA.date 2024 Jun 30) 0 0 0 - security_id = securityId - stock_plan_id = "PLAN_2021" - quantity = 25000.0 - reason_text = "Employee termination - unvested options cancelled and returned to pool" - comments = ["Employee B voluntary resignation effective June 30, 2024"]] + creates = + [ CT.OcfCreateStockCancellation StockCancellationOcfData with + id = "TX_CANCEL_FOR_RETURN_1" + date = DT.time (DA.date 2024 Jun 30) 0 0 0 + security_id = securityId + quantity = 25000.0 + balance_security_id = None + reason_text = "Employee termination - unvested options cancelled" + comments = ["Employee B voluntary resignation effective June 30, 2024"] + , CT.OcfCreateStockPlanReturnToPool StockPlanReturnToPoolOcfData with + id = "TX_RETURN_TO_POOL_1" + date = DT.time (DA.date 2024 Jun 30) 0 0 0 + security_id = securityId + stock_plan_id = "PLAN_2021" + quantity = 25000.0 + reason_text = "Employee termination - unvested options cancelled and returned to pool" + comments = ["Employee B voluntary resignation effective June 30, 2024"] + ] edits = [] deletes = [] pure () @@ -76,14 +87,24 @@ testCreateAndArchiveStockPlanReturnToPoolExpiration = script do -- Expired unexercised options return to pool _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockPlanReturnToPool StockPlanReturnToPoolOcfData with - id = "TX_RETURN_TO_POOL_2" - date = DT.time (DA.date 2025 Mar 15) 0 0 0 - security_id = securityId - stock_plan_id = "PLAN_2015" - quantity = 10000.0 - reason_text = "Options expired unexercised after 10-year term" - comments = []] + creates = + [ CT.OcfCreateStockCancellation StockCancellationOcfData with + id = "TX_CANCEL_FOR_RETURN_2" + date = DT.time (DA.date 2025 Mar 15) 0 0 0 + security_id = securityId + quantity = 10000.0 + balance_security_id = None + reason_text = "Options expired unexercised after 10-year term" + comments = [] + , CT.OcfCreateStockPlanReturnToPool StockPlanReturnToPoolOcfData with + id = "TX_RETURN_TO_POOL_2" + date = DT.time (DA.date 2025 Mar 15) 0 0 0 + security_id = securityId + stock_plan_id = "PLAN_2015" + quantity = 10000.0 + reason_text = "Options expired unexercised after 10-year term" + comments = [] + ] edits = [] deletes = [] pure () diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar new file mode 100644 index 00000000..12738bf0 --- /dev/null +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:419fb16000bd0390de5dec8cd594c7bab40504eeea6013dd2f1c17862ba9100f +size 2582480 diff --git a/dars/dars.lock b/dars/dars.lock index 3761b9ce..ee74e6bc 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -31,6 +31,13 @@ "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T12:34:07.425Z", "networks": [] + }, + "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { + "sha256": "419fb16000bd0390de5dec8cd594c7bab40504eeea6013dd2f1c17862ba9100f", + "size": 2582480, + "sdkVersion": "3.4.10", + "uploadedAt": "2026-07-09T12:44:18.654Z", + "networks": [] } } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 63faf63c..967a7ac6 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -765,6 +765,25 @@ stockClassShareLotEvents eventIndex lot = quantity_delta = lot.quantity) :: (securityEvents ++ splitEvents) +stockClassShareLotSplitAdjustedEvents : StockClassShareEventIndex -> StockClassShareLot -> [StockClassShareEvent] +stockClassShareLotSplitAdjustedEvents eventIndex lot = + let splitEvents = + filter + (stockClassShareEventOnOrAfter lot.event_date) + (fromOptional [] (Map.lookup lot.stock_class_id eventIndex.stock_class_split_events_by_stock_class_id)) + in (StockClassShareDelta with + event_date = lot.event_date + event_priority = 0 + quantity_delta = lot.quantity) + :: splitEvents + +stockClassShareLotSplitAdjustedQuantity : StockClassShareEventIndex -> StockClassShareLot -> Optional Decimal +stockClassShareLotSplitAdjustedQuantity eventIndex lot = + foldl + applyStockClassShareEventOptional + (Some 0.0) + (sortOn stockClassShareEventSortKey (stockClassShareLotSplitAdjustedEvents eventIndex lot)) + stockClassShareLotCurrentQuantity : StockClassShareEventIndex -> StockClassShareLot -> Optional Decimal stockClassShareLotCurrentQuantity eventIndex lot = foldl @@ -1027,13 +1046,25 @@ negateQuantityMap : Map Text Decimal -> Map Text Decimal negateQuantityMap quantities = foldl (\totals (key, quantity) -> Map.insert key (-quantity) totals) Map.empty (Map.toList quantities) -stockIssuancePlanAllocationIndex : CapTableMaps -> Update (Map Text Decimal) -stockIssuancePlanAllocationIndex maps = +assertNonNegativeQuantity : Text -> Decimal -> Update () +assertNonNegativeQuantity label quantity = + assertMsg (label <> " cannot be negative: " <> show quantity) (quantity >= 0.0) + +stockIssuancePlanAllocationIndex : StockClassShareEventIndex -> CapTableMaps -> Update (Map Text Decimal) +stockIssuancePlanAllocationIndex eventIndex maps = foldlA (\totals issuanceCid -> do issuance <- fetch issuanceCid case issuance.issuance_data.stock_plan_id of - Some stockPlanId -> pure (addQuantityById stockPlanId issuance.issuance_data.quantity totals) + Some stockPlanId -> do + let lot = stockIssuanceShareLot issuance.issuance_data + case stockClassShareLotSplitAdjustedQuantity eventIndex lot of + Some allocationQuantity -> do + assertNonNegativeQuantity ("Stock issuance plan allocation quantity for " <> issuance.issuance_data.id) allocationQuantity + pure (addQuantityById stockPlanId allocationQuantity totals) + None -> do + assertMsg ("Stock issuance plan allocation has an invalid quantity for " <> issuance.issuance_data.id) False + pure totals None -> pure totals) Map.empty (Map.values maps.stock_issuances) @@ -1044,7 +1075,9 @@ equityCompensationPlanAllocationIndex maps = (\totals issuanceCid -> do issuance <- fetch issuanceCid case issuance.issuance_data.stock_plan_id of - Some stockPlanId -> pure (addQuantityById stockPlanId issuance.issuance_data.quantity totals) + Some stockPlanId -> do + assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id) issuance.issuance_data.quantity + pure (addQuantityById stockPlanId issuance.issuance_data.quantity totals) None -> pure totals) Map.empty (Map.values maps.equity_compensation_issuances) @@ -1054,6 +1087,7 @@ stockPlanReturnedQuantityIndex maps = foldlA (\totals returnCid -> do returnToPool <- fetch returnCid + assertNonNegativeQuantity ("Stock plan return-to-pool quantity for " <> returnToPool.return_data.id) returnToPool.return_data.quantity pure (addQuantityById returnToPool.return_data.stock_plan_id returnToPool.return_data.quantity totals)) Map.empty (Map.values maps.stock_plan_return_to_pools) @@ -1063,12 +1097,13 @@ stockPlanReturnedQuantityBySecurityIndex maps = foldlA (\totals returnCid -> do returnToPool <- fetch returnCid + assertNonNegativeQuantity ("Stock plan return-to-pool quantity for " <> returnToPool.return_data.id) returnToPool.return_data.quantity pure (addQuantityById returnToPool.return_data.security_id returnToPool.return_data.quantity totals)) Map.empty (Map.values maps.stock_plan_return_to_pools) -stockPlanSecurityAllocation : CapTableMaps -> StockPlanReturnToPoolOcfData -> Update Decimal -stockPlanSecurityAllocation maps returnData = do +stockPlanSecurityReturnableQuantity : StockClassShareEventIndex -> CapTableMaps -> StockPlanReturnToPoolOcfData -> Update Decimal +stockPlanSecurityReturnableQuantity eventIndex maps returnData = do let stockCidOpt = Map.lookup returnData.security_id maps.stock_issuances_by_security_id let equityCompensationCidOpt = Map.lookup returnData.security_id maps.equity_compensation_issuances_by_security_id case (stockCidOpt, equityCompensationCidOpt) of @@ -1079,7 +1114,19 @@ stockPlanSecurityAllocation maps returnData = do assertMsg ("Stock plan return to pool references security " <> returnData.security_id <> " from stock plan " <> stockPlanId <> " but returns to " <> returnData.stock_plan_id) (stockPlanId == returnData.stock_plan_id) - pure issuance.issuance_data.quantity + let lot = stockIssuanceShareLot issuance.issuance_data + case (stockClassShareLotSplitAdjustedQuantity eventIndex lot, stockClassShareLotCurrentQuantity eventIndex lot) of + (Some allocationQuantity, Some outstandingQuantity) -> do + assertNonNegativeQuantity ("Stock issuance plan allocation quantity for " <> issuance.issuance_data.id) allocationQuantity + assertNonNegativeQuantity ("Stock issuance outstanding quantity for " <> issuance.issuance_data.id) outstandingQuantity + let returnableQuantity = allocationQuantity - outstandingQuantity + assertMsg + ("Stock plan return-to-pool quantity basis is invalid for " <> returnData.security_id) + (returnableQuantity >= 0.0) + pure returnableQuantity + _ -> do + assertMsg ("Stock plan return-to-pool security has invalid stock quantity history: " <> returnData.security_id) False + pure 0.0 None -> do assertMsg ("Stock plan return to pool references non-plan stock security: " <> returnData.security_id) False pure 0.0 @@ -1090,6 +1137,7 @@ stockPlanSecurityAllocation maps returnData = do assertMsg ("Stock plan return to pool references security " <> returnData.security_id <> " from stock plan " <> stockPlanId <> " but returns to " <> returnData.stock_plan_id) (stockPlanId == returnData.stock_plan_id) + assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id) issuance.issuance_data.quantity pure issuance.issuance_data.quantity None -> do assertMsg ("Stock plan return to pool references non-plan equity compensation security: " <> returnData.security_id) False @@ -1101,22 +1149,22 @@ stockPlanSecurityAllocation maps returnData = do assertMsg ("Stock plan return to pool security not found: " <> returnData.security_id) False pure 0.0 -validateStockPlanReturnToPoolQuantities : CapTableMaps -> Update () -validateStockPlanReturnToPoolQuantities maps = do +validateStockPlanReturnToPoolQuantities : StockClassShareEventIndex -> CapTableMaps -> Update () +validateStockPlanReturnToPoolQuantities eventIndex maps = do returnedQuantityBySecurity <- stockPlanReturnedQuantityBySecurityIndex maps mapA_ (\returnCid -> do returnToPool <- fetch returnCid - securityAllocation <- stockPlanSecurityAllocation maps returnToPool.return_data + securityAllocation <- stockPlanSecurityReturnableQuantity eventIndex maps returnToPool.return_data let returnedQuantity = fromOptional 0.0 (Map.lookup returnToPool.return_data.security_id returnedQuantityBySecurity) assertMsg - ("Stock plan return to pool exceeds security allocation for " <> returnToPool.return_data.security_id <> ": returned " <> show returnedQuantity <> " exceeds allocated " <> show securityAllocation) + ("Stock plan return to pool exceeds returnable quantity for " <> returnToPool.return_data.security_id <> ": returned " <> show returnedQuantity <> " exceeds returnable " <> show securityAllocation) (returnedQuantity <= securityAllocation)) (Map.values maps.stock_plan_return_to_pools) -stockPlanNetAllocatedQuantityIndex : CapTableMaps -> Update (Map Text Decimal) -stockPlanNetAllocatedQuantityIndex maps = do - stockAllocation <- stockIssuancePlanAllocationIndex maps +stockPlanNetAllocatedQuantityIndex : StockClassShareEventIndex -> CapTableMaps -> Update (Map Text Decimal) +stockPlanNetAllocatedQuantityIndex eventIndex maps = do + stockAllocation <- stockIssuancePlanAllocationIndex eventIndex maps equityCompensationAllocation <- equityCompensationPlanAllocationIndex maps returnedQuantity <- stockPlanReturnedQuantityIndex maps let combinedAllocation = mergeQuantityMaps stockAllocation equityCompensationAllocation @@ -1133,11 +1181,12 @@ validateStockPlanAllocationReferences maps netAllocatedQuantityIndex = (Map.toList netAllocatedQuantityIndex) validateStockPlanReservedShares : + StockClassShareEventIndex -> CapTableMaps -> Map Text StockPlanPoolAdjustmentOcfData -> Update () -validateStockPlanReservedShares maps adjustmentIndex = do - netAllocatedQuantityIndex <- stockPlanNetAllocatedQuantityIndex maps +validateStockPlanReservedShares eventIndex maps adjustmentIndex = do + netAllocatedQuantityIndex <- stockPlanNetAllocatedQuantityIndex eventIndex maps validateStockPlanAllocationReferences maps netAllocatedQuantityIndex mapA_ (\stockPlanCid -> do @@ -1149,11 +1198,11 @@ validateStockPlanReservedShares maps adjustmentIndex = do (allocatedQuantity <= reservedShares)) (Map.values maps.stock_plans) -validateStockPlanPoolCeilings : CapTableMaps -> Update () -validateStockPlanPoolCeilings maps = do +validateStockPlanPoolCeilings : StockClassShareEventIndex -> CapTableMaps -> Update () +validateStockPlanPoolCeilings eventIndex maps = do adjustmentIndex <- stockPlanPoolAdjustmentIndex maps - validateStockPlanReturnToPoolQuantities maps - validateStockPlanReservedShares maps adjustmentIndex + validateStockPlanReturnToPoolQuantities eventIndex maps + validateStockPlanReservedShares eventIndex maps adjustmentIndex validateAuthorizedShareCeilings : CapTableMaps -> @@ -1172,7 +1221,7 @@ validateAuthorizedShareCeilings maps adjustmentIndex issuerCid = do validateStockClassAuthorizedSharesAtDates eventIndex stockClasses lotsByStockClass stockClassAdjustmentHistory stockClassValidationDates validateIssuerAuthorizedShares stockClasses maps adjustmentIndex issuerCid validateIssuerAuthorizedSharesAtDates stockClasses maps stockClassAdjustmentHistory issuerAdjustmentHistory issuerCid - validateStockPlanPoolCeilings maps + validateStockPlanPoolCeilings eventIndex maps -- | Get the processing tier for a create operation getCreateTier : OcfCreateData -> Int From d8b7b584356d414717caa52781a15ba92a7ab4c2 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 09:40:47 -0400 Subject: [PATCH 24/62] Address stock plan ceiling review comments --- .../TestStockPlanReservedShares.daml | 61 +++- .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- .../codegen/templates/CapTable.daml.template | 290 +++++++++++++++++- 4 files changed, 353 insertions(+), 8 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index 807b1c99..a8df9c92 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -1,6 +1,7 @@ module OpenCapTable.TestStockPlanReservedShares where import qualified Fairmint.OpenCapTable.CapTable as CT +import Fairmint.OpenCapTable.OCF.EquityCompensationCancellation (EquityCompensationCancellationOcfData(..)) import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance (EquityCompensationIssuanceOcfData(..)) import Fairmint.OpenCapTable.OCF.StockCancellation (StockCancellationOcfData(..)) import Fairmint.OpenCapTable.OCF.StockClassSplit (StockClassSplitOcfData(..)) @@ -11,6 +12,9 @@ import Fairmint.OpenCapTable.Types.Stock (OcfRatio(..)) 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 stockPlanPoolAdjustment : Text -> Text -> Decimal -> StockPlanPoolAdjustmentOcfData @@ -75,6 +79,16 @@ equityCompensationIssuance issuanceId securityId stakeholderId stockClassId stoc quantity = quantity comments = [] +equityCompensationCancellation : Text -> Text -> Decimal -> EquityCompensationCancellationOcfData +equityCompensationCancellation cancellationId securityId quantity = EquityCompensationCancellationOcfData with + id = cancellationId + date = defaultTestDate + security_id = securityId + quantity = quantity + balance_security_id = None + reason_text = "Cancelled equity compensation" + comments = [] + createPlanAndStockIssuance : Party -> ContractId CT.CapTable -> @@ -140,7 +154,8 @@ testStockPlanReservedShares_NegativeEquityCompensationCannotOffsetOverallocatedP let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 let stockIssuance = (defaultStockIssuance "TX_STOCK_NEGATIVE_OFFSET" "SEC-STOCK-NEGATIVE-OFFSET" stakeholderId stockClassId) with stock_plan_id = Some stockPlanId - quantity = 150.0 + quantity = 100.0 + let positiveEquityCompensation = equityCompensationIssuance "TX_EC_POSITIVE_OFFSET" "SEC-EC-POSITIVE-OFFSET" stakeholderId stockClassId stockPlanId 50.0 let negativeEquityCompensation = equityCompensationIssuance "TX_EC_NEGATIVE_OFFSET" "SEC-EC-NEGATIVE-OFFSET" stakeholderId stockClassId stockPlanId (-50.0) submitMustFail issuer do @@ -148,6 +163,7 @@ testStockPlanReservedShares_NegativeEquityCompensationCannotOffsetOverallocatedP creates = [ CT.OcfCreateStockPlan plan , CT.OcfCreateStockIssuance stockIssuance + , CT.OcfCreateEquityCompensationIssuance positiveEquityCompensation , CT.OcfCreateEquityCompensationIssuance negativeEquityCompensation ] edits = [] @@ -194,6 +210,29 @@ testStockPlanReservedShares_AdjustmentAllowsAdditionalIssuanceSameBatch = script deletes = [] pure () +testStockPlanReservedShares_FutureAdjustmentCannotAuthorizeBackdatedIssuance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let earlierDate = DT.time (DA.date 2024 Jan 01) 0 0 0 + let laterDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let stockPlanId = "PLAN_BACKDATED" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let adjustment = (stockPlanPoolAdjustment "TX_POOL_BACKDATED_UP" stockPlanId 200.0) with date = laterDate + let issuance = (defaultStockIssuance "TX_STOCK_BACKDATED_POOL" "SEC-STOCK-BACKDATED-POOL" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + date = earlierDate + quantity = 150.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockPlanPoolAdjustment adjustment + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + testStockPlanReservedShares_SameDateLowerAdjustmentWins = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table @@ -222,6 +261,7 @@ testStockPlanReservedShares_ReturnToPoolAllowsReuse = script do let stockPlanId = "PLAN_RETURN_REUSE" let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 let firstIssuance = equityCompensationIssuance "TX_EC_ORIGINAL_POOL" "SEC-EC-ORIGINAL-POOL" stakeholderId stockClassId stockPlanId 100.0 + let cancellation = equityCompensationCancellation "TX_EC_CANCEL_RETURN_TO_POOL" firstIssuance.security_id 40.0 let returnToPool = stockPlanReturnToPool "TX_RETURN_TO_POOL" firstIssuance.security_id stockPlanId 40.0 let secondIssuance = equityCompensationIssuance "TX_EC_REUSED_POOL" "SEC-EC-REUSED-POOL" stakeholderId stockClassId stockPlanId 40.0 @@ -230,6 +270,7 @@ testStockPlanReservedShares_ReturnToPoolAllowsReuse = script do creates = [ CT.OcfCreateStockPlan plan , CT.OcfCreateEquityCompensationIssuance firstIssuance + , CT.OcfCreateEquityCompensationCancellation cancellation , CT.OcfCreateStockPlanReturnToPool returnToPool , CT.OcfCreateEquityCompensationIssuance secondIssuance ] @@ -237,6 +278,24 @@ testStockPlanReservedShares_ReturnToPoolAllowsReuse = script do deletes = [] pure () +testStockPlanReservedShares_EquityCompensationReturnRequiresReduction = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EC_RETURN_REQUIRES_REDUCTION" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = equityCompensationIssuance "TX_EC_RETURN_NO_REDUCTION" "SEC-EC-RETURN-NO-REDUCTION" stakeholderId stockClassId stockPlanId 100.0 + let returnToPool = stockPlanReturnToPool "TX_RETURN_NO_REDUCTION" issuance.security_id stockPlanId 40.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + , CT.OcfCreateStockPlanReturnToPool returnToPool + ] + edits = [] + deletes = [] + testStockPlanReservedShares_StockReturnRequiresReturnedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 12738bf0..bfb1f328 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:419fb16000bd0390de5dec8cd594c7bab40504eeea6013dd2f1c17862ba9100f -size 2582480 +oid sha256:49a769b1ced708d97e80ec5fdcff42ac4f0c44bacef404dee1dbe8738e81ff6f +size 2612524 diff --git a/dars/dars.lock b/dars/dars.lock index ee74e6bc..cf848a9b 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,10 +33,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "419fb16000bd0390de5dec8cd594c7bab40504eeea6013dd2f1c17862ba9100f", - "size": 2582480, + "sha256": "49a769b1ced708d97e80ec5fdcff42ac4f0c44bacef404dee1dbe8738e81ff6f", + "size": 2612524, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T12:44:18.654Z", + "uploadedAt": "2026-07-09T13:40:08.453Z", "networks": [] } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 967a7ac6..53a58fbb 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -208,6 +208,14 @@ addStockClassAuthorizedAdjustmentById : addStockClassAuthorizedAdjustmentById key adjustment index = Map.insert key (adjustment :: fromOptional [] (Map.lookup key index)) index +addStockPlanPoolAdjustmentById : + Text -> + StockPlanPoolAdjustmentOcfData -> + Map Text [StockPlanPoolAdjustmentOcfData] -> + Map Text [StockPlanPoolAdjustmentOcfData] +addStockPlanPoolAdjustmentById key adjustment index = + Map.insert key (adjustment :: fromOptional [] (Map.lookup key index)) index + addStockClassShareLotByStockClass : StockClassShareLot -> Map Text [StockClassShareLot] -> @@ -625,6 +633,34 @@ stockPlanPoolAdjustmentIndex maps = Map.empty (Map.values maps.stock_plan_pool_adjustments) +stockPlanPoolAdjustmentHistoryIndex : CapTableMaps -> Update (Map Text [StockPlanPoolAdjustmentOcfData]) +stockPlanPoolAdjustmentHistoryIndex maps = + foldlA + (\index adjustmentCid -> do + adjustment <- fetch adjustmentCid + pure (addStockPlanPoolAdjustmentById adjustment.adjustment_data.stock_plan_id adjustment.adjustment_data index)) + Map.empty + (Map.values maps.stock_plan_pool_adjustments) + +stockPlanPoolAdjustmentAsOf : + Map Text [StockPlanPoolAdjustmentOcfData] -> + Text -> + Time -> + Optional StockPlanPoolAdjustmentOcfData +stockPlanPoolAdjustmentAsOf adjustmentHistory stockPlanId asOf = + foldl + (\latest candidate -> + if candidate.date <= asOf + then case latest of + Some current -> + if stockPlanPoolAdjustmentTakesPrecedence candidate current + then Some candidate + else latest + None -> Some candidate + else latest) + None + (fromOptional [] (Map.lookup stockPlanId adjustmentHistory)) + currentStockPlanReservedShares : Map Text StockPlanPoolAdjustmentOcfData -> StockPlanOcfData -> @@ -634,6 +670,16 @@ currentStockPlanReservedShares adjustmentIndex stockPlanData = Some adjustment -> adjustment.shares_reserved None -> stockPlanData.initial_shares_reserved +stockPlanReservedSharesAt : + Map Text [StockPlanPoolAdjustmentOcfData] -> + StockPlanOcfData -> + Time -> + Decimal +stockPlanReservedSharesAt adjustmentHistory stockPlanData asOf = + case stockPlanPoolAdjustmentAsOf adjustmentHistory stockPlanData.id asOf of + Some adjustment -> adjustment.shares_reserved + None -> stockPlanData.initial_shares_reserved + issuerAuthorizedLimitAt : Map Text [IssuerAuthorizedSharesAdjustmentOcfData] -> IssuerOcfData -> @@ -784,6 +830,18 @@ stockClassShareLotSplitAdjustedQuantity eventIndex lot = (Some 0.0) (sortOn stockClassShareEventSortKey (stockClassShareLotSplitAdjustedEvents eventIndex lot)) +stockClassShareLotSplitAdjustedQuantityAt : StockClassShareEventIndex -> StockClassShareLot -> Time -> Optional Decimal +stockClassShareLotSplitAdjustedQuantityAt eventIndex lot asOf = + if lot.event_date <= asOf + then + foldl + applyStockClassShareEventOptional + (Some 0.0) + (sortOn + stockClassShareEventSortKey + (filter (stockClassShareEventOnOrBefore asOf) (stockClassShareLotSplitAdjustedEvents eventIndex lot))) + else Some 0.0 + stockClassShareLotCurrentQuantity : StockClassShareEventIndex -> StockClassShareLot -> Optional Decimal stockClassShareLotCurrentQuantity eventIndex lot = foldl @@ -1102,6 +1160,63 @@ stockPlanReturnedQuantityBySecurityIndex maps = Map.empty (Map.values maps.stock_plan_return_to_pools) +equityCompensationSecurityEvents : CapTableMaps -> Text -> Update [StockClassShareEvent] +equityCompensationSecurityEvents maps securityId = do + 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) + else pure events) + [] + (Map.values maps.equity_compensation_cancellations) + + exerciseEvents <- foldlA + (\events exerciseCid -> do + exercise <- fetch exerciseCid + if exercise.exercise_data.security_id == securityId + then pure ((StockClassShareDelta with + event_date = exercise.exercise_data.date + event_priority = 1 + quantity_delta = 0.0 - exercise.exercise_data.quantity) :: events) + else pure events) + cancellationEvents + (Map.values maps.equity_compensation_exercises) + + releaseEvents <- foldlA + (\events releaseCid -> do + release <- fetch releaseCid + if release.release_data.security_id == securityId + then pure ((StockClassShareDelta with + event_date = release.release_data.settlement_date + event_priority = 1 + quantity_delta = 0.0 - release.release_data.quantity) :: events) + else pure events) + exerciseEvents + (Map.values maps.equity_compensation_releases) + + foldlA + (\events retractionCid -> do + retraction <- fetch retractionCid + if retraction.retraction_data.security_id == securityId + then pure ((StockClassShareRetraction with + event_date = retraction.retraction_data.date + event_priority = 3) :: events) + else pure events) + releaseEvents + (Map.values maps.equity_compensation_retractions) + +equityCompensationCurrentQuantity : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update (Optional Decimal) +equityCompensationCurrentQuantity maps issuanceData = do + events <- equityCompensationSecurityEvents maps issuanceData.security_id + pure (foldl + applyStockClassShareEventOptional + (Some issuanceData.quantity) + (sortOn stockClassShareEventSortKey events)) + stockPlanSecurityReturnableQuantity : StockClassShareEventIndex -> CapTableMaps -> StockPlanReturnToPoolOcfData -> Update Decimal stockPlanSecurityReturnableQuantity eventIndex maps returnData = do let stockCidOpt = Map.lookup returnData.security_id maps.stock_issuances_by_security_id @@ -1137,8 +1252,19 @@ stockPlanSecurityReturnableQuantity eventIndex maps returnData = do assertMsg ("Stock plan return to pool references security " <> returnData.security_id <> " from stock plan " <> stockPlanId <> " but returns to " <> returnData.stock_plan_id) (stockPlanId == returnData.stock_plan_id) - assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id) issuance.issuance_data.quantity - pure issuance.issuance_data.quantity + outstandingQuantityOpt <- equityCompensationCurrentQuantity maps issuance.issuance_data + case outstandingQuantityOpt of + Some outstandingQuantity -> do + assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id) issuance.issuance_data.quantity + assertNonNegativeQuantity ("Equity compensation outstanding quantity for " <> issuance.issuance_data.id) outstandingQuantity + let returnableQuantity = issuance.issuance_data.quantity - outstandingQuantity + assertMsg + ("Stock plan return-to-pool quantity basis is invalid for " <> returnData.security_id) + (returnableQuantity >= 0.0) + pure returnableQuantity + None -> do + assertMsg ("Stock plan return-to-pool security has invalid equity compensation quantity history: " <> returnData.security_id) False + pure 0.0 None -> do assertMsg ("Stock plan return to pool references non-plan equity compensation security: " <> returnData.security_id) False pure 0.0 @@ -1171,6 +1297,65 @@ stockPlanNetAllocatedQuantityIndex eventIndex maps = do let netAllocatedByPlan = mergeQuantityMaps combinedAllocation (negateQuantityMap returnedQuantity) pure netAllocatedByPlan +stockIssuancePlanAllocationIndexAt : StockClassShareEventIndex -> CapTableMaps -> Time -> Update (Map Text Decimal) +stockIssuancePlanAllocationIndexAt eventIndex maps asOf = + foldlA + (\totals issuanceCid -> do + issuance <- fetch issuanceCid + case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> + if issuance.issuance_data.date <= asOf + then do + let lot = stockIssuanceShareLot issuance.issuance_data + case stockClassShareLotSplitAdjustedQuantityAt eventIndex lot asOf of + Some allocationQuantity -> do + assertNonNegativeQuantity ("Stock issuance plan allocation quantity for " <> issuance.issuance_data.id <> " on " <> show asOf) allocationQuantity + pure (addQuantityById stockPlanId allocationQuantity totals) + None -> do + assertMsg ("Stock issuance plan allocation has an invalid quantity for " <> issuance.issuance_data.id <> " on " <> show asOf) False + pure totals + else pure totals + None -> pure totals) + Map.empty + (Map.values maps.stock_issuances) + +equityCompensationPlanAllocationIndexAt : CapTableMaps -> Time -> Update (Map Text Decimal) +equityCompensationPlanAllocationIndexAt maps asOf = + foldlA + (\totals issuanceCid -> do + issuance <- fetch issuanceCid + case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> + if issuance.issuance_data.date <= asOf + then do + assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id <> " on " <> show asOf) issuance.issuance_data.quantity + pure (addQuantityById stockPlanId issuance.issuance_data.quantity totals) + else pure totals + None -> pure totals) + Map.empty + (Map.values maps.equity_compensation_issuances) + +stockPlanReturnedQuantityIndexAt : CapTableMaps -> Time -> Update (Map Text Decimal) +stockPlanReturnedQuantityIndexAt maps asOf = + foldlA + (\totals returnCid -> do + returnToPool <- fetch returnCid + if returnToPool.return_data.date <= asOf + then do + assertNonNegativeQuantity ("Stock plan return-to-pool quantity for " <> returnToPool.return_data.id <> " on " <> show asOf) returnToPool.return_data.quantity + pure (addQuantityById returnToPool.return_data.stock_plan_id returnToPool.return_data.quantity totals) + else pure totals) + Map.empty + (Map.values maps.stock_plan_return_to_pools) + +stockPlanNetAllocatedQuantityIndexAt : StockClassShareEventIndex -> CapTableMaps -> Time -> Update (Map Text Decimal) +stockPlanNetAllocatedQuantityIndexAt eventIndex maps asOf = do + stockAllocation <- stockIssuancePlanAllocationIndexAt eventIndex maps asOf + equityCompensationAllocation <- equityCompensationPlanAllocationIndexAt maps asOf + returnedQuantity <- stockPlanReturnedQuantityIndexAt maps asOf + let combinedAllocation = mergeQuantityMaps stockAllocation equityCompensationAllocation + pure (mergeQuantityMaps combinedAllocation (negateQuantityMap returnedQuantity)) + validateStockPlanAllocationReferences : CapTableMaps -> Map Text Decimal -> Update () validateStockPlanAllocationReferences maps netAllocatedQuantityIndex = mapA_ @@ -1198,11 +1383,112 @@ validateStockPlanReservedShares eventIndex maps adjustmentIndex = do (allocatedQuantity <= reservedShares)) (Map.values maps.stock_plans) +stockPlanEffectiveDate : StockPlanOcfData -> Optional Time +stockPlanEffectiveDate stockPlanData = + earliestOptionalTime stockPlanData.board_approval_date stockPlanData.stockholder_approval_date + +stockPlanEffectiveOnOrBefore : Time -> StockPlanOcfData -> Bool +stockPlanEffectiveOnOrBefore asOf stockPlanData = + case stockPlanEffectiveDate stockPlanData of + Some effectiveDate -> effectiveDate <= asOf + None -> True + +addStockPlanDate : Text -> Time -> Map Text [Time] -> Map Text [Time] +addStockPlanDate = addDateById + +addStockClassSplitDateToStockPlans : StockClassSplitOcfData -> CapTableMaps -> Map Text [Time] -> Update (Map Text [Time]) +addStockClassSplitDateToStockPlans splitData maps index = + foldlA + (\dates stockPlanCid -> do + stockPlan <- fetch stockPlanCid + if splitData.stock_class_id `elem` stockPlan.plan_data.stock_class_ids + then pure (addStockPlanDate stockPlan.plan_data.id splitData.date dates) + else pure dates) + index + (Map.values maps.stock_plans) + +stockPlanValidationDateIndex : CapTableMaps -> Update (Map Text [Time]) +stockPlanValidationDateIndex maps = do + planDates <- foldlA + (\index stockPlanCid -> do + stockPlan <- fetch stockPlanCid + pure $ case stockPlanEffectiveDate stockPlan.plan_data of + Some effectiveDate -> addStockPlanDate stockPlan.plan_data.id effectiveDate index + None -> index) + Map.empty + (Map.values maps.stock_plans) + + adjustmentDates <- foldlA + (\index adjustmentCid -> do + adjustment <- fetch adjustmentCid + pure (addStockPlanDate adjustment.adjustment_data.stock_plan_id adjustment.adjustment_data.date index)) + planDates + (Map.values maps.stock_plan_pool_adjustments) + + stockIssuanceDates <- foldlA + (\index issuanceCid -> do + issuance <- fetch issuanceCid + pure $ case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> addStockPlanDate stockPlanId issuance.issuance_data.date index + None -> index) + adjustmentDates + (Map.values maps.stock_issuances) + + equityCompensationDates <- foldlA + (\index issuanceCid -> do + issuance <- fetch issuanceCid + pure $ case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> addStockPlanDate stockPlanId issuance.issuance_data.date index + None -> index) + stockIssuanceDates + (Map.values maps.equity_compensation_issuances) + + returnDates <- foldlA + (\index returnCid -> do + returnToPool <- fetch returnCid + pure (addStockPlanDate returnToPool.return_data.stock_plan_id returnToPool.return_data.date index)) + equityCompensationDates + (Map.values maps.stock_plan_return_to_pools) + + foldlA + (\index splitCid -> do + split <- fetch splitCid + addStockClassSplitDateToStockPlans split.split_data maps index) + returnDates + (Map.values maps.stock_class_splits) + +validateStockPlanReservedSharesAtDates : + StockClassShareEventIndex -> + CapTableMaps -> + Map Text [StockPlanPoolAdjustmentOcfData] -> + Update () +validateStockPlanReservedSharesAtDates eventIndex maps adjustmentHistory = do + validationDates <- stockPlanValidationDateIndex maps + mapA_ + (\stockPlanCid -> do + stockPlan <- fetch stockPlanCid + mapA_ + (\asOf -> do + netAllocatedQuantityIndex <- stockPlanNetAllocatedQuantityIndexAt eventIndex maps asOf + validateStockPlanAllocationReferences maps netAllocatedQuantityIndex + if stockPlanEffectiveOnOrBefore asOf stockPlan.plan_data + then do + let reservedShares = stockPlanReservedSharesAt adjustmentHistory stockPlan.plan_data asOf + let allocatedQuantity = fromOptional 0.0 (Map.lookup stockPlan.plan_data.id netAllocatedQuantityIndex) + assertMsg + ("Stock plan reserved shares exceeded for " <> stockPlan.plan_data.id <> " on " <> show asOf <> ": allocated " <> show allocatedQuantity <> " exceeds reserved " <> show reservedShares) + (allocatedQuantity <= reservedShares) + else pure ()) + (fromOptional [] (Map.lookup stockPlan.plan_data.id validationDates))) + (Map.values maps.stock_plans) + validateStockPlanPoolCeilings : StockClassShareEventIndex -> CapTableMaps -> Update () validateStockPlanPoolCeilings eventIndex maps = do adjustmentIndex <- stockPlanPoolAdjustmentIndex maps + adjustmentHistory <- stockPlanPoolAdjustmentHistoryIndex maps validateStockPlanReturnToPoolQuantities eventIndex maps validateStockPlanReservedShares eventIndex maps adjustmentIndex + validateStockPlanReservedSharesAtDates eventIndex maps adjustmentHistory validateAuthorizedShareCeilings : CapTableMaps -> From 87adf5867af386285393806a65080fc6f95198d1 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 10:38:42 -0400 Subject: [PATCH 25/62] Address stock plan review feedback --- Test/daml/OpenCapTable/TestHelpers.daml | 46 ++++++- .../TestStockClassAuthorizedShares.daml | 22 ---- .../TestStockPlanReservedShares.daml | 121 ++++++++++-------- .../codegen/templates/CapTable.daml.template | 71 ++++++---- 4 files changed, 156 insertions(+), 104 deletions(-) diff --git a/Test/daml/OpenCapTable/TestHelpers.daml b/Test/daml/OpenCapTable/TestHelpers.daml index 954832db..93e86707 100644 --- a/Test/daml/OpenCapTable/TestHelpers.daml +++ b/Test/daml/OpenCapTable/TestHelpers.daml @@ -4,13 +4,15 @@ import Fairmint.OpenCapTable.OCF.Issuer import Fairmint.OpenCapTable.OCF.Stakeholder import Fairmint.OpenCapTable.OCF.StockClass import Fairmint.OpenCapTable.OCF.StockClassAuthorizedSharesAdjustment +import Fairmint.OpenCapTable.OCF.StockClassSplit (StockClassSplitOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockCancellation (StockCancellationOcfData(..)) import Fairmint.OpenCapTable.OCF.StockIssuance (StockIssuanceOcfData(..)) import Fairmint.OpenCapTable.OCF.StockPlan (StockPlanOcfData(..)) import Fairmint.OpenCapTable.OCF.WarrantIssuance (WarrantIssuanceOcfData(..)) import Fairmint.OpenCapTable.OCF.ConvertibleIssuance (ConvertibleIssuanceOcfData(..), OcfConvertibleConversionTrigger(..)) import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance (EquityCompensationIssuanceOcfData(..)) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) -import Fairmint.OpenCapTable.Types.Stock (OcfStockClassType(..), OcfInitialSharesAuthorized(..)) +import Fairmint.OpenCapTable.Types.Stock (OcfStockClassType(..), OcfInitialSharesAuthorized(..), OcfRatio(..)) import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) import Fairmint.OpenCapTable.Types.Conversion (OcfConvertibleType(..), OcfConversionTriggerType(..), OcfConvertibleConversionRight(..), OcfConvertibleConversionMechanism(..), OcfSAFEConversionMechanism(..)) import qualified Fairmint.OpenCapTable.CapTable as CT @@ -88,6 +90,48 @@ stockClassAuthorizedSharesAdjustment adjustmentId stockClassId authorizedShares stockholder_approval_date = None comments = [] +stockCancellation : Text -> Text -> Decimal -> StockCancellationOcfData +stockCancellation cancellationId securityId quantity = StockCancellationOcfData with + id = cancellationId + date = defaultTestDate + 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 + id = splitId + date = defaultTestDate + stock_class_id = stockClassId + split_ratio = OcfRatio with numerator = numerator, denominator = denominator + comments = [] + +stockPlanEquityCompensationIssuance : Text -> Text -> Text -> Text -> Text -> Decimal -> EquityCompensationIssuanceOcfData +stockPlanEquityCompensationIssuance issuanceId securityId stakeholderId stockClassId stockPlanId quantity = EquityCompensationIssuanceOcfData with + id = issuanceId + date = defaultTestDate + security_id = securityId + custom_id = "EC-" <> issuanceId + stakeholder_id = stakeholderId + base_price = None + board_approval_date = None + consideration_text = None + early_exercisable = Some False + exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + expiration_date = None + stock_class_id = Some stockClassId + stock_plan_id = Some stockPlanId + stockholder_approval_date = None + vesting_terms_id = None + security_law_exemptions = [] + vestings = [] + termination_exercise_windows = [] + compensation_type = OcfCompensationTypeOptionNSO + quantity = quantity + comments = [] + -- | Returns valid StakeholderOcfData with sensible defaults. -- Provide ID and name; all other fields use reasonable test values. -- diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index f231cbae..24abb337 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -1,7 +1,5 @@ module OpenCapTable.TestStockClassAuthorizedShares where -import Fairmint.OpenCapTable.OCF.StockCancellation -import Fairmint.OpenCapTable.OCF.StockClassSplit import Fairmint.OpenCapTable.OCF.StockConversion import Fairmint.OpenCapTable.OCF.EquityCompensationExercise import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance @@ -24,17 +22,6 @@ import DA.Date (Month(..)) import qualified DA.Time as DT import Daml.Script -stockCancellation : Text -> Text -> Decimal -> StockCancellationOcfData -stockCancellation cancellationId securityId quantity = - StockCancellationOcfData with - id = cancellationId - date = defaultTestDate - security_id = securityId - quantity = quantity - balance_security_id = None - reason_text = "Cancelled shares" - comments = [] - stockRepurchase : Text -> Text -> Decimal -> StockRepurchaseOcfData stockRepurchase repurchaseId securityId quantity = StockRepurchaseOcfData with @@ -67,15 +54,6 @@ stockRetraction retractionId securityId = reason_text = "Retracted stock issuance" comments = [] -stockClassSplit : Text -> Text -> Decimal -> Decimal -> StockClassSplitOcfData -stockClassSplit splitId stockClassId numerator denominator = - StockClassSplitOcfData with - id = splitId - date = defaultTestDate - stock_class_id = stockClassId - split_ratio = OcfRatio with numerator = numerator, denominator = denominator - comments = [] - stockClassConversionRightTo : Text -> OcfStockClassConversionRight stockClassConversionRightTo stockClassId = OcfStockClassConversionRight with diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index a8df9c92..0227ee1f 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -2,14 +2,9 @@ module OpenCapTable.TestStockPlanReservedShares where import qualified Fairmint.OpenCapTable.CapTable as CT import Fairmint.OpenCapTable.OCF.EquityCompensationCancellation (EquityCompensationCancellationOcfData(..)) -import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance (EquityCompensationIssuanceOcfData(..)) -import Fairmint.OpenCapTable.OCF.StockCancellation (StockCancellationOcfData(..)) -import Fairmint.OpenCapTable.OCF.StockClassSplit (StockClassSplitOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockRetraction (StockRetractionOcfData(..)) import Fairmint.OpenCapTable.OCF.StockPlanPoolAdjustment (StockPlanPoolAdjustmentOcfData(..)) import Fairmint.OpenCapTable.OCF.StockPlanReturnToPool (StockPlanReturnToPoolOcfData(..)) -import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) -import Fairmint.OpenCapTable.Types.Stock (OcfRatio(..)) -import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) import OpenCapTable.Setup import OpenCapTable.TestHelpers import qualified DA.Date as DA @@ -37,48 +32,6 @@ stockPlanReturnToPool returnId securityId stockPlanId quantity = StockPlanReturn reason_text = "Returned to pool" comments = [] -stockCancellation : Text -> Text -> Decimal -> StockCancellationOcfData -stockCancellation cancellationId securityId quantity = StockCancellationOcfData with - id = cancellationId - date = defaultTestDate - 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 - id = splitId - date = defaultTestDate - stock_class_id = stockClassId - split_ratio = OcfRatio with numerator = numerator, denominator = denominator - comments = [] - -equityCompensationIssuance : Text -> Text -> Text -> Text -> Text -> Decimal -> EquityCompensationIssuanceOcfData -equityCompensationIssuance issuanceId securityId stakeholderId stockClassId stockPlanId quantity = EquityCompensationIssuanceOcfData with - id = issuanceId - date = defaultTestDate - security_id = securityId - custom_id = "EC-" <> issuanceId - stakeholder_id = stakeholderId - base_price = None - board_approval_date = None - consideration_text = None - early_exercisable = Some False - exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") - expiration_date = None - stock_class_id = Some stockClassId - stock_plan_id = Some stockPlanId - stockholder_approval_date = None - vesting_terms_id = None - security_law_exemptions = [] - vestings = [] - termination_exercise_windows = [] - compensation_type = OcfCompensationTypeOptionNSO - quantity = quantity - comments = [] - equityCompensationCancellation : Text -> Text -> Decimal -> EquityCompensationCancellationOcfData equityCompensationCancellation cancellationId securityId quantity = EquityCompensationCancellationOcfData with id = cancellationId @@ -136,7 +89,7 @@ testStockPlanReservedShares_EquityCompensationCannotExceedInitialPool = script d (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table let stockPlanId = "PLAN_EC_LIMIT" let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 - let issuance = equityCompensationIssuance "TX_EC_OVER_POOL" "SEC-EC-OVER-POOL" stakeholderId stockClassId stockPlanId 101.0 + let issuance = stockPlanEquityCompensationIssuance "TX_EC_OVER_POOL" "SEC-EC-OVER-POOL" stakeholderId stockClassId stockPlanId 101.0 submitMustFail issuer do exerciseCmd capTableReady CT.UpdateCapTable with @@ -155,8 +108,8 @@ testStockPlanReservedShares_NegativeEquityCompensationCannotOffsetOverallocatedP let stockIssuance = (defaultStockIssuance "TX_STOCK_NEGATIVE_OFFSET" "SEC-STOCK-NEGATIVE-OFFSET" stakeholderId stockClassId) with stock_plan_id = Some stockPlanId quantity = 100.0 - let positiveEquityCompensation = equityCompensationIssuance "TX_EC_POSITIVE_OFFSET" "SEC-EC-POSITIVE-OFFSET" stakeholderId stockClassId stockPlanId 50.0 - let negativeEquityCompensation = equityCompensationIssuance "TX_EC_NEGATIVE_OFFSET" "SEC-EC-NEGATIVE-OFFSET" stakeholderId stockClassId stockPlanId (-50.0) + let positiveEquityCompensation = stockPlanEquityCompensationIssuance "TX_EC_POSITIVE_OFFSET" "SEC-EC-POSITIVE-OFFSET" stakeholderId stockClassId stockPlanId 50.0 + let negativeEquityCompensation = stockPlanEquityCompensationIssuance "TX_EC_NEGATIVE_OFFSET" "SEC-EC-NEGATIVE-OFFSET" stakeholderId stockClassId stockPlanId (-50.0) submitMustFail issuer do exerciseCmd capTableReady CT.UpdateCapTable with @@ -260,10 +213,10 @@ testStockPlanReservedShares_ReturnToPoolAllowsReuse = script do (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table let stockPlanId = "PLAN_RETURN_REUSE" let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 - let firstIssuance = equityCompensationIssuance "TX_EC_ORIGINAL_POOL" "SEC-EC-ORIGINAL-POOL" stakeholderId stockClassId stockPlanId 100.0 + let firstIssuance = stockPlanEquityCompensationIssuance "TX_EC_ORIGINAL_POOL" "SEC-EC-ORIGINAL-POOL" stakeholderId stockClassId stockPlanId 100.0 let cancellation = equityCompensationCancellation "TX_EC_CANCEL_RETURN_TO_POOL" firstIssuance.security_id 40.0 let returnToPool = stockPlanReturnToPool "TX_RETURN_TO_POOL" firstIssuance.security_id stockPlanId 40.0 - let secondIssuance = equityCompensationIssuance "TX_EC_REUSED_POOL" "SEC-EC-REUSED-POOL" stakeholderId stockClassId stockPlanId 40.0 + let secondIssuance = stockPlanEquityCompensationIssuance "TX_EC_REUSED_POOL" "SEC-EC-REUSED-POOL" stakeholderId stockClassId stockPlanId 40.0 _ <- submit issuer do exerciseCmd capTableReady CT.UpdateCapTable with @@ -283,7 +236,7 @@ testStockPlanReservedShares_EquityCompensationReturnRequiresReduction = script d (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table let stockPlanId = "PLAN_EC_RETURN_REQUIRES_REDUCTION" let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 - let issuance = equityCompensationIssuance "TX_EC_RETURN_NO_REDUCTION" "SEC-EC-RETURN-NO-REDUCTION" stakeholderId stockClassId stockPlanId 100.0 + let issuance = stockPlanEquityCompensationIssuance "TX_EC_RETURN_NO_REDUCTION" "SEC-EC-RETURN-NO-REDUCTION" stakeholderId stockClassId stockPlanId 100.0 let returnToPool = stockPlanReturnToPool "TX_RETURN_NO_REDUCTION" issuance.security_id stockPlanId 40.0 submitMustFail issuer do @@ -343,12 +296,68 @@ testStockPlanReservedShares_StockCancellationReturnAllowsReuse = script do deletes = [] pure () +testStockPlanReservedShares_BackdatedReturnCannotUseFutureCancellation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let issuanceDate = DT.time (DA.date 2024 Jan 01) 0 0 0 + let returnDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let cancellationDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let stockPlanId = "PLAN_BACKDATED_RETURN" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = (defaultStockIssuance "TX_STOCK_BACKDATED_RETURN" "SEC-STOCK-BACKDATED-RETURN" stakeholderId stockClassId) with + date = issuanceDate + stock_plan_id = Some stockPlanId + quantity = 100.0 + let cancellation = (stockCancellation "TX_CANCEL_AFTER_RETURN" issuance.security_id 40.0) with date = cancellationDate + let returnToPool = (stockPlanReturnToPool "TX_BACKDATED_RETURN" issuance.security_id stockPlanId 40.0) with date = returnDate + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockPlanReturnToPool returnToPool + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_StockRetractionRemovesPlanAllocation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_STOCK_RETRACTION_REALLOCATE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let firstIssuance = (defaultStockIssuance "TX_STOCK_RETRACTED_POOL" "SEC-STOCK-RETRACTED-POOL" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + let retraction = StockRetractionOcfData with + id = "TX_RETRACT_STOCK_POOL" + date = defaultTestDate + security_id = firstIssuance.security_id + reason_text = "Retracted stock issuance" + comments = [] + let secondIssuance = (defaultStockIssuance "TX_STOCK_AFTER_RETRACTION" "SEC-STOCK-AFTER-RETRACTION" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance firstIssuance + , CT.OcfCreateStockRetraction retraction + , CT.OcfCreateStockIssuance secondIssuance + ] + edits = [] + deletes = [] + pure () + testStockPlanReservedShares_ReturnToPoolCannotExceedSecurityAllocation = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table let stockPlanId = "PLAN_RETURN_OVER" let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 - let issuance = equityCompensationIssuance "TX_EC_RETURN_OVER" "SEC-EC-RETURN-OVER" stakeholderId stockClassId stockPlanId 100.0 + let issuance = stockPlanEquityCompensationIssuance "TX_EC_RETURN_OVER" "SEC-EC-RETURN-OVER" stakeholderId stockClassId stockPlanId 100.0 let returnToPool = stockPlanReturnToPool "TX_RETURN_OVER" issuance.security_id stockPlanId 101.0 submitMustFail issuer do @@ -368,7 +377,7 @@ testStockPlanReservedShares_ReturnToPoolMustMatchSecurityPlan = script do let targetPlanId = "PLAN_RETURN_TARGET" let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 100.0 let targetPlan = (defaultStockPlan targetPlanId stockClassId) with initial_shares_reserved = 100.0 - let issuance = equityCompensationIssuance "TX_EC_RETURN_WRONG_PLAN" "SEC-EC-RETURN-WRONG-PLAN" stakeholderId stockClassId sourcePlanId 50.0 + let issuance = stockPlanEquityCompensationIssuance "TX_EC_RETURN_WRONG_PLAN" "SEC-EC-RETURN-WRONG-PLAN" stakeholderId stockClassId sourcePlanId 50.0 let returnToPool = stockPlanReturnToPool "TX_RETURN_WRONG_PLAN" issuance.security_id targetPlanId 50.0 submitMustFail issuer do diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 53a58fbb..21418594 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -811,9 +811,17 @@ stockClassShareLotEvents eventIndex lot = quantity_delta = lot.quantity) :: (securityEvents ++ splitEvents) -stockClassShareLotSplitAdjustedEvents : StockClassShareEventIndex -> StockClassShareLot -> [StockClassShareEvent] -stockClassShareLotSplitAdjustedEvents eventIndex lot = - let splitEvents = +stockClassShareLotPlanAllocationEvents : StockClassShareEventIndex -> StockClassShareLot -> [StockClassShareEvent] +stockClassShareLotPlanAllocationEvents eventIndex lot = + let retractionEvents = case lot.security_id of + Some securityId -> + filter + (\event -> case event of + StockClassShareRetraction _ _ -> True + _ -> False) + (fromOptional [] (Map.lookup securityId eventIndex.stock_security_events_by_security_id)) + None -> [] + splitEvents = filter (stockClassShareEventOnOrAfter lot.event_date) (fromOptional [] (Map.lookup lot.stock_class_id eventIndex.stock_class_split_events_by_stock_class_id)) @@ -821,17 +829,17 @@ stockClassShareLotSplitAdjustedEvents eventIndex lot = event_date = lot.event_date event_priority = 0 quantity_delta = lot.quantity) - :: splitEvents + :: (retractionEvents ++ splitEvents) -stockClassShareLotSplitAdjustedQuantity : StockClassShareEventIndex -> StockClassShareLot -> Optional Decimal -stockClassShareLotSplitAdjustedQuantity eventIndex lot = +stockClassShareLotPlanAllocatedQuantity : StockClassShareEventIndex -> StockClassShareLot -> Optional Decimal +stockClassShareLotPlanAllocatedQuantity eventIndex lot = foldl applyStockClassShareEventOptional (Some 0.0) - (sortOn stockClassShareEventSortKey (stockClassShareLotSplitAdjustedEvents eventIndex lot)) + (sortOn stockClassShareEventSortKey (stockClassShareLotPlanAllocationEvents eventIndex lot)) -stockClassShareLotSplitAdjustedQuantityAt : StockClassShareEventIndex -> StockClassShareLot -> Time -> Optional Decimal -stockClassShareLotSplitAdjustedQuantityAt eventIndex lot asOf = +stockClassShareLotPlanAllocatedQuantityAt : StockClassShareEventIndex -> StockClassShareLot -> Time -> Optional Decimal +stockClassShareLotPlanAllocatedQuantityAt eventIndex lot asOf = if lot.event_date <= asOf then foldl @@ -839,7 +847,7 @@ stockClassShareLotSplitAdjustedQuantityAt eventIndex lot asOf = (Some 0.0) (sortOn stockClassShareEventSortKey - (filter (stockClassShareEventOnOrBefore asOf) (stockClassShareLotSplitAdjustedEvents eventIndex lot))) + (filter (stockClassShareEventOnOrBefore asOf) (stockClassShareLotPlanAllocationEvents eventIndex lot))) else Some 0.0 stockClassShareLotCurrentQuantity : StockClassShareEventIndex -> StockClassShareLot -> Optional Decimal @@ -1116,7 +1124,7 @@ stockIssuancePlanAllocationIndex eventIndex maps = case issuance.issuance_data.stock_plan_id of Some stockPlanId -> do let lot = stockIssuanceShareLot issuance.issuance_data - case stockClassShareLotSplitAdjustedQuantity eventIndex lot of + case stockClassShareLotPlanAllocatedQuantity eventIndex lot of Some allocationQuantity -> do assertNonNegativeQuantity ("Stock issuance plan allocation quantity for " <> issuance.issuance_data.id) allocationQuantity pure (addQuantityById stockPlanId allocationQuantity totals) @@ -1150,14 +1158,17 @@ stockPlanReturnedQuantityIndex maps = Map.empty (Map.values maps.stock_plan_return_to_pools) -stockPlanReturnedQuantityBySecurityIndex : CapTableMaps -> Update (Map Text Decimal) -stockPlanReturnedQuantityBySecurityIndex maps = +stockPlanReturnedQuantityBySecurityAt : CapTableMaps -> Text -> Time -> Update Decimal +stockPlanReturnedQuantityBySecurityAt maps securityId asOf = foldlA - (\totals returnCid -> do + (\total returnCid -> do returnToPool <- fetch returnCid - assertNonNegativeQuantity ("Stock plan return-to-pool quantity for " <> returnToPool.return_data.id) returnToPool.return_data.quantity - pure (addQuantityById returnToPool.return_data.security_id returnToPool.return_data.quantity totals)) - Map.empty + if returnToPool.return_data.security_id == securityId && returnToPool.return_data.date <= asOf + then do + assertNonNegativeQuantity ("Stock plan return-to-pool quantity for " <> returnToPool.return_data.id <> " on " <> show asOf) returnToPool.return_data.quantity + pure (total + returnToPool.return_data.quantity) + else pure total) + 0.0 (Map.values maps.stock_plan_return_to_pools) equityCompensationSecurityEvents : CapTableMaps -> Text -> Update [StockClassShareEvent] @@ -1217,8 +1228,19 @@ equityCompensationCurrentQuantity maps issuanceData = do (Some issuanceData.quantity) (sortOn stockClassShareEventSortKey events)) -stockPlanSecurityReturnableQuantity : StockClassShareEventIndex -> CapTableMaps -> StockPlanReturnToPoolOcfData -> Update Decimal -stockPlanSecurityReturnableQuantity eventIndex maps returnData = do +equityCompensationQuantityAt : CapTableMaps -> EquityCompensationIssuanceOcfData -> Time -> Update (Optional Decimal) +equityCompensationQuantityAt maps issuanceData asOf = do + if issuanceData.date <= asOf + then do + events <- equityCompensationSecurityEvents maps issuanceData.security_id + pure (foldl + applyStockClassShareEventOptional + (Some issuanceData.quantity) + (sortOn stockClassShareEventSortKey (filter (stockClassShareEventOnOrBefore asOf) events))) + else pure (Some 0.0) + +stockPlanSecurityReturnableQuantityAt : StockClassShareEventIndex -> CapTableMaps -> StockPlanReturnToPoolOcfData -> Time -> Update Decimal +stockPlanSecurityReturnableQuantityAt eventIndex maps returnData asOf = do let stockCidOpt = Map.lookup returnData.security_id maps.stock_issuances_by_security_id let equityCompensationCidOpt = Map.lookup returnData.security_id maps.equity_compensation_issuances_by_security_id case (stockCidOpt, equityCompensationCidOpt) of @@ -1230,7 +1252,7 @@ stockPlanSecurityReturnableQuantity eventIndex maps returnData = do ("Stock plan return to pool references security " <> returnData.security_id <> " from stock plan " <> stockPlanId <> " but returns to " <> returnData.stock_plan_id) (stockPlanId == returnData.stock_plan_id) let lot = stockIssuanceShareLot issuance.issuance_data - case (stockClassShareLotSplitAdjustedQuantity eventIndex lot, stockClassShareLotCurrentQuantity eventIndex lot) of + case (stockClassShareLotPlanAllocatedQuantityAt eventIndex lot asOf, stockClassShareLotQuantityAt eventIndex lot asOf) of (Some allocationQuantity, Some outstandingQuantity) -> do assertNonNegativeQuantity ("Stock issuance plan allocation quantity for " <> issuance.issuance_data.id) allocationQuantity assertNonNegativeQuantity ("Stock issuance outstanding quantity for " <> issuance.issuance_data.id) outstandingQuantity @@ -1252,7 +1274,7 @@ stockPlanSecurityReturnableQuantity eventIndex maps returnData = do assertMsg ("Stock plan return to pool references security " <> returnData.security_id <> " from stock plan " <> stockPlanId <> " but returns to " <> returnData.stock_plan_id) (stockPlanId == returnData.stock_plan_id) - outstandingQuantityOpt <- equityCompensationCurrentQuantity maps issuance.issuance_data + outstandingQuantityOpt <- equityCompensationQuantityAt maps issuance.issuance_data asOf case outstandingQuantityOpt of Some outstandingQuantity -> do assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id) issuance.issuance_data.quantity @@ -1277,12 +1299,11 @@ stockPlanSecurityReturnableQuantity eventIndex maps returnData = do validateStockPlanReturnToPoolQuantities : StockClassShareEventIndex -> CapTableMaps -> Update () validateStockPlanReturnToPoolQuantities eventIndex maps = do - returnedQuantityBySecurity <- stockPlanReturnedQuantityBySecurityIndex maps mapA_ (\returnCid -> do returnToPool <- fetch returnCid - securityAllocation <- stockPlanSecurityReturnableQuantity eventIndex maps returnToPool.return_data - let returnedQuantity = fromOptional 0.0 (Map.lookup returnToPool.return_data.security_id returnedQuantityBySecurity) + securityAllocation <- stockPlanSecurityReturnableQuantityAt eventIndex maps returnToPool.return_data returnToPool.return_data.date + returnedQuantity <- stockPlanReturnedQuantityBySecurityAt maps returnToPool.return_data.security_id returnToPool.return_data.date assertMsg ("Stock plan return to pool exceeds returnable quantity for " <> returnToPool.return_data.security_id <> ": returned " <> show returnedQuantity <> " exceeds returnable " <> show securityAllocation) (returnedQuantity <= securityAllocation)) @@ -1307,7 +1328,7 @@ stockIssuancePlanAllocationIndexAt eventIndex maps asOf = if issuance.issuance_data.date <= asOf then do let lot = stockIssuanceShareLot issuance.issuance_data - case stockClassShareLotSplitAdjustedQuantityAt eventIndex lot asOf of + case stockClassShareLotPlanAllocatedQuantityAt eventIndex lot asOf of Some allocationQuantity -> do assertNonNegativeQuantity ("Stock issuance plan allocation quantity for " <> issuance.issuance_data.id <> " on " <> show asOf) allocationQuantity pure (addQuantityById stockPlanId allocationQuantity totals) From 536718268d07b4f1d7f49f67e947c75bd9e49294 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 10:57:51 -0400 Subject: [PATCH 26/62] Update share ceiling DAR backup --- dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index bfb1f328..3147ea5c 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49a769b1ced708d97e80ec5fdcff42ac4f0c44bacef404dee1dbe8738e81ff6f -size 2612524 +oid sha256:0f31afecdad46f3be55f1d964f67e06eef04a50e3699620d1a8d5c729efdd792 +size 2615237 diff --git a/dars/dars.lock b/dars/dars.lock index cf848a9b..727bceff 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "49a769b1ced708d97e80ec5fdcff42ac4f0c44bacef404dee1dbe8738e81ff6f", - "size": 2612524, + "sha256": "0f31afecdad46f3be55f1d964f67e06eef04a50e3699620d1a8d5c729efdd792", + "size": 2615237, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T13:40:08.453Z", "networks": [] From 77f038e685c50556333a2ae4405851d79920a7b4 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 11:19:30 -0400 Subject: [PATCH 27/62] Account for equity comp retractions in plan pools --- .../TestStockPlanReservedShares.daml | 27 +++++++++ .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 58 ++++++++++++++++++- 4 files changed, 87 insertions(+), 6 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index 0227ee1f..f5465991 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -2,6 +2,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.StockRetraction (StockRetractionOcfData(..)) import Fairmint.OpenCapTable.OCF.StockPlanPoolAdjustment (StockPlanPoolAdjustmentOcfData(..)) import Fairmint.OpenCapTable.OCF.StockPlanReturnToPool (StockPlanReturnToPoolOcfData(..)) @@ -352,6 +353,32 @@ testStockPlanReservedShares_StockRetractionRemovesPlanAllocation = script do deletes = [] pure () +testStockPlanReservedShares_EquityCompensationRetractionRemovesPlanAllocation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EC_RETRACTION_REALLOCATE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let firstIssuance = stockPlanEquityCompensationIssuance "TX_EC_RETRACTED_POOL" "SEC-EC-RETRACTED-POOL" stakeholderId stockClassId stockPlanId 100.0 + let retraction = EquityCompensationRetractionOcfData with + id = "TX_RETRACT_EC_POOL" + date = defaultTestDate + security_id = firstIssuance.security_id + reason_text = "Retracted equity compensation issuance" + comments = [] + let secondIssuance = stockPlanEquityCompensationIssuance "TX_EC_AFTER_RETRACTION" "SEC-EC-AFTER-RETRACTION" stakeholderId stockClassId stockPlanId 100.0 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance firstIssuance + , CT.OcfCreateEquityCompensationRetraction retraction + , CT.OcfCreateEquityCompensationIssuance secondIssuance + ] + edits = [] + deletes = [] + pure () + testStockPlanReservedShares_ReturnToPoolCannotExceedSecurityAllocation = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 3147ea5c..1b21ab75 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f31afecdad46f3be55f1d964f67e06eef04a50e3699620d1a8d5c729efdd792 -size 2615237 +oid sha256:24d48e08b66035ed373e3645cc4c8c63e1f83834abf51f3ceec76019d23c4731 +size 2620667 diff --git a/dars/dars.lock b/dars/dars.lock index 727bceff..e5cfd3fc 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "0f31afecdad46f3be55f1d964f67e06eef04a50e3699620d1a8d5c729efdd792", - "size": 2615237, + "sha256": "24d48e08b66035ed373e3645cc4c8c63e1f83834abf51f3ceec76019d23c4731", + "size": 2620667, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T13:40:08.453Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 21418594..ebca6c25 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -1135,6 +1135,46 @@ stockIssuancePlanAllocationIndex eventIndex maps = Map.empty (Map.values maps.stock_issuances) +equityCompensationPlanAllocationEvents : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update [StockClassShareEvent] +equityCompensationPlanAllocationEvents maps issuanceData = do + 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.equity_compensation_retractions) + + pure ((StockClassShareDelta with + event_date = issuanceData.date + event_priority = 0 + quantity_delta = issuanceData.quantity) + :: retractionEvents) + +equityCompensationPlanAllocatedQuantity : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update (Optional Decimal) +equityCompensationPlanAllocatedQuantity maps issuanceData = do + events <- equityCompensationPlanAllocationEvents maps issuanceData + pure (foldl + applyStockClassShareEventOptional + (Some 0.0) + (sortOn stockClassShareEventSortKey events)) + +equityCompensationPlanAllocatedQuantityAt : CapTableMaps -> EquityCompensationIssuanceOcfData -> Time -> Update (Optional Decimal) +equityCompensationPlanAllocatedQuantityAt maps issuanceData asOf = do + if issuanceData.date <= asOf + then do + events <- equityCompensationPlanAllocationEvents maps issuanceData + pure (foldl + applyStockClassShareEventOptional + (Some 0.0) + (sortOn + stockClassShareEventSortKey + (filter (stockClassShareEventOnOrBefore asOf) events))) + else pure (Some 0.0) + equityCompensationPlanAllocationIndex : CapTableMaps -> Update (Map Text Decimal) equityCompensationPlanAllocationIndex maps = foldlA @@ -1143,7 +1183,14 @@ equityCompensationPlanAllocationIndex maps = case issuance.issuance_data.stock_plan_id of Some stockPlanId -> do assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id) issuance.issuance_data.quantity - pure (addQuantityById stockPlanId issuance.issuance_data.quantity totals) + allocationQuantityOpt <- equityCompensationPlanAllocatedQuantity maps issuance.issuance_data + case allocationQuantityOpt of + Some allocationQuantity -> do + assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id) allocationQuantity + pure (addQuantityById stockPlanId allocationQuantity totals) + None -> do + assertMsg ("Equity compensation plan allocation has an invalid quantity history for " <> issuance.issuance_data.id) False + pure totals None -> pure totals) Map.empty (Map.values maps.equity_compensation_issuances) @@ -1350,7 +1397,14 @@ equityCompensationPlanAllocationIndexAt maps asOf = if issuance.issuance_data.date <= asOf then do assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id <> " on " <> show asOf) issuance.issuance_data.quantity - pure (addQuantityById stockPlanId issuance.issuance_data.quantity totals) + allocationQuantityOpt <- equityCompensationPlanAllocatedQuantityAt maps issuance.issuance_data asOf + case allocationQuantityOpt of + Some allocationQuantity -> do + assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id <> " on " <> show asOf) allocationQuantity + pure (addQuantityById stockPlanId allocationQuantity totals) + None -> do + assertMsg ("Equity compensation plan allocation has an invalid quantity history for " <> issuance.issuance_data.id <> " on " <> show asOf) False + pure totals else pure totals None -> pure totals) Map.empty From 03c37d100ae6076d9364437b5f0deee067afa73f Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 11:39:41 -0400 Subject: [PATCH 28/62] Tighten plan returns after equity retractions --- .../TestIssuerAuthorizedShares.daml | 21 ++++++++++++++++ .../TestStockPlanReservedShares.daml | 25 +++++++++++++++++++ .../0.0.5/OpenCapTable-v34.dar | 4 +-- dars/dars.lock | 4 +-- .../codegen/templates/CapTable.daml.template | 22 ++++++++-------- 5 files changed, 62 insertions(+), 14 deletions(-) diff --git a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml index d32ccae0..a019c589 100644 --- a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml @@ -74,6 +74,27 @@ testIssuerHistoricalTotalsExcludeFutureEffectiveStockClasses = script do deletes = [] pure () +testIssuerHistoricalTotalsWaitForBothStockClassApprovals = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + (capTableWithExistingClass, _) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_ISSUER_BOTH_APPROVALS_EXISTING" 100.0) + let jan1 = DT.time (DA.date 2024 Jan 01) 0 0 0 + let jan2 = DT.time (DA.date 2024 Jan 02) 0 0 0 + let pendingClass = (limitedStockClass "SC_ISSUER_BOTH_APPROVALS_PENDING" 100.0) with + board_approval_date = Some jan1 + stockholder_approval_date = Some jan2 + + _ <- submit issuer do + exerciseCmd capTableWithExistingClass CT.UpdateCapTable with + creates = + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment ((issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_AFTER_BOARD_APPROVAL" 150.0) with date = jan1) + , CT.OcfCreateStockClass pendingClass + , CT.OcfCreateIssuerAuthorizedSharesAdjustment ((issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_AFTER_STOCKHOLDER_APPROVAL" 200.0) with date = jan2) + ] + edits = [] + deletes = [] + pure () + testIssuerAuthorizedSharesAdjustmentCannotDropBelowStockClasses = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index f5465991..f0f22c05 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -379,6 +379,31 @@ testStockPlanReservedShares_EquityCompensationRetractionRemovesPlanAllocation = deletes = [] pure () +testStockPlanReservedShares_RetractedEquityCompensationCannotBeReturnedToPool = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EC_RETRACTION_RETURN" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = stockPlanEquityCompensationIssuance "TX_EC_RETRACTION_RETURN_GRANTED" "SEC-EC-RETRACTION-RETURN" stakeholderId stockClassId stockPlanId 100.0 + let retraction = EquityCompensationRetractionOcfData with + id = "TX_RETRACT_EC_RETURN" + date = defaultTestDate + security_id = issuance.security_id + reason_text = "Retracted equity compensation issuance" + comments = [] + let returnToPool = stockPlanReturnToPool "TX_RETURN_RETRACTED_EC" issuance.security_id stockPlanId 100.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + , CT.OcfCreateEquityCompensationRetraction retraction + , CT.OcfCreateStockPlanReturnToPool returnToPool + ] + edits = [] + deletes = [] + testStockPlanReservedShares_ReturnToPoolCannotExceedSecurityAllocation = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 1b21ab75..eb2844b2 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24d48e08b66035ed373e3645cc4c8c63e1f83834abf51f3ceec76019d23c4731 -size 2620667 +oid sha256:37aff51c4b661f9e1608a5286a5679b31eb63a4540f5a9b716db2b63399a3363 +size 2621158 diff --git a/dars/dars.lock b/dars/dars.lock index e5cfd3fc..ff96c29d 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "24d48e08b66035ed373e3645cc4c8c63e1f83834abf51f3ceec76019d23c4731", - "size": 2620667, + "sha256": "37aff51c4b661f9e1608a5286a5679b31eb63a4540f5a9b716db2b63399a3363", + "size": 2621158, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T13:40:08.453Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index ebca6c25..726acb39 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -984,17 +984,17 @@ addStockClassAuthorizedContribution totalOpt ceiling = case (totalOpt, ceiling) (_, AuthorizedSharesUnlimited) -> None (None, _) -> None -earliestOptionalTime : Optional Time -> Optional Time -> Optional Time -earliestOptionalTime leftOpt rightOpt = +latestOptionalTime : Optional Time -> Optional Time -> Optional Time +latestOptionalTime leftOpt rightOpt = case (leftOpt, rightOpt) of - (Some left, Some right) -> Some (if left <= right then left else right) + (Some left, Some right) -> Some (if left >= right then left else right) (Some left, None) -> Some left (None, Some right) -> Some right (None, None) -> None stockClassEffectiveDate : StockClassOcfData -> Optional Time stockClassEffectiveDate stockClassData = - earliestOptionalTime stockClassData.board_approval_date stockClassData.stockholder_approval_date + latestOptionalTime stockClassData.board_approval_date stockClassData.stockholder_approval_date stockClassEffectiveOnOrBefore : Time -> StockClassOcfData -> Bool stockClassEffectiveOnOrBefore asOf stockClassData = @@ -1321,17 +1321,18 @@ stockPlanSecurityReturnableQuantityAt eventIndex maps returnData asOf = do assertMsg ("Stock plan return to pool references security " <> returnData.security_id <> " from stock plan " <> stockPlanId <> " but returns to " <> returnData.stock_plan_id) (stockPlanId == returnData.stock_plan_id) + allocationQuantityOpt <- equityCompensationPlanAllocatedQuantityAt maps issuance.issuance_data asOf outstandingQuantityOpt <- equityCompensationQuantityAt maps issuance.issuance_data asOf - case outstandingQuantityOpt of - Some outstandingQuantity -> do - assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id) issuance.issuance_data.quantity + case (allocationQuantityOpt, outstandingQuantityOpt) of + (Some allocationQuantity, Some outstandingQuantity) -> do + assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id) allocationQuantity assertNonNegativeQuantity ("Equity compensation outstanding quantity for " <> issuance.issuance_data.id) outstandingQuantity - let returnableQuantity = issuance.issuance_data.quantity - outstandingQuantity + let returnableQuantity = allocationQuantity - outstandingQuantity assertMsg ("Stock plan return-to-pool quantity basis is invalid for " <> returnData.security_id) (returnableQuantity >= 0.0) pure returnableQuantity - None -> do + _ -> do assertMsg ("Stock plan return-to-pool security has invalid equity compensation quantity history: " <> returnData.security_id) False pure 0.0 None -> do @@ -1453,6 +1454,7 @@ validateStockPlanReservedShares eventIndex maps adjustmentIndex = do stockPlan <- fetch stockPlanCid let reservedShares = currentStockPlanReservedShares adjustmentIndex stockPlan.plan_data let allocatedQuantity = fromOptional 0.0 (Map.lookup stockPlan.plan_data.id netAllocatedQuantityIndex) + assertNonNegativeQuantity ("Stock plan net allocated quantity for " <> stockPlan.plan_data.id) allocatedQuantity assertMsg ("Stock plan reserved shares exceeded for " <> stockPlan.plan_data.id <> ": allocated " <> show allocatedQuantity <> " exceeds reserved " <> show reservedShares) (allocatedQuantity <= reservedShares)) @@ -1460,7 +1462,7 @@ validateStockPlanReservedShares eventIndex maps adjustmentIndex = do stockPlanEffectiveDate : StockPlanOcfData -> Optional Time stockPlanEffectiveDate stockPlanData = - earliestOptionalTime stockPlanData.board_approval_date stockPlanData.stockholder_approval_date + latestOptionalTime stockPlanData.board_approval_date stockPlanData.stockholder_approval_date stockPlanEffectiveOnOrBefore : Time -> StockPlanOcfData -> Bool stockPlanEffectiveOnOrBefore asOf stockPlanData = From 5d1d05abdac79e0bc327b171b1100b9783c7b268 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Thu, 9 Jul 2026 13:42:08 -0400 Subject: [PATCH 29/62] Address authorized share ceiling review feedback --- .../TestStockClassAuthorizedShares.daml | 127 +++++++++ .../TestStockPlanReservedShares.daml | 70 +++++ .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- .../codegen/templates/CapTable.daml.template | 242 ++++++++++++++++-- 5 files changed, 424 insertions(+), 25 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 24abb337..9be83940 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -1,11 +1,14 @@ module OpenCapTable.TestStockClassAuthorizedShares where import Fairmint.OpenCapTable.OCF.StockConversion +import Fairmint.OpenCapTable.OCF.StockConsolidation 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.StockReissuance +import Fairmint.OpenCapTable.OCF.StockTransfer import Fairmint.OpenCapTable.OCF.StockClassConversionRatioAdjustment import Fairmint.OpenCapTable.OCF.WarrantExercise import Fairmint.OpenCapTable.OCF.WarrantIssuance @@ -54,6 +57,39 @@ stockRetraction retractionId securityId = reason_text = "Retracted stock issuance" comments = [] +stockTransfer : Text -> Text -> Decimal -> [Text] -> StockTransferOcfData +stockTransfer transferId securityId quantity resultingSecurityIds = + StockTransferOcfData with + id = transferId + date = defaultTestDate + security_id = securityId + quantity = quantity + resulting_security_ids = resultingSecurityIds + balance_security_id = None + consideration_text = None + comments = [] + +stockReissuance : Text -> Text -> [Text] -> StockReissuanceOcfData +stockReissuance reissuanceId securityId resultingSecurityIds = + StockReissuanceOcfData with + id = reissuanceId + date = defaultTestDate + security_id = securityId + resulting_security_ids = resultingSecurityIds + reason_text = Some "Reissued stock certificate" + split_transaction_id = None + comments = [] + +stockConsolidation : Text -> [Text] -> Text -> StockConsolidationOcfData +stockConsolidation consolidationId securityIds resultingSecurityId = + StockConsolidationOcfData with + id = consolidationId + date = defaultTestDate + security_ids = securityIds + resulting_security_id = resultingSecurityId + reason_text = Some "Consolidated stock certificates" + comments = [] + stockClassConversionRightTo : Text -> OcfStockClassConversionRight stockClassConversionRightTo stockClassId = OcfStockClassConversionRight with @@ -545,6 +581,58 @@ testStockRepurchaseReducesIssuedSharesForCeiling = script do _ <- createStockIssuanceWith issuer result.updatedCapTableCid ((defaultStockIssuance "TX_REPURCHASE_REISSUE" "SEC_REPURCHASE_REISSUE" stakeholderId stockClassId) with quantity = 50.0) pure () +testStockTransferWithResultingIssuanceDoesNotDoubleCountForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_TRANSFER_RESULTING" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_TRANSFER_RESULTING" "Transfer Resulting Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_TRANSFER_SOURCE" "SEC_TRANSFER_SOURCE" stakeholderId stockClassId) with quantity = 100.0) + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (stockTransfer "TX_TRANSFER_RESULTING" "SEC_TRANSFER_SOURCE" 100.0 ["SEC_TRANSFER_RESULT"]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_TRANSFER_RESULT" "SEC_TRANSFER_RESULT" stakeholderId stockClassId) with quantity = 100.0) + ] + edits = [] + deletes = [] + pure () + +testStockReissuanceWithResultingIssuanceDoesNotDoubleCountForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REISSUANCE_RESULTING" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_REISSUANCE_RESULTING" "Reissuance Resulting Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_REISSUANCE_SOURCE" "SEC_REISSUANCE_SOURCE" stakeholderId stockClassId) with quantity = 100.0) + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance (stockReissuance "TX_REISSUANCE_RESULTING" "SEC_REISSUANCE_SOURCE" ["SEC_REISSUANCE_RESULT"]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_REISSUANCE_RESULT" "SEC_REISSUANCE_RESULT" stakeholderId stockClassId) with quantity = 100.0) + ] + edits = [] + deletes = [] + pure () + +testStockConsolidationWithResultingIssuanceDoesNotDoubleCountForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONSOLIDATION_RESULTING" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONSOLIDATION_RESULTING" "Consolidation Resulting Holder" + capTableOne <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONSOLIDATION_SOURCE_A" "SEC_CONSOLIDATION_SOURCE_A" stakeholderId stockClassId) with quantity = 50.0) + capTableTwo <- createStockIssuanceWith issuer capTableOne ((defaultStockIssuance "TX_CONSOLIDATION_SOURCE_B" "SEC_CONSOLIDATION_SOURCE_B" stakeholderId stockClassId) with quantity = 50.0) + + _ <- submit issuer do + exerciseCmd capTableTwo CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation (stockConsolidation "TX_CONSOLIDATION_RESULTING" ["SEC_CONSOLIDATION_SOURCE_A", "SEC_CONSOLIDATION_SOURCE_B"] "SEC_CONSOLIDATION_RESULT") + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CONSOLIDATION_RESULT" "SEC_CONSOLIDATION_RESULT" stakeholderId stockClassId) with quantity = 100.0) + ] + edits = [] + deletes = [] + pure () + testStockConversionReducesIssuedSharesForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -581,6 +669,28 @@ testStockConversionCreditsDestinationClassForCeiling = script do edits = [] deletes = [] +testStockConversionWithPartialResultingIssuancesFailsForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_PARTIAL_COMMON" 100.0) + let preferredClassId = "SC_CONVERSION_PARTIAL_SOURCE" + let preferredClass = (limitedStockClass preferredClassId 200.0) with + conversion_rights = [stockClassConversionRightTo commonClassId] + (capTableWithPreferred, _) <- setupStockClassWith issuer capTableWithCommon preferredClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_PARTIAL" "Conversion Partial Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_PARTIAL_SOURCE" "SEC_CONVERSION_PARTIAL_SOURCE" stakeholderId preferredClassId) with quantity = 75.0) + let conversion = (stockConversion "TX_CONVERSION_PARTIAL" "SEC_CONVERSION_PARTIAL_SOURCE" 75.0) with + resulting_security_ids = ["SEC_CONVERSION_PARTIAL_RESULT", "SEC_CONVERSION_PARTIAL_MISSING"] + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConversion conversion + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CONVERSION_PARTIAL_RESULT" "SEC_CONVERSION_PARTIAL_RESULT" stakeholderId commonClassId) with quantity = 75.0) + ] + edits = [] + deletes = [] + testStockConversionCannotReduceSecurityBelowZero = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -782,6 +892,23 @@ testEquityCompensationExerciseCannotExceedStockClassAuthorizedShares = script do edits = [] deletes = [] +testNegativeEquityCompensationExerciseCannotOffsetIssuedSharesForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_COMP_NEGATIVE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_COMP_NEGATIVE" "Equity Comp Negative Holder" + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_EQUITY_COMP_NEGATIVE_GRANTED" "ECSEC_NEGATIVE" stakeholderId stockClassId 100.0)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise (equityCompensationExercise "TX_EQUITY_COMP_NEGATIVE_EXERCISE" "ECSEC_NEGATIVE" (-50.0))] + edits = [] + deletes = [] + testEquityCompensationExerciseWithResultingStockIssuanceDoesNotDoubleCount = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index f0f22c05..a04efc70 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -143,6 +143,76 @@ testStockPlanReservedShares_StockSplitCountsAgainstPlanPool = script do edits = [] deletes = [] +testStockPlanReservedShares_EquityCompensationStockSplitCountsAgainstPlanPool = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EC_SPLIT_POOL" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = stockPlanEquityCompensationIssuance "TX_EC_SPLIT_POOL" "SEC-EC-SPLIT-POOL" stakeholderId stockClassId stockPlanId 100.0 + let split = stockClassSplit "TX_EC_SPLIT_POOL" stockClassId 2.0 1.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + , CT.OcfCreateStockClassSplit split + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ReturnBeforeSplitAdjustsWithPlanPool = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_RETURN_BEFORE_SPLIT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let firstIssuance = (defaultStockIssuance "TX_RETURN_SPLIT_ORIGINAL" "SEC-RETURN-SPLIT-ORIGINAL" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + let cancellation = stockCancellation "TX_RETURN_SPLIT_CANCEL" firstIssuance.security_id 100.0 + let returnToPool = stockPlanReturnToPool "TX_RETURN_SPLIT_POOL" firstIssuance.security_id stockPlanId 100.0 + let split = (stockClassSplit "TX_RETURN_SPLIT" stockClassId 2.0 1.0) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + let replacementIssuance = (defaultStockIssuance "TX_RETURN_SPLIT_REPLACEMENT" "SEC-RETURN-SPLIT-REPLACEMENT" stakeholderId stockClassId) with + date = DT.time (DA.date 2024 Jan 03) 0 0 0 + stock_plan_id = Some stockPlanId + quantity = 100.0 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance firstIssuance + , CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockPlanReturnToPool returnToPool + , CT.OcfCreateStockClassSplit split + , CT.OcfCreateStockIssuance replacementIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_StockIssuanceBeforePlanEffectiveFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_FUTURE_EFFECTIVE" + let plan = (defaultStockPlan stockPlanId stockClassId) with + initial_shares_reserved = 100.0 + board_approval_date = Some (DT.time (DA.date 2024 Jan 02) 0 0 0) + let issuance = (defaultStockIssuance "TX_BEFORE_PLAN_EFFECTIVE" "SEC-BEFORE-PLAN-EFFECTIVE" stakeholderId stockClassId) with + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + stock_plan_id = Some stockPlanId + quantity = 10.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + testStockPlanReservedShares_AdjustmentAllowsAdditionalIssuanceSameBatch = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index eb2844b2..63aa7528 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37aff51c4b661f9e1608a5286a5679b31eb63a4540f5a9b716db2b63399a3363 -size 2621158 +oid sha256:5b72a2f0774ddbfd2e8ca9ef12b28b799542ba7911f4f24878e27b28de536c78 +size 2643310 diff --git a/dars/dars.lock b/dars/dars.lock index ff96c29d..49debf2c 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,10 +33,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "37aff51c4b661f9e1608a5286a5679b31eb63a4540f5a9b716db2b63399a3363", - "size": 2621158, + "sha256": "5b72a2f0774ddbfd2e8ca9ef12b28b799542ba7911f4f24878e27b28de536c78", + "size": 2643310, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T13:40:08.453Z", + "uploadedAt": "2026-07-09T17:41:13Z", "networks": [] } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 726acb39..fd47e7f8 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -223,9 +223,25 @@ addStockClassShareLotByStockClass : addStockClassShareLotByStockClass lot index = Map.insert lot.stock_class_id (lot :: fromOptional [] (Map.lookup lot.stock_class_id index)) index -resultingStockIssuanceExists : CapTableMaps -> [Text] -> Bool -resultingStockIssuanceExists maps securityIds = - any (\securityId -> isSome (Map.lookup securityId maps.stock_issuances_by_security_id)) securityIds +allResultingStockIssuancesExist : CapTableMaps -> [Text] -> Bool +allResultingStockIssuancesExist maps securityIds = + not (null securityIds) && all (\securityId -> isSome (Map.lookup securityId maps.stock_issuances_by_security_id)) securityIds + +noResultingStockIssuancesExist : CapTableMaps -> [Text] -> Bool +noResultingStockIssuancesExist maps securityIds = + all (\securityId -> isNone (Map.lookup securityId maps.stock_issuances_by_security_id)) securityIds + +assertNoPartialResultingStockIssuanceCoverage : Text -> CapTableMaps -> [Text] -> Update () +assertNoPartialResultingStockIssuanceCoverage label maps securityIds = + let hasPartialCoverage = + not (null securityIds) + && not (allResultingStockIssuancesExist maps securityIds) + && not (noResultingStockIssuancesExist maps securityIds) + in assertMsg (label <> " has only some resulting stock issuances recorded") (not hasPartialCoverage) + +assertAllResultingStockIssuancesExist : Text -> CapTableMaps -> [Text] -> Update () +assertAllResultingStockIssuancesExist label maps securityIds = + assertMsg (label <> " is missing a resulting stock issuance") (allResultingStockIssuancesExist maps securityIds) stockIssuanceShareLot : StockIssuanceOcfData -> StockClassShareLot stockIssuanceShareLot issuanceData = @@ -383,16 +399,17 @@ warrantExerciseOutputShareLot issuanceData exerciseData = equityCompensationExerciseOutputShareLot : EquityCompensationIssuanceOcfData -> EquityCompensationExerciseOcfData -> - Optional StockClassShareLot + Update (Optional StockClassShareLot) equityCompensationExerciseOutputShareLot issuanceData exerciseData = case issuanceData.stock_class_id of - Some stockClassId -> - Some (StockClassShareLot with - stock_class_id = stockClassId - security_id = None - event_date = exerciseData.date - quantity = exerciseData.quantity) - None -> None + Some stockClassId -> do + assertNonNegativeQuantity ("Equity compensation exercise output quantity for " <> exerciseData.id) exerciseData.quantity + pure (Some (StockClassShareLot with + stock_class_id = stockClassId + security_id = None + event_date = exerciseData.date + quantity = exerciseData.quantity)) + None -> pure None equityCompensationReleaseOutputShareLot : EquityCompensationIssuanceOcfData -> @@ -432,7 +449,8 @@ stockClassShareLotsByStockClass maps stockClassIndex = do stockConversionLots <- foldlA (\index conversionCid -> do conversion <- fetch conversionCid - if resultingStockIssuanceExists maps conversion.conversion_data.resulting_security_ids + assertNoPartialResultingStockIssuanceCoverage ("Stock conversion " <> conversion.conversion_data.id) maps conversion.conversion_data.resulting_security_ids + if allResultingStockIssuancesExist maps conversion.conversion_data.resulting_security_ids then pure index else case Map.lookup conversion.conversion_data.security_id maps.stock_issuances_by_security_id of Some sourceIssuanceCid -> do @@ -446,7 +464,8 @@ stockClassShareLotsByStockClass maps stockClassIndex = do warrantExerciseLots <- foldlA (\index exerciseCid -> do exercise <- fetch exerciseCid - if resultingStockIssuanceExists maps exercise.exercise_data.resulting_security_ids + assertNoPartialResultingStockIssuanceCoverage ("Warrant exercise " <> exercise.exercise_data.id) maps exercise.exercise_data.resulting_security_ids + if allResultingStockIssuancesExist maps exercise.exercise_data.resulting_security_ids then pure index else case Map.lookup exercise.exercise_data.security_id maps.warrant_issuances_by_security_id of Some issuanceCid -> do @@ -460,12 +479,14 @@ stockClassShareLotsByStockClass maps stockClassIndex = do equityExerciseLots <- foldlA (\index exerciseCid -> do exercise <- fetch exerciseCid - if resultingStockIssuanceExists maps exercise.exercise_data.resulting_security_ids + assertNoPartialResultingStockIssuanceCoverage ("Equity compensation exercise " <> exercise.exercise_data.id) maps exercise.exercise_data.resulting_security_ids + if allResultingStockIssuancesExist maps exercise.exercise_data.resulting_security_ids then pure index else case Map.lookup exercise.exercise_data.security_id maps.equity_compensation_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid - pure (addOptionalStockClassShareLot (equityCompensationExerciseOutputShareLot issuance.issuance_data exercise.exercise_data) index) + lotOpt <- equityCompensationExerciseOutputShareLot issuance.issuance_data exercise.exercise_data + pure (addOptionalStockClassShareLot lotOpt index) None -> pure index) warrantExerciseLots (Map.values maps.equity_compensation_exercises) @@ -473,7 +494,8 @@ stockClassShareLotsByStockClass maps stockClassIndex = do foldlA (\index releaseCid -> do release <- fetch releaseCid - if resultingStockIssuanceExists maps release.release_data.resulting_security_ids + assertNoPartialResultingStockIssuanceCoverage ("Equity compensation release " <> release.release_data.id) maps release.release_data.resulting_security_ids + if allResultingStockIssuancesExist maps release.release_data.resulting_security_ids then pure index else case Map.lookup release.release_data.security_id maps.equity_compensation_issuances_by_security_id of Some issuanceCid -> do @@ -726,6 +748,14 @@ 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 + +addStockClassShareEventForKeys : [Text] -> StockClassShareEvent -> Map Text [StockClassShareEvent] -> Map Text [StockClassShareEvent] +addStockClassShareEventForKeys keys event index = + foldl (\updatedIndex key -> addStockClassShareEvent key event updatedIndex) index keys + stockClassShareEventIndex : CapTableMaps -> Update StockClassShareEventIndex stockClassShareEventIndex maps = do cancellationEventsBySecurityId <- foldlA @@ -767,6 +797,49 @@ stockClassShareEventIndex maps = do repurchaseEventsBySecurityId (Map.values maps.stock_conversions) + transferEventsBySecurityId <- foldlA + (\index 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 + (Map.values maps.stock_transfers) + + reissuanceEventsBySecurityId <- foldlA + (\index reissuanceCid -> do + reissuance <- fetch reissuanceCid + if allResultingStockIssuancesExist maps reissuance.reissuance_data.resulting_security_ids + then pure (addStockClassShareEvent + reissuance.reissuance_data.security_id + (StockClassShareRetraction with + event_date = reissuance.reissuance_data.date + event_priority = 3) + index) + else pure index) + transferEventsBySecurityId + (Map.values maps.stock_reissuances) + + consolidationEventsBySecurityId <- foldlA + (\index 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 + event_date = consolidation.consolidation_data.date + event_priority = 3) + index) + else pure index) + reissuanceEventsBySecurityId + (Map.values maps.stock_consolidations) + retractionEventsBySecurityId <- foldlA (\index retractionCid -> do retraction <- fetch retractionCid @@ -776,7 +849,7 @@ stockClassShareEventIndex maps = do event_date = retraction.retraction_data.date event_priority = 3) index)) - conversionEventsBySecurityId + consolidationEventsBySecurityId (Map.values maps.stock_retractions) splitEventsByStockClassId <- foldlA @@ -1148,11 +1221,26 @@ equityCompensationPlanAllocationEvents maps issuanceData = do [] (Map.values maps.equity_compensation_retractions) + 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 ((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) + None -> pure [] + pure ((StockClassShareDelta with event_date = issuanceData.date event_priority = 0 quantity_delta = issuanceData.quantity) - :: retractionEvents) + :: (retractionEvents ++ splitEvents)) equityCompensationPlanAllocatedQuantity : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update (Optional Decimal) equityCompensationPlanAllocatedQuantity maps issuanceData = do @@ -1195,13 +1283,78 @@ equityCompensationPlanAllocationIndex maps = Map.empty (Map.values maps.equity_compensation_issuances) +stockPlanReturnToPoolStockClassId : CapTableMaps -> Text -> Update (Optional Text) +stockPlanReturnToPoolStockClassId maps securityId = + case (Map.lookup securityId maps.stock_issuances_by_security_id, Map.lookup securityId maps.equity_compensation_issuances_by_security_id) of + (Some stockCid, None) -> do + issuance <- fetch stockCid + pure (Some issuance.issuance_data.stock_class_id) + (None, Some equityCompensationCid) -> do + issuance <- fetch equityCompensationCid + pure issuance.issuance_data.stock_class_id + (Some _, Some _) -> do + assertMsg ("Stock plan return-to-pool security id is ambiguous: " <> securityId) False + pure None + (None, None) -> pure None + +stockPlanReturnToPoolEvents : CapTableMaps -> StockPlanReturnToPoolOcfData -> Update [StockClassShareEvent] +stockPlanReturnToPoolEvents maps returnData = do + stockClassIdOpt <- stockPlanReturnToPoolStockClassId maps returnData.security_id + splitEvents <- case stockClassIdOpt of + Some stockClassId -> + foldlA + (\events splitCid -> do + split <- fetch splitCid + if split.split_data.stock_class_id == stockClassId && split.split_data.date >= returnData.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) + None -> pure [] + + pure ((StockClassShareDelta with + event_date = returnData.date + event_priority = 0 + quantity_delta = returnData.quantity) :: splitEvents) + +stockPlanReturnToPoolQuantity : CapTableMaps -> StockPlanReturnToPoolOcfData -> Update (Optional Decimal) +stockPlanReturnToPoolQuantity maps returnData = do + events <- stockPlanReturnToPoolEvents maps returnData + pure (foldl + applyStockClassShareEventOptional + (Some 0.0) + (sortOn stockClassShareEventSortKey events)) + +stockPlanReturnToPoolQuantityAt : CapTableMaps -> StockPlanReturnToPoolOcfData -> Time -> Update (Optional Decimal) +stockPlanReturnToPoolQuantityAt maps returnData asOf = do + if returnData.date <= asOf + then do + events <- stockPlanReturnToPoolEvents maps returnData + pure (foldl + applyStockClassShareEventOptional + (Some 0.0) + (sortOn + stockClassShareEventSortKey + (filter (stockClassShareEventOnOrBefore asOf) events))) + else pure (Some 0.0) + stockPlanReturnedQuantityIndex : CapTableMaps -> Update (Map Text Decimal) stockPlanReturnedQuantityIndex maps = foldlA (\totals returnCid -> do returnToPool <- fetch returnCid assertNonNegativeQuantity ("Stock plan return-to-pool quantity for " <> returnToPool.return_data.id) returnToPool.return_data.quantity - pure (addQuantityById returnToPool.return_data.stock_plan_id returnToPool.return_data.quantity totals)) + returnedQuantityOpt <- stockPlanReturnToPoolQuantity maps returnToPool.return_data + case returnedQuantityOpt of + Some returnedQuantity -> do + assertNonNegativeQuantity ("Stock plan return-to-pool quantity for " <> returnToPool.return_data.id) returnedQuantity + pure (addQuantityById returnToPool.return_data.stock_plan_id returnedQuantity totals) + None -> do + assertMsg ("Stock plan return-to-pool has an invalid quantity history for " <> returnToPool.return_data.id) False + pure totals) Map.empty (Map.values maps.stock_plan_return_to_pools) @@ -1419,7 +1572,14 @@ stockPlanReturnedQuantityIndexAt maps asOf = if returnToPool.return_data.date <= asOf then do assertNonNegativeQuantity ("Stock plan return-to-pool quantity for " <> returnToPool.return_data.id <> " on " <> show asOf) returnToPool.return_data.quantity - pure (addQuantityById returnToPool.return_data.stock_plan_id returnToPool.return_data.quantity totals) + returnedQuantityOpt <- stockPlanReturnToPoolQuantityAt maps returnToPool.return_data asOf + case returnedQuantityOpt of + Some returnedQuantity -> do + assertNonNegativeQuantity ("Stock plan return-to-pool quantity for " <> returnToPool.return_data.id <> " on " <> show asOf) returnedQuantity + pure (addQuantityById returnToPool.return_data.stock_plan_id returnedQuantity totals) + None -> do + assertMsg ("Stock plan return-to-pool has an invalid quantity history for " <> returnToPool.return_data.id <> " on " <> show asOf) False + pure totals else pure totals) Map.empty (Map.values maps.stock_plan_return_to_pools) @@ -1441,6 +1601,47 @@ validateStockPlanAllocationReferences maps netAllocatedQuantityIndex = (isSome (Map.lookup stockPlanId maps.stock_plans))) (Map.toList netAllocatedQuantityIndex) +assertStockPlanEffectiveForAllocation : Text -> CapTableMaps -> Text -> Time -> Update () +assertStockPlanEffectiveForAllocation label maps stockPlanId allocationDate = + case Map.lookup stockPlanId maps.stock_plans of + Some stockPlanCid -> do + stockPlan <- fetch stockPlanCid + case stockPlanEffectiveDate stockPlan.plan_data of + Some effectiveDate -> + assertMsg + (label <> " references stock plan " <> stockPlanId <> " before it is effective") + (allocationDate >= effectiveDate) + None -> pure () + None -> pure () + +validateStockPlanAllocationEffectiveDates : CapTableMaps -> Update () +validateStockPlanAllocationEffectiveDates maps = do + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> + assertStockPlanEffectiveForAllocation + ("Stock issuance " <> issuance.issuance_data.id) + maps + stockPlanId + issuance.issuance_data.date + None -> pure ()) + (Map.values maps.stock_issuances) + + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> + assertStockPlanEffectiveForAllocation + ("Equity compensation issuance " <> issuance.issuance_data.id) + maps + stockPlanId + issuance.issuance_data.date + None -> pure ()) + (Map.values maps.equity_compensation_issuances) + validateStockPlanReservedShares : StockClassShareEventIndex -> CapTableMaps -> @@ -1563,6 +1764,7 @@ validateStockPlanPoolCeilings : StockClassShareEventIndex -> CapTableMaps -> Upd validateStockPlanPoolCeilings eventIndex maps = do adjustmentIndex <- stockPlanPoolAdjustmentIndex maps adjustmentHistory <- stockPlanPoolAdjustmentHistoryIndex maps + validateStockPlanAllocationEffectiveDates maps validateStockPlanReturnToPoolQuantities eventIndex maps validateStockPlanReservedShares eventIndex maps adjustmentIndex validateStockPlanReservedSharesAtDates eventIndex maps adjustmentHistory From e54727e95adb5c9c663c3308c20f6c222dc03a02 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 08:57:07 -0400 Subject: [PATCH 30/62] fix: harden authorized share accounting --- .../TestStockClassAuthorizedShares.daml | 56 ++ .../TestStockPlanReservedShares.daml | 215 +++++++- .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- .../codegen/templates/CapTable.daml.template | 508 ++++++++++++------ 5 files changed, 621 insertions(+), 168 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 9be83940..432f96f4 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -1000,3 +1000,59 @@ testUnlimitedStockClassAuthorizedSharesAllowsLargeIssuance = script do _ <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_UNLIMITED" "SEC_UNLIMITED" stakeholderId stockClassId) with quantity = 2_000_000.0) pure () + +testBalanceSecurityReplacementPreservesAuthorizedShareHeadroom = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_BALANCE_SUCCESSOR" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_BALANCE_SUCCESSOR" "Balance Successor Holder" + let sourceSecurityId = "SEC_BALANCE_SUCCESSOR_SOURCE" + let balanceSecurityId = "SEC_BALANCE_SUCCESSOR_REMAINDER" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_BALANCE_SUCCESSOR_SOURCE" sourceSecurityId stakeholderId stockClassId) with quantity = 100.0) + let cancellation = (stockCancellation "TX_BALANCE_SUCCESSOR_CANCEL" sourceSecurityId 50.0) with + balance_security_id = Some balanceSecurityId + let balanceIssuance = (defaultStockIssuance "TX_BALANCE_SUCCESSOR_REMAINDER" balanceSecurityId stakeholderId stockClassId) with quantity = 50.0 + let replacementIssuance = (defaultStockIssuance "TX_BALANCE_SUCCESSOR_REPLACEMENT" "SEC_BALANCE_SUCCESSOR_REPLACEMENT" stakeholderId stockClassId) with quantity = 50.0 + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockIssuance balanceIssuance + , CT.OcfCreateStockIssuance replacementIssuance + ] + edits = [] + deletes = [] + pure () + +testStockTransferPartialResultingIssuanceCoverageFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_TRANSFER_PARTIAL_RESULTS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_TRANSFER_PARTIAL_RESULTS" "Partial Transfer Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_TRANSFER_PARTIAL_SOURCE" "SEC_TRANSFER_PARTIAL_SOURCE" stakeholderId stockClassId) with quantity = 100.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (stockTransfer "TX_TRANSFER_PARTIAL_RESULTS" "SEC_TRANSFER_PARTIAL_SOURCE" 100.0 ["SEC_TRANSFER_PARTIAL_PRESENT", "SEC_TRANSFER_PARTIAL_MISSING"]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_TRANSFER_PARTIAL_PRESENT" "SEC_TRANSFER_PARTIAL_PRESENT" stakeholderId stockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + +testStockReissuancePartialResultingIssuanceCoverageFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REISSUE_PARTIAL_RESULTS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_REISSUE_PARTIAL_RESULTS" "Partial Reissuance Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_REISSUE_PARTIAL_SOURCE" "SEC_REISSUE_PARTIAL_SOURCE" stakeholderId stockClassId) with quantity = 100.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance (stockReissuance "TX_REISSUE_PARTIAL_RESULTS" "SEC_REISSUE_PARTIAL_SOURCE" ["SEC_REISSUE_PARTIAL_PRESENT", "SEC_REISSUE_PARTIAL_MISSING"]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_REISSUE_PARTIAL_PRESENT" "SEC_REISSUE_PARTIAL_PRESENT" stakeholderId stockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index a04efc70..6e1aaceb 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -3,7 +3,10 @@ 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.StockReissuance (StockReissuanceOcfData(..)) import Fairmint.OpenCapTable.OCF.StockRetraction (StockRetractionOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockTransfer (StockTransferOcfData(..)) import Fairmint.OpenCapTable.OCF.StockPlanPoolAdjustment (StockPlanPoolAdjustmentOcfData(..)) import Fairmint.OpenCapTable.OCF.StockPlanReturnToPool (StockPlanReturnToPoolOcfData(..)) import OpenCapTable.Setup @@ -43,6 +46,38 @@ equityCompensationCancellation cancellationId securityId quantity = EquityCompen reason_text = "Cancelled equity compensation" comments = [] +planStockTransfer : Text -> Text -> Text -> Decimal -> StockTransferOcfData +planStockTransfer transferId securityId resultingSecurityId quantity = StockTransferOcfData with + id = transferId + date = defaultTestDate + security_id = securityId + quantity = quantity + resulting_security_ids = [resultingSecurityId] + balance_security_id = None + consideration_text = None + comments = [] + +planStockReissuance : Text -> Text -> Text -> StockReissuanceOcfData +planStockReissuance reissuanceId securityId resultingSecurityId = StockReissuanceOcfData with + id = reissuanceId + date = defaultTestDate + security_id = securityId + resulting_security_ids = [resultingSecurityId] + split_transaction_id = None + reason_text = Some "Replacement certificate" + comments = [] + +planEquityCompensationTransfer : Text -> Text -> Text -> Decimal -> EquityCompensationTransferOcfData +planEquityCompensationTransfer transferId securityId resultingSecurityId quantity = EquityCompensationTransferOcfData with + id = transferId + date = defaultTestDate + security_id = securityId + quantity = quantity + resulting_security_ids = [resultingSecurityId] + balance_security_id = None + consideration_text = None + comments = [] + createPlanAndStockIssuance : Party -> ContractId CT.CapTable -> @@ -492,26 +527,31 @@ testStockPlanReservedShares_ReturnToPoolCannotExceedSecurityAllocation = script edits = [] deletes = [] -testStockPlanReservedShares_ReturnToPoolMustMatchSecurityPlan = script do +testStockPlanReservedShares_ReturnToPoolCanRolloverToDifferentPlan = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table let sourcePlanId = "PLAN_RETURN_SOURCE" let targetPlanId = "PLAN_RETURN_TARGET" - let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 100.0 - let targetPlan = (defaultStockPlan targetPlanId stockClassId) with initial_shares_reserved = 100.0 - let issuance = stockPlanEquityCompensationIssuance "TX_EC_RETURN_WRONG_PLAN" "SEC-EC-RETURN-WRONG-PLAN" stakeholderId stockClassId sourcePlanId 50.0 - let returnToPool = stockPlanReturnToPool "TX_RETURN_WRONG_PLAN" issuance.security_id targetPlanId 50.0 + let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 50.0 + let targetPlan = (defaultStockPlan targetPlanId stockClassId) with initial_shares_reserved = 50.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EC_RETURN_ROLLOVER_SOURCE" "SEC-EC-RETURN-ROLLOVER-SOURCE" stakeholderId stockClassId sourcePlanId 50.0 + let cancellation = equityCompensationCancellation "TX_EC_RETURN_ROLLOVER_CANCEL" sourceIssuance.security_id 50.0 + let returnToPool = stockPlanReturnToPool "TX_RETURN_ROLLOVER" sourceIssuance.security_id targetPlanId 50.0 + let targetIssuance = stockPlanEquityCompensationIssuance "TX_EC_RETURN_ROLLOVER_TARGET" "SEC-EC-RETURN-ROLLOVER-TARGET" stakeholderId stockClassId targetPlanId 100.0 - submitMustFail issuer do + _ <- submit issuer do exerciseCmd capTableReady CT.UpdateCapTable with creates = [ CT.OcfCreateStockPlan sourcePlan , CT.OcfCreateStockPlan targetPlan - , CT.OcfCreateEquityCompensationIssuance issuance + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + , CT.OcfCreateEquityCompensationCancellation cancellation , CT.OcfCreateStockPlanReturnToPool returnToPool + , CT.OcfCreateEquityCompensationIssuance targetIssuance ] edits = [] deletes = [] + pure () testStockPlanReservedShares_AdjustmentCannotDropBelowAllocatedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -569,3 +609,164 @@ testStockPlanReservedShares_StockPlanEditCannotDropBelowAllocatedShares = script creates = [] edits = [CT.OcfEditStockPlan editedPlan] deletes = [] + +testStockPlanReservedShares_StockTransferSuccessorDoesNotDoubleAllocate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockPlanId = "PLAN_STOCK_TRANSFER_SUCCESSOR" + (capTableIssued, stockClassId, stakeholderId) <- createPlanAndStockIssuance issuer cap_table stockPlanId 100.0 100.0 + let sourceSecurityId = "SEC-" <> stockPlanId + let resultSecurityId = "SEC-STOCK-TRANSFER-SUCCESSOR" + let resultIssuance = (defaultStockIssuance "TX_STOCK_TRANSFER_SUCCESSOR_RESULT" resultSecurityId stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (planStockTransfer "TX_STOCK_TRANSFER_SUCCESSOR" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_StockTransferDoesNotCreateReturnableShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockPlanId = "PLAN_STOCK_TRANSFER_NO_RETURN" + (capTableIssued, stockClassId, stakeholderId) <- createPlanAndStockIssuance issuer cap_table stockPlanId 100.0 100.0 + let sourceSecurityId = "SEC-" <> stockPlanId + let resultSecurityId = "SEC-STOCK-TRANSFER-NO-RETURN-RESULT" + let resultIssuance = (defaultStockIssuance "TX_STOCK_TRANSFER_NO_RETURN_RESULT" resultSecurityId stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + let returnToPool = stockPlanReturnToPool "TX_STOCK_TRANSFER_INVALID_RETURN" sourceSecurityId stockPlanId 100.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (planStockTransfer "TX_STOCK_TRANSFER_NO_RETURN" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateStockIssuance resultIssuance + , CT.OcfCreateStockPlanReturnToPool returnToPool + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_BalanceSuccessorOnlyReturnsCancelledShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockPlanId = "PLAN_STOCK_BALANCE_RETURN" + (capTableIssued, stockClassId, stakeholderId) <- createPlanAndStockIssuance issuer cap_table stockPlanId 100.0 100.0 + let sourceSecurityId = "SEC-" <> stockPlanId + let balanceSecurityId = "SEC-STOCK-BALANCE-RETURN-RESULT" + let balanceIssuance = (defaultStockIssuance "TX_STOCK_BALANCE_RETURN_RESULT" balanceSecurityId stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 60.0 + let cancellation = (stockCancellation "TX_STOCK_BALANCE_RETURN_CANCEL" sourceSecurityId 40.0) with + balance_security_id = Some balanceSecurityId + let returnToPool = stockPlanReturnToPool "TX_STOCK_BALANCE_EXCESS_RETURN" sourceSecurityId stockPlanId 100.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockIssuance balanceIssuance + , CT.OcfCreateStockPlanReturnToPool returnToPool + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityTransferSuccessorDoesNotDoubleAllocate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_TRANSFER_SUCCESSOR" + let sourceSecurityId = "SEC-EQUITY-TRANSFER-SOURCE" + let resultSecurityId = "SEC-EQUITY-TRANSFER-SUCCESSOR" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + let resultIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_SUCCESSOR" resultSecurityId stakeholderId stockClassId stockPlanId 100.0 + + _ <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer (planEquityCompensationTransfer "TX_EQUITY_TRANSFER" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance resultIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_ReissuancePreservesSourceAllocation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockPlanId = "PLAN_REISSUANCE_LINEAGE" + (capTableIssued, stockClassId, stakeholderId) <- createPlanAndStockIssuance issuer cap_table stockPlanId 100.0 100.0 + let sourceSecurityId = "SEC-" <> stockPlanId + let resultSecurityId = "SEC-REISSUANCE-LINEAGE-RESULT" + let resultIssuance = (defaultStockIssuance "TX_REISSUANCE_LINEAGE_RESULT" resultSecurityId stakeholderId stockClassId) with quantity = 100.0 + let excessIssuance = (defaultStockIssuance "TX_REISSUANCE_LINEAGE_EXCESS" "SEC-REISSUANCE-LINEAGE-EXCESS" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 1.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance (planStockReissuance "TX_REISSUANCE_LINEAGE" sourceSecurityId resultSecurityId) + , CT.OcfCreateStockIssuance resultIssuance + , CT.OcfCreateStockIssuance excessIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquitySplitDoesNotCreateReturnableShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_SPLIT_RETURN" + let securityId = "SEC-EQUITY-SPLIT-RETURN" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 200.0 + let issuance = stockPlanEquityCompensationIssuance "TX_EQUITY_SPLIT_RETURN" securityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + let splitDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let returnDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassSplit ((stockClassSplit "TX_EQUITY_SPLIT_RETURN_SPLIT" stockClassId 2.0 1.0) with date = splitDate) + , CT.OcfCreateStockPlanReturnToPool ((stockPlanReturnToPool "TX_EQUITY_SPLIT_INVALID_RETURN" securityId stockPlanId 100.0) with date = returnDate) + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_CumulativeReturnsAreSplitAdjusted = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockPlanId = "PLAN_CUMULATIVE_SPLIT_RETURNS" + (capTableIssued, stockClassId, _) <- createPlanAndStockIssuance issuer cap_table stockPlanId 200.0 100.0 + let securityId = "SEC-" <> stockPlanId + let splitDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let secondReturnDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let firstReturn = stockPlanReturnToPool "TX_CUMULATIVE_SPLIT_RETURN_FIRST" securityId stockPlanId 50.0 + let secondReturn = (stockPlanReturnToPool "TX_CUMULATIVE_SPLIT_RETURN_SECOND" securityId stockPlanId 50.0) with date = secondReturnDate + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockCancellation (stockCancellation "TX_CUMULATIVE_SPLIT_CANCEL" securityId 50.0) + , CT.OcfCreateStockPlanReturnToPool firstReturn + , CT.OcfCreateStockClassSplit ((stockClassSplit "TX_CUMULATIVE_SPLIT" stockClassId 2.0 1.0) with date = splitDate) + , CT.OcfCreateStockPlanReturnToPool secondReturn + ] + edits = [] + deletes = [] diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 63aa7528..ce847bad 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b72a2f0774ddbfd2e8ca9ef12b28b799542ba7911f4f24878e27b28de536c78 -size 2643310 +oid sha256:ad2a104906a464784f0af1289d7c44d30460d5ab0d66a68f31fe65663f2fd2cb +size 2671549 diff --git a/dars/dars.lock b/dars/dars.lock index 49debf2c..2f5e5334 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,10 +33,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "5b72a2f0774ddbfd2e8ca9ef12b28b799542ba7911f4f24878e27b28de536c78", - "size": 2643310, + "sha256": "ad2a104906a464784f0af1289d7c44d30460d5ab0d66a68f31fe65663f2fd2cb", + "size": 2671549, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T17:41:13Z", + "uploadedAt": "2026-07-10T12:56:00Z", "networks": [] } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index fd47e7f8..ce39c99d 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -118,6 +118,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 @@ -130,6 +134,12 @@ 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_plan_retraction_events_by_security_id: Optional (Map Text [StockClassShareEvent]) + deriving (Eq, Show) + +data StockPlanSuccessorIndex = StockPlanSuccessorIndex with + stock_successors_by_security_id: Map Text Time + equity_compensation_successors_by_security_id: Map Text Time deriving (Eq, Show) authorizedSharesCeiling : OcfInitialSharesAuthorized -> AuthorizedSharesCeiling @@ -227,6 +237,16 @@ 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 + +equityCompensationBalanceSecurityIssuanceExists : CapTableMaps -> Optional Text -> Bool +equityCompensationBalanceSecurityIssuanceExists maps balanceSecurityId = case balanceSecurityId of + Some securityId -> isSome (Map.lookup securityId maps.equity_compensation_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 @@ -712,21 +732,24 @@ 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) - 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) 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 @@ -735,6 +758,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 @@ -744,6 +770,21 @@ applyStockClassShareEventOptional currentOpt event = Some current -> applyStockClassShareEvent current event None -> None +stockSecurityReductionEvents : Time -> Decimal -> Optional Text -> [StockClassShareEvent] +stockSecurityReductionEvents eventDate quantity balanceSecurityId = case balanceSecurityId of + Some _ -> + [ StockClassShareTerminalDelta with + event_date = eventDate + event_priority = 2 + quantity_delta = 0.0 - quantity + ] + None -> + [ StockClassShareDelta 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 = Map.insert key (event :: fromOptional [] (Map.lookup key index)) index @@ -761,53 +802,41 @@ stockClassShareEventIndex maps = do cancellationEventsBySecurityId <- foldlA (\index cancellationCid -> do cancellation <- fetch cancellationCid - pure (addStockClassShareEvent - cancellation.cancellation_data.security_id - (StockClassShareDelta with - event_date = cancellation.cancellation_data.date - event_priority = 1 - quantity_delta = 0.0 - cancellation.cancellation_data.quantity) - index)) + let events = stockSecurityReductionEvents cancellation.cancellation_data.date cancellation.cancellation_data.quantity cancellation.cancellation_data.balance_security_id + if stockBalanceSecurityIssuanceExists maps cancellation.cancellation_data.balance_security_id + then pure (addStockClassShareEvents cancellation.cancellation_data.security_id events index) + else pure index) Map.empty (Map.values maps.stock_cancellations) repurchaseEventsBySecurityId <- foldlA (\index repurchaseCid -> do repurchase <- fetch repurchaseCid - pure (addStockClassShareEvent - repurchase.repurchase_data.security_id - (StockClassShareDelta with - event_date = repurchase.repurchase_data.date - event_priority = 1 - quantity_delta = 0.0 - repurchase.repurchase_data.quantity) - index)) + let events = stockSecurityReductionEvents repurchase.repurchase_data.date repurchase.repurchase_data.quantity repurchase.repurchase_data.balance_security_id + if stockBalanceSecurityIssuanceExists maps repurchase.repurchase_data.balance_security_id + then pure (addStockClassShareEvents repurchase.repurchase_data.security_id events index) + else pure index) cancellationEventsBySecurityId (Map.values maps.stock_repurchases) conversionEventsBySecurityId <- foldlA (\index conversionCid -> do conversion <- fetch conversionCid - pure (addStockClassShareEvent - 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) - index)) + let events = stockSecurityReductionEvents conversion.conversion_data.date conversion.conversion_data.quantity_converted conversion.conversion_data.balance_security_id + if stockBalanceSecurityIssuanceExists maps conversion.conversion_data.balance_security_id + then pure (addStockClassShareEvents conversion.conversion_data.security_id events index) + else pure index) repurchaseEventsBySecurityId (Map.values maps.stock_conversions) transferEventsBySecurityId <- foldlA (\index transferCid -> do transfer <- fetch transferCid + assertNoPartialResultingStockIssuanceCoverage ("Stock transfer " <> transfer.transfer_data.id) maps transfer.transfer_data.resulting_security_ids + let events = stockSecurityReductionEvents transfer.transfer_data.date transfer.transfer_data.quantity transfer.transfer_data.balance_security_id 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) + && stockBalanceSecurityIssuanceExists maps transfer.transfer_data.balance_security_id + then pure (addStockClassShareEvents transfer.transfer_data.security_id events index) else pure index) conversionEventsBySecurityId (Map.values maps.stock_transfers) @@ -815,6 +844,7 @@ stockClassShareEventIndex maps = do reissuanceEventsBySecurityId <- foldlA (\index reissuanceCid -> do reissuance <- fetch reissuanceCid + assertNoPartialResultingStockIssuanceCoverage ("Stock reissuance " <> reissuance.reissuance_data.id) maps reissuance.reissuance_data.resulting_security_ids if allResultingStockIssuancesExist maps reissuance.reissuance_data.resulting_security_ids then pure (addStockClassShareEvent reissuance.reissuance_data.security_id @@ -840,16 +870,17 @@ stockClassShareEventIndex maps = do reissuanceEventsBySecurityId (Map.values maps.stock_consolidations) - retractionEventsBySecurityId <- foldlA - (\index retractionCid -> do + (retractionEventsBySecurityId, planRetractionEventsBySecurityId) <- foldlA + (\(shareIndex, planIndex) 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 planIndex + )) + (consolidationEventsBySecurityId, Map.empty) (Map.values maps.stock_retractions) splitEventsByStockClassId <- foldlA @@ -867,6 +898,7 @@ stockClassShareEventIndex maps = do pure StockClassShareEventIndex with stock_security_events_by_security_id = retractionEventsBySecurityId + stock_plan_retraction_events_by_security_id = Some planRetractionEventsBySecurityId stock_class_split_events_by_stock_class_id = splitEventsByStockClassId stockClassShareLotEvents : StockClassShareEventIndex -> StockClassShareLot -> [StockClassShareEvent] @@ -892,7 +924,7 @@ stockClassShareLotPlanAllocationEvents eventIndex lot = (\event -> case event of StockClassShareRetraction _ _ -> True _ -> False) - (fromOptional [] (Map.lookup securityId eventIndex.stock_security_events_by_security_id)) + (fromOptional [] (Map.lookup securityId (fromOptional Map.empty eventIndex.stock_plan_retraction_events_by_security_id))) None -> [] splitEvents = filter @@ -1189,22 +1221,124 @@ assertNonNegativeQuantity : Text -> Decimal -> Update () assertNonNegativeQuantity label quantity = assertMsg (label <> " cannot be negative: " <> show quantity) (quantity >= 0.0) -stockIssuancePlanAllocationIndex : StockClassShareEventIndex -> CapTableMaps -> Update (Map Text Decimal) -stockIssuancePlanAllocationIndex eventIndex maps = +addPlanSuccessor : Time -> Text -> Map Text Time -> Map Text Time +addPlanSuccessor producedAt securityId index = + case Map.lookup securityId index of + Some existingDate -> Map.insert securityId (if producedAt < existingDate then producedAt else existingDate) index + None -> Map.insert securityId producedAt index + +addOptionalPlanSuccessor : Time -> Optional Text -> Map Text Time -> Map Text Time +addOptionalPlanSuccessor producedAt securityIdOpt index = case securityIdOpt of + Some securityId -> addPlanSuccessor producedAt securityId index + None -> index + +addPlanSuccessors : Time -> [Text] -> Map Text Time -> Map Text Time +addPlanSuccessors producedAt securityIds index = + foldl (\updatedIndex securityId -> addPlanSuccessor producedAt securityId updatedIndex) index securityIds + +planSuccessorProducedOnOrBefore : Map Text Time -> Text -> Time -> Bool +planSuccessorProducedOnOrBefore successorIndex securityId asOf = + case Map.lookup securityId successorIndex of + Some producedAt -> producedAt <= asOf + None -> False + +stockPlanSuccessorIndex : CapTableMaps -> Update StockPlanSuccessorIndex +stockPlanSuccessorIndex maps = do + stockCancellationSuccessors <- foldlA + (\index cancellationCid -> do + cancellation <- fetch cancellationCid + pure (addOptionalPlanSuccessor cancellation.cancellation_data.date cancellation.cancellation_data.balance_security_id index)) + Map.empty + (Map.values maps.stock_cancellations) + + stockRepurchaseSuccessors <- foldlA + (\index repurchaseCid -> do + repurchase <- fetch repurchaseCid + pure (addOptionalPlanSuccessor repurchase.repurchase_data.date repurchase.repurchase_data.balance_security_id index)) + stockCancellationSuccessors + (Map.values maps.stock_repurchases) + + stockTransferSuccessors <- foldlA + (\index transferCid -> do + transfer <- fetch transferCid + let withBalance = addOptionalPlanSuccessor transfer.transfer_data.date transfer.transfer_data.balance_security_id index + pure (addPlanSuccessors transfer.transfer_data.date transfer.transfer_data.resulting_security_ids withBalance)) + stockRepurchaseSuccessors + (Map.values maps.stock_transfers) + + stockConversionSuccessors <- foldlA + (\index conversionCid -> do + conversion <- fetch conversionCid + let withBalance = addOptionalPlanSuccessor conversion.conversion_data.date conversion.conversion_data.balance_security_id index + pure (addPlanSuccessors conversion.conversion_data.date conversion.conversion_data.resulting_security_ids withBalance)) + stockTransferSuccessors + (Map.values maps.stock_conversions) + + stockReissuanceSuccessors <- foldlA + (\index reissuanceCid -> do + reissuance <- fetch reissuanceCid + pure (addPlanSuccessors reissuance.reissuance_data.date reissuance.reissuance_data.resulting_security_ids index)) + stockConversionSuccessors + (Map.values maps.stock_reissuances) + + stockConsolidationSuccessors <- foldlA + (\index consolidationCid -> do + consolidation <- fetch consolidationCid + pure (addPlanSuccessor consolidation.consolidation_data.date consolidation.consolidation_data.resulting_security_id index)) + stockReissuanceSuccessors + (Map.values maps.stock_consolidations) + + stockExerciseSuccessors <- foldlA + (\index exerciseCid -> do + exercise <- fetch exerciseCid + pure (addPlanSuccessors exercise.exercise_data.date exercise.exercise_data.resulting_security_ids index)) + stockConsolidationSuccessors + (Map.values maps.equity_compensation_exercises) + + stockSuccessors <- foldlA + (\index releaseCid -> do + release <- fetch releaseCid + pure (addPlanSuccessors release.release_data.settlement_date release.release_data.resulting_security_ids index)) + stockExerciseSuccessors + (Map.values maps.equity_compensation_releases) + + equityCancellationSuccessors <- foldlA + (\index cancellationCid -> do + cancellation <- fetch cancellationCid + pure (addOptionalPlanSuccessor cancellation.cancellation_data.date cancellation.cancellation_data.balance_security_id index)) + Map.empty + (Map.values maps.equity_compensation_cancellations) + + equitySuccessors <- foldlA + (\index transferCid -> do + transfer <- fetch transferCid + let withBalance = addOptionalPlanSuccessor transfer.transfer_data.date transfer.transfer_data.balance_security_id index + pure (addPlanSuccessors transfer.transfer_data.date transfer.transfer_data.resulting_security_ids withBalance)) + equityCancellationSuccessors + (Map.values maps.equity_compensation_transfers) + + pure StockPlanSuccessorIndex with + stock_successors_by_security_id = stockSuccessors + equity_compensation_successors_by_security_id = equitySuccessors + +stockIssuancePlanAllocationIndex : StockClassShareEventIndex -> StockPlanSuccessorIndex -> CapTableMaps -> Update (Map Text Decimal) +stockIssuancePlanAllocationIndex eventIndex successorIndex maps = foldlA (\totals issuanceCid -> do issuance <- fetch issuanceCid - case issuance.issuance_data.stock_plan_id of - Some stockPlanId -> do - let lot = stockIssuanceShareLot issuance.issuance_data - case stockClassShareLotPlanAllocatedQuantity eventIndex lot of - Some allocationQuantity -> do - assertNonNegativeQuantity ("Stock issuance plan allocation quantity for " <> issuance.issuance_data.id) allocationQuantity - pure (addQuantityById stockPlanId allocationQuantity totals) - None -> do - assertMsg ("Stock issuance plan allocation has an invalid quantity for " <> issuance.issuance_data.id) False - pure totals - None -> pure totals) + if isSome (Map.lookup issuance.issuance_data.security_id successorIndex.stock_successors_by_security_id) + then pure totals + else case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> do + let lot = stockIssuanceShareLot issuance.issuance_data + case stockClassShareLotPlanAllocatedQuantity eventIndex lot of + Some allocationQuantity -> do + assertNonNegativeQuantity ("Stock issuance plan allocation quantity for " <> issuance.issuance_data.id) allocationQuantity + pure (addQuantityById stockPlanId allocationQuantity totals) + None -> do + assertMsg ("Stock issuance plan allocation has an invalid quantity for " <> issuance.issuance_data.id) False + pure totals + None -> pure totals) Map.empty (Map.values maps.stock_issuances) @@ -1263,23 +1397,25 @@ equityCompensationPlanAllocatedQuantityAt maps issuanceData asOf = do (filter (stockClassShareEventOnOrBefore asOf) events))) else pure (Some 0.0) -equityCompensationPlanAllocationIndex : CapTableMaps -> Update (Map Text Decimal) -equityCompensationPlanAllocationIndex maps = +equityCompensationPlanAllocationIndex : StockPlanSuccessorIndex -> CapTableMaps -> Update (Map Text Decimal) +equityCompensationPlanAllocationIndex successorIndex maps = foldlA (\totals issuanceCid -> do issuance <- fetch issuanceCid - case issuance.issuance_data.stock_plan_id of - Some stockPlanId -> do - assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id) issuance.issuance_data.quantity - allocationQuantityOpt <- equityCompensationPlanAllocatedQuantity maps issuance.issuance_data - case allocationQuantityOpt of - Some allocationQuantity -> do - assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id) allocationQuantity - pure (addQuantityById stockPlanId allocationQuantity totals) - None -> do - assertMsg ("Equity compensation plan allocation has an invalid quantity history for " <> issuance.issuance_data.id) False - pure totals - None -> pure totals) + if isSome (Map.lookup issuance.issuance_data.security_id successorIndex.equity_compensation_successors_by_security_id) + then pure totals + else case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> do + assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id) issuance.issuance_data.quantity + allocationQuantityOpt <- equityCompensationPlanAllocatedQuantity maps issuance.issuance_data + case allocationQuantityOpt of + Some allocationQuantity -> do + assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id) allocationQuantity + pure (addQuantityById stockPlanId allocationQuantity totals) + None -> do + assertMsg ("Equity compensation plan allocation has an invalid quantity history for " <> issuance.issuance_data.id) False + pure totals + None -> pure totals) Map.empty (Map.values maps.equity_compensation_issuances) @@ -1366,17 +1502,24 @@ stockPlanReturnedQuantityBySecurityAt maps securityId asOf = if returnToPool.return_data.security_id == securityId && returnToPool.return_data.date <= asOf then do assertNonNegativeQuantity ("Stock plan return-to-pool quantity for " <> returnToPool.return_data.id <> " on " <> show asOf) returnToPool.return_data.quantity - pure (total + returnToPool.return_data.quantity) + returnedQuantityOpt <- stockPlanReturnToPoolQuantityAt maps returnToPool.return_data asOf + case returnedQuantityOpt of + Some returnedQuantity -> do + assertNonNegativeQuantity ("Stock plan split-adjusted return-to-pool quantity for " <> returnToPool.return_data.id <> " on " <> show asOf) returnedQuantity + pure (total + returnedQuantity) + None -> do + assertMsg ("Stock plan return-to-pool has an invalid quantity history for " <> returnToPool.return_data.id <> " on " <> show asOf) False + pure total else pure total) 0.0 (Map.values maps.stock_plan_return_to_pools) -equityCompensationSecurityEvents : CapTableMaps -> Text -> Update [StockClassShareEvent] -equityCompensationSecurityEvents maps securityId = do +equityCompensationSecurityEvents : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update [StockClassShareEvent] +equityCompensationSecurityEvents maps issuanceData = do cancellationEvents <- foldlA (\events cancellationCid -> do cancellation <- fetch cancellationCid - if cancellation.cancellation_data.security_id == securityId + if cancellation.cancellation_data.security_id == issuanceData.security_id then pure ((StockClassShareDelta with event_date = cancellation.cancellation_data.date event_priority = 1 @@ -1388,7 +1531,7 @@ equityCompensationSecurityEvents maps securityId = do exerciseEvents <- foldlA (\events exerciseCid -> do exercise <- fetch exerciseCid - if exercise.exercise_data.security_id == securityId + if exercise.exercise_data.security_id == issuanceData.security_id then pure ((StockClassShareDelta with event_date = exercise.exercise_data.date event_priority = 1 @@ -1400,7 +1543,7 @@ equityCompensationSecurityEvents maps securityId = do releaseEvents <- foldlA (\events releaseCid -> do release <- fetch releaseCid - if release.release_data.security_id == securityId + if release.release_data.security_id == issuanceData.security_id then pure ((StockClassShareDelta with event_date = release.release_data.settlement_date event_priority = 1 @@ -1409,10 +1552,10 @@ equityCompensationSecurityEvents maps securityId = do exerciseEvents (Map.values maps.equity_compensation_releases) - foldlA + retractionEvents <- foldlA (\events retractionCid -> do retraction <- fetch retractionCid - if retraction.retraction_data.security_id == securityId + if retraction.retraction_data.security_id == issuanceData.security_id then pure ((StockClassShareRetraction with event_date = retraction.retraction_data.date event_priority = 3) :: events) @@ -1420,9 +1563,26 @@ equityCompensationSecurityEvents maps securityId = do releaseEvents (Map.values maps.equity_compensation_retractions) + 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 ((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) + None -> pure [] + + pure (retractionEvents ++ splitEvents) + equityCompensationCurrentQuantity : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update (Optional Decimal) equityCompensationCurrentQuantity maps issuanceData = do - events <- equityCompensationSecurityEvents maps issuanceData.security_id + events <- equityCompensationSecurityEvents maps issuanceData pure (foldl applyStockClassShareEventOptional (Some issuanceData.quantity) @@ -1432,62 +1592,92 @@ equityCompensationQuantityAt : CapTableMaps -> EquityCompensationIssuanceOcfData equityCompensationQuantityAt maps issuanceData asOf = do if issuanceData.date <= asOf then do - events <- equityCompensationSecurityEvents maps issuanceData.security_id + events <- equityCompensationSecurityEvents maps issuanceData pure (foldl applyStockClassShareEventOptional (Some issuanceData.quantity) (sortOn stockClassShareEventSortKey (filter (stockClassShareEventOnOrBefore asOf) events))) else pure (Some 0.0) -stockPlanSecurityReturnableQuantityAt : StockClassShareEventIndex -> CapTableMaps -> StockPlanReturnToPoolOcfData -> Time -> Update Decimal -stockPlanSecurityReturnableQuantityAt eventIndex maps returnData asOf = do +stockPlanReturnableSplitEvents : CapTableMaps -> Optional Text -> Update [StockClassShareEvent] +stockPlanReturnableSplitEvents maps stockClassIdOpt = case stockClassIdOpt of + Some stockClassId -> + foldlA + (\events splitCid -> do + split <- fetch splitCid + if split.split_data.stock_class_id == stockClassId + 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) + None -> pure [] + +stockPlanCancellationQuantityAt : Text -> [StockClassShareEvent] -> Time -> Update Decimal +stockPlanCancellationQuantityAt securityId events asOf = + case foldl + applyStockClassShareEventOptional + (Some 0.0) + (sortOn stockClassShareEventSortKey (filter (stockClassShareEventOnOrBefore asOf) events)) of + Some quantity -> do + assertNonNegativeQuantity ("Stock plan split-adjusted cancellation quantity for " <> securityId <> " on " <> show asOf) quantity + pure quantity + None -> do + assertMsg ("Stock plan cancellation quantity history is invalid for " <> securityId <> " on " <> show asOf) False + pure 0.0 + +stockCancellationQuantityAt : CapTableMaps -> StockIssuanceOcfData -> Time -> Update Decimal +stockCancellationQuantityAt maps issuanceData asOf = do + cancellationEvents <- foldlA + (\events cancellationCid -> do + cancellation <- fetch cancellationCid + if cancellation.cancellation_data.security_id == issuanceData.security_id + && stockBalanceSecurityIssuanceExists maps cancellation.cancellation_data.balance_security_id + then pure ((StockClassShareDelta with + event_date = cancellation.cancellation_data.date + event_priority = 1 + quantity_delta = cancellation.cancellation_data.quantity) :: events) + else pure events) + [] + (Map.values maps.stock_cancellations) + splitEvents <- stockPlanReturnableSplitEvents maps (Some issuanceData.stock_class_id) + stockPlanCancellationQuantityAt issuanceData.security_id (cancellationEvents ++ splitEvents) asOf + +equityCompensationCancellationQuantityAt : CapTableMaps -> EquityCompensationIssuanceOcfData -> Time -> Update Decimal +equityCompensationCancellationQuantityAt maps issuanceData asOf = do + cancellationEvents <- foldlA + (\events cancellationCid -> do + cancellation <- fetch cancellationCid + if cancellation.cancellation_data.security_id == issuanceData.security_id + && equityCompensationBalanceSecurityIssuanceExists maps cancellation.cancellation_data.balance_security_id + then pure ((StockClassShareDelta with + event_date = cancellation.cancellation_data.date + event_priority = 1 + quantity_delta = cancellation.cancellation_data.quantity) :: events) + else pure events) + [] + (Map.values maps.equity_compensation_cancellations) + splitEvents <- stockPlanReturnableSplitEvents maps issuanceData.stock_class_id + stockPlanCancellationQuantityAt issuanceData.security_id (cancellationEvents ++ splitEvents) asOf + +stockPlanSecurityReturnableQuantityAt : CapTableMaps -> StockPlanReturnToPoolOcfData -> Time -> Update Decimal +stockPlanSecurityReturnableQuantityAt maps returnData asOf = do let stockCidOpt = Map.lookup returnData.security_id maps.stock_issuances_by_security_id let equityCompensationCidOpt = Map.lookup returnData.security_id maps.equity_compensation_issuances_by_security_id case (stockCidOpt, equityCompensationCidOpt) of (Some stockCid, None) -> do issuance <- fetch stockCid case issuance.issuance_data.stock_plan_id of - Some stockPlanId -> do - assertMsg - ("Stock plan return to pool references security " <> returnData.security_id <> " from stock plan " <> stockPlanId <> " but returns to " <> returnData.stock_plan_id) - (stockPlanId == returnData.stock_plan_id) - let lot = stockIssuanceShareLot issuance.issuance_data - case (stockClassShareLotPlanAllocatedQuantityAt eventIndex lot asOf, stockClassShareLotQuantityAt eventIndex lot asOf) of - (Some allocationQuantity, Some outstandingQuantity) -> do - assertNonNegativeQuantity ("Stock issuance plan allocation quantity for " <> issuance.issuance_data.id) allocationQuantity - assertNonNegativeQuantity ("Stock issuance outstanding quantity for " <> issuance.issuance_data.id) outstandingQuantity - let returnableQuantity = allocationQuantity - outstandingQuantity - assertMsg - ("Stock plan return-to-pool quantity basis is invalid for " <> returnData.security_id) - (returnableQuantity >= 0.0) - pure returnableQuantity - _ -> do - assertMsg ("Stock plan return-to-pool security has invalid stock quantity history: " <> returnData.security_id) False - pure 0.0 + Some _ -> stockCancellationQuantityAt maps issuance.issuance_data asOf None -> do assertMsg ("Stock plan return to pool references non-plan stock security: " <> returnData.security_id) False pure 0.0 (None, Some equityCompensationCid) -> do issuance <- fetch equityCompensationCid case issuance.issuance_data.stock_plan_id of - Some stockPlanId -> do - assertMsg - ("Stock plan return to pool references security " <> returnData.security_id <> " from stock plan " <> stockPlanId <> " but returns to " <> returnData.stock_plan_id) - (stockPlanId == returnData.stock_plan_id) - allocationQuantityOpt <- equityCompensationPlanAllocatedQuantityAt maps issuance.issuance_data asOf - outstandingQuantityOpt <- equityCompensationQuantityAt maps issuance.issuance_data asOf - case (allocationQuantityOpt, outstandingQuantityOpt) of - (Some allocationQuantity, Some outstandingQuantity) -> do - assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id) allocationQuantity - assertNonNegativeQuantity ("Equity compensation outstanding quantity for " <> issuance.issuance_data.id) outstandingQuantity - let returnableQuantity = allocationQuantity - outstandingQuantity - assertMsg - ("Stock plan return-to-pool quantity basis is invalid for " <> returnData.security_id) - (returnableQuantity >= 0.0) - pure returnableQuantity - _ -> do - assertMsg ("Stock plan return-to-pool security has invalid equity compensation quantity history: " <> returnData.security_id) False - pure 0.0 + Some _ -> equityCompensationCancellationQuantityAt maps issuance.issuance_data asOf None -> do assertMsg ("Stock plan return to pool references non-plan equity compensation security: " <> returnData.security_id) False pure 0.0 @@ -1498,12 +1688,12 @@ stockPlanSecurityReturnableQuantityAt eventIndex maps returnData asOf = do assertMsg ("Stock plan return to pool security not found: " <> returnData.security_id) False pure 0.0 -validateStockPlanReturnToPoolQuantities : StockClassShareEventIndex -> CapTableMaps -> Update () -validateStockPlanReturnToPoolQuantities eventIndex maps = do +validateStockPlanReturnToPoolQuantities : CapTableMaps -> Update () +validateStockPlanReturnToPoolQuantities maps = do mapA_ (\returnCid -> do returnToPool <- fetch returnCid - securityAllocation <- stockPlanSecurityReturnableQuantityAt eventIndex maps returnToPool.return_data returnToPool.return_data.date + securityAllocation <- stockPlanSecurityReturnableQuantityAt maps returnToPool.return_data returnToPool.return_data.date returnedQuantity <- stockPlanReturnedQuantityBySecurityAt maps returnToPool.return_data.security_id returnToPool.return_data.date assertMsg ("Stock plan return to pool exceeds returnable quantity for " <> returnToPool.return_data.security_id <> ": returned " <> show returnedQuantity <> " exceeds returnable " <> show securityAllocation) @@ -1512,55 +1702,60 @@ validateStockPlanReturnToPoolQuantities eventIndex maps = do stockPlanNetAllocatedQuantityIndex : StockClassShareEventIndex -> CapTableMaps -> Update (Map Text Decimal) stockPlanNetAllocatedQuantityIndex eventIndex maps = do - stockAllocation <- stockIssuancePlanAllocationIndex eventIndex maps - equityCompensationAllocation <- equityCompensationPlanAllocationIndex maps + successorIndex <- stockPlanSuccessorIndex maps + stockAllocation <- stockIssuancePlanAllocationIndex eventIndex successorIndex maps + equityCompensationAllocation <- equityCompensationPlanAllocationIndex successorIndex maps returnedQuantity <- stockPlanReturnedQuantityIndex maps let combinedAllocation = mergeQuantityMaps stockAllocation equityCompensationAllocation let netAllocatedByPlan = mergeQuantityMaps combinedAllocation (negateQuantityMap returnedQuantity) pure netAllocatedByPlan -stockIssuancePlanAllocationIndexAt : StockClassShareEventIndex -> CapTableMaps -> Time -> Update (Map Text Decimal) -stockIssuancePlanAllocationIndexAt eventIndex maps asOf = +stockIssuancePlanAllocationIndexAt : StockClassShareEventIndex -> StockPlanSuccessorIndex -> CapTableMaps -> Time -> Update (Map Text Decimal) +stockIssuancePlanAllocationIndexAt eventIndex successorIndex maps asOf = foldlA (\totals issuanceCid -> do issuance <- fetch issuanceCid - case issuance.issuance_data.stock_plan_id of - Some stockPlanId -> - if issuance.issuance_data.date <= asOf - then do - let lot = stockIssuanceShareLot issuance.issuance_data - case stockClassShareLotPlanAllocatedQuantityAt eventIndex lot asOf of - Some allocationQuantity -> do - assertNonNegativeQuantity ("Stock issuance plan allocation quantity for " <> issuance.issuance_data.id <> " on " <> show asOf) allocationQuantity - pure (addQuantityById stockPlanId allocationQuantity totals) - None -> do - assertMsg ("Stock issuance plan allocation has an invalid quantity for " <> issuance.issuance_data.id <> " on " <> show asOf) False - pure totals - else pure totals - None -> pure totals) + if planSuccessorProducedOnOrBefore successorIndex.stock_successors_by_security_id issuance.issuance_data.security_id asOf + then pure totals + else case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> + if issuance.issuance_data.date <= asOf + then do + let lot = stockIssuanceShareLot issuance.issuance_data + case stockClassShareLotPlanAllocatedQuantityAt eventIndex lot asOf of + Some allocationQuantity -> do + assertNonNegativeQuantity ("Stock issuance plan allocation quantity for " <> issuance.issuance_data.id <> " on " <> show asOf) allocationQuantity + pure (addQuantityById stockPlanId allocationQuantity totals) + None -> do + assertMsg ("Stock issuance plan allocation has an invalid quantity for " <> issuance.issuance_data.id <> " on " <> show asOf) False + pure totals + else pure totals + None -> pure totals) Map.empty (Map.values maps.stock_issuances) -equityCompensationPlanAllocationIndexAt : CapTableMaps -> Time -> Update (Map Text Decimal) -equityCompensationPlanAllocationIndexAt maps asOf = +equityCompensationPlanAllocationIndexAt : StockPlanSuccessorIndex -> CapTableMaps -> Time -> Update (Map Text Decimal) +equityCompensationPlanAllocationIndexAt successorIndex maps asOf = foldlA (\totals issuanceCid -> do issuance <- fetch issuanceCid - case issuance.issuance_data.stock_plan_id of - Some stockPlanId -> - if issuance.issuance_data.date <= asOf - then do - assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id <> " on " <> show asOf) issuance.issuance_data.quantity - allocationQuantityOpt <- equityCompensationPlanAllocatedQuantityAt maps issuance.issuance_data asOf - case allocationQuantityOpt of - Some allocationQuantity -> do - assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id <> " on " <> show asOf) allocationQuantity - pure (addQuantityById stockPlanId allocationQuantity totals) - None -> do - assertMsg ("Equity compensation plan allocation has an invalid quantity history for " <> issuance.issuance_data.id <> " on " <> show asOf) False - pure totals - else pure totals - None -> pure totals) + if planSuccessorProducedOnOrBefore successorIndex.equity_compensation_successors_by_security_id issuance.issuance_data.security_id asOf + then pure totals + else case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> + if issuance.issuance_data.date <= asOf + then do + assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id <> " on " <> show asOf) issuance.issuance_data.quantity + allocationQuantityOpt <- equityCompensationPlanAllocatedQuantityAt maps issuance.issuance_data asOf + case allocationQuantityOpt of + Some allocationQuantity -> do + assertNonNegativeQuantity ("Equity compensation plan allocation quantity for " <> issuance.issuance_data.id <> " on " <> show asOf) allocationQuantity + pure (addQuantityById stockPlanId allocationQuantity totals) + None -> do + assertMsg ("Equity compensation plan allocation has an invalid quantity history for " <> issuance.issuance_data.id <> " on " <> show asOf) False + pure totals + else pure totals + None -> pure totals) Map.empty (Map.values maps.equity_compensation_issuances) @@ -1586,8 +1781,9 @@ stockPlanReturnedQuantityIndexAt maps asOf = stockPlanNetAllocatedQuantityIndexAt : StockClassShareEventIndex -> CapTableMaps -> Time -> Update (Map Text Decimal) stockPlanNetAllocatedQuantityIndexAt eventIndex maps asOf = do - stockAllocation <- stockIssuancePlanAllocationIndexAt eventIndex maps asOf - equityCompensationAllocation <- equityCompensationPlanAllocationIndexAt maps asOf + successorIndex <- stockPlanSuccessorIndex maps + stockAllocation <- stockIssuancePlanAllocationIndexAt eventIndex successorIndex maps asOf + equityCompensationAllocation <- equityCompensationPlanAllocationIndexAt successorIndex maps asOf returnedQuantity <- stockPlanReturnedQuantityIndexAt maps asOf let combinedAllocation = mergeQuantityMaps stockAllocation equityCompensationAllocation pure (mergeQuantityMaps combinedAllocation (negateQuantityMap returnedQuantity)) @@ -1765,7 +1961,7 @@ validateStockPlanPoolCeilings eventIndex maps = do adjustmentIndex <- stockPlanPoolAdjustmentIndex maps adjustmentHistory <- stockPlanPoolAdjustmentHistoryIndex maps validateStockPlanAllocationEffectiveDates maps - validateStockPlanReturnToPoolQuantities eventIndex maps + validateStockPlanReturnToPoolQuantities maps validateStockPlanReservedShares eventIndex maps adjustmentIndex validateStockPlanReservedSharesAtDates eventIndex maps adjustmentHistory From 1177689069f46cd9b1f777cca9dbc08fbf145b12 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 09:49:22 -0400 Subject: [PATCH 31/62] fix: close historical ceiling gaps --- .../TestIssuerAuthorizedShares.daml | 21 +- .../TestStockPlanReservedShares.daml | 81 ++++++ .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- .../codegen/templates/CapTable.daml.template | 253 +++++++++++++----- 5 files changed, 294 insertions(+), 71 deletions(-) diff --git a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml index a019c589..82ccbaca 100644 --- a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml @@ -48,7 +48,7 @@ testIssuerAuthorizedSharesAdjustmentAllowsSameBatchStockClassIncrease = script d exerciseCmd capTableLimited CT.UpdateCapTable with creates = [ CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_FOR_CLASS" 150.0) - , CT.OcfCreateStockClass (limitedStockClass "SC_ALLOWED_BY_ISSUER_RAISE" 150.0) + , CT.OcfCreateStockClass ((limitedStockClass "SC_ALLOWED_BY_ISSUER_RAISE" 150.0) with board_approval_date = Some defaultTestDate) ] edits = [] deletes = [] @@ -74,6 +74,21 @@ testIssuerHistoricalTotalsExcludeFutureEffectiveStockClasses = script do deletes = [] pure () +testIssuerInitialAuthorizationAppliesToUndatedStockClasses = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + let laterAdjustment = (issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_AFTER_UNDATED_CLASS" 150.0) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + + submitMustFail issuer do + exerciseCmd capTableLimited CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass (limitedStockClass "SC_ISSUER_UNDATED_OVER_INITIAL" 150.0) + , CT.OcfCreateIssuerAuthorizedSharesAdjustment laterAdjustment + ] + edits = [] + deletes = [] + testIssuerHistoricalTotalsWaitForBothStockClassApprovals = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 @@ -184,7 +199,7 @@ testIssuerEditAllowsInitialBelowCurrentTotalWhenAdjustmentCoversHistory = script exerciseCmd capTableLimited CT.UpdateCapTable with creates = [ CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_HISTORY_OK" 150.0) - , CT.OcfCreateStockClass (limitedStockClass "SC_ISSUER_EDIT_HISTORY_OK" 125.0) + , CT.OcfCreateStockClass ((limitedStockClass "SC_ISSUER_EDIT_HISTORY_OK" 125.0) with board_approval_date = Some defaultTestDate) ] edits = [] deletes = [] @@ -203,7 +218,7 @@ testIssuerMetadataEditAllowedWhenAdjustmentRaisesAuthorizedShares = script do exerciseCmd capTableLimited CT.UpdateCapTable with creates = [ CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_METADATA" 150.0) - , CT.OcfCreateStockClass (limitedStockClass "SC_ISSUER_METADATA_WITH_ADJUSTMENT" 125.0) + , CT.OcfCreateStockClass ((limitedStockClass "SC_ISSUER_METADATA_WITH_ADJUSTMENT" 125.0) with board_approval_date = Some defaultTestDate) ] edits = [] deletes = [] diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index 6e1aaceb..db29c24a 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -553,6 +553,36 @@ testStockPlanReservedShares_ReturnToPoolCanRolloverToDifferentPlan = script do deletes = [] pure () +testStockPlanReservedShares_ReturnCannotCreateNegativeHistoricalAllocation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourcePlanId = "PLAN_RETURN_HISTORY_SOURCE" + let targetPlanId = "PLAN_RETURN_HISTORY_TARGET" + let sourceSecurityId = "SEC-RETURN-HISTORY-SOURCE" + let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 50.0 + let targetPlan = (defaultStockPlan targetPlanId stockClassId) with initial_shares_reserved = 50.0 + let sourceIssuance = (stockPlanEquityCompensationIssuance "TX_RETURN_HISTORY_SOURCE" sourceSecurityId stakeholderId stockClassId sourcePlanId 50.0) with + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + let cancellation = (equityCompensationCancellation "TX_RETURN_HISTORY_CANCEL" sourceSecurityId 50.0) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + let returnToPool = (stockPlanReturnToPool "TX_RETURN_HISTORY_POOL" sourceSecurityId targetPlanId 50.0) with + date = DT.time (DA.date 2024 Jan 02) 0 0 0 + let targetIssuance = (stockPlanEquityCompensationIssuance "TX_RETURN_HISTORY_TARGET" "SEC-RETURN-HISTORY-TARGET" stakeholderId stockClassId targetPlanId 100.0) with + date = DT.time (DA.date 2024 Jan 03) 0 0 0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan sourcePlan + , CT.OcfCreateStockPlan targetPlan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + , CT.OcfCreateEquityCompensationCancellation cancellation + , CT.OcfCreateStockPlanReturnToPool returnToPool + , CT.OcfCreateEquityCompensationIssuance targetIssuance + ] + edits = [] + deletes = [] + testStockPlanReservedShares_AdjustmentCannotDropBelowAllocatedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithIssuance, _, _) <- createPlanAndStockIssuance issuer cap_table "PLAN_ADJ_DROP" 200.0 150.0 @@ -630,6 +660,33 @@ testStockPlanReservedShares_StockTransferSuccessorDoesNotDoubleAllocate = script deletes = [] pure () +testStockPlanReservedShares_NonPlanStockSourceDoesNotHidePlanSuccessor = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_NON_PLAN_STOCK_SOURCE" + let sourceSecurityId = "SEC-NON-PLAN-STOCK-SOURCE" + let resultSecurityId = "SEC-NON-PLAN-STOCK-RESULT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (defaultStockIssuance "TX_NON_PLAN_STOCK_SOURCE" sourceSecurityId stakeholderId stockClassId) with quantity = 100.0 + let resultIssuance = (defaultStockIssuance "TX_NON_PLAN_STOCK_RESULT" resultSecurityId stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + let excessIssuance = (defaultStockIssuance "TX_NON_PLAN_STOCK_EXCESS" "SEC-NON-PLAN-STOCK-EXCESS" stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 1.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance sourceIssuance + , CT.OcfCreateStockTransfer (planStockTransfer "TX_NON_PLAN_STOCK_TRANSFER" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateStockIssuance resultIssuance + , CT.OcfCreateStockIssuance excessIssuance + ] + edits = [] + deletes = [] + testStockPlanReservedShares_StockTransferDoesNotCreateReturnableShares = script do TestOcp{issuer, cap_table} <- setupTestOcp let stockPlanId = "PLAN_STOCK_TRANSFER_NO_RETURN" @@ -702,6 +759,30 @@ testStockPlanReservedShares_EquityTransferSuccessorDoesNotDoubleAllocate = scrip deletes = [] pure () +testStockPlanReservedShares_NonPlanEquitySourceDoesNotHidePlanSuccessor = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_NON_PLAN_EQUITY_SOURCE" + let sourceSecurityId = "SEC-NON-PLAN-EQUITY-SOURCE" + let resultSecurityId = "SEC-NON-PLAN-EQUITY-RESULT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (stockPlanEquityCompensationIssuance "TX_NON_PLAN_EQUITY_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0) with + stock_plan_id = None + let resultIssuance = stockPlanEquityCompensationIssuance "TX_NON_PLAN_EQUITY_RESULT" resultSecurityId stakeholderId stockClassId stockPlanId 100.0 + let excessIssuance = stockPlanEquityCompensationIssuance "TX_NON_PLAN_EQUITY_EXCESS" "SEC-NON-PLAN-EQUITY-EXCESS" stakeholderId stockClassId stockPlanId 1.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + , CT.OcfCreateEquityCompensationTransfer (planEquityCompensationTransfer "TX_NON_PLAN_EQUITY_TRANSFER" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance resultIssuance + , CT.OcfCreateEquityCompensationIssuance excessIssuance + ] + edits = [] + deletes = [] + testStockPlanReservedShares_ReissuancePreservesSourceAllocation = script do TestOcp{issuer, cap_table} <- setupTestOcp let stockPlanId = "PLAN_REISSUANCE_LINEAGE" diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index ce847bad..942b0b37 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad2a104906a464784f0af1289d7c44d30460d5ab0d66a68f31fe65663f2fd2cb -size 2671549 +oid sha256:e761a486eb3155bfe475064b9867436ea48f94e5bde9ee467f2995d9f3d0887a +size 2687952 diff --git a/dars/dars.lock b/dars/dars.lock index 2f5e5334..c7133d8f 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,10 +33,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "ad2a104906a464784f0af1289d7c44d30460d5ab0d66a68f31fe65663f2fd2cb", - "size": 2671549, + "sha256": "e761a486eb3155bfe475064b9867436ea48f94e5bde9ee467f2995d9f3d0887a", + "size": 2687952, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-10T12:56:00Z", + "uploadedAt": "2026-07-10T13:44:14Z", "networks": [] } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index ce39c99d..19030fa0 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -138,8 +138,8 @@ data StockClassShareEventIndex = StockClassShareEventIndex with deriving (Eq, Show) data StockPlanSuccessorIndex = StockPlanSuccessorIndex with - stock_successors_by_security_id: Map Text Time - equity_compensation_successors_by_security_id: Map Text Time + stock_successors_by_security_id: Map Text (Time, Optional Text) + equity_compensation_successors_by_security_id: Map Text (Time, Optional Text) deriving (Eq, Show) authorizedSharesCeiling : OcfInitialSharesAuthorized -> AuthorizedSharesCeiling @@ -1202,7 +1202,7 @@ validateIssuerAuthorizedSharesAtDates stockClasses maps stockClassAdjustmentHist (stockClassTotal <= issuerLimit) None -> assertMsg ("Issuer authorized shares exceeded on " <> show asOf <> ": stock class authorization is unlimited but issuer authorization is finite") False) - validationDates + (issuerContract.issuer_data.formation_date :: validationDates) addQuantityById : Text -> Decimal -> Map Text Decimal -> Map Text Decimal addQuantityById key quantity totals = @@ -1221,101 +1221,211 @@ assertNonNegativeQuantity : Text -> Decimal -> Update () assertNonNegativeQuantity label quantity = assertMsg (label <> " cannot be negative: " <> show quantity) (quantity >= 0.0) -addPlanSuccessor : Time -> Text -> Map Text Time -> Map Text Time -addPlanSuccessor producedAt securityId index = +addPlanSuccessor : Time -> Text -> Text -> Map Text (Time, Optional Text) -> Map Text (Time, Optional Text) +addPlanSuccessor producedAt stockPlanId securityId index = case Map.lookup securityId index of - Some existingDate -> Map.insert securityId (if producedAt < existingDate then producedAt else existingDate) index - None -> Map.insert securityId producedAt index - -addOptionalPlanSuccessor : Time -> Optional Text -> Map Text Time -> Map Text Time -addOptionalPlanSuccessor producedAt securityIdOpt index = case securityIdOpt of - Some securityId -> addPlanSuccessor producedAt securityId index + Some (existingDate, existingPlanIdOpt) -> + let earliestDate = if producedAt < existingDate then producedAt else existingDate + inheritedPlanIdOpt = case existingPlanIdOpt of + Some existingPlanId -> if existingPlanId == stockPlanId then Some stockPlanId else None + None -> None + in Map.insert securityId (earliestDate, inheritedPlanIdOpt) index + None -> Map.insert securityId (producedAt, Some stockPlanId) index + +addOptionalPlanSuccessor : Time -> Text -> Optional Text -> Map Text (Time, Optional Text) -> Map Text (Time, Optional Text) +addOptionalPlanSuccessor producedAt stockPlanId securityIdOpt index = case securityIdOpt of + Some securityId -> addPlanSuccessor producedAt stockPlanId securityId index None -> index -addPlanSuccessors : Time -> [Text] -> Map Text Time -> Map Text Time -addPlanSuccessors producedAt securityIds index = - foldl (\updatedIndex securityId -> addPlanSuccessor producedAt securityId updatedIndex) index securityIds +addPlanSuccessors : Time -> Text -> [Text] -> Map Text (Time, Optional Text) -> Map Text (Time, Optional Text) +addPlanSuccessors producedAt stockPlanId securityIds index = + foldl (\updatedIndex securityId -> addPlanSuccessor producedAt stockPlanId securityId updatedIndex) index securityIds + +declaredPlanForSecurity : Map Text Text -> Map Text (Time, Optional Text) -> Text -> Optional Text +declaredPlanForSecurity declaredPlans successorIndex securityId = + case Map.lookup securityId declaredPlans of + Some stockPlanId -> Some stockPlanId + None -> case Map.lookup securityId successorIndex of + Some (_, stockPlanIdOpt) -> stockPlanIdOpt + None -> None + +addInheritedPlanSuccessorsForSource : + Map Text Text -> + Map Text (Time, Optional Text) -> + Text -> + Time -> + Optional Text -> + [Text] -> + Map Text (Time, Optional Text) +addInheritedPlanSuccessorsForSource declaredPlans successorIndex sourceSecurityId producedAt balanceSecurityId resultingSecurityIds = + case declaredPlanForSecurity declaredPlans successorIndex sourceSecurityId of + Some stockPlanId -> + let withBalance = addOptionalPlanSuccessor producedAt stockPlanId balanceSecurityId successorIndex + in addPlanSuccessors producedAt stockPlanId resultingSecurityIds withBalance + None -> successorIndex + +successorCarriesPlanAllocation : Map Text (Time, Optional Text) -> Text -> Optional Text -> Bool +successorCarriesPlanAllocation successorIndex securityId declaredPlanIdOpt = + case Map.lookup securityId successorIndex of + Some (_, Some inheritedPlanId) -> case declaredPlanIdOpt of + Some declaredPlanId -> declaredPlanId == inheritedPlanId + None -> True + _ -> False -planSuccessorProducedOnOrBefore : Map Text Time -> Text -> Time -> Bool -planSuccessorProducedOnOrBefore successorIndex securityId asOf = +successorCarriesPlanAllocationOnOrBefore : Map Text (Time, Optional Text) -> Text -> Optional Text -> Time -> Bool +successorCarriesPlanAllocationOnOrBefore successorIndex securityId declaredPlanIdOpt asOf = case Map.lookup securityId successorIndex of - Some producedAt -> producedAt <= asOf - None -> False + Some (producedAt, Some inheritedPlanId) -> + producedAt <= asOf && case declaredPlanIdOpt of + Some declaredPlanId -> declaredPlanId == inheritedPlanId + None -> True + _ -> False + +effectivePlanIndex : Map Text Text -> Map Text (Time, Optional Text) -> Map Text Text +effectivePlanIndex declaredPlans successorIndex = + foldl + (\plans (securityId, (_, stockPlanIdOpt)) -> + case (Map.lookup securityId plans, stockPlanIdOpt) of + (None, Some stockPlanId) -> Map.insert securityId stockPlanId plans + _ -> plans) + declaredPlans + (Map.toList successorIndex) + +mergePlanIndexes : Map Text Text -> Map Text Text -> Map Text Text +mergePlanIndexes base additions = + foldl + (\plans (securityId, stockPlanId) -> + if isSome (Map.lookup securityId plans) then plans else Map.insert securityId stockPlanId plans) + base + (Map.toList additions) + +equityPlanSuccessorPass : CapTableMaps -> Map Text Text -> Map Text (Time, Optional Text) -> Update (Map Text (Time, Optional Text)) +equityPlanSuccessorPass maps declaredPlans initialIndex = do + cancellationSuccessors <- foldlA + (\index cancellationCid -> do + cancellation <- fetch cancellationCid + pure (addInheritedPlanSuccessorsForSource + declaredPlans + index + cancellation.cancellation_data.security_id + cancellation.cancellation_data.date + cancellation.cancellation_data.balance_security_id + [])) + initialIndex + (Map.values maps.equity_compensation_cancellations) -stockPlanSuccessorIndex : CapTableMaps -> Update StockPlanSuccessorIndex -stockPlanSuccessorIndex maps = do - stockCancellationSuccessors <- foldlA + foldlA + (\index transferCid -> do + transfer <- fetch transferCid + pure (addInheritedPlanSuccessorsForSource + declaredPlans + index + transfer.transfer_data.security_id + transfer.transfer_data.date + transfer.transfer_data.balance_security_id + transfer.transfer_data.resulting_security_ids)) + cancellationSuccessors + (Map.values maps.equity_compensation_transfers) + +stabilizeEquityPlanSuccessors : CapTableMaps -> Map Text Text -> Map Text (Time, Optional Text) -> Update (Map Text (Time, Optional Text)) +stabilizeEquityPlanSuccessors maps declaredPlans currentIndex = do + nextIndex <- equityPlanSuccessorPass maps declaredPlans currentIndex + if nextIndex == currentIndex + then pure currentIndex + else stabilizeEquityPlanSuccessors maps declaredPlans nextIndex + +stockPlanSuccessorPass : CapTableMaps -> Map Text Text -> Map Text (Time, Optional Text) -> Update (Map Text (Time, Optional Text)) +stockPlanSuccessorPass maps sourcePlans initialIndex = do + cancellationSuccessors <- foldlA (\index cancellationCid -> do cancellation <- fetch cancellationCid - pure (addOptionalPlanSuccessor cancellation.cancellation_data.date cancellation.cancellation_data.balance_security_id index)) - Map.empty + pure (addInheritedPlanSuccessorsForSource sourcePlans index cancellation.cancellation_data.security_id cancellation.cancellation_data.date cancellation.cancellation_data.balance_security_id [])) + initialIndex (Map.values maps.stock_cancellations) - stockRepurchaseSuccessors <- foldlA + repurchaseSuccessors <- foldlA (\index repurchaseCid -> do repurchase <- fetch repurchaseCid - pure (addOptionalPlanSuccessor repurchase.repurchase_data.date repurchase.repurchase_data.balance_security_id index)) - stockCancellationSuccessors + pure (addInheritedPlanSuccessorsForSource sourcePlans index repurchase.repurchase_data.security_id repurchase.repurchase_data.date repurchase.repurchase_data.balance_security_id [])) + cancellationSuccessors (Map.values maps.stock_repurchases) - stockTransferSuccessors <- foldlA + transferSuccessors <- foldlA (\index transferCid -> do transfer <- fetch transferCid - let withBalance = addOptionalPlanSuccessor transfer.transfer_data.date transfer.transfer_data.balance_security_id index - pure (addPlanSuccessors transfer.transfer_data.date transfer.transfer_data.resulting_security_ids withBalance)) - stockRepurchaseSuccessors + pure (addInheritedPlanSuccessorsForSource sourcePlans index transfer.transfer_data.security_id transfer.transfer_data.date transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids)) + repurchaseSuccessors (Map.values maps.stock_transfers) - stockConversionSuccessors <- foldlA + conversionSuccessors <- foldlA (\index conversionCid -> do conversion <- fetch conversionCid - let withBalance = addOptionalPlanSuccessor conversion.conversion_data.date conversion.conversion_data.balance_security_id index - pure (addPlanSuccessors conversion.conversion_data.date conversion.conversion_data.resulting_security_ids withBalance)) - stockTransferSuccessors + pure (addInheritedPlanSuccessorsForSource sourcePlans index conversion.conversion_data.security_id conversion.conversion_data.date conversion.conversion_data.balance_security_id conversion.conversion_data.resulting_security_ids)) + transferSuccessors (Map.values maps.stock_conversions) - stockReissuanceSuccessors <- foldlA + reissuanceSuccessors <- foldlA (\index reissuanceCid -> do reissuance <- fetch reissuanceCid - pure (addPlanSuccessors reissuance.reissuance_data.date reissuance.reissuance_data.resulting_security_ids index)) - stockConversionSuccessors + pure (addInheritedPlanSuccessorsForSource sourcePlans index reissuance.reissuance_data.security_id reissuance.reissuance_data.date None reissuance.reissuance_data.resulting_security_ids)) + conversionSuccessors (Map.values maps.stock_reissuances) - stockConsolidationSuccessors <- foldlA + consolidationSuccessors <- foldlA (\index consolidationCid -> do consolidation <- fetch consolidationCid - pure (addPlanSuccessor consolidation.consolidation_data.date consolidation.consolidation_data.resulting_security_id index)) - stockReissuanceSuccessors + foldlA + (\nextIndex sourceSecurityId -> + pure (addInheritedPlanSuccessorsForSource sourcePlans nextIndex sourceSecurityId consolidation.consolidation_data.date None [consolidation.consolidation_data.resulting_security_id])) + index + consolidation.consolidation_data.security_ids) + reissuanceSuccessors (Map.values maps.stock_consolidations) - stockExerciseSuccessors <- foldlA + exerciseSuccessors <- foldlA (\index exerciseCid -> do exercise <- fetch exerciseCid - pure (addPlanSuccessors exercise.exercise_data.date exercise.exercise_data.resulting_security_ids index)) - stockConsolidationSuccessors + pure (addInheritedPlanSuccessorsForSource sourcePlans index exercise.exercise_data.security_id exercise.exercise_data.date None exercise.exercise_data.resulting_security_ids)) + consolidationSuccessors (Map.values maps.equity_compensation_exercises) - stockSuccessors <- foldlA + foldlA (\index releaseCid -> do release <- fetch releaseCid - pure (addPlanSuccessors release.release_data.settlement_date release.release_data.resulting_security_ids index)) - stockExerciseSuccessors + pure (addInheritedPlanSuccessorsForSource sourcePlans index release.release_data.security_id release.release_data.settlement_date None release.release_data.resulting_security_ids)) + exerciseSuccessors (Map.values maps.equity_compensation_releases) - equityCancellationSuccessors <- foldlA - (\index cancellationCid -> do - cancellation <- fetch cancellationCid - pure (addOptionalPlanSuccessor cancellation.cancellation_data.date cancellation.cancellation_data.balance_security_id index)) +stabilizeStockPlanSuccessors : CapTableMaps -> Map Text Text -> Map Text (Time, Optional Text) -> Update (Map Text (Time, Optional Text)) +stabilizeStockPlanSuccessors maps sourcePlans currentIndex = do + nextIndex <- stockPlanSuccessorPass maps sourcePlans currentIndex + if nextIndex == currentIndex + then pure currentIndex + else stabilizeStockPlanSuccessors maps sourcePlans nextIndex + +stockPlanSuccessorIndex : CapTableMaps -> Update StockPlanSuccessorIndex +stockPlanSuccessorIndex maps = do + equityDeclaredPlans <- foldlA + (\index issuanceCid -> do + issuance <- fetch issuanceCid + pure $ case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> Map.insert issuance.issuance_data.security_id stockPlanId index + None -> index) Map.empty - (Map.values maps.equity_compensation_cancellations) + (Map.values maps.equity_compensation_issuances) + equitySuccessors <- stabilizeEquityPlanSuccessors maps equityDeclaredPlans Map.empty + let equityEffectivePlans = effectivePlanIndex equityDeclaredPlans equitySuccessors - equitySuccessors <- foldlA - (\index transferCid -> do - transfer <- fetch transferCid - let withBalance = addOptionalPlanSuccessor transfer.transfer_data.date transfer.transfer_data.balance_security_id index - pure (addPlanSuccessors transfer.transfer_data.date transfer.transfer_data.resulting_security_ids withBalance)) - equityCancellationSuccessors - (Map.values maps.equity_compensation_transfers) + stockDeclaredPlans <- foldlA + (\index issuanceCid -> do + issuance <- fetch issuanceCid + pure $ case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> Map.insert issuance.issuance_data.security_id stockPlanId index + None -> index) + Map.empty + (Map.values maps.stock_issuances) + let stockSourcePlans = mergePlanIndexes stockDeclaredPlans equityEffectivePlans + stockSuccessors <- stabilizeStockPlanSuccessors maps stockSourcePlans Map.empty pure StockPlanSuccessorIndex with stock_successors_by_security_id = stockSuccessors @@ -1326,7 +1436,10 @@ stockIssuancePlanAllocationIndex eventIndex successorIndex maps = foldlA (\totals issuanceCid -> do issuance <- fetch issuanceCid - if isSome (Map.lookup issuance.issuance_data.security_id successorIndex.stock_successors_by_security_id) + if successorCarriesPlanAllocation + successorIndex.stock_successors_by_security_id + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id then pure totals else case issuance.issuance_data.stock_plan_id of Some stockPlanId -> do @@ -1402,7 +1515,10 @@ equityCompensationPlanAllocationIndex successorIndex maps = foldlA (\totals issuanceCid -> do issuance <- fetch issuanceCid - if isSome (Map.lookup issuance.issuance_data.security_id successorIndex.equity_compensation_successors_by_security_id) + if successorCarriesPlanAllocation + successorIndex.equity_compensation_successors_by_security_id + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id then pure totals else case issuance.issuance_data.stock_plan_id of Some stockPlanId -> do @@ -1715,7 +1831,11 @@ stockIssuancePlanAllocationIndexAt eventIndex successorIndex maps asOf = foldlA (\totals issuanceCid -> do issuance <- fetch issuanceCid - if planSuccessorProducedOnOrBefore successorIndex.stock_successors_by_security_id issuance.issuance_data.security_id asOf + if successorCarriesPlanAllocationOnOrBefore + successorIndex.stock_successors_by_security_id + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id + asOf then pure totals else case issuance.issuance_data.stock_plan_id of Some stockPlanId -> @@ -1739,7 +1859,11 @@ equityCompensationPlanAllocationIndexAt successorIndex maps asOf = foldlA (\totals issuanceCid -> do issuance <- fetch issuanceCid - if planSuccessorProducedOnOrBefore successorIndex.equity_compensation_successors_by_security_id issuance.issuance_data.security_id asOf + if successorCarriesPlanAllocationOnOrBefore + successorIndex.equity_compensation_successors_by_security_id + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id + asOf then pure totals else case issuance.issuance_data.stock_plan_id of Some stockPlanId -> @@ -1945,10 +2069,13 @@ validateStockPlanReservedSharesAtDates eventIndex maps adjustmentHistory = do (\asOf -> do netAllocatedQuantityIndex <- stockPlanNetAllocatedQuantityIndexAt eventIndex maps asOf validateStockPlanAllocationReferences maps netAllocatedQuantityIndex + let allocatedQuantity = fromOptional 0.0 (Map.lookup stockPlan.plan_data.id netAllocatedQuantityIndex) + assertNonNegativeQuantity + ("Stock plan allocated quantity for " <> stockPlan.plan_data.id <> " on " <> show asOf) + allocatedQuantity if stockPlanEffectiveOnOrBefore asOf stockPlan.plan_data then do let reservedShares = stockPlanReservedSharesAt adjustmentHistory stockPlan.plan_data asOf - let allocatedQuantity = fromOptional 0.0 (Map.lookup stockPlan.plan_data.id netAllocatedQuantityIndex) assertMsg ("Stock plan reserved shares exceeded for " <> stockPlan.plan_data.id <> " on " <> show asOf <> ": allocated " <> show allocatedQuantity <> " exceeds reserved " <> show reservedShares) (allocatedQuantity <= reservedShares) From c4f98a5e6cb6cec7a75562e9f747d23c55b1bad6 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 12:15:35 -0400 Subject: [PATCH 32/62] fix: close remaining ceiling review gaps --- .../TestStockClassAuthorizedShares.daml | 23 +++++ .../TestStockPlanReservedShares.daml | 65 ++++++++++++++ .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 89 +++++++++---------- 5 files changed, 136 insertions(+), 49 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 432f96f4..080600c0 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -858,6 +858,29 @@ testWarrantExerciseWithUnknownOutputQuantityFailsForCeiling = script do edits = [] deletes = [] +testNonFixedWarrantExerciseRequiresResultingStockIssuanceForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_NON_FIXED" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_NON_FIXED" "Warrant Non Fixed Holder" + let issuance = (warrantIssuance "TX_WARRANT_NON_FIXED_ISSUED" "WSEC_NON_FIXED" stakeholderId stockClassId 1.0) with + exercise_triggers = [warrantCustomExerciseTrigger "TRIGGER_CUSTOM_NON_FIXED" stockClassId] + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise + ((warrantExercise "TX_WARRANT_NON_FIXED_EXERCISE" "WSEC_NON_FIXED" 1.0) with + trigger_id = "TRIGGER_CUSTOM_NON_FIXED") + ] + edits = [] + deletes = [] + testWarrantExerciseWithUnknownTriggerFailsForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index db29c24a..0928f522 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -196,6 +196,71 @@ testStockPlanReservedShares_EquityCompensationStockSplitCountsAgainstPlanPool = edits = [] deletes = [] +testStockPlanReservedShares_ClasslessEquityCompensationUsesSinglePlanClassForSplit = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EC_CLASSLESS_SPLIT_POOL" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = + (stockPlanEquityCompensationIssuance "TX_EC_CLASSLESS_SPLIT_POOL" "SEC-EC-CLASSLESS-SPLIT-POOL" stakeholderId stockClassId stockPlanId 100.0) with + stock_class_id = None + let split = stockClassSplit "TX_EC_CLASSLESS_SPLIT_POOL" stockClassId 2.0 1.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + , CT.OcfCreateStockClassSplit split + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ClasslessEquityCompensationAllowsMultiClassPlanWithoutSplit = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithFirstClass, firstStockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + (capTableReady, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_EC_CLASSLESS_SECOND" + let stockPlanId = "PLAN_EC_CLASSLESS_MULTI_CLASS" + let plan = (defaultStockPlan stockPlanId firstStockClassId) with + initial_shares_reserved = 100.0 + stock_class_ids = [firstStockClassId, secondStockClassId] + let issuance = + (stockPlanEquityCompensationIssuance "TX_EC_CLASSLESS_MULTI_CLASS" "SEC-EC-CLASSLESS-MULTI-CLASS" stakeholderId firstStockClassId stockPlanId 100.0) with + stock_class_id = None + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_ClasslessEquityCompensationRejectsAmbiguousMultiClassSplit = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithFirstClass, firstStockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + (capTableReady, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_EC_CLASSLESS_SPLIT_SECOND" + let stockPlanId = "PLAN_EC_CLASSLESS_AMBIGUOUS_SPLIT" + let plan = (defaultStockPlan stockPlanId firstStockClassId) with + initial_shares_reserved = 200.0 + stock_class_ids = [firstStockClassId, secondStockClassId] + let issuance = + (stockPlanEquityCompensationIssuance "TX_EC_CLASSLESS_AMBIGUOUS_SPLIT" "SEC-EC-CLASSLESS-AMBIGUOUS-SPLIT" stakeholderId firstStockClassId stockPlanId 100.0) with + stock_class_id = None + let split = stockClassSplit "TX_EC_CLASSLESS_AMBIGUOUS_SPLIT" firstStockClassId 2.0 1.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + , CT.OcfCreateStockClassSplit split + ] + edits = [] + deletes = [] + testStockPlanReservedShares_ReturnBeforeSplitAdjustsWithPlanPool = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 942b0b37..daa1309a 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e761a486eb3155bfe475064b9867436ea48f94e5bde9ee467f2995d9f3d0887a -size 2687952 +oid sha256:e9c72132fe0887950fab7d44752a8113933ef5d7cd62abfc9d80c9b9b61a82f0 +size 2689160 diff --git a/dars/dars.lock b/dars/dars.lock index c7133d8f..e6da6c01 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "e761a486eb3155bfe475064b9867436ea48f94e5bde9ee467f2995d9f3d0887a", - "size": 2687952, + "sha256": "e9c72132fe0887950fab7d44752a8113933ef5d7cd62abfc9d80c9b9b61a82f0", + "size": 2689160, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-10T13:44:14Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 19030fa0..2b4c9199 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -384,27 +384,15 @@ warrantExerciseOutputShareLot issuanceData exerciseData = OcfRightWarrant conversionRight -> case conversionRight.converts_to_stock_class_id of Some stockClassId -> - case (conversionRight.conversion_mechanism, exerciseData.quantity, issuanceData.quantity) of - (OcfWarrantMechanismFixedAmount _, _, _) -> + case conversionRight.conversion_mechanism of + OcfWarrantMechanismFixedAmount _ -> pure (Some (StockClassShareLot with stock_class_id = stockClassId security_id = None event_date = exerciseData.date quantity = warrantExerciseQuantity issuanceData exerciseData conversionRight)) - (_, Some _, _) -> - pure (Some (StockClassShareLot with - stock_class_id = stockClassId - security_id = None - event_date = exerciseData.date - quantity = warrantExerciseQuantity issuanceData exerciseData conversionRight)) - (_, None, Some _) -> - pure (Some (StockClassShareLot with - stock_class_id = stockClassId - security_id = None - event_date = exerciseData.date - quantity = warrantExerciseQuantity issuanceData exerciseData conversionRight)) - (_, None, None) -> do - assertMsg ("Warrant exercise output cannot be reconstructed because exercise and issuance quantities are missing: " <> exerciseData.id) False + _ -> do + assertMsg ("Warrant exercise output cannot be reconstructed for a non-fixed conversion mechanism; a resulting stock issuance is required: " <> exerciseData.id) False pure None None -> do assertMsg ("Warrant exercise output cannot be reconstructed because trigger has no destination stock class: " <> exerciseData.trigger_id) False @@ -1455,6 +1443,40 @@ stockIssuancePlanAllocationIndex eventIndex successorIndex maps = Map.empty (Map.values maps.stock_issuances) +equityCompensationPlanStockClassIds : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update [Text] +equityCompensationPlanStockClassIds maps issuanceData = + case issuanceData.stock_class_id of + Some stockClassId -> pure [stockClassId] + None -> case issuanceData.stock_plan_id of + Some stockPlanId -> case Map.lookup stockPlanId maps.stock_plans of + Some stockPlanCid -> do + stockPlan <- fetch stockPlanCid + pure stockPlan.plan_data.stock_class_ids + None -> pure [] + None -> pure [] + +equityCompensationPlanSplitEvents : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update [StockClassShareEvent] +equityCompensationPlanSplitEvents maps issuanceData = do + stockClassIds <- equityCompensationPlanStockClassIds maps issuanceData + splitEvents <- foldlA + (\events splitCid -> do + split <- fetch splitCid + if split.split_data.stock_class_id `elem` stockClassIds && 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) + case (issuanceData.stock_class_id, stockClassIds, splitEvents) of + (None, _ :: _ :: _, _ :: _) -> do + assertMsg + ("Equity compensation issuance without stock_class_id cannot be split-adjusted across multiple stock-plan classes: " <> issuanceData.id) + False + pure [] + _ -> pure splitEvents + equityCompensationPlanAllocationEvents : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update [StockClassShareEvent] equityCompensationPlanAllocationEvents maps issuanceData = do retractionEvents <- foldlA @@ -1468,20 +1490,7 @@ equityCompensationPlanAllocationEvents maps issuanceData = do [] (Map.values maps.equity_compensation_retractions) - 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 ((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) - None -> pure [] + splitEvents <- equityCompensationPlanSplitEvents maps issuanceData pure ((StockClassShareDelta with event_date = issuanceData.date @@ -1543,7 +1552,10 @@ stockPlanReturnToPoolStockClassId maps securityId = pure (Some issuance.issuance_data.stock_class_id) (None, Some equityCompensationCid) -> do issuance <- fetch equityCompensationCid - pure issuance.issuance_data.stock_class_id + stockClassIds <- equityCompensationPlanStockClassIds maps issuance.issuance_data + case stockClassIds of + [stockClassId] -> pure (Some stockClassId) + _ -> pure None (Some _, Some _) -> do assertMsg ("Stock plan return-to-pool security id is ambiguous: " <> securityId) False pure None @@ -1679,20 +1691,7 @@ equityCompensationSecurityEvents maps issuanceData = do releaseEvents (Map.values maps.equity_compensation_retractions) - 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 ((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) - None -> pure [] + splitEvents <- equityCompensationPlanSplitEvents maps issuanceData pure (retractionEvents ++ splitEvents) From fad22d4fbc33db859b206dad68a6ff8f2011d77f Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 12:22:21 -0400 Subject: [PATCH 33/62] fix: reject orphaned ceiling references --- .../TestIssuerAuthorizedShares.daml | 12 +++++ .../TestStockClassAuthorizedShares.daml | 46 +++++++++++++++++++ .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 45 +++++++++++++++--- 5 files changed, 101 insertions(+), 10 deletions(-) diff --git a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml index 82ccbaca..2f7b7515 100644 --- a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml @@ -54,6 +54,18 @@ testIssuerAuthorizedSharesAdjustmentAllowsSameBatchStockClassIncrease = script d deletes = [] pure () +testIssuerAuthorizedSharesAdjustmentForOtherIssuerFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment + ((issuerAuthorizedSharesAdjustment "TX_ISSUER_OTHER_ISSUER" 100.0) with issuer_id = "OTHER_ISSUER") + ] + edits = [] + deletes = [] + testIssuerHistoricalTotalsExcludeFutureEffectiveStockClasses = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 080600c0..420ed0dd 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -426,6 +426,19 @@ testStockClassAuthorizedSharesReplacementAllowedInSameBatch = script do deletes = [CT.OcfDeleteStockClassAuthorizedSharesAdjustment "TX_AUTH_REPLACE_OLD"] pure () +testStockClassDeleteWithLiveStockIssuanceFailsForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_DELETE_LIVE_LOTS" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_DELETE_LIVE_LOTS" "Delete Live Lots Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_DELETE_LIVE_LOTS" "SEC_DELETE_LIVE_LOTS" stakeholderId stockClassId) with quantity = 50.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockClass stockClassId] + testSameDateLowerStockClassAuthorizedSharesAdjustmentWins = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -669,6 +682,39 @@ testStockConversionCreditsDestinationClassForCeiling = script do edits = [] deletes = [] +testStockConversionMissingDestinationClassFailsForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + let preferredClassId = "SC_CONVERSION_MISSING_DEST_SOURCE" + let preferredClass = (limitedStockClass preferredClassId 200.0) with + conversion_rights = [stockClassConversionRightTo "SC_CONVERSION_MISSING_DEST"] + (capTableWithPreferred, _) <- setupStockClassWith issuer cap_table preferredClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_MISSING_DEST" "Conversion Missing Destination Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_MISSING_DEST_SOURCE" "SEC_CONVERSION_MISSING_DEST_SOURCE" stakeholderId preferredClassId) with quantity = 50.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_MISSING_DEST" "SEC_CONVERSION_MISSING_DEST_SOURCE" 50.0)] + edits = [] + deletes = [] + +testStockConversionMissingSourceIssuanceFailsForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_MISSING_SOURCE_COMMON" 100.0) + let preferredClassId = "SC_CONVERSION_MISSING_SOURCE" + let preferredClass = (limitedStockClass preferredClassId 200.0) with + conversion_rights = [stockClassConversionRightTo commonClassId] + (capTableWithPreferred, _) <- setupStockClassWith issuer capTableWithCommon preferredClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_MISSING_SOURCE" "Conversion Missing Source Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_MISSING_SOURCE_ISSUED" "SEC_CONVERSION_MISSING_SOURCE" stakeholderId preferredClassId) with quantity = 50.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = [CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_MISSING_SOURCE" "SEC_CONVERSION_MISSING_SOURCE" 50.0)] + edits = [] + deletes = [CT.OcfDeleteStockIssuance "TX_CONVERSION_MISSING_SOURCE_ISSUED"] + testStockConversionWithPartialResultingIssuancesFailsForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index daa1309a..697c9132 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9c72132fe0887950fab7d44752a8113933ef5d7cd62abfc9d80c9b9b61a82f0 -size 2689160 +oid sha256:ae78edbb7206da778a31c46ace2cda66c33805549c5efadd4f93b68ffdd94d65 +size 2692310 diff --git a/dars/dars.lock b/dars/dars.lock index e6da6c01..19a62d7b 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "e9c72132fe0887950fab7d44752a8113933ef5d7cd62abfc9d80c9b9b61a82f0", - "size": 2689160, + "sha256": "ae78edbb7206da778a31c46ace2cda66c33805549c5efadd4f93b68ffdd94d65", + "size": 2692310, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-10T13:44:14Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 2b4c9199..1a62035a 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -343,6 +343,9 @@ stockConversionOutputShareLot maps stockClassIndex sourceIssuanceData conversion Some sourceStockClass -> case sourceStockClass.conversion_rights of [conversionRight] -> do + assertMsg + ("Stock conversion output references missing stock class: " <> conversionRight.converts_to_stock_class_id) + (isSome (Map.lookup conversionRight.converts_to_stock_class_id stockClassIndex)) outputQuantity <- stockClassConversionOutputQuantity maps sourceStockClass conversionRight conversionData pure (Some (StockClassShareLot with stock_class_id = conversionRight.converts_to_stock_class_id @@ -460,12 +463,14 @@ stockClassShareLotsByStockClass maps stockClassIndex = do assertNoPartialResultingStockIssuanceCoverage ("Stock conversion " <> conversion.conversion_data.id) maps conversion.conversion_data.resulting_security_ids if allResultingStockIssuancesExist maps conversion.conversion_data.resulting_security_ids then pure index - else case Map.lookup conversion.conversion_data.security_id maps.stock_issuances_by_security_id of - Some sourceIssuanceCid -> do - sourceIssuance <- fetch sourceIssuanceCid - lotOpt <- stockConversionOutputShareLot maps stockClassIndex sourceIssuance.issuance_data conversion.conversion_data - pure (addOptionalStockClassShareLot lotOpt index) - None -> pure index) + else case Map.lookup conversion.conversion_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + lotOpt <- stockConversionOutputShareLot maps stockClassIndex sourceIssuance.issuance_data conversion.conversion_data + pure (addOptionalStockClassShareLot lotOpt index) + None -> do + assertMsg ("Stock conversion references missing source stock issuance: " <> conversion.conversion_data.security_id) False + pure index) stockIssuanceLots (Map.values maps.stock_conversions) @@ -1192,6 +1197,32 @@ validateIssuerAuthorizedSharesAtDates stockClasses maps stockClassAdjustmentHist assertMsg ("Issuer authorized shares exceeded on " <> show asOf <> ": stock class authorization is unlimited but issuer authorization is finite") False) (issuerContract.issuer_data.formation_date :: validationDates) +validateStockClassShareLotReferences : + Map Text StockClassOcfData -> + Map Text [StockClassShareLot] -> + Update () +validateStockClassShareLotReferences stockClassIndex lotsByStockClass = + mapA_ + (\stockClassId -> + assertMsg + ("Stock class share lot references missing stock class: " <> stockClassId) + (isSome (Map.lookup stockClassId stockClassIndex))) + (Map.keys lotsByStockClass) + +validateIssuerAuthorizedShareAdjustmentReferences : + CapTableMaps -> + ContractId Issuer -> + Update () +validateIssuerAuthorizedShareAdjustmentReferences maps issuerCid = do + issuerContract <- fetch issuerCid + mapA_ + (\adjustmentCid -> do + adjustment <- fetch adjustmentCid + assertMsg + ("Issuer authorized shares adjustment references another issuer: " <> adjustment.adjustment_data.id) + (adjustment.adjustment_data.issuer_id == issuerContract.issuer_data.id)) + (Map.values maps.issuer_authorized_shares_adjustments) + addQuantityById : Text -> Decimal -> Map Text Decimal -> Map Text Decimal addQuantityById key quantity totals = let current = fromOptional 0.0 (Map.lookup key totals) @@ -2104,6 +2135,8 @@ validateAuthorizedShareCeilings maps adjustmentIndex issuerCid = do stockClassAdjustmentHistory <- stockClassAuthorizedAdjustmentHistoryIndex maps stockClassValidationDates <- stockClassAuthorizedValidationDateIndex maps lotsByStockClass issuerAdjustmentHistory <- issuerAuthorizedAdjustmentHistoryIndex maps + validateStockClassShareLotReferences stockClassIndex lotsByStockClass + validateIssuerAuthorizedShareAdjustmentReferences maps issuerCid validateAllStockClassAuthorizedShares eventIndex stockClasses lotsByStockClass adjustmentIndex validateStockClassAuthorizedSharesAtDates eventIndex stockClasses lotsByStockClass stockClassAdjustmentHistory stockClassValidationDates validateIssuerAuthorizedShares stockClasses maps adjustmentIndex issuerCid From d72c73d0101bd6294fdf5abbbcea4bc449100d17 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 13:03:42 -0400 Subject: [PATCH 34/62] fix: enforce exact issuance and balance invariants --- .../OpenCapTable/TestStockCancellation.daml | 25 +- .../TestStockClassAuthorizedShares.daml | 238 +++++++++ .../OpenCapTable/TestStockConversion.daml | 25 +- .../TestStockPlanReservedShares.daml | 132 +++++ .../OpenCapTable/TestStockRepurchase.daml | 27 +- Test/daml/OpenCapTable/TestStockTransfer.daml | 27 +- .../codegen/templates/CapTable.daml.template | 487 ++++++++++++++++-- 7 files changed, 878 insertions(+), 83 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockCancellation.daml b/Test/daml/OpenCapTable/TestStockCancellation.daml index e10b5cf1..d468ebe4 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,23 @@ testStockCancellation_OptionalsSome = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addPrerequisites issuer cap_table result <- createStockIssuance issuer capTableCid + let cancellationDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let balanceIssuance = (defaultStockIssuance "TX_STOCK_CANCEL_2_BALANCE" "SEC-1-BAL2" "SH-1" "SC_COMMON") with + date = cancellationDate + 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 = cancellationDate + security_id = "SEC-1" + quantity = 25.0 + balance_security_id = Some balanceIssuance.security_id + reason_text = "Return to pool" + comments = ["Admin correction"] + , CT.OcfCreateStockIssuance balanceIssuance + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 420ed0dd..e0086270 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -737,6 +737,76 @@ testStockConversionWithPartialResultingIssuancesFailsForCeiling = script do edits = [] deletes = [] +testStockConversionResultStockClassMustMatchConversionRight = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithExpected, expectedClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_RESULT_EXPECTED" 1_000.0) + (capTableWithWrong, wrongClassId) <- setupStockClassWith issuer capTableWithExpected (limitedStockClass "SC_CONVERSION_RESULT_WRONG" 1_000.0) + let sourceClassId = "SC_CONVERSION_RESULT_SOURCE" + let sourceClass = (limitedStockClass sourceClassId 1_000.0) with + conversion_rights = [stockClassConversionRightTo expectedClassId] + (capTableWithSource, _) <- setupStockClassWith issuer capTableWithWrong sourceClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithSource "SH_CONVERSION_RESULT_CLASS" "Conversion Result Class Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_CONVERSION_RESULT_CLASS_SOURCE" "SEC_CONVERSION_RESULT_CLASS_SOURCE" stakeholderId sourceClassId) with quantity = 50.0) + let conversion = stockConversion "TX_CONVERSION_RESULT_CLASS" "SEC_CONVERSION_RESULT_CLASS_SOURCE" 50.0 + let resultIssuance = (defaultStockIssuance "TX_CONVERSION_RESULT_CLASS_OUTPUT" "TX_CONVERSION_RESULT_CLASS_RESULT" stakeholderId wrongClassId) with quantity = 50.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConversion conversion + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + +testStockConversionResultQuantityMustMatchConversionOutput = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithDestination, destinationClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_RESULT_QUANTITY_DEST" 1_000.0) + let sourceClassId = "SC_CONVERSION_RESULT_QUANTITY_SOURCE" + let sourceClass = (limitedStockClass sourceClassId 1_000.0) with + conversion_rights = [stockClassConversionRightTo destinationClassId] + (capTableWithSource, _) <- setupStockClassWith issuer capTableWithDestination sourceClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithSource "SH_CONVERSION_RESULT_QUANTITY" "Conversion Result Quantity Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_CONVERSION_RESULT_QUANTITY_SOURCE" "SEC_CONVERSION_RESULT_QUANTITY_SOURCE" stakeholderId sourceClassId) with quantity = 50.0) + let conversion = stockConversion "TX_CONVERSION_RESULT_QUANTITY" "SEC_CONVERSION_RESULT_QUANTITY_SOURCE" 50.0 + let resultIssuance = (defaultStockIssuance "TX_CONVERSION_RESULT_QUANTITY_OUTPUT" "TX_CONVERSION_RESULT_QUANTITY_RESULT" stakeholderId destinationClassId) with quantity = 25.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConversion conversion + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + +testStockConversionResultDateMustMatchConversionDate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithDestination, destinationClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_RESULT_DATE_DEST" 1_000.0) + let sourceClassId = "SC_CONVERSION_RESULT_DATE_SOURCE" + let sourceClass = (limitedStockClass sourceClassId 1_000.0) with + conversion_rights = [stockClassConversionRightTo destinationClassId] + (capTableWithSource, _) <- setupStockClassWith issuer capTableWithDestination sourceClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithSource "SH_CONVERSION_RESULT_DATE" "Conversion Result Date Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_CONVERSION_RESULT_DATE_SOURCE" "SEC_CONVERSION_RESULT_DATE_SOURCE" stakeholderId sourceClassId) with quantity = 50.0) + let conversion = stockConversion "TX_CONVERSION_RESULT_DATE" "SEC_CONVERSION_RESULT_DATE_SOURCE" 50.0 + let laterDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let resultIssuance = (defaultStockIssuance "TX_CONVERSION_RESULT_DATE_OUTPUT" "TX_CONVERSION_RESULT_DATE_RESULT" stakeholderId destinationClassId) with + date = laterDate + quantity = 50.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConversion conversion + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + testStockConversionCannotReduceSecurityBelowZero = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -774,6 +844,33 @@ testStockConversionWithAmbiguousConversionRightsFailsForCeiling = script do edits = [] deletes = [] +testStockConversionWithMultipleRightsAcceptsMatchingExplicitResult = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithFirst, firstClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_MULTI_FIRST" 1_000.0) + (capTableWithSecond, secondClassId) <- setupStockClassWith issuer capTableWithFirst (limitedStockClass "SC_CONVERSION_MULTI_SECOND" 1_000.0) + let sourceClassId = "SC_CONVERSION_MULTI_SOURCE" + let sourceClass = (limitedStockClass sourceClassId 1_000.0) with + conversion_rights = + [ stockClassConversionRightTo firstClassId + , stockClassConversionRightTo secondClassId + ] + (capTableWithSource, _) <- setupStockClassWith issuer capTableWithSecond sourceClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithSource "SH_CONVERSION_MULTI" "Conversion Multi Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_CONVERSION_MULTI_SOURCE" "SEC_CONVERSION_MULTI_SOURCE" stakeholderId sourceClassId) with quantity = 50.0) + let conversion = stockConversion "TX_CONVERSION_MULTI" "SEC_CONVERSION_MULTI_SOURCE" 50.0 + let resultIssuance = (defaultStockIssuance "TX_CONVERSION_MULTI_OUTPUT" "TX_CONVERSION_MULTI_RESULT" stakeholderId firstClassId) with quantity = 50.0 + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConversion conversion + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + pure () + testStockConversionWithoutConversionRightsFailsForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -927,6 +1024,33 @@ testNonFixedWarrantExerciseRequiresResultingStockIssuanceForCeiling = script do edits = [] deletes = [] +testWarrantExerciseResultStockClassMustMatchTrigger = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithExpected, expectedClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_RESULT_EXPECTED" 1_000.0) + (capTableWithWrong, wrongClassId) <- setupStockClassWith issuer capTableWithExpected (limitedStockClass "SC_WARRANT_RESULT_WRONG" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithWrong "SH_WARRANT_RESULT_CLASS" "Warrant Result Class Holder" + let issuance = (warrantIssuance "TX_WARRANT_RESULT_CLASS_ISSUED" "WSEC_RESULT_CLASS" stakeholderId expectedClassId 100.0) with + quantity = None + exercise_triggers = [warrantCustomExerciseTrigger "TRIGGER_RESULT_CLASS" expectedClassId] + issuanceResult <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + + let exercise = (warrantExercise "TX_WARRANT_RESULT_CLASS_EXERCISE" "WSEC_RESULT_CLASS" 50.0) with + trigger_id = "TRIGGER_RESULT_CLASS" + quantity = None + let resultIssuance = (defaultStockIssuance "TX_WARRANT_RESULT_CLASS_OUTPUT" "TX_WARRANT_RESULT_CLASS_EXERCISE_RESULT" stakeholderId wrongClassId) with quantity = 50.0 + submitMustFail issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise exercise + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + testWarrantExerciseWithUnknownTriggerFailsForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -1000,6 +1124,27 @@ testEquityCompensationExerciseWithResultingStockIssuanceDoesNotDoubleCount = scr deletes = [] pure () +testEquityCompensationExerciseResultQuantityMustMatchExercise = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_COMP_RESULT_QUANTITY" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_COMP_RESULT_QUANTITY" "Equity Comp Result Quantity Holder" + issuanceResult <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_EQUITY_COMP_RESULT_QUANTITY_GRANTED" "ECSEC_RESULT_QUANTITY" stakeholderId stockClassId 100.0)] + edits = [] + deletes = [] + + let exercise = equityCompensationExercise "TX_EQUITY_COMP_RESULT_QUANTITY_EXERCISE" "ECSEC_RESULT_QUANTITY" 50.0 + let resultIssuance = (defaultStockIssuance "TX_EQUITY_COMP_RESULT_QUANTITY_OUTPUT" "TX_EQUITY_COMP_RESULT_QUANTITY_EXERCISE_RESULT" stakeholderId stockClassId) with quantity = 25.0 + submitMustFail issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationExercise exercise + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + testEquityCompensationReleaseUsesSettlementDateForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -1023,6 +1168,32 @@ testEquityCompensationReleaseUsesSettlementDateForCeiling = script do deletes = [] pure () +testEquityCompensationReleaseResultDateMustMatchSettlementDate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_COMP_RESULT_DATE" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_COMP_RESULT_DATE" "Equity Comp Result Date Holder" + issuanceResult <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (equityCompensationIssuance "TX_EQUITY_COMP_RESULT_DATE_GRANTED" "ECSEC_RESULT_DATE" stakeholderId stockClassId 100.0)] + edits = [] + deletes = [] + + let releaseDate = DT.time (DA.date 2024 Jan 01) 0 0 0 + let settlementDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let laterDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let release = equityCompensationRelease "TX_EQUITY_COMP_RESULT_DATE_RELEASE" "ECSEC_RESULT_DATE" releaseDate settlementDate 50.0 + let resultIssuance = (defaultStockIssuance "TX_EQUITY_COMP_RESULT_DATE_OUTPUT" "TX_EQUITY_COMP_RESULT_DATE_RELEASE_RESULT" stakeholderId stockClassId) with + date = laterDate + quantity = 50.0 + submitMustFail issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationRelease release + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + testStockRetractionReducesIssuedSharesForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -1094,6 +1265,73 @@ testBalanceSecurityReplacementPreservesAuthorizedShareHeadroom = script do deletes = [] pure () +testBalanceSecurityQuantityMustMatchSourceRemainder = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_BALANCE_QUANTITY" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_BALANCE_QUANTITY" "Balance Quantity Holder" + let sourceSecurityId = "SEC_BALANCE_QUANTITY_SOURCE" + let balanceSecurityId = "SEC_BALANCE_QUANTITY_RESULT" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_BALANCE_QUANTITY_SOURCE" sourceSecurityId stakeholderId stockClassId) with quantity = 100.0) + let cancellation = (stockCancellation "TX_BALANCE_QUANTITY_CANCEL" sourceSecurityId 50.0) with + balance_security_id = Some balanceSecurityId + let wrongBalance = (defaultStockIssuance "TX_BALANCE_QUANTITY_RESULT" balanceSecurityId stakeholderId stockClassId) with quantity = 40.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockIssuance wrongBalance + ] + edits = [] + deletes = [] + +testBalanceSecurityStockClassMustMatchSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithSource, sourceClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_BALANCE_CLASS_SOURCE" 1_000.0) + (capTableWithClasses, wrongClassId) <- setupStockClassWith issuer capTableWithSource (limitedStockClass "SC_BALANCE_CLASS_WRONG" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_BALANCE_CLASS" "Balance Class Holder" + let sourceSecurityId = "SEC_BALANCE_CLASS_SOURCE" + let balanceSecurityId = "SEC_BALANCE_CLASS_RESULT" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_BALANCE_CLASS_SOURCE" sourceSecurityId stakeholderId sourceClassId) with quantity = 100.0) + let cancellation = (stockCancellation "TX_BALANCE_CLASS_CANCEL" sourceSecurityId 50.0) with + balance_security_id = Some balanceSecurityId + let wrongBalance = (defaultStockIssuance "TX_BALANCE_CLASS_RESULT" balanceSecurityId stakeholderId wrongClassId) with quantity = 50.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockIssuance wrongBalance + ] + edits = [] + deletes = [] + +testBalanceSecurityDateMustMatchReductionDate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_BALANCE_DATE" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_BALANCE_DATE" "Balance Date Holder" + let sourceSecurityId = "SEC_BALANCE_DATE_SOURCE" + let balanceSecurityId = "SEC_BALANCE_DATE_RESULT" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_BALANCE_DATE_SOURCE" sourceSecurityId stakeholderId stockClassId) with quantity = 100.0) + let cancellation = (stockCancellation "TX_BALANCE_DATE_CANCEL" sourceSecurityId 50.0) with + balance_security_id = Some balanceSecurityId + let laterDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let lateBalance = (defaultStockIssuance "TX_BALANCE_DATE_RESULT" balanceSecurityId stakeholderId stockClassId) with + date = laterDate + quantity = 50.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockIssuance lateBalance + ] + edits = [] + deletes = [] + testStockTransferPartialResultingIssuanceCoverageFails = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_TRANSFER_PARTIAL_RESULTS" 1_000.0) diff --git a/Test/daml/OpenCapTable/TestStockConversion.daml b/Test/daml/OpenCapTable/TestStockConversion.daml index ecae377e..98dcca7b 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,23 @@ 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 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.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 balanceIssuance.security_id + , CT.OcfCreateStockIssuance balanceIssuance + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index 0928f522..7dd2d7a1 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -2,6 +2,7 @@ module OpenCapTable.TestStockPlanReservedShares where import qualified Fairmint.OpenCapTable.CapTable as CT import Fairmint.OpenCapTable.OCF.EquityCompensationCancellation (EquityCompensationCancellationOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationExercise (EquityCompensationExerciseOcfData(..)) import Fairmint.OpenCapTable.OCF.EquityCompensationRetraction (EquityCompensationRetractionOcfData(..)) import Fairmint.OpenCapTable.OCF.EquityCompensationTransfer (EquityCompensationTransferOcfData(..)) import Fairmint.OpenCapTable.OCF.StockReissuance (StockReissuanceOcfData(..)) @@ -46,6 +47,16 @@ equityCompensationCancellation cancellationId securityId quantity = EquityCompen reason_text = "Cancelled equity compensation" comments = [] +planEquityCompensationExercise : Text -> Text -> Decimal -> EquityCompensationExerciseOcfData +planEquityCompensationExercise exerciseId securityId quantity = EquityCompensationExerciseOcfData with + id = exerciseId + date = defaultTestDate + security_id = securityId + quantity = quantity + resulting_security_ids = [] + consideration_text = None + comments = [] + planStockTransfer : Text -> Text -> Text -> Decimal -> StockTransferOcfData planStockTransfer transferId securityId resultingSecurityId quantity = StockTransferOcfData with id = transferId @@ -136,6 +147,127 @@ testStockPlanReservedShares_EquityCompensationCannotExceedInitialPool = script d edits = [] deletes = [] +testStockPlanReservedShares_StockIssuanceClassMustBelongToPlan = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithPlanClass, planClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + (capTableReady, otherClassId) <- setupStockClassWithId issuer capTableWithPlanClass "SC_PLAN_MEMBERSHIP_OTHER" + let stockPlanId = "PLAN_STOCK_CLASS_MEMBERSHIP" + let plan = (defaultStockPlan stockPlanId planClassId) with initial_shares_reserved = 100.0 + let issuance = (defaultStockIssuance "TX_STOCK_CLASS_OUTSIDE_PLAN" "SEC-STOCK-CLASS-OUTSIDE-PLAN" stakeholderId otherClassId) with + stock_plan_id = Some stockPlanId + quantity = 50.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance issuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityCompensationClassMustBelongToPlan = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithPlanClass, planClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + (capTableReady, otherClassId) <- setupStockClassWithId issuer capTableWithPlanClass "SC_EQUITY_PLAN_MEMBERSHIP_OTHER" + let stockPlanId = "PLAN_EQUITY_CLASS_MEMBERSHIP" + let plan = (defaultStockPlan stockPlanId planClassId) with initial_shares_reserved = 100.0 + let issuance = stockPlanEquityCompensationIssuance + "TX_EQUITY_CLASS_OUTSIDE_PLAN" + "SEC-EQUITY-CLASS-OUTSIDE-PLAN" + stakeholderId + otherClassId + stockPlanId + 50.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ExercisedEquityCannotBeCancelledAndReturnedAgain = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_SOURCE_HISTORY" + let securityId = "SEC-EQUITY-SOURCE-HISTORY" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = stockPlanEquityCompensationIssuance + "TX_EQUITY_SOURCE_HISTORY" + securityId + stakeholderId + stockClassId + stockPlanId + 100.0 + initialResult <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd initialResult.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationExercise (planEquityCompensationExercise "TX_EQUITY_SOURCE_EXERCISE" securityId 100.0) + , CT.OcfCreateEquityCompensationCancellation (equityCompensationCancellation "TX_EQUITY_SOURCE_CANCEL" securityId 100.0) + , CT.OcfCreateStockPlanReturnToPool (stockPlanReturnToPool "TX_EQUITY_SOURCE_RETURN" securityId stockPlanId 100.0) + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ExercisedEquityCannotBeRetracted = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_RETRACTION_HISTORY" + let securityId = "SEC-EQUITY-RETRACTION-HISTORY" + let resultSecurityId = "SEC-EQUITY-RETRACTION-STOCK" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let issuance = stockPlanEquityCompensationIssuance + "TX_EQUITY_RETRACTION_HISTORY" + securityId + stakeholderId + stockClassId + stockPlanId + 100.0 + initialResult <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + + let exercise = (planEquityCompensationExercise "TX_EQUITY_RETRACTION_EXERCISE" securityId 40.0) with + resulting_security_ids = [resultSecurityId] + let resultIssuance = (defaultStockIssuance "TX_EQUITY_RETRACTION_STOCK" resultSecurityId stakeholderId stockClassId) with quantity = 40.0 + exerciseResult <- submit issuer do + exerciseCmd initialResult.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationExercise exercise + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + + let retraction = EquityCompensationRetractionOcfData with + id = "TX_EQUITY_RETRACTION_AFTER_EXERCISE" + date = defaultTestDate + security_id = securityId + reason_text = "Cannot retract exercised equity compensation" + comments = [] + submitMustFail issuer do + exerciseCmd exerciseResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationRetraction retraction] + edits = [] + deletes = [] + testStockPlanReservedShares_NegativeEquityCompensationCannotOffsetOverallocatedPool = 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..0546623d 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 balanceIssuance.security_id + 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/TestStockTransfer.daml b/Test/daml/OpenCapTable/TestStockTransfer.daml index 5f65170d..d8474d1e 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 @@ -68,17 +68,24 @@ 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 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 balanceIssuance.security_id + consideration_text = Some "Transfer of 75 shares at $0.0001 per share for aggregate consideration of $0.01." + , CT.OcfCreateStockIssuance balanceIssuance + ] edits = [] deletes = [] pure () diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 1a62035a..f6481d39 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -18,7 +18,7 @@ import DA.Foldable (mapA_) import DA.Optional (fromOptional, isNone, isSome) import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfConversionMechanism(..), OcfConversionTrigger(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) +import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfConversionMechanism(..), OcfConversionTrigger(..), OcfConversionTriggerType(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..)) import Fairmint.OpenCapTable.OCF.Issuer (Issuer(..), IssuerOcfData) @@ -131,6 +131,11 @@ data StockClassShareLot = StockClassShareLot with quantity: Decimal deriving (Eq, Show) +data StockConversionResultTerm = StockConversionResultTerm with + stock_class_id: Text + quantity: Decimal + deriving (Eq, Show) + data StockClassShareEventIndex = StockClassShareEventIndex with stock_security_events_by_security_id: Map Text [StockClassShareEvent] stock_class_split_events_by_stock_class_id: Map Text [StockClassShareEvent] @@ -315,7 +320,7 @@ stockClassConversionOutputQuantity : StockClassOcfData -> OcfStockClassConversionRight -> StockConversionOcfData -> - Update Decimal + Update (Optional Decimal) stockClassConversionOutputQuantity maps sourceStockClass conversionRight conversionData = case conversionRight.conversion_mechanism of OcfConversionMechanismRatioConversion -> do @@ -324,13 +329,57 @@ stockClassConversionOutputQuantity maps sourceStockClass conversionRight convers Some adjustment -> Some adjustment.new_ratio_conversion_mechanism.ratio None -> conversionRight.ratio case ratioOpt of - Some ratio -> pure (conversionData.quantity_converted * ratio.numerator / ratio.denominator) - None -> do - assertMsg ("Stock conversion output cannot be reconstructed because ratio conversion has no ratio: " <> sourceStockClass.id) False - pure 0.0 - _ -> do - assertMsg ("Stock conversion output cannot be reconstructed for non-ratio conversion mechanism: " <> sourceStockClass.id) False - pure 0.0 + Some ratio -> pure (Some (conversionData.quantity_converted * ratio.numerator / ratio.denominator)) + None -> pure None + _ -> pure None + +stockClassConversionRightDateEligible : Time -> OcfStockClassConversionRight -> Bool +stockClassConversionRightDateEligible conversionDate conversionRight = + let trigger = conversionRight.conversion_trigger + beforeExpiration = case conversionRight.expires_at of + Some expiresAt -> conversionDate <= expiresAt + None -> True + triggerDateEligible = case trigger.type_ of + OcfTriggerTypeTypeAutomaticOnDate -> trigger.trigger_date == Some conversionDate + OcfTriggerTypeTypeElectiveInRange -> case (trigger.start_date, trigger.end_date) of + (Some startDate, Some endDate) -> startDate <= conversionDate && conversionDate <= endDate + _ -> False + _ -> True + in beforeExpiration && triggerDateEligible + +stockConversionResultTerms : + CapTableMaps -> + Map Text StockClassOcfData -> + StockIssuanceOcfData -> + StockConversionOcfData -> + Update [StockConversionResultTerm] +stockConversionResultTerms maps stockClassIndex sourceIssuanceData conversionData = + case Map.lookup sourceIssuanceData.stock_class_id stockClassIndex of + Some sourceStockClass -> do + terms <- foldlA + (\resultTerms conversionRight -> + if stockClassConversionRightDateEligible conversionData.date conversionRight + then do + quantityOpt <- stockClassConversionOutputQuantity maps sourceStockClass conversionRight conversionData + case quantityOpt of + Some quantity -> do + assertMsg + ("Stock conversion output references missing stock class: " <> conversionRight.converts_to_stock_class_id) + (isSome (Map.lookup conversionRight.converts_to_stock_class_id stockClassIndex)) + pure ((StockConversionResultTerm with + stock_class_id = conversionRight.converts_to_stock_class_id + quantity = quantity) :: resultTerms) + None -> pure resultTerms + else pure resultTerms) + [] + sourceStockClass.conversion_rights + assertMsg + ("Stock conversion has no computable date-eligible conversion right: " <> conversionData.id) + (not (null terms)) + pure terms + None -> do + assertMsg ("Stock conversion source stock class not found: " <> sourceIssuanceData.stock_class_id) False + pure [] stockConversionOutputShareLot : CapTableMaps -> @@ -338,27 +387,20 @@ stockConversionOutputShareLot : StockIssuanceOcfData -> StockConversionOcfData -> Update (Optional StockClassShareLot) -stockConversionOutputShareLot maps stockClassIndex sourceIssuanceData conversionData = - case Map.lookup sourceIssuanceData.stock_class_id stockClassIndex of - Some sourceStockClass -> - case sourceStockClass.conversion_rights of - [conversionRight] -> do - assertMsg - ("Stock conversion output references missing stock class: " <> conversionRight.converts_to_stock_class_id) - (isSome (Map.lookup conversionRight.converts_to_stock_class_id stockClassIndex)) - outputQuantity <- stockClassConversionOutputQuantity maps sourceStockClass conversionRight conversionData - pure (Some (StockClassShareLot with - stock_class_id = conversionRight.converts_to_stock_class_id - security_id = None - event_date = conversionData.date - quantity = outputQuantity)) - [] -> do - assertMsg ("Stock conversion output cannot be reconstructed because stock class has no conversion rights: " <> sourceStockClass.id) False - pure None - _ -> do - assertMsg ("Stock conversion output cannot be reconstructed because stock class has multiple conversion rights: " <> sourceStockClass.id) False - pure None - None -> pure None +stockConversionOutputShareLot maps stockClassIndex sourceIssuanceData conversionData = do + resultTerms <- stockConversionResultTerms maps stockClassIndex sourceIssuanceData conversionData + case resultTerms of + [resultTerm] -> pure (Some (StockClassShareLot with + stock_class_id = resultTerm.stock_class_id + security_id = None + event_date = conversionData.date + quantity = resultTerm.quantity)) + [] -> pure None + _ -> do + assertMsg + ("Stock conversion output cannot be reconstructed without explicit results because multiple conversion rights are date-eligible: " <> conversionData.id) + False + pure None warrantExerciseTrigger : WarrantIssuanceOcfData -> Text -> Optional OcfConversionTrigger warrantExerciseTrigger issuanceData triggerId = @@ -436,6 +478,121 @@ equityCompensationReleaseOutputShareLot issuanceData releaseData = quantity = releaseData.quantity) None -> None +validateStockResultIssuances : + CapTableMaps -> + Text -> + Time -> + Optional Text -> + Optional Decimal -> + [Text] -> + Update () +validateStockResultIssuances maps label expectedDate expectedStockClassIdOpt expectedQuantityOpt securityIds = do + assertMsg (label <> " has no resulting stock issuance") (not (null securityIds)) + actualQuantity <- foldlA + (\total securityId -> + case Map.lookup securityId maps.stock_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertMsg + (label <> " result date mismatch for " <> securityId <> ": expected " <> show expectedDate <> ", found " <> show issuance.issuance_data.date) + (issuance.issuance_data.date == expectedDate) + case expectedStockClassIdOpt of + Some expectedStockClassId -> + assertMsg + (label <> " result stock class mismatch for " <> securityId <> ": expected " <> expectedStockClassId <> ", found " <> issuance.issuance_data.stock_class_id) + (issuance.issuance_data.stock_class_id == expectedStockClassId) + None -> pure () + assertNonNegativeQuantity (label <> " result quantity for " <> securityId) issuance.issuance_data.quantity + pure (total + issuance.issuance_data.quantity) + None -> do + assertMsg (label <> " result stock issuance not found: " <> securityId) False + pure total) + 0.0 + securityIds + case expectedQuantityOpt of + Some expectedQuantity -> + assertMsg + (label <> " resulting quantity mismatch: expected " <> show expectedQuantity <> ", found " <> show actualQuantity) + (actualQuantity == expectedQuantity) + None -> pure () + +validateStockConversionResultIssuances : + CapTableMaps -> + StockConversionOcfData -> + [StockConversionResultTerm] -> + Update () +validateStockConversionResultIssuances maps conversionData expectedTerms = do + (actualStockClassIdOpt, actualQuantity) <- foldlA + (\(stockClassIdOpt, total) securityId -> + case Map.lookup securityId maps.stock_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertMsg + ("Stock conversion result date mismatch for " <> securityId <> ": expected " <> show conversionData.date <> ", found " <> show issuance.issuance_data.date) + (issuance.issuance_data.date == conversionData.date) + case stockClassIdOpt of + Some stockClassId -> + assertMsg + ("Stock conversion results must use one stock class for " <> conversionData.id) + (issuance.issuance_data.stock_class_id == stockClassId) + None -> pure () + assertNonNegativeQuantity ("Stock conversion result quantity for " <> securityId) issuance.issuance_data.quantity + pure (Some issuance.issuance_data.stock_class_id, total + issuance.issuance_data.quantity) + None -> do + assertMsg ("Stock conversion result security not found: " <> securityId) False + pure (stockClassIdOpt, total)) + (None, 0.0) + conversionData.resulting_security_ids + case actualStockClassIdOpt of + Some actualStockClassId -> + assertMsg + ("Stock conversion results do not match any date-eligible conversion right for " <> conversionData.id <> ": found class " <> actualStockClassId <> " and quantity " <> show actualQuantity) + (any + (\expectedTerm -> + expectedTerm.stock_class_id == actualStockClassId && + expectedTerm.quantity == actualQuantity) + expectedTerms) + None -> + assertMsg ("Stock conversion has no resulting security: " <> conversionData.id) False + +validateWarrantExerciseResultIssuances : CapTableMaps -> WarrantIssuanceOcfData -> WarrantExerciseOcfData -> Update () +validateWarrantExerciseResultIssuances maps issuanceData exerciseData = + case warrantExerciseTrigger issuanceData exerciseData.trigger_id of + Some trigger -> case trigger.conversion_right of + OcfRightWarrant conversionRight -> + let expectedQuantityOpt = case conversionRight.conversion_mechanism of + OcfWarrantMechanismFixedAmount _ -> Some (warrantExerciseQuantity issuanceData exerciseData conversionRight) + _ -> None + in validateStockResultIssuances + maps + ("Warrant exercise " <> exerciseData.id) + exerciseData.date + conversionRight.converts_to_stock_class_id + expectedQuantityOpt + exerciseData.resulting_security_ids + _ -> assertMsg ("Warrant exercise trigger is not a warrant conversion right: " <> exerciseData.trigger_id) False + None -> assertMsg ("Warrant exercise trigger not found: " <> exerciseData.trigger_id) False + +validateEquityCompensationExerciseResultIssuances : CapTableMaps -> EquityCompensationIssuanceOcfData -> EquityCompensationExerciseOcfData -> Update () +validateEquityCompensationExerciseResultIssuances maps issuanceData exerciseData = + validateStockResultIssuances + maps + ("Equity compensation exercise " <> exerciseData.id) + exerciseData.date + issuanceData.stock_class_id + (Some exerciseData.quantity) + exerciseData.resulting_security_ids + +validateEquityCompensationReleaseResultIssuances : CapTableMaps -> EquityCompensationIssuanceOcfData -> EquityCompensationReleaseOcfData -> Update () +validateEquityCompensationReleaseResultIssuances maps issuanceData releaseData = + validateStockResultIssuances + maps + ("Equity compensation release " <> releaseData.id) + releaseData.settlement_date + issuanceData.stock_class_id + (Some releaseData.quantity) + releaseData.resulting_security_ids + addOptionalStockClassShareLot : Optional StockClassShareLot -> Map Text [StockClassShareLot] -> @@ -462,15 +619,23 @@ stockClassShareLotsByStockClass maps stockClassIndex = do conversion <- fetch conversionCid assertNoPartialResultingStockIssuanceCoverage ("Stock conversion " <> conversion.conversion_data.id) maps conversion.conversion_data.resulting_security_ids if allResultingStockIssuancesExist maps conversion.conversion_data.resulting_security_ids - then pure index - else case Map.lookup conversion.conversion_data.security_id maps.stock_issuances_by_security_id of - Some sourceIssuanceCid -> do - sourceIssuance <- fetch sourceIssuanceCid - lotOpt <- stockConversionOutputShareLot maps stockClassIndex sourceIssuance.issuance_data conversion.conversion_data - pure (addOptionalStockClassShareLot lotOpt index) - None -> do - assertMsg ("Stock conversion references missing source stock issuance: " <> conversion.conversion_data.security_id) False - pure index) + then case Map.lookup conversion.conversion_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + expectedTerms <- stockConversionResultTerms maps stockClassIndex sourceIssuance.issuance_data conversion.conversion_data + validateStockConversionResultIssuances maps conversion.conversion_data expectedTerms + pure index + None -> do + assertMsg ("Stock conversion references missing source stock issuance: " <> conversion.conversion_data.security_id) False + pure index + else case Map.lookup conversion.conversion_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + lotOpt <- stockConversionOutputShareLot maps stockClassIndex sourceIssuance.issuance_data conversion.conversion_data + pure (addOptionalStockClassShareLot lotOpt index) + None -> do + assertMsg ("Stock conversion references missing source stock issuance: " <> conversion.conversion_data.security_id) False + pure index) stockIssuanceLots (Map.values maps.stock_conversions) @@ -479,13 +644,22 @@ stockClassShareLotsByStockClass maps stockClassIndex = do exercise <- fetch exerciseCid assertNoPartialResultingStockIssuanceCoverage ("Warrant exercise " <> exercise.exercise_data.id) maps exercise.exercise_data.resulting_security_ids if allResultingStockIssuancesExist maps exercise.exercise_data.resulting_security_ids - then pure index + then case Map.lookup exercise.exercise_data.security_id maps.warrant_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + validateWarrantExerciseResultIssuances maps issuance.issuance_data exercise.exercise_data + pure index + None -> do + assertMsg ("Warrant exercise references missing warrant issuance: " <> exercise.exercise_data.security_id) False + pure index else case Map.lookup exercise.exercise_data.security_id maps.warrant_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid lotOpt <- warrantExerciseOutputShareLot issuance.issuance_data exercise.exercise_data pure (addOptionalStockClassShareLot lotOpt index) - None -> pure index) + None -> do + assertMsg ("Warrant exercise references missing warrant issuance: " <> exercise.exercise_data.security_id) False + pure index) stockConversionLots (Map.values maps.warrant_exercises) @@ -494,13 +668,22 @@ stockClassShareLotsByStockClass maps stockClassIndex = do exercise <- fetch exerciseCid assertNoPartialResultingStockIssuanceCoverage ("Equity compensation exercise " <> exercise.exercise_data.id) maps exercise.exercise_data.resulting_security_ids if allResultingStockIssuancesExist maps exercise.exercise_data.resulting_security_ids - then pure index + then case Map.lookup exercise.exercise_data.security_id maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + validateEquityCompensationExerciseResultIssuances maps issuance.issuance_data exercise.exercise_data + pure index + None -> do + assertMsg ("Equity compensation exercise references missing issuance: " <> exercise.exercise_data.security_id) False + pure index else case Map.lookup exercise.exercise_data.security_id maps.equity_compensation_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid lotOpt <- equityCompensationExerciseOutputShareLot issuance.issuance_data exercise.exercise_data pure (addOptionalStockClassShareLot lotOpt index) - None -> pure index) + None -> do + assertMsg ("Equity compensation exercise references missing issuance: " <> exercise.exercise_data.security_id) False + pure index) warrantExerciseLots (Map.values maps.equity_compensation_exercises) @@ -509,12 +692,21 @@ stockClassShareLotsByStockClass maps stockClassIndex = do release <- fetch releaseCid assertNoPartialResultingStockIssuanceCoverage ("Equity compensation release " <> release.release_data.id) maps release.release_data.resulting_security_ids if allResultingStockIssuancesExist maps release.release_data.resulting_security_ids - then pure index + then case Map.lookup release.release_data.security_id maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + validateEquityCompensationReleaseResultIssuances maps issuance.issuance_data release.release_data + pure index + None -> do + assertMsg ("Equity compensation release references missing issuance: " <> release.release_data.security_id) False + pure index else case Map.lookup release.release_data.security_id maps.equity_compensation_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid pure (addOptionalStockClassShareLot (equityCompensationReleaseOutputShareLot issuance.issuance_data release.release_data) index) - None -> pure index) + None -> do + assertMsg ("Equity compensation release references missing issuance: " <> release.release_data.security_id) False + pure index) equityExerciseLots (Map.values maps.equity_compensation_releases) @@ -967,6 +1159,129 @@ stockClassShareLotQuantityAt eventIndex lot asOf = (filter (stockClassShareEventOnOrBefore asOf) (stockClassShareLotEvents eventIndex lot))) else Some 0.0 +stockSecurityQuantityBeforeTerminal : + StockClassShareEventIndex -> + StockIssuanceOcfData -> + Time -> + Update Decimal +stockSecurityQuantityBeforeTerminal eventIndex issuanceData eventDate = + let targetSortKey = (eventDate, 2, 1) + lot = stockIssuanceShareLot issuanceData + priorEvents = filter + (\event -> stockClassShareEventSortKey event < targetSortKey) + (sortOn stockClassShareEventSortKey (stockClassShareLotEvents eventIndex lot)) + in case foldl applyStockClassShareEventOptional (Some 0.0) priorEvents of + Some quantity -> pure quantity + None -> do + assertMsg ("Stock security quantity history is invalid for " <> issuanceData.security_id <> " before " <> show eventDate) False + pure 0.0 + +validateStockBalanceSuccessor : + CapTableMaps -> + Text -> + StockIssuanceOcfData -> + Time -> + Decimal -> + Text -> + Update () +validateStockBalanceSuccessor maps label sourceIssuanceData eventDate expectedQuantity balanceSecurityId = + case Map.lookup balanceSecurityId maps.stock_issuances_by_security_id of + Some balanceIssuanceCid -> do + balanceIssuance <- fetch balanceIssuanceCid + assertMsg + (label <> " balance result date mismatch for " <> balanceSecurityId <> ": expected " <> show eventDate <> ", found " <> show balanceIssuance.issuance_data.date) + (balanceIssuance.issuance_data.date == eventDate) + assertMsg + (label <> " balance stock class mismatch for " <> balanceSecurityId <> ": expected " <> sourceIssuanceData.stock_class_id <> ", found " <> balanceIssuance.issuance_data.stock_class_id) + (balanceIssuance.issuance_data.stock_class_id == sourceIssuanceData.stock_class_id) + assertMsg + (label <> " balance quantity mismatch for " <> balanceSecurityId <> ": expected " <> show expectedQuantity <> ", found " <> show balanceIssuance.issuance_data.quantity) + (balanceIssuance.issuance_data.quantity == expectedQuantity) + None -> + assertMsg (label <> " balance stock issuance not found: " <> balanceSecurityId) 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 + currentQuantity <- stockSecurityQuantityBeforeTerminal eventIndex sourceIssuanceData eventDate + let expectedQuantity = currentQuantity - reductionQuantity + assertMsg + (label <> " exceeds the current source quantity for " <> sourceIssuanceData.security_id) + (expectedQuantity >= 0.0) + validateStockBalanceSuccessor maps label sourceIssuanceData eventDate expectedQuantity securityId + None -> pure () + +validateStockBalanceSuccessors : StockClassShareEventIndex -> CapTableMaps -> Update () +validateStockBalanceSuccessors eventIndex maps = do + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + case cancellation.cancellation_data.balance_security_id of + Some _ -> case Map.lookup cancellation.cancellation_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + validateStockTerminalBalanceSuccessor + eventIndex maps ("Stock cancellation " <> cancellation.cancellation_data.id) + sourceIssuance.issuance_data cancellation.cancellation_data.date cancellation.cancellation_data.quantity + cancellation.cancellation_data.balance_security_id + None -> assertMsg ("Stock cancellation source issuance not found: " <> cancellation.cancellation_data.security_id) False + None -> pure ()) + (Map.values maps.stock_cancellations) + + mapA_ + (\repurchaseCid -> do + repurchase <- fetch repurchaseCid + case repurchase.repurchase_data.balance_security_id of + Some _ -> case Map.lookup repurchase.repurchase_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + validateStockTerminalBalanceSuccessor + eventIndex maps ("Stock repurchase " <> repurchase.repurchase_data.id) + sourceIssuance.issuance_data repurchase.repurchase_data.date repurchase.repurchase_data.quantity + repurchase.repurchase_data.balance_security_id + None -> assertMsg ("Stock repurchase source issuance not found: " <> repurchase.repurchase_data.security_id) False + None -> pure ()) + (Map.values maps.stock_repurchases) + + mapA_ + (\conversionCid -> do + conversion <- fetch conversionCid + case conversion.conversion_data.balance_security_id of + Some _ -> case Map.lookup conversion.conversion_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + validateStockTerminalBalanceSuccessor + eventIndex maps ("Stock conversion " <> conversion.conversion_data.id) + sourceIssuance.issuance_data conversion.conversion_data.date conversion.conversion_data.quantity_converted + conversion.conversion_data.balance_security_id + None -> assertMsg ("Stock conversion source issuance not found: " <> conversion.conversion_data.security_id) False + None -> pure ()) + (Map.values maps.stock_conversions) + + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + case transfer.transfer_data.balance_security_id of + Some _ -> case Map.lookup transfer.transfer_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + validateStockTerminalBalanceSuccessor + eventIndex maps ("Stock transfer " <> transfer.transfer_data.id) + sourceIssuance.issuance_data transfer.transfer_data.date transfer.transfer_data.quantity + transfer.transfer_data.balance_security_id + None -> assertMsg ("Stock transfer source issuance not found: " <> transfer.transfer_data.security_id) False + None -> pure ()) + (Map.values maps.stock_transfers) + addOptionalDecimal : Optional Decimal -> Optional Decimal -> Optional Decimal addOptionalDecimal leftOpt rightOpt = case (leftOpt, rightOpt) of @@ -1745,6 +2060,47 @@ equityCompensationQuantityAt maps issuanceData asOf = do (sortOn stockClassShareEventSortKey (filter (stockClassShareEventOnOrBefore asOf) events))) else pure (Some 0.0) +validateStockPlanEquityCompensationSourceQuantities : CapTableMaps -> Update () +validateStockPlanEquityCompensationSourceQuantities maps = + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + case issuance.issuance_data.stock_plan_id of + Some _ -> do + hasExercise <- foldlA + (\found exerciseCid -> do + exercise <- fetch exerciseCid + pure (found || exercise.exercise_data.security_id == issuance.issuance_data.security_id)) + False + (Map.values maps.equity_compensation_exercises) + hasRelease <- foldlA + (\found releaseCid -> do + release <- fetch releaseCid + pure (found || release.release_data.security_id == issuance.issuance_data.security_id)) + False + (Map.values maps.equity_compensation_releases) + hasRetraction <- foldlA + (\found retractionCid -> do + retraction <- fetch retractionCid + pure (found || retraction.retraction_data.security_id == issuance.issuance_data.security_id)) + False + (Map.values maps.equity_compensation_retractions) + assertMsg + ("Stock-plan equity compensation cannot be retracted after producing stock: " <> issuance.issuance_data.security_id) + (not (hasRetraction && (hasExercise || hasRelease))) + currentQuantityOpt <- equityCompensationCurrentQuantity maps issuance.issuance_data + case currentQuantityOpt of + Some currentQuantity -> + assertNonNegativeQuantity + ("Stock-plan equity compensation source quantity for " <> issuance.issuance_data.security_id) + currentQuantity + None -> + assertMsg + ("Stock-plan equity compensation source was reduced below zero: " <> issuance.issuance_data.security_id) + False + None -> pure ()) + (Map.values maps.equity_compensation_issuances) + stockPlanReturnableSplitEvents : CapTableMaps -> Optional Text -> Update [StockClassShareEvent] stockPlanReturnableSplitEvents maps stockClassIdOpt = case stockClassIdOpt of Some stockClassId -> @@ -1951,6 +2307,44 @@ validateStockPlanAllocationReferences maps netAllocatedQuantityIndex = (isSome (Map.lookup stockPlanId maps.stock_plans))) (Map.toList netAllocatedQuantityIndex) +assertStockPlanIncludesClass : Text -> CapTableMaps -> Text -> Text -> Update () +assertStockPlanIncludesClass label maps stockPlanId stockClassId = + case Map.lookup stockPlanId maps.stock_plans of + Some stockPlanCid -> do + stockPlan <- fetch stockPlanCid + assertMsg + (label <> " references stock plan " <> stockPlanId <> " outside stock class " <> stockClassId) + (stockClassId `elem` stockPlan.plan_data.stock_class_ids) + None -> assertMsg (label <> " references missing stock plan " <> stockPlanId) False + +validateStockPlanAllocationClasses : CapTableMaps -> Update () +validateStockPlanAllocationClasses maps = do + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + case issuance.issuance_data.stock_plan_id of + Some stockPlanId -> + assertStockPlanIncludesClass + ("Stock issuance " <> issuance.issuance_data.id) + maps + stockPlanId + issuance.issuance_data.stock_class_id + None -> pure ()) + (Map.values maps.stock_issuances) + + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + case (issuance.issuance_data.stock_plan_id, issuance.issuance_data.stock_class_id) of + (Some stockPlanId, Some stockClassId) -> + assertStockPlanIncludesClass + ("Equity compensation issuance " <> issuance.issuance_data.id) + maps + stockPlanId + stockClassId + _ -> pure ()) + (Map.values maps.equity_compensation_issuances) + assertStockPlanEffectiveForAllocation : Text -> CapTableMaps -> Text -> Time -> Update () assertStockPlanEffectiveForAllocation label maps stockPlanId allocationDate = case Map.lookup stockPlanId maps.stock_plans of @@ -2117,7 +2511,9 @@ validateStockPlanPoolCeilings : StockClassShareEventIndex -> CapTableMaps -> Upd validateStockPlanPoolCeilings eventIndex maps = do adjustmentIndex <- stockPlanPoolAdjustmentIndex maps adjustmentHistory <- stockPlanPoolAdjustmentHistoryIndex maps + validateStockPlanAllocationClasses maps validateStockPlanAllocationEffectiveDates maps + validateStockPlanEquityCompensationSourceQuantities maps validateStockPlanReturnToPoolQuantities maps validateStockPlanReservedShares eventIndex maps adjustmentIndex validateStockPlanReservedSharesAtDates eventIndex maps adjustmentHistory @@ -2129,6 +2525,7 @@ validateAuthorizedShareCeilings : Update () validateAuthorizedShareCeilings maps adjustmentIndex issuerCid = do eventIndex <- stockClassShareEventIndex maps + validateStockBalanceSuccessors eventIndex maps stockClassIndex <- stockClassDataIndex maps stockClasses <- stockClassDataList maps lotsByStockClass <- stockClassShareLotsByStockClass maps stockClassIndex From d63f6900f04ecedec6ed41098b93c3b732141dcb Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 13:09:55 -0400 Subject: [PATCH 35/62] chore: refresh OCP DAR backup --- dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 697c9132..48ab662c 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae78edbb7206da778a31c46ace2cda66c33805549c5efadd4f93b68ffdd94d65 -size 2692310 +oid sha256:cca4e1bf7f72372aeb67bfdbfa15d99a85041afc31377a907d5745d39778ea43 +size 2736924 diff --git a/dars/dars.lock b/dars/dars.lock index 19a62d7b..ce976ea2 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "ae78edbb7206da778a31c46ace2cda66c33805549c5efadd4f93b68ffdd94d65", - "size": 2692310, + "sha256": "cca4e1bf7f72372aeb67bfdbfa15d99a85041afc31377a907d5745d39778ea43", + "size": 2736924, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-10T13:44:14Z", "networks": [] From ac8a405ee15cff886fccfe6f4b3f784267955ef5 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 13:36:01 -0400 Subject: [PATCH 36/62] fix: close remaining share ceiling gaps --- .../TestStockClassAuthorizedShares.daml | 19 + .../TestStockPlanReservedShares.daml | 182 +++++++++ .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 350 +++++++++++++++++- 5 files changed, 540 insertions(+), 19 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index e0086270..aabc0a71 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -439,6 +439,25 @@ testStockClassDeleteWithLiveStockIssuanceFailsForCeiling = script do edits = [] deletes = [CT.OcfDeleteStockClass stockClassId] +testStockClassDeleteWithAuthorizedSharesAdjustmentFailsForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_DELETE_WITH_ADJUSTMENT" 100.0) + result <- submit issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassAuthorizedSharesAdjustment + (stockClassAuthorizedSharesAdjustment "TX_DELETE_WITH_ADJUSTMENT" stockClassId 150.0) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockClass stockClassId] + testSameDateLowerStockClassAuthorizedSharesAdjustmentWins = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index 7dd2d7a1..7af12beb 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -681,6 +681,87 @@ testStockPlanReservedShares_EquityCompensationRetractionRemovesPlanAllocation = deletes = [] pure () +testStockPlanReservedShares_StockRetractionDateChecksHistoricalAllocation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_STOCK_RETRACTION_HISTORY" + let securityId = "SEC-STOCK-RETRACTION-HISTORY" + let returnDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let retractionDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let replacementDate = DT.time (DA.date 2024 Jan 04) 0 0 0 + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (defaultStockIssuance "TX_STOCK_RETRACTION_HISTORY_SOURCE" securityId stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + let replacementIssuance = (defaultStockIssuance "TX_STOCK_RETRACTION_HISTORY_REPLACEMENT" "SEC-STOCK-RETRACTION-HISTORY-REPLACEMENT" stakeholderId stockClassId) with + date = replacementDate + stock_plan_id = Some stockPlanId + quantity = 100.0 + let retraction = StockRetractionOcfData with + id = "TX_STOCK_RETRACTION_HISTORY_RETRACT" + date = retractionDate + security_id = securityId + reason_text = "Retracted after return" + comments = [] + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance sourceIssuance + , CT.OcfCreateStockCancellation ((stockCancellation "TX_STOCK_RETRACTION_HISTORY_CANCEL" securityId 100.0) with date = returnDate) + , CT.OcfCreateStockPlanReturnToPool ((stockPlanReturnToPool "TX_STOCK_RETRACTION_HISTORY_RETURN" securityId stockPlanId 100.0) with date = returnDate) + , CT.OcfCreateStockRetraction retraction + , CT.OcfCreateStockIssuance replacementIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityRetractionDateChecksHistoricalAllocation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_RETRACTION_DATE_HISTORY" + let securityId = "SEC-EQUITY-RETRACTION-DATE-HISTORY" + let returnDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let retractionDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let replacementDate = DT.time (DA.date 2024 Jan 04) 0 0 0 + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance + "TX_EQUITY_RETRACTION_DATE_HISTORY_SOURCE" + securityId + stakeholderId + stockClassId + stockPlanId + 100.0 + let replacementIssuance = (stockPlanEquityCompensationIssuance + "TX_EQUITY_RETRACTION_DATE_HISTORY_REPLACEMENT" + "SEC-EQUITY-RETRACTION-DATE-HISTORY-REPLACEMENT" + stakeholderId + stockClassId + stockPlanId + 100.0) with date = replacementDate + let retraction = EquityCompensationRetractionOcfData with + id = "TX_EQUITY_RETRACTION_DATE_HISTORY_RETRACT" + date = retractionDate + security_id = securityId + reason_text = "Retracted after return" + comments = [] + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + , CT.OcfCreateEquityCompensationCancellation + ((equityCompensationCancellation "TX_EQUITY_RETRACTION_DATE_HISTORY_CANCEL" securityId 100.0) with date = returnDate) + , CT.OcfCreateStockPlanReturnToPool + ((stockPlanReturnToPool "TX_EQUITY_RETRACTION_DATE_HISTORY_RETURN" securityId stockPlanId 100.0) with date = returnDate) + , CT.OcfCreateEquityCompensationRetraction retraction + , CT.OcfCreateEquityCompensationIssuance replacementIssuance + ] + edits = [] + deletes = [] + testStockPlanReservedShares_RetractedEquityCompensationCannotBeReturnedToPool = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table @@ -857,6 +938,56 @@ testStockPlanReservedShares_StockTransferSuccessorDoesNotDoubleAllocate = script deletes = [] pure () +testStockPlanReservedShares_InheritedStockSuccessorCanReturnCancelledShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockPlanId = "PLAN_INHERITED_STOCK_RETURN" + (capTableIssued, stockClassId, stakeholderId) <- createPlanAndStockIssuance issuer cap_table stockPlanId 100.0 100.0 + let sourceSecurityId = "SEC-" <> stockPlanId + let resultSecurityId = "SEC-INHERITED-STOCK-RETURN" + let transferDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let returnDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let resultIssuance = (defaultStockIssuance "TX_INHERITED_STOCK_RETURN_RESULT" resultSecurityId stakeholderId stockClassId) with + date = transferDate + stock_plan_id = None + quantity = 100.0 + transferred <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer ((planStockTransfer "TX_INHERITED_STOCK_RETURN_TRANSFER" sourceSecurityId resultSecurityId 100.0) with date = transferDate) + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockCancellation ((stockCancellation "TX_INHERITED_STOCK_RETURN_CANCEL" resultSecurityId 100.0) with date = returnDate) + , CT.OcfCreateStockPlanReturnToPool ((stockPlanReturnToPool "TX_INHERITED_STOCK_RETURN_POOL" resultSecurityId stockPlanId 100.0) with date = returnDate) + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_StockTransferQuantityIncreaseConsumesPlanHeadroom = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockPlanId = "PLAN_STOCK_TRANSFER_INCREASE" + (capTableIssued, stockClassId, stakeholderId) <- createPlanAndStockIssuance issuer cap_table stockPlanId 150.0 100.0 + let sourceSecurityId = "SEC-" <> stockPlanId + let resultSecurityId = "SEC-STOCK-TRANSFER-INCREASE" + let resultIssuance = (defaultStockIssuance "TX_STOCK_TRANSFER_INCREASE_RESULT" resultSecurityId stakeholderId stockClassId) with + stock_plan_id = None + quantity = 200.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (planStockTransfer "TX_STOCK_TRANSFER_INCREASE" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + testStockPlanReservedShares_NonPlanStockSourceDoesNotHidePlanSuccessor = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table @@ -1001,6 +1132,25 @@ testStockPlanReservedShares_ReissuancePreservesSourceAllocation = script do edits = [] deletes = [] +testStockPlanReservedShares_ReissuanceQuantityIncreaseConsumesPlanHeadroom = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockPlanId = "PLAN_REISSUANCE_INCREASE" + (capTableIssued, stockClassId, stakeholderId) <- createPlanAndStockIssuance issuer cap_table stockPlanId 150.0 100.0 + let sourceSecurityId = "SEC-" <> stockPlanId + let resultSecurityId = "SEC-REISSUANCE-INCREASE" + let resultIssuance = (defaultStockIssuance "TX_REISSUANCE_INCREASE_RESULT" resultSecurityId stakeholderId stockClassId) with + stock_plan_id = None + quantity = 200.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance (planStockReissuance "TX_REISSUANCE_INCREASE" sourceSecurityId resultSecurityId) + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + testStockPlanReservedShares_EquitySplitDoesNotCreateReturnableShares = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table @@ -1028,6 +1178,38 @@ testStockPlanReservedShares_EquitySplitDoesNotCreateReturnableShares = script do edits = [] deletes = [] +testStockPlanReservedShares_ClasslessEquityCancellationReturnUsesPlanClassSplit = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_CLASSLESS_EQUITY_RETURN_SPLIT" + let securityId = "SEC-CLASSLESS-EQUITY-RETURN-SPLIT" + let splitDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let returnDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 200.0 + let issuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_EQUITY_RETURN_SPLIT" + securityId + stakeholderId + stockClassId + stockPlanId + 100.0) with stock_class_id = None + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + , CT.OcfCreateEquityCompensationCancellation + ((equityCompensationCancellation "TX_CLASSLESS_EQUITY_RETURN_CANCEL" securityId 50.0) with date = defaultTestDate) + , CT.OcfCreateStockClassSplit + ((stockClassSplit "TX_CLASSLESS_EQUITY_RETURN_SPLIT_EVENT" stockClassId 2.0 1.0) with date = splitDate) + , CT.OcfCreateStockPlanReturnToPool + ((stockPlanReturnToPool "TX_CLASSLESS_EQUITY_RETURN_POOL" securityId stockPlanId 100.0) with date = returnDate) + ] + edits = [] + deletes = [] + pure () + testStockPlanReservedShares_CumulativeReturnsAreSplitAdjusted = script do TestOcp{issuer, cap_table} <- setupTestOcp let stockPlanId = "PLAN_CUMULATIVE_SPLIT_RETURNS" diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 48ab662c..cda50012 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cca4e1bf7f72372aeb67bfdbfa15d99a85041afc31377a907d5745d39778ea43 -size 2736924 +oid sha256:4da50e6e16bb604cf8655b31dce9c0832babf330395b1204d139f7eeb7b16c09 +size 2763000 diff --git a/dars/dars.lock b/dars/dars.lock index ce976ea2..23ae0267 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "cca4e1bf7f72372aeb67bfdbfa15d99a85041afc31377a907d5745d39778ea43", - "size": 2736924, + "sha256": "4da50e6e16bb604cf8655b31dce9c0832babf330395b1204d139f7eeb7b16c09", + "size": 2763000, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-10T13:44:14Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index f6481d39..c4fc47bc 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -1159,23 +1159,30 @@ stockClassShareLotQuantityAt eventIndex lot asOf = (filter (stockClassShareEventOnOrBefore asOf) (stockClassShareLotEvents eventIndex lot))) else Some 0.0 -stockSecurityQuantityBeforeTerminal : +stockSecurityQuantityBeforeEvent : StockClassShareEventIndex -> StockIssuanceOcfData -> - Time -> + (Time, Int, Int) -> Update Decimal -stockSecurityQuantityBeforeTerminal eventIndex issuanceData eventDate = - let targetSortKey = (eventDate, 2, 1) - lot = stockIssuanceShareLot issuanceData +stockSecurityQuantityBeforeEvent eventIndex issuanceData targetSortKey = + let lot = stockIssuanceShareLot issuanceData priorEvents = filter (\event -> stockClassShareEventSortKey event < targetSortKey) (sortOn stockClassShareEventSortKey (stockClassShareLotEvents eventIndex lot)) in case foldl applyStockClassShareEventOptional (Some 0.0) priorEvents of Some quantity -> pure quantity None -> do - assertMsg ("Stock security quantity history is invalid for " <> issuanceData.security_id <> " before " <> show eventDate) False + assertMsg ("Stock security quantity history is invalid for " <> issuanceData.security_id <> " before " <> show targetSortKey) False pure 0.0 +stockSecurityQuantityBeforeTerminal : + StockClassShareEventIndex -> + StockIssuanceOcfData -> + Time -> + Update Decimal +stockSecurityQuantityBeforeTerminal eventIndex issuanceData eventDate = + stockSecurityQuantityBeforeEvent eventIndex issuanceData (eventDate, 2, 1) + validateStockBalanceSuccessor : CapTableMaps -> Text -> @@ -1524,6 +1531,19 @@ validateStockClassShareLotReferences stockClassIndex lotsByStockClass = (isSome (Map.lookup stockClassId stockClassIndex))) (Map.keys lotsByStockClass) +validateStockClassAuthorizedShareAdjustmentReferences : + CapTableMaps -> + Map Text StockClassOcfData -> + Update () +validateStockClassAuthorizedShareAdjustmentReferences maps stockClassIndex = + mapA_ + (\adjustmentCid -> do + adjustment <- fetch adjustmentCid + assertMsg + ("Stock class authorized shares adjustment references missing stock class: " <> adjustment.adjustment_data.stock_class_id) + (isSome (Map.lookup adjustment.adjustment_data.stock_class_id stockClassIndex))) + (Map.values maps.stock_class_authorized_shares_adjustments) + validateIssuerAuthorizedShareAdjustmentReferences : CapTableMaps -> ContractId Issuer -> @@ -1583,6 +1603,18 @@ declaredPlanForSecurity declaredPlans successorIndex securityId = Some (_, stockPlanIdOpt) -> stockPlanIdOpt None -> None +effectivePlanForSecurity : + Map Text (Time, Optional Text) -> + Text -> + Optional Text -> + Optional Text +effectivePlanForSecurity successorIndex securityId declaredPlanIdOpt = + case declaredPlanIdOpt of + Some stockPlanId -> Some stockPlanId + None -> case Map.lookup securityId successorIndex of + Some (_, stockPlanIdOpt) -> stockPlanIdOpt + None -> None + addInheritedPlanSuccessorsForSource : Map Text Text -> Map Text (Time, Optional Text) -> @@ -1765,6 +1797,201 @@ stockPlanSuccessorIndex maps = do stock_successors_by_security_id = stockSuccessors equity_compensation_successors_by_security_id = equitySuccessors +recordedInheritedStockIssuanceSummary : + Map Text (Time, Optional Text) -> + Text -> + CapTableMaps -> + [Text] -> + Update (Optional Text, Decimal) +recordedInheritedStockIssuanceSummary successorIndex stockPlanId maps securityIds = + foldlA + (\(stockClassIdOpt, total) securityId -> + case Map.lookup securityId maps.stock_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + if successorCarriesPlanAllocation successorIndex securityId issuance.issuance_data.stock_plan_id + && effectivePlanForSecurity successorIndex securityId issuance.issuance_data.stock_plan_id == Some stockPlanId + then pure (Some issuance.issuance_data.stock_class_id, total + issuance.issuance_data.quantity) + else pure (stockClassIdOpt, total) + None -> pure (stockClassIdOpt, total)) + (None, 0.0) + securityIds + +stockConversionPlanOutputShareLot : + Map Text (Time, Optional Text) -> + Text -> + CapTableMaps -> + Map Text StockClassOcfData -> + StockIssuanceOcfData -> + StockConversionOcfData -> + Update (Optional StockClassShareLot) +stockConversionPlanOutputShareLot successorIndex stockPlanId maps stockClassIndex sourceIssuanceData conversionData = + if allResultingStockIssuancesExist maps conversionData.resulting_security_ids + then do + (stockClassIdOpt, quantity) <- recordedInheritedStockIssuanceSummary + successorIndex stockPlanId maps conversionData.resulting_security_ids + pure $ case stockClassIdOpt of + Some stockClassId -> Some (StockClassShareLot with + stock_class_id = stockClassId + security_id = None + event_date = conversionData.date + quantity = quantity) + None -> None + else stockConversionOutputShareLot maps stockClassIndex sourceIssuanceData conversionData + +stockPlanSuccessorExcessLot : + Text -> + Text -> + Time -> + Decimal -> + Decimal -> + [(Text, StockClassShareLot)] +stockPlanSuccessorExcessLot stockPlanId stockClassId eventDate sourceQuantity successorQuantity = + if successorQuantity > sourceQuantity + then + [ (stockPlanId, StockClassShareLot with + stock_class_id = stockClassId + security_id = None + event_date = eventDate + quantity = successorQuantity - sourceQuantity) + ] + else [] + +stockPlanInheritedSuccessorExcessLots : + StockClassShareEventIndex -> + StockPlanSuccessorIndex -> + CapTableMaps -> + Update [(Text, StockClassShareLot)] +stockPlanInheritedSuccessorExcessLots eventIndex successorIndex maps = do + transferLots <- foldlA + (\lots transferCid -> do + transfer <- fetch transferCid + case Map.lookup transfer.transfer_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + case effectivePlanForSecurity + successorIndex.stock_successors_by_security_id + sourceIssuance.issuance_data.security_id + sourceIssuance.issuance_data.stock_plan_id of + Some stockPlanId -> do + (_, successorQuantity) <- recordedInheritedStockIssuanceSummary + successorIndex.stock_successors_by_security_id + stockPlanId + maps + transfer.transfer_data.resulting_security_ids + pure (stockPlanSuccessorExcessLot + stockPlanId + sourceIssuance.issuance_data.stock_class_id + transfer.transfer_data.date + transfer.transfer_data.quantity + successorQuantity ++ lots) + None -> pure lots + None -> pure lots) + [] + (Map.values maps.stock_transfers) + + reissuanceLots <- foldlA + (\lots reissuanceCid -> do + reissuance <- fetch reissuanceCid + case Map.lookup reissuance.reissuance_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + case effectivePlanForSecurity + successorIndex.stock_successors_by_security_id + sourceIssuance.issuance_data.security_id + sourceIssuance.issuance_data.stock_plan_id of + Some stockPlanId -> do + sourceQuantity <- stockSecurityQuantityBeforeEvent + eventIndex + sourceIssuance.issuance_data + (reissuance.reissuance_data.date, 3, 3) + (_, successorQuantity) <- recordedInheritedStockIssuanceSummary + successorIndex.stock_successors_by_security_id + stockPlanId + maps + reissuance.reissuance_data.resulting_security_ids + pure (stockPlanSuccessorExcessLot + stockPlanId + sourceIssuance.issuance_data.stock_class_id + reissuance.reissuance_data.date + sourceQuantity + successorQuantity ++ lots) + None -> pure lots + None -> pure lots) + transferLots + (Map.values maps.stock_reissuances) + + stockClassIndex <- stockClassDataIndex maps + foldlA + (\lots conversionCid -> do + conversion <- fetch conversionCid + case Map.lookup conversion.conversion_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + case effectivePlanForSecurity + successorIndex.stock_successors_by_security_id + sourceIssuance.issuance_data.security_id + sourceIssuance.issuance_data.stock_plan_id of + Some stockPlanId -> do + outputLotOpt <- stockConversionPlanOutputShareLot + successorIndex.stock_successors_by_security_id + stockPlanId + maps + stockClassIndex + sourceIssuance.issuance_data + conversion.conversion_data + pure $ case outputLotOpt of + Some outputLot -> stockPlanSuccessorExcessLot + stockPlanId + outputLot.stock_class_id + conversion.conversion_data.date + conversion.conversion_data.quantity_converted + outputLot.quantity ++ lots + None -> lots + None -> pure lots + None -> pure lots) + reissuanceLots + (Map.values maps.stock_conversions) + +stockPlanSuccessorExcessAllocationIndex : + StockClassShareEventIndex -> + StockPlanSuccessorIndex -> + CapTableMaps -> + Update (Map Text Decimal) +stockPlanSuccessorExcessAllocationIndex eventIndex successorIndex maps = do + excessLots <- stockPlanInheritedSuccessorExcessLots eventIndex successorIndex maps + foldlA + (\totals (stockPlanId, lot) -> + case stockClassShareLotPlanAllocatedQuantity eventIndex lot of + Some quantity -> do + assertNonNegativeQuantity ("Inherited stock-plan successor excess for " <> stockPlanId) quantity + pure (addQuantityById stockPlanId quantity totals) + None -> do + assertMsg ("Inherited stock-plan successor excess has an invalid quantity for " <> stockPlanId) False + pure totals) + Map.empty + excessLots + +stockPlanSuccessorExcessAllocationIndexAt : + StockClassShareEventIndex -> + StockPlanSuccessorIndex -> + CapTableMaps -> + Time -> + Update (Map Text Decimal) +stockPlanSuccessorExcessAllocationIndexAt eventIndex successorIndex maps asOf = do + excessLots <- stockPlanInheritedSuccessorExcessLots eventIndex successorIndex maps + foldlA + (\totals (stockPlanId, lot) -> + case stockClassShareLotPlanAllocatedQuantityAt eventIndex lot asOf of + Some quantity -> do + assertNonNegativeQuantity ("Inherited stock-plan successor excess for " <> stockPlanId <> " on " <> show asOf) quantity + pure (addQuantityById stockPlanId quantity totals) + None -> do + assertMsg ("Inherited stock-plan successor excess has an invalid quantity for " <> stockPlanId <> " on " <> show asOf) False + pure totals) + Map.empty + excessLots + stockIssuancePlanAllocationIndex : StockClassShareEventIndex -> StockPlanSuccessorIndex -> CapTableMaps -> Update (Map Text Decimal) stockIssuancePlanAllocationIndex eventIndex successorIndex maps = foldlA @@ -2161,24 +2388,30 @@ equityCompensationCancellationQuantityAt maps issuanceData asOf = do else pure events) [] (Map.values maps.equity_compensation_cancellations) - splitEvents <- stockPlanReturnableSplitEvents maps issuanceData.stock_class_id + splitEvents <- equityCompensationPlanSplitEvents maps issuanceData stockPlanCancellationQuantityAt issuanceData.security_id (cancellationEvents ++ splitEvents) asOf -stockPlanSecurityReturnableQuantityAt : CapTableMaps -> StockPlanReturnToPoolOcfData -> Time -> Update Decimal -stockPlanSecurityReturnableQuantityAt maps returnData asOf = do +stockPlanSecurityReturnableQuantityAt : StockPlanSuccessorIndex -> CapTableMaps -> StockPlanReturnToPoolOcfData -> Time -> Update Decimal +stockPlanSecurityReturnableQuantityAt successorIndex maps returnData asOf = do let stockCidOpt = Map.lookup returnData.security_id maps.stock_issuances_by_security_id let equityCompensationCidOpt = Map.lookup returnData.security_id maps.equity_compensation_issuances_by_security_id case (stockCidOpt, equityCompensationCidOpt) of (Some stockCid, None) -> do issuance <- fetch stockCid - case issuance.issuance_data.stock_plan_id of + case effectivePlanForSecurity + successorIndex.stock_successors_by_security_id + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id of Some _ -> stockCancellationQuantityAt maps issuance.issuance_data asOf None -> do assertMsg ("Stock plan return to pool references non-plan stock security: " <> returnData.security_id) False pure 0.0 (None, Some equityCompensationCid) -> do issuance <- fetch equityCompensationCid - case issuance.issuance_data.stock_plan_id of + case effectivePlanForSecurity + successorIndex.equity_compensation_successors_by_security_id + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id of Some _ -> equityCompensationCancellationQuantityAt maps issuance.issuance_data asOf None -> do assertMsg ("Stock plan return to pool references non-plan equity compensation security: " <> returnData.security_id) False @@ -2192,10 +2425,11 @@ stockPlanSecurityReturnableQuantityAt maps returnData asOf = do validateStockPlanReturnToPoolQuantities : CapTableMaps -> Update () validateStockPlanReturnToPoolQuantities maps = do + successorIndex <- stockPlanSuccessorIndex maps mapA_ (\returnCid -> do returnToPool <- fetch returnCid - securityAllocation <- stockPlanSecurityReturnableQuantityAt maps returnToPool.return_data returnToPool.return_data.date + securityAllocation <- stockPlanSecurityReturnableQuantityAt successorIndex maps returnToPool.return_data returnToPool.return_data.date returnedQuantity <- stockPlanReturnedQuantityBySecurityAt maps returnToPool.return_data.security_id returnToPool.return_data.date assertMsg ("Stock plan return to pool exceeds returnable quantity for " <> returnToPool.return_data.security_id <> ": returned " <> show returnedQuantity <> " exceeds returnable " <> show securityAllocation) @@ -2207,8 +2441,11 @@ stockPlanNetAllocatedQuantityIndex eventIndex maps = do successorIndex <- stockPlanSuccessorIndex maps stockAllocation <- stockIssuancePlanAllocationIndex eventIndex successorIndex maps equityCompensationAllocation <- equityCompensationPlanAllocationIndex successorIndex maps + successorExcessAllocation <- stockPlanSuccessorExcessAllocationIndex eventIndex successorIndex maps returnedQuantity <- stockPlanReturnedQuantityIndex maps - let combinedAllocation = mergeQuantityMaps stockAllocation equityCompensationAllocation + let combinedAllocation = mergeQuantityMaps + (mergeQuantityMaps stockAllocation equityCompensationAllocation) + successorExcessAllocation let netAllocatedByPlan = mergeQuantityMaps combinedAllocation (negateQuantityMap returnedQuantity) pure netAllocatedByPlan @@ -2294,8 +2531,11 @@ stockPlanNetAllocatedQuantityIndexAt eventIndex maps asOf = do successorIndex <- stockPlanSuccessorIndex maps stockAllocation <- stockIssuancePlanAllocationIndexAt eventIndex successorIndex maps asOf equityCompensationAllocation <- equityCompensationPlanAllocationIndexAt successorIndex maps asOf + successorExcessAllocation <- stockPlanSuccessorExcessAllocationIndexAt eventIndex successorIndex maps asOf returnedQuantity <- stockPlanReturnedQuantityIndexAt maps asOf - let combinedAllocation = mergeQuantityMaps stockAllocation equityCompensationAllocation + let combinedAllocation = mergeQuantityMaps + (mergeQuantityMaps stockAllocation equityCompensationAllocation) + successorExcessAllocation pure (mergeQuantityMaps combinedAllocation (negateQuantityMap returnedQuantity)) validateStockPlanAllocationReferences : CapTableMaps -> Map Text Decimal -> Update () @@ -2418,6 +2658,44 @@ stockPlanEffectiveOnOrBefore asOf stockPlanData = addStockPlanDate : Text -> Time -> Map Text [Time] -> Map Text [Time] addStockPlanDate = addDateById +addStockSecurityPlanDate : + StockPlanSuccessorIndex -> + CapTableMaps -> + Text -> + Time -> + Map Text [Time] -> + Update (Map Text [Time]) +addStockSecurityPlanDate successorIndex maps securityId eventDate index = + case Map.lookup securityId maps.stock_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + pure $ case effectivePlanForSecurity + successorIndex.stock_successors_by_security_id + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id of + Some stockPlanId -> addStockPlanDate stockPlanId eventDate index + None -> index + None -> pure index + +addEquityCompensationSecurityPlanDate : + StockPlanSuccessorIndex -> + CapTableMaps -> + Text -> + Time -> + Map Text [Time] -> + Update (Map Text [Time]) +addEquityCompensationSecurityPlanDate successorIndex maps securityId eventDate index = + case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + pure $ case effectivePlanForSecurity + successorIndex.equity_compensation_successors_by_security_id + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id of + Some stockPlanId -> addStockPlanDate stockPlanId eventDate index + None -> index + None -> pure index + addStockClassSplitDateToStockPlans : StockClassSplitOcfData -> CapTableMaps -> Map Text [Time] -> Update (Map Text [Time]) addStockClassSplitDateToStockPlans splitData maps index = foldlA @@ -2431,6 +2709,7 @@ addStockClassSplitDateToStockPlans splitData maps index = stockPlanValidationDateIndex : CapTableMaps -> Update (Map Text [Time]) stockPlanValidationDateIndex maps = do + successorIndex <- stockPlanSuccessorIndex maps planDates <- foldlA (\index stockPlanCid -> do stockPlan <- fetch stockPlanCid @@ -2472,11 +2751,51 @@ stockPlanValidationDateIndex maps = do equityCompensationDates (Map.values maps.stock_plan_return_to_pools) + transferDates <- foldlA + (\index transferCid -> do + transfer <- fetch transferCid + addStockSecurityPlanDate + successorIndex maps transfer.transfer_data.security_id transfer.transfer_data.date index) + returnDates + (Map.values maps.stock_transfers) + + reissuanceDates <- foldlA + (\index reissuanceCid -> do + reissuance <- fetch reissuanceCid + addStockSecurityPlanDate + successorIndex maps reissuance.reissuance_data.security_id reissuance.reissuance_data.date index) + transferDates + (Map.values maps.stock_reissuances) + + conversionDates <- foldlA + (\index conversionCid -> do + conversion <- fetch conversionCid + addStockSecurityPlanDate + successorIndex maps conversion.conversion_data.security_id conversion.conversion_data.date index) + reissuanceDates + (Map.values maps.stock_conversions) + + stockRetractionDates <- foldlA + (\index retractionCid -> do + retraction <- fetch retractionCid + addStockSecurityPlanDate + successorIndex maps retraction.retraction_data.security_id retraction.retraction_data.date index) + conversionDates + (Map.values maps.stock_retractions) + + equityCompensationRetractionDates <- foldlA + (\index retractionCid -> do + retraction <- fetch retractionCid + addEquityCompensationSecurityPlanDate + successorIndex maps retraction.retraction_data.security_id retraction.retraction_data.date index) + stockRetractionDates + (Map.values maps.equity_compensation_retractions) + foldlA (\index splitCid -> do split <- fetch splitCid addStockClassSplitDateToStockPlans split.split_data maps index) - returnDates + equityCompensationRetractionDates (Map.values maps.stock_class_splits) validateStockPlanReservedSharesAtDates : @@ -2533,6 +2852,7 @@ validateAuthorizedShareCeilings maps adjustmentIndex issuerCid = do stockClassValidationDates <- stockClassAuthorizedValidationDateIndex maps lotsByStockClass issuerAdjustmentHistory <- issuerAuthorizedAdjustmentHistoryIndex maps validateStockClassShareLotReferences stockClassIndex lotsByStockClass + validateStockClassAuthorizedShareAdjustmentReferences maps stockClassIndex validateIssuerAuthorizedShareAdjustmentReferences maps issuerCid validateAllStockClassAuthorizedShares eventIndex stockClasses lotsByStockClass adjustmentIndex validateStockClassAuthorizedSharesAtDates eventIndex stockClasses lotsByStockClass stockClassAdjustmentHistory stockClassValidationDates From 636c44d556fb55916ccf4f12a1a3008b4d45db55 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 15:08:35 -0400 Subject: [PATCH 37/62] fix: enforce successor quantity continuity --- .../TestEquityCompensationExercise.daml | 8 +- .../TestEquityCompensationRelease.daml | 6 +- Test/daml/OpenCapTable/TestHelpers.daml | 4 +- .../TestStockClassAuthorizedShares.daml | 253 +++++++ .../TestStockPlanReservedShares.daml | 501 +++++++++++++ .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 695 ++++++++++++++++-- 8 files changed, 1389 insertions(+), 86 deletions(-) diff --git a/Test/daml/OpenCapTable/TestEquityCompensationExercise.daml b/Test/daml/OpenCapTable/TestEquityCompensationExercise.daml index 48b46d44..81cec73d 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationExercise.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationExercise.daml @@ -2,7 +2,7 @@ module OpenCapTable.TestEquityCompensationExercise where import Fairmint.OpenCapTable.OCF.EquityCompensationExercise import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addDefaultStakeholder, addEquityCompensationIssuance) +import OpenCapTable.TestHelpers (addPrerequisites, addEquityCompensationIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -11,7 +11,7 @@ import Daml.Script testCreateAndArchiveEquityCompensationExercise = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp -- Add stakeholder and equity compensation issuance before exercising - cap_table <- addDefaultStakeholder issuer cap_table + cap_table <- addPrerequisites issuer cap_table cap_table <- addEquityCompensationIssuance issuer cap_table "EC-ISS-1" "EC-1" "SH-1" _ <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with @@ -30,7 +30,7 @@ testCreateAndArchiveEquityCompensationExercise = script do testCreateAndArchiveEquityCompensationExerciseBuiltIn = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp -- Add stakeholder and equity compensation issuance before exercising - cap_table <- addDefaultStakeholder issuer cap_table + cap_table <- addPrerequisites issuer cap_table cap_table <- addEquityCompensationIssuance issuer cap_table "EC-ISS-1" "EC-1" "SH-1" _ <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with @@ -50,7 +50,7 @@ testCreateAndArchiveEquityCompensationExerciseBuiltIn = script do testEquityCompensationExercise_OptionalsSome = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp -- Add stakeholder and equity compensation issuance before exercising - cap_table <- addDefaultStakeholder issuer cap_table + cap_table <- addPrerequisites issuer cap_table cap_table <- addEquityCompensationIssuance issuer cap_table "EC-ISS-2" "EC-2" "SH-1" _ <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with diff --git a/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml b/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml index e676d86e..36f71b7d 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml @@ -3,7 +3,7 @@ module OpenCapTable.TestEquityCompensationRelease where import Fairmint.OpenCapTable.OCF.EquityCompensationRelease import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addDefaultStakeholder, addEquityCompensationIssuance) +import OpenCapTable.TestHelpers (addPrerequisites, addEquityCompensationIssuance) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import qualified DA.Date as DA import DA.Date (Month(..)) @@ -15,7 +15,7 @@ import Daml.Script 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 <- addPrerequisites issuer cap_table cap_table <- addEquityCompensationIssuance issuer cap_table "EC-ISS-RSU-1" "SEC_RSU_001" "SH-1" -- RSU vesting and release _ <- submit issuer do @@ -39,7 +39,7 @@ testCreateAndArchiveEquityCompensationRelease = script do 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 <- addPrerequisites issuer cap_table cap_table <- addEquityCompensationIssuance issuer cap_table "EC-ISS-RSU-2" "SEC_RSU_002" "SH-1" -- Quarterly RSU vest with tax withholding _ <- submit issuer do diff --git a/Test/daml/OpenCapTable/TestHelpers.daml b/Test/daml/OpenCapTable/TestHelpers.daml index 93e86707..dccb931c 100644 --- a/Test/daml/OpenCapTable/TestHelpers.daml +++ b/Test/daml/OpenCapTable/TestHelpers.daml @@ -446,7 +446,7 @@ addDefaultStockIssuance : Party -> ContractId CT.CapTable -> Script (ContractId addDefaultStockIssuance issuer capTableCid = addStockIssuance issuer capTableCid "STOCK-ISS-1" "SEC-STOCK-1" "SH-1" "SC_COMMON" --- Helper to add an equity compensation issuance (requires stakeholder) +-- Helper to add an equity compensation issuance (requires stakeholder and SC_COMMON stock class) addEquityCompensationIssuance : Party -> ContractId CT.CapTable -> Text -> Text -> Text -> Script (ContractId CT.CapTable) addEquityCompensationIssuance issuer capTableCid issuanceId securityId stakeholderId = do result <- submit issuer do @@ -469,7 +469,7 @@ addEquityCompensationIssuance issuer capTableCid issuanceId securityId stakehold early_exercisable = None exercise_price = Some (OcfMonetary with amount = 1.0, currency = "USD") expiration_date = None - stock_class_id = None + stock_class_id = Some "SC_COMMON" stock_plan_id = None stockholder_approval_date = None vesting_terms_id = None] diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index aabc0a71..0bc75ee4 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -630,6 +630,65 @@ testStockTransferWithResultingIssuanceDoesNotDoubleCountForCeiling = script do deletes = [] pure () +testStockTransferResultQuantityMustMatchTransferredQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_TRANSFER_QUANTITY" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_TRANSFER_QUANTITY" "Transfer Quantity Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_TRANSFER_QUANTITY_SOURCE" "SEC_TRANSFER_QUANTITY_SOURCE" stakeholderId stockClassId) with quantity = 100.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (stockTransfer "TX_TRANSFER_QUANTITY" "SEC_TRANSFER_QUANTITY_SOURCE" 50.0 ["SEC_TRANSFER_QUANTITY_RESULT"]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_TRANSFER_QUANTITY_RESULT" "SEC_TRANSFER_QUANTITY_RESULT" stakeholderId stockClassId) with quantity = 10.0) + ] + edits = [] + deletes = [] + +testStockTransferResultMustRemainInSourceStockClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithSource, sourceStockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_TRANSFER_SOURCE_CLASS" 1_000.0) + (capTableWithResult, resultStockClassId) <- setupStockClassWith issuer capTableWithSource (limitedStockClass "SC_TRANSFER_RESULT_CLASS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithResult "SH_TRANSFER_CLASS" "Transfer Class Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_TRANSFER_CLASS_SOURCE" "SEC_TRANSFER_CLASS_SOURCE" stakeholderId sourceStockClassId) with quantity = 100.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (stockTransfer "TX_TRANSFER_CLASS" "SEC_TRANSFER_CLASS_SOURCE" 50.0 ["SEC_TRANSFER_CLASS_RESULT"]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_TRANSFER_CLASS_RESULT" "SEC_TRANSFER_CLASS_RESULT" stakeholderId resultStockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + +testStockTransferResultQuantityNormalizesInterveningSplit = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_TRANSFER_SPLIT_NORMALIZED" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_TRANSFER_SPLIT_NORMALIZED" "Transfer Split Holder" + let sourceDate = DT.time (DA.date 2024 Jan 01) 0 0 0 + let transferDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let splitDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let resultDate = DT.time (DA.date 2024 Jan 04) 0 0 0 + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_TRANSFER_SPLIT_SOURCE" "SEC_TRANSFER_SPLIT_SOURCE" stakeholderId stockClassId) with + date = sourceDate + quantity = 100.0) + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer ((stockTransfer "TX_TRANSFER_SPLIT_NORMALIZED" "SEC_TRANSFER_SPLIT_SOURCE" 100.0 ["SEC_TRANSFER_SPLIT_RESULT"]) with date = transferDate) + , CT.OcfCreateStockClassSplit ((stockClassSplit "TX_TRANSFER_INTERVENING_SPLIT" stockClassId 2.0 1.0) with date = splitDate) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_TRANSFER_SPLIT_RESULT" "SEC_TRANSFER_SPLIT_RESULT" stakeholderId stockClassId) with + date = resultDate + quantity = 200.0) + ] + edits = [] + deletes = [] + pure () + testStockReissuanceWithResultingIssuanceDoesNotDoubleCountForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -665,6 +724,63 @@ testStockConsolidationWithResultingIssuanceDoesNotDoubleCountForCeiling = script deletes = [] pure () +testStockConsolidationResultQuantityMustEqualSourceTotal = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONSOLIDATION_QUANTITY" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONSOLIDATION_QUANTITY" "Consolidation Quantity Holder" + capTableOne <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONSOLIDATION_QUANTITY_SOURCE_A" "SEC_CONSOLIDATION_QUANTITY_SOURCE_A" stakeholderId stockClassId) with quantity = 50.0) + capTableTwo <- createStockIssuanceWith issuer capTableOne ((defaultStockIssuance "TX_CONSOLIDATION_QUANTITY_SOURCE_B" "SEC_CONSOLIDATION_QUANTITY_SOURCE_B" stakeholderId stockClassId) with quantity = 50.0) + + submitMustFail issuer do + exerciseCmd capTableTwo CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation (stockConsolidation "TX_CONSOLIDATION_QUANTITY" ["SEC_CONSOLIDATION_QUANTITY_SOURCE_A", "SEC_CONSOLIDATION_QUANTITY_SOURCE_B"] "SEC_CONSOLIDATION_QUANTITY_RESULT") + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CONSOLIDATION_QUANTITY_RESULT" "SEC_CONSOLIDATION_QUANTITY_RESULT" stakeholderId stockClassId) with quantity = 10.0) + ] + edits = [] + deletes = [] + +testStockConsolidationSourcesMustMatchResultStockClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithSource, sourceStockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONSOLIDATION_SOURCE_CLASS" 1_000.0) + (capTableWithResult, resultStockClassId) <- setupStockClassWith issuer capTableWithSource (limitedStockClass "SC_CONSOLIDATION_RESULT_CLASS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithResult "SH_CONSOLIDATION_CLASS" "Consolidation Class Holder" + capTableOne <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONSOLIDATION_CLASS_SOURCE_A" "SEC_CONSOLIDATION_CLASS_SOURCE_A" stakeholderId sourceStockClassId) with quantity = 25.0) + capTableTwo <- createStockIssuanceWith issuer capTableOne ((defaultStockIssuance "TX_CONSOLIDATION_CLASS_SOURCE_B" "SEC_CONSOLIDATION_CLASS_SOURCE_B" stakeholderId sourceStockClassId) with quantity = 25.0) + + submitMustFail issuer do + exerciseCmd capTableTwo CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation (stockConsolidation "TX_CONSOLIDATION_CLASS" ["SEC_CONSOLIDATION_CLASS_SOURCE_A", "SEC_CONSOLIDATION_CLASS_SOURCE_B"] "SEC_CONSOLIDATION_CLASS_RESULT") + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CONSOLIDATION_CLASS_RESULT" "SEC_CONSOLIDATION_CLASS_RESULT" stakeholderId resultStockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + +testStockConsolidationSameTimestampSplitDoesNotResplitResult = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONSOLIDATION_SAME_TIMESTAMP" 50.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONSOLIDATION_SAME_TIMESTAMP" "Same Timestamp Consolidation Holder" + capTableOne <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONSOLIDATION_SAME_TIMESTAMP_SOURCE_A" "SEC_CONSOLIDATION_SAME_TIMESTAMP_SOURCE_A" stakeholderId stockClassId) with quantity = 10.0) + capTableTwo <- createStockIssuanceWith issuer capTableOne ((defaultStockIssuance "TX_CONSOLIDATION_SAME_TIMESTAMP_SOURCE_B" "SEC_CONSOLIDATION_SAME_TIMESTAMP_SOURCE_B" stakeholderId stockClassId) with quantity = 15.0) + let eventDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + + _ <- submit issuer do + exerciseCmd capTableTwo CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassSplit ((stockClassSplit "TX_CONSOLIDATION_SAME_TIMESTAMP_SPLIT" stockClassId 2.0 1.0) with date = eventDate) + , CT.OcfCreateStockConsolidation ((stockConsolidation "TX_CONSOLIDATION_SAME_TIMESTAMP" ["SEC_CONSOLIDATION_SAME_TIMESTAMP_SOURCE_A", "SEC_CONSOLIDATION_SAME_TIMESTAMP_SOURCE_B"] "SEC_CONSOLIDATION_SAME_TIMESTAMP_RESULT") with date = eventDate) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CONSOLIDATION_SAME_TIMESTAMP_RESULT" "SEC_CONSOLIDATION_SAME_TIMESTAMP_RESULT" stakeholderId stockClassId) with + date = eventDate + quantity = 50.0) + ] + edits = [] + deletes = [] + pure () + testStockConversionReducesIssuedSharesForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -1104,6 +1220,143 @@ testEquityCompensationExerciseCannotExceedStockClassAuthorizedShares = script do edits = [] deletes = [] +testClasslessEquityCompensationExerciseUsesSinglePlanClassForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CLASSLESS_EQUITY_OUTPUT" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CLASSLESS_EQUITY_OUTPUT" "Classless Equity Output Holder" + let stockPlanId = "PLAN_CLASSLESS_EQUITY_OUTPUT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 200.0 + let issuance = (equityCompensationIssuance "TX_CLASSLESS_EQUITY_GRANTED" "ECSEC_CLASSLESS_OUTPUT" stakeholderId stockClassId 150.0) with + stock_class_id = None + stock_plan_id = Some stockPlanId + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise ((equityCompensationExercise "TX_CLASSLESS_EQUITY_EXERCISE" "ECSEC_CLASSLESS_OUTPUT" 150.0) with resulting_security_ids = [])] + edits = [] + deletes = [] + +testClasslessEquityCompensationExerciseRejectsAmbiguousPlanClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithFirstClass, firstStockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CLASSLESS_EQUITY_FIRST" 1_000.0) + (capTableWithSecondClass, secondStockClassId) <- setupStockClassWith issuer capTableWithFirstClass (limitedStockClass "SC_CLASSLESS_EQUITY_SECOND" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithSecondClass "SH_CLASSLESS_EQUITY_AMBIGUOUS" "Ambiguous Classless Equity Holder" + let stockPlanId = "PLAN_CLASSLESS_EQUITY_AMBIGUOUS" + let plan = (defaultStockPlan stockPlanId firstStockClassId) with + initial_shares_reserved = 100.0 + stock_class_ids = [firstStockClassId, secondStockClassId] + let issuance = (equityCompensationIssuance "TX_CLASSLESS_EQUITY_AMBIGUOUS_GRANTED" "ECSEC_CLASSLESS_AMBIGUOUS" stakeholderId firstStockClassId 50.0) with + stock_class_id = None + stock_plan_id = Some stockPlanId + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise ((equityCompensationExercise "TX_CLASSLESS_EQUITY_AMBIGUOUS_EXERCISE" "ECSEC_CLASSLESS_AMBIGUOUS" 50.0) with resulting_security_ids = [])] + edits = [] + deletes = [] + +testClasslessCashSettledSarExerciseAllowsNoStockOutput = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CASH_SETTLED_SAR" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CASH_SETTLED_SAR" "Cash Settled SAR Holder" + let issuance = (equityCompensationIssuance "TX_CASH_SETTLED_SAR_GRANTED" "ECSEC_CASH_SETTLED_SAR" stakeholderId stockClassId 50.0) with + compensation_type = OcfCompensationTypeCSAR + base_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + stock_class_id = None + stock_plan_id = None + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance issuance] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise ((equityCompensationExercise "TX_CASH_SETTLED_SAR_EXERCISE" "ECSEC_CASH_SETTLED_SAR" 50.0) with resulting_security_ids = [])] + edits = [] + deletes = [] + pure () + +testClasslessEquityCompensationReleaseUsesSinglePlanClassForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CLASSLESS_EQUITY_RELEASE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CLASSLESS_EQUITY_RELEASE" "Classless Equity Release Holder" + let stockPlanId = "PLAN_CLASSLESS_EQUITY_RELEASE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 200.0 + let issuance = (equityCompensationIssuance "TX_CLASSLESS_EQUITY_RELEASE_GRANTED" "ECSEC_CLASSLESS_RELEASE" stakeholderId stockClassId 150.0) with + compensation_type = OcfCompensationTypeRSU + exercise_price = None + stock_class_id = None + stock_plan_id = Some stockPlanId + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + + let releaseDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let settlementDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationRelease (equityCompensationRelease "TX_CLASSLESS_EQUITY_RELEASE" "ECSEC_CLASSLESS_RELEASE" releaseDate settlementDate 150.0)] + edits = [] + deletes = [] + +testClasslessEquityCompensationReleaseRejectsAmbiguousPlanClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithFirstClass, firstStockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CLASSLESS_RELEASE_FIRST" 1_000.0) + (capTableWithSecondClass, secondStockClassId) <- setupStockClassWith issuer capTableWithFirstClass (limitedStockClass "SC_CLASSLESS_RELEASE_SECOND" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithSecondClass "SH_CLASSLESS_RELEASE_AMBIGUOUS" "Ambiguous Classless Release Holder" + let stockPlanId = "PLAN_CLASSLESS_RELEASE_AMBIGUOUS" + let plan = (defaultStockPlan stockPlanId firstStockClassId) with + initial_shares_reserved = 100.0 + stock_class_ids = [firstStockClassId, secondStockClassId] + let issuance = (equityCompensationIssuance "TX_CLASSLESS_RELEASE_AMBIGUOUS_GRANTED" "ECSEC_CLASSLESS_RELEASE_AMBIGUOUS" stakeholderId firstStockClassId 50.0) with + compensation_type = OcfCompensationTypeRSU + exercise_price = None + stock_class_id = None + stock_plan_id = Some stockPlanId + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + + let releaseDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let settlementDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationRelease (equityCompensationRelease "TX_CLASSLESS_RELEASE_AMBIGUOUS" "ECSEC_CLASSLESS_RELEASE_AMBIGUOUS" releaseDate settlementDate 50.0)] + edits = [] + deletes = [] + testNegativeEquityCompensationExerciseCannotOffsetIssuedSharesForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index 7af12beb..b758ebce 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -5,11 +5,16 @@ import Fairmint.OpenCapTable.OCF.EquityCompensationCancellation (EquityCompensat import Fairmint.OpenCapTable.OCF.EquityCompensationExercise (EquityCompensationExerciseOcfData(..)) import Fairmint.OpenCapTable.OCF.EquityCompensationRetraction (EquityCompensationRetractionOcfData(..)) import Fairmint.OpenCapTable.OCF.EquityCompensationTransfer (EquityCompensationTransferOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockConsolidation (StockConsolidationOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockConversion (StockConversionOcfData(..)) import Fairmint.OpenCapTable.OCF.StockReissuance (StockReissuanceOcfData(..)) import Fairmint.OpenCapTable.OCF.StockRetraction (StockRetractionOcfData(..)) import Fairmint.OpenCapTable.OCF.StockTransfer (StockTransferOcfData(..)) import Fairmint.OpenCapTable.OCF.StockPlanPoolAdjustment (StockPlanPoolAdjustmentOcfData(..)) import Fairmint.OpenCapTable.OCF.StockPlanReturnToPool (StockPlanReturnToPoolOcfData(..)) +import Fairmint.OpenCapTable.Types.Conversion (OcfConversionMechanism(..), OcfStockClassConversionRight(..)) +import Fairmint.OpenCapTable.Types.Stock (OcfRatio(..)) +import OpenCapTable.HelpersTriggers (ocfTriggerElectiveAtWill) import OpenCapTable.Setup import OpenCapTable.TestHelpers import qualified DA.Date as DA @@ -68,6 +73,52 @@ planStockTransfer transferId securityId resultingSecurityId quantity = StockTran consideration_text = None comments = [] +planStockConversion : Text -> Text -> Text -> Time -> Decimal -> StockConversionOcfData +planStockConversion conversionId securityId resultingSecurityId conversionDate quantity = StockConversionOcfData with + id = conversionId + date = conversionDate + security_id = securityId + quantity_converted = quantity + resulting_security_ids = [resultingSecurityId] + balance_security_id = None + comments = [] + +stockClassConversionRightTo : Text -> OcfStockClassConversionRight +stockClassConversionRightTo stockClassId = OcfStockClassConversionRight with + ceiling_price_per_share = None + conversion_mechanism = OcfConversionMechanismRatioConversion + conversion_price = None + conversion_trigger = ocfTriggerElectiveAtWill + converts_to_future_round = None + converts_to_stock_class_id = stockClassId + custom_description = None + discount_rate = None + expires_at = None + floor_price_per_share = None + percent_of_capitalization = None + ratio = Some (OcfRatio with numerator = 1.0, denominator = 1.0) + reference_share_price = None + reference_valuation_price_per_share = None + type_ = "STOCK_CLASS_CONVERSION_RIGHT" + valuation_cap = None + +planStockConsolidation : Text -> [Text] -> Text -> Time -> StockConsolidationOcfData +planStockConsolidation consolidationId securityIds resultingSecurityId consolidationDate = StockConsolidationOcfData with + id = consolidationId + date = consolidationDate + security_ids = securityIds + resulting_security_id = resultingSecurityId + reason_text = Some "Consolidated plan-backed stock" + comments = [] + +planStockRetraction : Text -> Text -> Time -> StockRetractionOcfData +planStockRetraction retractionId securityId retractionDate = StockRetractionOcfData with + id = retractionId + date = retractionDate + security_id = securityId + reason_text = "Retracted plan-backed stock issuance" + comments = [] + planStockReissuance : Text -> Text -> Text -> StockReissuanceOcfData planStockReissuance reissuanceId securityId resultingSecurityId = StockReissuanceOcfData with id = reissuanceId @@ -113,6 +164,52 @@ createPlanAndStockIssuance issuer capTableCid stockPlanId reservedShares issuedS deletes = [] pure (result.updatedCapTableCid, stockClassId, stakeholderId) +setupPlanBackedConversion : + Party -> + ContractId CT.CapTable -> + Text -> + Decimal -> + Script (ContractId CT.CapTable, Text, Text, Text, Text) +setupPlanBackedConversion issuer capTableCid suffix reservedShares = do + let sourceStockClassId = "SC_PLAN_CONVERSION_SOURCE_" <> suffix + let destinationStockClassId = "SC_PLAN_CONVERSION_DESTINATION_" <> suffix + let stockPlanId = "PLAN_CONVERSION_" <> suffix + let sourceSecurityId = "SEC_PLAN_CONVERSION_SOURCE_" <> suffix + let resultSecurityId = "SEC_PLAN_CONVERSION_RESULT_" <> suffix + let conversionDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + (capTableWithDestination, _) <- setupStockClassWith issuer capTableCid (defaultStockClass destinationStockClassId) + let sourceStockClass = (defaultStockClass sourceStockClassId) with + conversion_rights = [stockClassConversionRightTo destinationStockClassId] + (capTableWithSource, _) <- setupStockClassWith issuer capTableWithDestination sourceStockClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithSource ("SH_PLAN_CONVERSION_" <> suffix) ("Plan Conversion Holder " <> suffix) + let plan = (defaultStockPlan stockPlanId sourceStockClassId) with + initial_shares_reserved = reservedShares + stock_class_ids = [sourceStockClassId, destinationStockClassId] + let sourceIssuance = (defaultStockIssuance ("TX_PLAN_CONVERSION_SOURCE_" <> suffix) sourceSecurityId stakeholderId sourceStockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance sourceIssuance + ] + edits = [] + deletes = [] + let resultIssuance = (defaultStockIssuance ("TX_PLAN_CONVERSION_RESULT_" <> suffix) resultSecurityId stakeholderId destinationStockClassId) with + date = conversionDate + stock_plan_id = None + quantity = 100.0 + converted <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConversion (planStockConversion ("TX_PLAN_CONVERSION_" <> suffix) sourceSecurityId resultSecurityId conversionDate 100.0) + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + pure (converted.updatedCapTableCid, stockPlanId, sourceSecurityId, resultSecurityId, stakeholderId) + testStockPlanReservedShares_StockIssuanceCannotExceedInitialPool = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table @@ -988,6 +1085,219 @@ testStockPlanReservedShares_StockTransferQuantityIncreaseConsumesPlanHeadroom = edits = [] deletes = [] +testStockPlanReservedShares_ConvertedAllocationUsesDestinationClassSplit = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let sourceStockClassId = "SC_PLAN_CONVERSION_SOURCE" + let destinationStockClassId = "SC_PLAN_CONVERSION_DESTINATION" + let stockPlanId = "PLAN_CONVERSION_DESTINATION_SPLIT" + let sourceSecurityId = "SEC_PLAN_CONVERSION_SOURCE" + let resultSecurityId = "SEC_PLAN_CONVERSION_RESULT" + let conversionDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let splitDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + (capTableWithDestination, _) <- setupStockClassWith issuer cap_table (defaultStockClass destinationStockClassId) + let sourceStockClass = (defaultStockClass sourceStockClassId) with + conversion_rights = [stockClassConversionRightTo destinationStockClassId] + (capTableWithSource, _) <- setupStockClassWith issuer capTableWithDestination sourceStockClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithSource "SH_PLAN_CONVERSION" "Plan Conversion Holder" + let plan = (defaultStockPlan stockPlanId sourceStockClassId) with + initial_shares_reserved = 150.0 + stock_class_ids = [sourceStockClassId, destinationStockClassId] + let sourceIssuance = (defaultStockIssuance "TX_PLAN_CONVERSION_SOURCE" sourceSecurityId stakeholderId sourceStockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance sourceIssuance + ] + edits = [] + deletes = [] + let resultIssuance = (defaultStockIssuance "TX_PLAN_CONVERSION_RESULT" resultSecurityId stakeholderId destinationStockClassId) with + date = conversionDate + stock_plan_id = None + quantity = 100.0 + + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConversion (planStockConversion "TX_PLAN_CONVERSION" sourceSecurityId resultSecurityId conversionDate 100.0) + , CT.OcfCreateStockIssuance resultIssuance + , CT.OcfCreateStockClassSplit ((stockClassSplit "TX_PLAN_CONVERSION_DESTINATION_SPLIT" destinationStockClassId 2.0 1.0) with date = splitDate) + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ConvertedAllocationFitsAdjustedPool = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let sourceStockClassId = "SC_PLAN_CONVERSION_SOURCE_ALLOWED" + let destinationStockClassId = "SC_PLAN_CONVERSION_DESTINATION_ALLOWED" + let stockPlanId = "PLAN_CONVERSION_DESTINATION_SPLIT_ALLOWED" + let sourceSecurityId = "SEC_PLAN_CONVERSION_SOURCE_ALLOWED" + let resultSecurityId = "SEC_PLAN_CONVERSION_RESULT_ALLOWED" + let conversionDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let splitDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + (capTableWithDestination, _) <- setupStockClassWith issuer cap_table (defaultStockClass destinationStockClassId) + let sourceStockClass = (defaultStockClass sourceStockClassId) with + conversion_rights = [stockClassConversionRightTo destinationStockClassId] + (capTableWithSource, _) <- setupStockClassWith issuer capTableWithDestination sourceStockClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithSource "SH_PLAN_CONVERSION_ALLOWED" "Allowed Plan Conversion Holder" + let plan = (defaultStockPlan stockPlanId sourceStockClassId) with + initial_shares_reserved = 200.0 + stock_class_ids = [sourceStockClassId, destinationStockClassId] + let sourceIssuance = (defaultStockIssuance "TX_PLAN_CONVERSION_SOURCE_ALLOWED" sourceSecurityId stakeholderId sourceStockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance sourceIssuance + ] + edits = [] + deletes = [] + let resultIssuance = (defaultStockIssuance "TX_PLAN_CONVERSION_RESULT_ALLOWED" resultSecurityId stakeholderId destinationStockClassId) with + date = conversionDate + stock_plan_id = None + quantity = 100.0 + + _ <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConversion (planStockConversion "TX_PLAN_CONVERSION_ALLOWED" sourceSecurityId resultSecurityId conversionDate 100.0) + , CT.OcfCreateStockIssuance resultIssuance + , CT.OcfCreateStockClassSplit ((stockClassSplit "TX_PLAN_CONVERSION_DESTINATION_SPLIT_ALLOWED" destinationStockClassId 2.0 1.0) with date = splitDate) + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_SourceRetractionDoesNotEraseConvertedAllocation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (convertedCapTable, stockPlanId, sourceSecurityId, _, stakeholderId) <- + setupPlanBackedConversion issuer cap_table "SOURCE_RETRACTION" 100.0 + let sourceStockClassId = "SC_PLAN_CONVERSION_SOURCE_SOURCE_RETRACTION" + let retractionDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let excessIssuance = (defaultStockIssuance "TX_PLAN_CONVERSION_SOURCE_RETRACTION_EXCESS" "SEC_PLAN_CONVERSION_SOURCE_RETRACTION_EXCESS" stakeholderId sourceStockClassId) with + date = DT.time (DA.date 2024 Jan 04) 0 0 0 + stock_plan_id = Some stockPlanId + quantity = 1.0 + + submitMustFail issuer do + exerciseCmd convertedCapTable CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockRetraction (planStockRetraction "TX_PLAN_CONVERSION_SOURCE_RETRACTION" sourceSecurityId retractionDate) + , CT.OcfCreateStockIssuance excessIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_PartialConversionBalancePreservesAllocationAfterSourceRetraction = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let sourceStockClassId = "SC_PLAN_PARTIAL_CONVERSION_SOURCE" + let destinationStockClassId = "SC_PLAN_PARTIAL_CONVERSION_DESTINATION" + let stockPlanId = "PLAN_PARTIAL_CONVERSION_BALANCE" + let sourceSecurityId = "SEC_PLAN_PARTIAL_CONVERSION_SOURCE" + let resultSecurityId = "SEC_PLAN_PARTIAL_CONVERSION_RESULT" + let balanceSecurityId = "SEC_PLAN_PARTIAL_CONVERSION_BALANCE" + let conversionDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let retractionDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let excessDate = DT.time (DA.date 2024 Jan 04) 0 0 0 + (capTableWithDestination, _) <- setupStockClassWith issuer cap_table (defaultStockClass destinationStockClassId) + let sourceStockClass = (defaultStockClass sourceStockClassId) with + conversion_rights = [stockClassConversionRightTo destinationStockClassId] + (capTableWithSource, _) <- setupStockClassWith issuer capTableWithDestination sourceStockClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithSource "SH_PLAN_PARTIAL_CONVERSION" "Partial Conversion Holder" + let plan = (defaultStockPlan stockPlanId sourceStockClassId) with + initial_shares_reserved = 100.0 + stock_class_ids = [sourceStockClassId, destinationStockClassId] + let sourceIssuance = (defaultStockIssuance "TX_PLAN_PARTIAL_CONVERSION_SOURCE" sourceSecurityId stakeholderId sourceStockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let conversion = (planStockConversion "TX_PLAN_PARTIAL_CONVERSION" sourceSecurityId resultSecurityId conversionDate 40.0) with + balance_security_id = Some balanceSecurityId + let resultIssuance = (defaultStockIssuance "TX_PLAN_PARTIAL_CONVERSION_RESULT" resultSecurityId stakeholderId destinationStockClassId) with + date = conversionDate + stock_plan_id = None + quantity = 40.0 + let balanceIssuance = (defaultStockIssuance "TX_PLAN_PARTIAL_CONVERSION_BALANCE" balanceSecurityId stakeholderId sourceStockClassId) with + date = conversionDate + stock_plan_id = None + quantity = 60.0 + converted <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConversion conversion + , CT.OcfCreateStockIssuance resultIssuance + , CT.OcfCreateStockIssuance balanceIssuance + ] + edits = [] + deletes = [] + + let excessIssuance = (defaultStockIssuance "TX_PLAN_PARTIAL_CONVERSION_EXCESS" "SEC_PLAN_PARTIAL_CONVERSION_EXCESS" stakeholderId sourceStockClassId) with + date = excessDate + stock_plan_id = Some stockPlanId + quantity = 60.0 + submitMustFail issuer do + exerciseCmd converted.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockRetraction (planStockRetraction "TX_PLAN_PARTIAL_CONVERSION_SOURCE_RETRACTION" sourceSecurityId retractionDate) + , CT.OcfCreateStockIssuance excessIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ResultRetractionReleasesConvertedAllocation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (convertedCapTable, stockPlanId, _, resultSecurityId, stakeholderId) <- + setupPlanBackedConversion issuer cap_table "RESULT_RETRACTION" 100.0 + let sourceStockClassId = "SC_PLAN_CONVERSION_SOURCE_RESULT_RETRACTION" + let retractionDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let replacementIssuance = (defaultStockIssuance "TX_PLAN_CONVERSION_RESULT_RETRACTION_REPLACEMENT" "SEC_PLAN_CONVERSION_RESULT_RETRACTION_REPLACEMENT" stakeholderId sourceStockClassId) with + date = DT.time (DA.date 2024 Jan 04) 0 0 0 + stock_plan_id = Some stockPlanId + quantity = 100.0 + + _ <- submit issuer do + exerciseCmd convertedCapTable CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockRetraction (planStockRetraction "TX_PLAN_CONVERSION_RESULT_RETRACTION" resultSecurityId retractionDate) + , CT.OcfCreateStockIssuance replacementIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_ConsolidationCannotInflatePlanAllocation = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockPlanId = "PLAN_CONSOLIDATION_INFLATION" + (capTableIssued, stockClassId, stakeholderId) <- createPlanAndStockIssuance issuer cap_table stockPlanId 150.0 100.0 + let sourceSecurityId = "SEC-" <> stockPlanId + let resultSecurityId = "SEC_PLAN_CONSOLIDATION_INFLATED_RESULT" + let consolidationDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let resultIssuance = (defaultStockIssuance "TX_PLAN_CONSOLIDATION_INFLATED_RESULT" resultSecurityId stakeholderId stockClassId) with + date = consolidationDate + stock_plan_id = None + quantity = 200.0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation (planStockConsolidation "TX_PLAN_CONSOLIDATION_INFLATION" [sourceSecurityId] resultSecurityId consolidationDate) + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + testStockPlanReservedShares_NonPlanStockSourceDoesNotHidePlanSuccessor = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table @@ -1087,6 +1397,197 @@ testStockPlanReservedShares_EquityTransferSuccessorDoesNotDoubleAllocate = scrip deletes = [] pure () +testStockPlanReservedShares_EquityTransferReducesSourceQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_TRANSFER_SOURCE_REDUCTION" + let sourceSecurityId = "SEC-EQUITY-TRANSFER-REDUCED-SOURCE" + let resultSecurityId = "SEC-EQUITY-TRANSFER-REDUCED-RESULT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_REDUCED_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let resultIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_REDUCED_RESULT" resultSecurityId stakeholderId stockClassId stockPlanId 100.0 + transferred <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer (planEquityCompensationTransfer "TX_EQUITY_TRANSFER_SOURCE_REDUCTION" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance resultIssuance + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise (planEquityCompensationExercise "TX_EQUITY_TRANSFER_REDUCED_SOURCE_EXERCISE" sourceSecurityId 100.0)] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityTransferRejectsOverstatedResult = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_TRANSFER_OVERSTATED_RESULT" + let sourceSecurityId = "SEC_EQUITY_TRANSFER_OVERSTATED_RESULT_SOURCE" + let resultSecurityId = "SEC_EQUITY_TRANSFER_OVERSTATED_RESULT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 300.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_OVERSTATED_RESULT_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let resultIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_OVERSTATED_RESULT" resultSecurityId stakeholderId stockClassId stockPlanId 200.0 + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer (planEquityCompensationTransfer "TX_EQUITY_TRANSFER_OVERSTATED_RESULT" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance resultIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityTransferRejectsOverstatedBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_TRANSFER_OVERSTATED_BALANCE" + let sourceSecurityId = "SEC_EQUITY_TRANSFER_OVERSTATED_BALANCE_SOURCE" + let resultSecurityId = "SEC_EQUITY_TRANSFER_OVERSTATED_BALANCE_RESULT" + let balanceSecurityId = "SEC_EQUITY_TRANSFER_OVERSTATED_BALANCE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 300.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_OVERSTATED_BALANCE_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let transfer = (planEquityCompensationTransfer "TX_EQUITY_TRANSFER_OVERSTATED_BALANCE" sourceSecurityId resultSecurityId 40.0) with + balance_security_id = Some balanceSecurityId + let resultIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_OVERSTATED_BALANCE_RESULT" resultSecurityId stakeholderId stockClassId stockPlanId 40.0 + let balanceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_OVERSTATED_BALANCE" balanceSecurityId stakeholderId stockClassId stockPlanId 100.0 + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer transfer + , CT.OcfCreateEquityCompensationIssuance resultIssuance + , CT.OcfCreateEquityCompensationIssuance balanceIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityTransferAcceptsExactBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_TRANSFER_EXACT_BALANCE" + let sourceSecurityId = "SEC_EQUITY_TRANSFER_EXACT_BALANCE_SOURCE" + let resultSecurityId = "SEC_EQUITY_TRANSFER_EXACT_BALANCE_RESULT" + let balanceSecurityId = "SEC_EQUITY_TRANSFER_EXACT_BALANCE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_EXACT_BALANCE_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let transfer = (planEquityCompensationTransfer "TX_EQUITY_TRANSFER_EXACT_BALANCE" sourceSecurityId resultSecurityId 40.0) with + balance_security_id = Some balanceSecurityId + let resultIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_EXACT_BALANCE_RESULT" resultSecurityId stakeholderId stockClassId stockPlanId 40.0 + let balanceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_EXACT_BALANCE" balanceSecurityId stakeholderId stockClassId stockPlanId 60.0 + _ <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer transfer + , CT.OcfCreateEquityCompensationIssuance resultIssuance + , CT.OcfCreateEquityCompensationIssuance balanceIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_EquityTransferRejectsResultStockClassMismatch = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let sourceStockClassId = "SC_EQUITY_TRANSFER_CLASS_SOURCE" + let resultStockClassId = "SC_EQUITY_TRANSFER_CLASS_RESULT" + let stockPlanId = "PLAN_EQUITY_TRANSFER_CLASS_MISMATCH" + let sourceSecurityId = "SEC_EQUITY_TRANSFER_CLASS_SOURCE" + let resultSecurityId = "SEC_EQUITY_TRANSFER_CLASS_RESULT" + (capTableWithSourceClass, _) <- setupStockClassWith issuer cap_table (defaultStockClass sourceStockClassId) + (capTableWithResultClass, _) <- setupStockClassWith issuer capTableWithSourceClass (defaultStockClass resultStockClassId) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithResultClass "SH_EQUITY_TRANSFER_CLASS_MISMATCH" "Equity Transfer Class Holder" + let plan = (defaultStockPlan stockPlanId sourceStockClassId) with + initial_shares_reserved = 200.0 + stock_class_ids = [sourceStockClassId, resultStockClassId] + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_CLASS_SOURCE" sourceSecurityId stakeholderId sourceStockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let resultIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_CLASS_RESULT" resultSecurityId stakeholderId resultStockClassId stockPlanId 100.0 + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer (planEquityCompensationTransfer "TX_EQUITY_TRANSFER_CLASS_MISMATCH" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance resultIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityTransferSuccessorCanExercise = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_TRANSFER_SUCCESSOR_EXERCISE" + let sourceSecurityId = "SEC-EQUITY-TRANSFER-EXERCISE-SOURCE" + let resultSecurityId = "SEC-EQUITY-TRANSFER-EXERCISE-RESULT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_EXERCISE_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + let resultIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_EXERCISE_RESULT" resultSecurityId stakeholderId stockClassId stockPlanId 100.0 + transferred <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer (planEquityCompensationTransfer "TX_EQUITY_TRANSFER_SUCCESSOR_EXERCISE" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance resultIssuance + ] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise (planEquityCompensationExercise "TX_EQUITY_TRANSFER_SUCCESSOR_EXERCISED" resultSecurityId 100.0)] + edits = [] + deletes = [] + pure () + testStockPlanReservedShares_NonPlanEquitySourceDoesNotHidePlanSuccessor = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index cda50012..7e5e0f58 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4da50e6e16bb604cf8655b31dce9c0832babf330395b1204d139f7eeb7b16c09 -size 2763000 +oid sha256:355ace808e4b4388e6e5c402273f6776af80141283d3703a2e7dc8d154954288 +size 2823348 diff --git a/dars/dars.lock b/dars/dars.lock index 23ae0267..4d0c0d1a 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "4da50e6e16bb604cf8655b31dce9c0832babf330395b1204d139f7eeb7b16c09", - "size": 2763000, + "sha256": "355ace808e4b4388e6e5c402273f6776af80141283d3703a2e7dc8d154954288", + "size": 2823348, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-10T13:44:14Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index c4fc47bc..7213da5e 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -20,6 +20,7 @@ import DA.Optional (fromOptional, isNone, isSome) import Fairmint.OpenCapTable.Types.Core (Context) import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfConversionMechanism(..), OcfConversionTrigger(..), OcfConversionTriggerType(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..)) +import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) import Fairmint.OpenCapTable.OCF.Issuer (Issuer(..), IssuerOcfData) -- OCF Types @@ -140,8 +141,12 @@ 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_plan_retraction_events_by_security_id: Optional (Map Text [StockClassShareEvent]) + stock_successor_sort_keys_by_security_id: Optional (Map Text (Time, Int, Int)) deriving (Eq, Show) +type StockSuccessorEdge = (Text, Time, [Text]) +type StockSuccessorEdgeIndex = Map Text [(Time, [Text])] + data StockPlanSuccessorIndex = StockPlanSuccessorIndex with stock_successors_by_security_id: Map Text (Time, Optional Text) equity_compensation_successors_by_security_id: Map Text (Time, Optional Text) @@ -252,6 +257,51 @@ equityCompensationBalanceSecurityIssuanceExists maps balanceSecurityId = case ba Some securityId -> isSome (Map.lookup securityId maps.equity_compensation_issuances_by_security_id) None -> True +noEquityCompensationBalanceSecurityIssuanceExists : CapTableMaps -> Optional Text -> Bool +noEquityCompensationBalanceSecurityIssuanceExists maps balanceSecurityId = case balanceSecurityId of + Some securityId -> isNone (Map.lookup securityId maps.equity_compensation_issuances_by_security_id) + None -> True + +allResultingEquityCompensationIssuancesExist : CapTableMaps -> [Text] -> Bool +allResultingEquityCompensationIssuancesExist maps securityIds = + not (null securityIds) && all (\securityId -> isSome (Map.lookup securityId maps.equity_compensation_issuances_by_security_id)) securityIds + +noResultingEquityCompensationIssuancesExist : CapTableMaps -> [Text] -> Bool +noResultingEquityCompensationIssuancesExist maps securityIds = + all (\securityId -> isNone (Map.lookup securityId maps.equity_compensation_issuances_by_security_id)) securityIds + +assertNoPartialResultingEquityCompensationIssuanceCoverage : Text -> CapTableMaps -> [Text] -> Update () +assertNoPartialResultingEquityCompensationIssuanceCoverage label maps securityIds = + let hasPartialCoverage = + not (null securityIds) + && not (allResultingEquityCompensationIssuancesExist maps securityIds) + && not (noResultingEquityCompensationIssuancesExist maps securityIds) + in assertMsg (label <> " has only some resulting equity compensation issuances recorded") (not hasPartialCoverage) + +allEquityCompensationSuccessorIssuancesExist : CapTableMaps -> [Text] -> Optional Text -> Bool +allEquityCompensationSuccessorIssuancesExist maps resultingSecurityIds balanceSecurityId = + allResultingEquityCompensationIssuancesExist maps resultingSecurityIds + && equityCompensationBalanceSecurityIssuanceExists maps balanceSecurityId + +noEquityCompensationSuccessorIssuancesExist : CapTableMaps -> [Text] -> Optional Text -> Bool +noEquityCompensationSuccessorIssuancesExist maps resultingSecurityIds balanceSecurityId = + noResultingEquityCompensationIssuancesExist maps resultingSecurityIds + && noEquityCompensationBalanceSecurityIssuanceExists maps balanceSecurityId + +assertNoPartialEquityCompensationSuccessorCoverage : + Text -> + CapTableMaps -> + [Text] -> + Optional Text -> + Update () +assertNoPartialEquityCompensationSuccessorCoverage label maps resultingSecurityIds balanceSecurityId = do + assertNoPartialResultingEquityCompensationIssuanceCoverage label maps resultingSecurityIds + assertMsg + (label <> " has only some successor equity compensation issuances recorded") + ( allEquityCompensationSuccessorIssuancesExist maps resultingSecurityIds balanceSecurityId + || noEquityCompensationSuccessorIssuancesExist maps resultingSecurityIds balanceSecurityId + ) + noResultingStockIssuancesExist : CapTableMaps -> [Text] -> Bool noResultingStockIssuancesExist maps securityIds = all (\securityId -> isNone (Map.lookup securityId maps.stock_issuances_by_security_id)) securityIds @@ -449,12 +499,46 @@ warrantExerciseOutputShareLot issuanceData exerciseData = assertMsg ("Warrant exercise output cannot be reconstructed because trigger does not exist on warrant issuance: " <> exerciseData.trigger_id) False pure None +equityCompensationOutputStockClassId : + CapTableMaps -> + Text -> + EquityCompensationIssuanceOcfData -> + Update (Optional Text) +equityCompensationOutputStockClassId maps label issuanceData = + case issuanceData.compensation_type of + OcfCompensationTypeCSAR -> pure None + _ -> case issuanceData.stock_class_id of + Some stockClassId -> pure (Some stockClassId) + None -> case issuanceData.stock_plan_id of + Some stockPlanId -> case Map.lookup stockPlanId maps.stock_plans of + Some stockPlanCid -> do + stockPlan <- fetch stockPlanCid + case stockPlan.plan_data.stock_class_ids of + [stockClassId] -> pure (Some stockClassId) + [] -> do + assertMsg (label <> " cannot infer an output stock class because stock plan " <> stockPlanId <> " has no stock class") False + pure None + _ -> do + assertMsg (label <> " cannot infer an output stock class because stock plan " <> stockPlanId <> " has multiple stock classes") False + pure None + None -> do + assertMsg (label <> " cannot infer an output stock class because stock plan " <> stockPlanId <> " does not exist") False + pure None + None -> do + assertMsg (label <> " cannot infer an output stock class because its source issuance has neither stock_class_id nor stock_plan_id") False + pure None + equityCompensationExerciseOutputShareLot : + CapTableMaps -> EquityCompensationIssuanceOcfData -> EquityCompensationExerciseOcfData -> Update (Optional StockClassShareLot) -equityCompensationExerciseOutputShareLot issuanceData exerciseData = - case issuanceData.stock_class_id of +equityCompensationExerciseOutputShareLot maps issuanceData exerciseData = do + stockClassIdOpt <- equityCompensationOutputStockClassId + maps + ("Equity compensation exercise " <> exerciseData.id) + issuanceData + case stockClassIdOpt of Some stockClassId -> do assertNonNegativeQuantity ("Equity compensation exercise output quantity for " <> exerciseData.id) exerciseData.quantity pure (Some (StockClassShareLot with @@ -465,13 +549,17 @@ equityCompensationExerciseOutputShareLot issuanceData exerciseData = None -> pure None equityCompensationReleaseOutputShareLot : + CapTableMaps -> EquityCompensationIssuanceOcfData -> EquityCompensationReleaseOcfData -> - Optional StockClassShareLot -equityCompensationReleaseOutputShareLot issuanceData releaseData = - case issuanceData.stock_class_id of - Some stockClassId -> - Some (StockClassShareLot with + Update (Optional StockClassShareLot) +equityCompensationReleaseOutputShareLot maps issuanceData releaseData = do + stockClassIdOpt <- equityCompensationOutputStockClassId + maps + ("Equity compensation release " <> releaseData.id) + issuanceData + pure $ case stockClassIdOpt of + Some stockClassId -> Some (StockClassShareLot with stock_class_id = stockClassId security_id = None event_date = releaseData.settlement_date @@ -679,7 +767,7 @@ stockClassShareLotsByStockClass maps stockClassIndex = do else case Map.lookup exercise.exercise_data.security_id maps.equity_compensation_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid - lotOpt <- equityCompensationExerciseOutputShareLot issuance.issuance_data exercise.exercise_data + lotOpt <- equityCompensationExerciseOutputShareLot maps issuance.issuance_data exercise.exercise_data pure (addOptionalStockClassShareLot lotOpt index) None -> do assertMsg ("Equity compensation exercise references missing issuance: " <> exercise.exercise_data.security_id) False @@ -703,7 +791,8 @@ stockClassShareLotsByStockClass maps stockClassIndex = do else case Map.lookup release.release_data.security_id maps.equity_compensation_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid - pure (addOptionalStockClassShareLot (equityCompensationReleaseOutputShareLot issuance.issuance_data release.release_data) index) + lotOpt <- equityCompensationReleaseOutputShareLot maps issuance.issuance_data release.release_data + pure (addOptionalStockClassShareLot lotOpt index) None -> do assertMsg ("Equity compensation release references missing issuance: " <> release.release_data.security_id) False pure index) @@ -982,6 +1071,56 @@ addStockClassShareEventForKeys : [Text] -> StockClassShareEvent -> Map Text [Sto addStockClassShareEventForKeys keys event index = foldl (\updatedIndex key -> addStockClassShareEvent key event updatedIndex) index keys +stockSuccessorEdgeIndex : [StockSuccessorEdge] -> StockSuccessorEdgeIndex +stockSuccessorEdgeIndex = + foldl + (\index (sourceSecurityId, eventDate, successorSecurityIds) -> + Map.insert + sourceSecurityId + ((eventDate, successorSecurityIds) :: fromOptional [] (Map.lookup sourceSecurityId index)) + index) + Map.empty + +propagateStockSuccessorSortKeysFrom : + StockSuccessorEdgeIndex -> + [(Text, (Time, Int, Int))] -> + Map Text (Time, Int, Int) -> + Map Text (Time, Int, Int) -> + Map Text (Time, Int, Int) +propagateStockSuccessorSortKeysFrom edgeIndex pending expanded sortKeys = + case pending of + [] -> sortKeys + (sourceSecurityId, sourceSortKey) :: remaining -> + if Map.lookup sourceSecurityId expanded == Some sourceSortKey + then propagateStockSuccessorSortKeysFrom edgeIndex remaining expanded sortKeys + else + let (sourceDate, _, _) = sourceSortKey + successorSecurityIds = concatMap + (\(eventDate, edgeSuccessorIds) -> + if eventDate == sourceDate then edgeSuccessorIds else []) + (fromOptional [] (Map.lookup sourceSecurityId edgeIndex)) + nextSortKeys = foldl + (\index successorSecurityId -> Map.insert successorSecurityId sourceSortKey index) + sortKeys + successorSecurityIds + nextPending = map (\successorSecurityId -> (successorSecurityId, sourceSortKey)) successorSecurityIds ++ remaining + in propagateStockSuccessorSortKeysFrom + edgeIndex + nextPending + (Map.insert sourceSecurityId sourceSortKey expanded) + nextSortKeys + +propagateStockSuccessorSortKeys : + [StockSuccessorEdge] -> + Map Text (Time, Int, Int) -> + Map Text (Time, Int, Int) +propagateStockSuccessorSortKeys edges initialSortKeys = + propagateStockSuccessorSortKeysFrom + (stockSuccessorEdgeIndex edges) + (Map.toList initialSortKeys) + Map.empty + initialSortKeys + stockClassShareEventIndex : CapTableMaps -> Update StockClassShareEventIndex stockClassShareEventIndex maps = do cancellationEventsBySecurityId <- foldlA @@ -1081,19 +1220,94 @@ stockClassShareEventIndex maps = do Map.empty (Map.values maps.stock_class_splits) + reissuanceSuccessorSortKeys <- foldlA + (\index reissuanceCid -> do + reissuance <- fetch reissuanceCid + pure (foldl + (\updatedIndex securityId -> Map.insert securityId (reissuance.reissuance_data.date, 3, 0) updatedIndex) + index + reissuance.reissuance_data.resulting_security_ids)) + Map.empty + (Map.values maps.stock_reissuances) + + successorSortKeys <- foldlA + (\index consolidationCid -> do + consolidation <- fetch consolidationCid + pure (Map.insert + consolidation.consolidation_data.resulting_security_id + (consolidation.consolidation_data.date, 3, 0) + index)) + reissuanceSuccessorSortKeys + (Map.values maps.stock_consolidations) + + stockTransferSuccessorEdges <- foldlA + (\edges transferCid -> do + transfer <- fetch transferCid + let successorSecurityIds = case transfer.transfer_data.balance_security_id of + Some balanceSecurityId -> balanceSecurityId :: transfer.transfer_data.resulting_security_ids + None -> transfer.transfer_data.resulting_security_ids + pure ((transfer.transfer_data.security_id, transfer.transfer_data.date, successorSecurityIds) :: edges)) + [] + (Map.values maps.stock_transfers) + + stockCancellationSuccessorEdges <- foldlA + (\edges cancellationCid -> do + cancellation <- fetch cancellationCid + case cancellation.cancellation_data.balance_security_id of + Some balanceSecurityId -> + pure ((cancellation.cancellation_data.security_id, cancellation.cancellation_data.date, [balanceSecurityId]) :: edges) + None -> pure edges) + stockTransferSuccessorEdges + (Map.values maps.stock_cancellations) + + stockRepurchaseSuccessorEdges <- foldlA + (\edges repurchaseCid -> do + repurchase <- fetch repurchaseCid + case repurchase.repurchase_data.balance_security_id of + Some balanceSecurityId -> + pure ((repurchase.repurchase_data.security_id, repurchase.repurchase_data.date, [balanceSecurityId]) :: edges) + None -> pure edges) + stockCancellationSuccessorEdges + (Map.values maps.stock_repurchases) + + stockSuccessorEdges <- foldlA + (\edges conversionCid -> do + conversion <- fetch conversionCid + let successorSecurityIds = case conversion.conversion_data.balance_security_id of + Some balanceSecurityId -> balanceSecurityId :: conversion.conversion_data.resulting_security_ids + None -> conversion.conversion_data.resulting_security_ids + pure ((conversion.conversion_data.security_id, conversion.conversion_data.date, successorSecurityIds) :: edges)) + stockRepurchaseSuccessorEdges + (Map.values maps.stock_conversions) + + let propagatedSuccessorSortKeys = propagateStockSuccessorSortKeys stockSuccessorEdges successorSortKeys + pure StockClassShareEventIndex with stock_security_events_by_security_id = retractionEventsBySecurityId stock_plan_retraction_events_by_security_id = Some planRetractionEventsBySecurityId stock_class_split_events_by_stock_class_id = splitEventsByStockClassId + stock_successor_sort_keys_by_security_id = Some propagatedSuccessorSortKeys + +stockClassShareLotSplitCutoffSortKey : StockClassShareEventIndex -> StockClassShareLot -> (Time, Int, Int) +stockClassShareLotSplitCutoffSortKey eventIndex lot = + let defaultSortKey = (lot.event_date, 0, 0) + in case lot.security_id of + Some securityId -> case Map.lookup securityId (fromOptional Map.empty eventIndex.stock_successor_sort_keys_by_security_id) of + Some successorSortKey -> + let (successorDate, _, _) = successorSortKey + in if successorDate == lot.event_date then successorSortKey else defaultSortKey + None -> defaultSortKey + None -> defaultSortKey stockClassShareLotEvents : StockClassShareEventIndex -> StockClassShareLot -> [StockClassShareEvent] stockClassShareLotEvents eventIndex lot = - let securityEvents = case lot.security_id of + let splitCutoffSortKey = stockClassShareLotSplitCutoffSortKey eventIndex lot + securityEvents = case lot.security_id of Some securityId -> fromOptional [] (Map.lookup securityId eventIndex.stock_security_events_by_security_id) None -> [] splitEvents = filter - (stockClassShareEventOnOrAfter lot.event_date) + (\event -> stockClassShareEventSortKey event >= splitCutoffSortKey) (fromOptional [] (Map.lookup lot.stock_class_id eventIndex.stock_class_split_events_by_stock_class_id)) in (StockClassShareDelta with event_date = lot.event_date @@ -1103,7 +1317,8 @@ stockClassShareLotEvents eventIndex lot = stockClassShareLotPlanAllocationEvents : StockClassShareEventIndex -> StockClassShareLot -> [StockClassShareEvent] stockClassShareLotPlanAllocationEvents eventIndex lot = - let retractionEvents = case lot.security_id of + let splitCutoffSortKey = stockClassShareLotSplitCutoffSortKey eventIndex lot + retractionEvents = case lot.security_id of Some securityId -> filter (\event -> case event of @@ -1113,7 +1328,7 @@ stockClassShareLotPlanAllocationEvents eventIndex lot = None -> [] splitEvents = filter - (stockClassShareEventOnOrAfter lot.event_date) + (\event -> stockClassShareEventSortKey event >= splitCutoffSortKey) (fromOptional [] (Map.lookup lot.stock_class_id eventIndex.stock_class_split_events_by_stock_class_id)) in (StockClassShareDelta with event_date = lot.event_date @@ -1183,6 +1398,94 @@ stockSecurityQuantityBeforeTerminal : stockSecurityQuantityBeforeTerminal eventIndex issuanceData eventDate = stockSecurityQuantityBeforeEvent eventIndex issuanceData (eventDate, 2, 1) +stockReductionSortKey : Time -> Optional Text -> (Time, Int, Int) +stockReductionSortKey eventDate balanceSecurityId = + case balanceSecurityId of + Some _ -> (eventDate, 2, 1) + None -> (eventDate, 1, 0) + +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) + +stockIssuanceQuantityAtSortKey : CapTableMaps -> StockIssuanceOcfData -> (Time, Int, Int) -> Update Decimal +stockIssuanceQuantityAtSortKey maps issuanceData = + stockClassQuantityAtSortKey maps issuanceData.stock_class_id issuanceData.date issuanceData.quantity + +equityCompensationIssuanceQuantityAtSortKey : + CapTableMaps -> + EquityCompensationIssuanceOcfData -> + (Time, Int, Int) -> + Update Decimal +equityCompensationIssuanceQuantityAtSortKey maps issuanceData targetSortKey = + case issuanceData.stock_class_id of + Some stockClassId -> + stockClassQuantityAtSortKey maps stockClassId issuanceData.date issuanceData.quantity targetSortKey + None -> pure issuanceData.quantity + +equityCompensationIssuanceQuantityTotal : + CapTableMaps -> + Text -> + Optional Text -> + (Time, Int, Int) -> + [Text] -> + Update Decimal +equityCompensationIssuanceQuantityTotal maps label expectedStockClassId targetSortKey securityIds = + foldlA + (\total securityId -> + case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertMsg + (label <> " successor stock class mismatch for " <> securityId <> ": expected " <> show expectedStockClassId <> ", found " <> show issuance.issuance_data.stock_class_id) + (issuance.issuance_data.stock_class_id == expectedStockClassId) + quantity <- equityCompensationIssuanceQuantityAtSortKey maps issuance.issuance_data targetSortKey + pure (total + quantity) + None -> do + assertMsg (label <> " successor equity compensation issuance not found: " <> securityId) False + pure total) + 0.0 + securityIds + +stockIssuanceQuantityTotal : CapTableMaps -> Text -> Text -> (Time, Int, Int) -> [Text] -> Update Decimal +stockIssuanceQuantityTotal maps label expectedStockClassId targetSortKey securityIds = + foldlA + (\total securityId -> + case Map.lookup securityId maps.stock_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertMsg + (label <> " successor stock class mismatch for " <> securityId <> ": expected " <> expectedStockClassId <> ", found " <> issuance.issuance_data.stock_class_id) + (issuance.issuance_data.stock_class_id == expectedStockClassId) + quantity <- stockIssuanceQuantityAtSortKey maps issuance.issuance_data targetSortKey + pure (total + quantity) + None -> do + assertMsg (label <> " successor stock issuance not found: " <> securityId) False + pure total) + 0.0 + securityIds + validateStockBalanceSuccessor : CapTableMaps -> Text -> @@ -1289,6 +1592,66 @@ validateStockBalanceSuccessors eventIndex maps = do None -> pure ()) (Map.values maps.stock_transfers) +validateStockTransferResultIssuances : CapTableMaps -> Update () +validateStockTransferResultIssuances maps = + mapA_ + (\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 case Map.lookup transfer.transfer_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + let targetSortKey = stockReductionSortKey transfer.transfer_data.date transfer.transfer_data.balance_security_id + resultingQuantity <- stockIssuanceQuantityTotal + maps + label + sourceIssuance.issuance_data.stock_class_id + targetSortKey + transfer.transfer_data.resulting_security_ids + assertMsg + (label <> " resulting quantity mismatch: expected " <> show transfer.transfer_data.quantity <> ", found " <> show resultingQuantity) + (resultingQuantity == transfer.transfer_data.quantity) + None -> assertMsg (label <> " source stock issuance not found: " <> transfer.transfer_data.security_id) False + else pure ()) + (Map.values maps.stock_transfers) + +validateStockConsolidationResultIssuances : StockClassShareEventIndex -> CapTableMaps -> Update () +validateStockConsolidationResultIssuances eventIndex maps = + mapA_ + (\consolidationCid -> do + consolidation <- fetch consolidationCid + let label = "Stock consolidation " <> consolidation.consolidation_data.id + case Map.lookup consolidation.consolidation_data.resulting_security_id maps.stock_issuances_by_security_id of + Some resultIssuanceCid -> do + resultIssuance <- fetch resultIssuanceCid + let targetSortKey = (consolidation.consolidation_data.date, 3, 3) + sourceQuantity <- foldlA + (\total sourceSecurityId -> + case Map.lookup sourceSecurityId maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + assertMsg + (label <> " source stock class mismatch for " <> sourceSecurityId <> ": expected " <> resultIssuance.issuance_data.stock_class_id <> ", found " <> sourceIssuance.issuance_data.stock_class_id) + (sourceIssuance.issuance_data.stock_class_id == resultIssuance.issuance_data.stock_class_id) + quantity <- stockSecurityQuantityBeforeEvent + eventIndex + sourceIssuance.issuance_data + targetSortKey + pure (total + quantity) + None -> do + assertMsg (label <> " source stock issuance not found: " <> sourceSecurityId) False + pure total) + 0.0 + consolidation.consolidation_data.security_ids + resultQuantity <- stockIssuanceQuantityAtSortKey maps resultIssuance.issuance_data targetSortKey + assertMsg + (label <> " resulting quantity mismatch: expected " <> show sourceQuantity <> ", found " <> show resultQuantity) + (resultQuantity == sourceQuantity) + None -> pure ()) + (Map.values maps.stock_consolidations) + addOptionalDecimal : Optional Decimal -> Optional Decimal -> Optional Decimal addOptionalDecimal leftOpt rightOpt = case (leftOpt, rightOpt) of @@ -1817,27 +2180,47 @@ recordedInheritedStockIssuanceSummary successorIndex stockPlanId maps securityId (None, 0.0) securityIds -stockConversionPlanOutputShareLot : +recordedInheritedStockIssuanceLots : + Map Text (Time, Optional Text) -> + Text -> + CapTableMaps -> + [Text] -> + Update [StockClassShareLot] +recordedInheritedStockIssuanceLots successorIndex stockPlanId maps securityIds = + foldlA + (\lots securityId -> + case Map.lookup securityId maps.stock_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + if successorCarriesPlanAllocation successorIndex securityId issuance.issuance_data.stock_plan_id + && effectivePlanForSecurity successorIndex securityId issuance.issuance_data.stock_plan_id == Some stockPlanId + then pure (stockIssuanceShareLot issuance.issuance_data :: lots) + else pure lots + None -> pure lots) + [] + securityIds + +stockConversionPlanOutputShareLots : Map Text (Time, Optional Text) -> Text -> CapTableMaps -> Map Text StockClassOcfData -> StockIssuanceOcfData -> StockConversionOcfData -> - Update (Optional StockClassShareLot) -stockConversionPlanOutputShareLot successorIndex stockPlanId maps stockClassIndex sourceIssuanceData conversionData = - if allResultingStockIssuancesExist maps conversionData.resulting_security_ids - then do - (stockClassIdOpt, quantity) <- recordedInheritedStockIssuanceSummary - successorIndex stockPlanId maps conversionData.resulting_security_ids - pure $ case stockClassIdOpt of - Some stockClassId -> Some (StockClassShareLot with - stock_class_id = stockClassId - security_id = None - event_date = conversionData.date - quantity = quantity) - None -> None - else stockConversionOutputShareLot maps stockClassIndex sourceIssuanceData conversionData + Update [StockClassShareLot] +stockConversionPlanOutputShareLots successorIndex stockPlanId maps stockClassIndex sourceIssuanceData conversionData = do + resultLots <- if allResultingStockIssuancesExist maps conversionData.resulting_security_ids + then recordedInheritedStockIssuanceLots successorIndex stockPlanId maps conversionData.resulting_security_ids + else do + outputLotOpt <- stockConversionOutputShareLot maps stockClassIndex sourceIssuanceData conversionData + pure $ case outputLotOpt of + Some outputLot -> [outputLot] + None -> [] + balanceLots <- case conversionData.balance_security_id of + Some balanceSecurityId -> + recordedInheritedStockIssuanceLots successorIndex stockPlanId maps [balanceSecurityId] + None -> pure [] + pure (resultLots ++ balanceLots) stockPlanSuccessorExcessLot : Text -> @@ -1863,33 +2246,6 @@ stockPlanInheritedSuccessorExcessLots : CapTableMaps -> Update [(Text, StockClassShareLot)] stockPlanInheritedSuccessorExcessLots eventIndex successorIndex maps = do - transferLots <- foldlA - (\lots transferCid -> do - transfer <- fetch transferCid - case Map.lookup transfer.transfer_data.security_id maps.stock_issuances_by_security_id of - Some sourceIssuanceCid -> do - sourceIssuance <- fetch sourceIssuanceCid - case effectivePlanForSecurity - successorIndex.stock_successors_by_security_id - sourceIssuance.issuance_data.security_id - sourceIssuance.issuance_data.stock_plan_id of - Some stockPlanId -> do - (_, successorQuantity) <- recordedInheritedStockIssuanceSummary - successorIndex.stock_successors_by_security_id - stockPlanId - maps - transfer.transfer_data.resulting_security_ids - pure (stockPlanSuccessorExcessLot - stockPlanId - sourceIssuance.issuance_data.stock_class_id - transfer.transfer_data.date - transfer.transfer_data.quantity - successorQuantity ++ lots) - None -> pure lots - None -> pure lots) - [] - (Map.values maps.stock_transfers) - reissuanceLots <- foldlA (\lots reissuanceCid -> do reissuance <- fetch reissuanceCid @@ -1918,12 +2274,20 @@ stockPlanInheritedSuccessorExcessLots eventIndex successorIndex maps = do successorQuantity ++ lots) None -> pure lots None -> pure lots) - transferLots + [] (Map.values maps.stock_reissuances) + pure reissuanceLots + +stockPlanConversionAllocationTransitions : + StockClassShareEventIndex -> + StockPlanSuccessorIndex -> + CapTableMaps -> + Update [(Text, StockClassShareLot, [StockClassShareLot])] +stockPlanConversionAllocationTransitions eventIndex successorIndex maps = do stockClassIndex <- stockClassDataIndex maps foldlA - (\lots conversionCid -> do + (\transitions conversionCid -> do conversion <- fetch conversionCid case Map.lookup conversion.conversion_data.security_id maps.stock_issuances_by_security_id of Some sourceIssuanceCid -> do @@ -1933,26 +2297,88 @@ stockPlanInheritedSuccessorExcessLots eventIndex successorIndex maps = do sourceIssuance.issuance_data.security_id sourceIssuance.issuance_data.stock_plan_id of Some stockPlanId -> do - outputLotOpt <- stockConversionPlanOutputShareLot + outputLots <- stockConversionPlanOutputShareLots successorIndex.stock_successors_by_security_id stockPlanId maps stockClassIndex sourceIssuance.issuance_data conversion.conversion_data - pure $ case outputLotOpt of - Some outputLot -> stockPlanSuccessorExcessLot - stockPlanId - outputLot.stock_class_id + sourceBasisQuantity <- case conversion.conversion_data.balance_security_id of + Some _ -> stockSecurityQuantityBeforeTerminal + eventIndex + sourceIssuance.issuance_data conversion.conversion_data.date - conversion.conversion_data.quantity_converted - outputLot.quantity ++ lots - None -> lots - None -> pure lots - None -> pure lots) - reissuanceLots + None -> pure conversion.conversion_data.quantity_converted + pure $ case outputLots of + _ :: _ -> + ( stockPlanId + , StockClassShareLot with + stock_class_id = sourceIssuance.issuance_data.stock_class_id + security_id = Some sourceIssuance.issuance_data.security_id + event_date = conversion.conversion_data.date + quantity = sourceBasisQuantity + , outputLots + ) :: transitions + [] -> transitions + None -> pure transitions + None -> pure transitions) + [] (Map.values maps.stock_conversions) +stockPlanConversionAllocationAdjustmentIndex : + StockClassShareEventIndex -> + StockPlanSuccessorIndex -> + CapTableMaps -> + Update (Map Text Decimal) +stockPlanConversionAllocationAdjustmentIndex eventIndex successorIndex maps = do + transitions <- stockPlanConversionAllocationTransitions eventIndex successorIndex maps + foldlA + (\totals (stockPlanId, sourceLot, outputLots) -> + let outputQuantityOpt = foldl + (\totalOpt outputLot -> addOptionalDecimal totalOpt (stockClassShareLotPlanAllocatedQuantity eventIndex outputLot)) + (Some 0.0) + outputLots + in + case + ( stockClassShareLotPlanAllocatedQuantity eventIndex sourceLot + , outputQuantityOpt + ) of + (Some sourceQuantity, Some outputQuantity) -> + pure (addQuantityById stockPlanId (outputQuantity - sourceQuantity) totals) + _ -> do + assertMsg ("Stock conversion plan-allocation transition has an invalid quantity for " <> stockPlanId) False + pure totals) + Map.empty + transitions + +stockPlanConversionAllocationAdjustmentIndexAt : + StockClassShareEventIndex -> + StockPlanSuccessorIndex -> + CapTableMaps -> + Time -> + Update (Map Text Decimal) +stockPlanConversionAllocationAdjustmentIndexAt eventIndex successorIndex maps asOf = do + transitions <- stockPlanConversionAllocationTransitions eventIndex successorIndex maps + foldlA + (\totals (stockPlanId, sourceLot, outputLots) -> + let outputQuantityOpt = foldl + (\totalOpt outputLot -> addOptionalDecimal totalOpt (stockClassShareLotPlanAllocatedQuantityAt eventIndex outputLot asOf)) + (Some 0.0) + outputLots + in + case + ( stockClassShareLotPlanAllocatedQuantityAt eventIndex sourceLot asOf + , outputQuantityOpt + ) of + (Some sourceQuantity, Some outputQuantity) -> + pure (addQuantityById stockPlanId (outputQuantity - sourceQuantity) totals) + _ -> do + assertMsg ("Stock conversion plan-allocation transition has an invalid quantity for " <> stockPlanId <> " on " <> show asOf) False + pure totals) + Map.empty + transitions + stockPlanSuccessorExcessAllocationIndex : StockClassShareEventIndex -> StockPlanSuccessorIndex -> @@ -2229,6 +2655,30 @@ 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 do + let label = "Equity compensation transfer " <> transfer.transfer_data.id + assertNoPartialEquityCompensationSuccessorCoverage + label + maps + transfer.transfer_data.resulting_security_ids + transfer.transfer_data.balance_security_id + if allEquityCompensationSuccessorIssuancesExist + maps + transfer.transfer_data.resulting_security_ids + transfer.transfer_data.balance_security_id + then pure (stockSecurityReductionEvents + transfer.transfer_data.date + transfer.transfer_data.quantity + transfer.transfer_data.balance_security_id ++ events) + else pure events + else pure events) + cancellationEvents + (Map.values maps.equity_compensation_transfers) + exerciseEvents <- foldlA (\events exerciseCid -> do exercise <- fetch exerciseCid @@ -2238,7 +2688,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 @@ -2287,6 +2737,96 @@ equityCompensationQuantityAt maps issuanceData asOf = do (sortOn stockClassShareEventSortKey (filter (stockClassShareEventOnOrBefore asOf) events))) else pure (Some 0.0) +equityCompensationSecurityQuantityBeforeEvent : + CapTableMaps -> + EquityCompensationIssuanceOcfData -> + (Time, Int, Int) -> + Update Decimal +equityCompensationSecurityQuantityBeforeEvent maps issuanceData targetSortKey = do + events <- equityCompensationSecurityEvents maps issuanceData + let priorEvents = filter + (\event -> stockClassShareEventSortKey event < targetSortKey) + (sortOn stockClassShareEventSortKey events) + case foldl applyStockClassShareEventOptional (Some issuanceData.quantity) priorEvents of + Some quantity -> pure quantity + None -> do + assertMsg ("Equity compensation quantity history is invalid for " <> issuanceData.security_id <> " before " <> show targetSortKey) False + pure 0.0 + +validateEquityCompensationBalanceSuccessorQuantity : + CapTableMaps -> + Text -> + Optional Text -> + (Time, Int, Int) -> + Decimal -> + Text -> + Update () +validateEquityCompensationBalanceSuccessorQuantity maps label expectedStockClassId targetSortKey expectedQuantity securityId = + case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertMsg + (label <> " balance stock class mismatch for " <> securityId <> ": expected " <> show expectedStockClassId <> ", found " <> show issuance.issuance_data.stock_class_id) + (issuance.issuance_data.stock_class_id == expectedStockClassId) + actualQuantity <- equityCompensationIssuanceQuantityAtSortKey maps issuance.issuance_data targetSortKey + 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 + +validateEquityCompensationTransferSuccessorContinuity : CapTableMaps -> Update () +validateEquityCompensationTransferSuccessorContinuity maps = + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + let label = "Equity compensation transfer " <> transfer.transfer_data.id + assertNoPartialEquityCompensationSuccessorCoverage + label + maps + transfer.transfer_data.resulting_security_ids + transfer.transfer_data.balance_security_id + if allEquityCompensationSuccessorIssuancesExist + maps + transfer.transfer_data.resulting_security_ids + transfer.transfer_data.balance_security_id + then case Map.lookup transfer.transfer_data.security_id maps.equity_compensation_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + let targetSortKey = stockReductionSortKey transfer.transfer_data.date transfer.transfer_data.balance_security_id + resultingQuantity <- equityCompensationIssuanceQuantityTotal + maps + label + sourceIssuance.issuance_data.stock_class_id + targetSortKey + transfer.transfer_data.resulting_security_ids + assertMsg + (label <> " resulting quantity mismatch: expected " <> show transfer.transfer_data.quantity <> ", found " <> show resultingQuantity) + (resultingQuantity == transfer.transfer_data.quantity) + case transfer.transfer_data.balance_security_id of + Some balanceSecurityId -> do + let terminalSortKey = (transfer.transfer_data.date, 2, 1) + currentQuantity <- equityCompensationSecurityQuantityBeforeEvent + maps + sourceIssuance.issuance_data + terminalSortKey + let expectedBalanceQuantity = currentQuantity - transfer.transfer_data.quantity + assertMsg + (label <> " exceeds the current source quantity for " <> sourceIssuance.issuance_data.security_id) + (expectedBalanceQuantity >= 0.0) + validateEquityCompensationBalanceSuccessorQuantity + maps + label + sourceIssuance.issuance_data.stock_class_id + terminalSortKey + expectedBalanceQuantity + balanceSecurityId + None -> pure () + None -> + assertMsg (label <> " source equity compensation issuance not found: " <> transfer.transfer_data.security_id) False + else pure ()) + (Map.values maps.equity_compensation_transfers) + validateStockPlanEquityCompensationSourceQuantities : CapTableMaps -> Update () validateStockPlanEquityCompensationSourceQuantities maps = mapA_ @@ -2442,10 +2982,13 @@ stockPlanNetAllocatedQuantityIndex eventIndex maps = do stockAllocation <- stockIssuancePlanAllocationIndex eventIndex successorIndex maps equityCompensationAllocation <- equityCompensationPlanAllocationIndex successorIndex maps successorExcessAllocation <- stockPlanSuccessorExcessAllocationIndex eventIndex successorIndex maps + conversionAdjustment <- stockPlanConversionAllocationAdjustmentIndex eventIndex successorIndex maps returnedQuantity <- stockPlanReturnedQuantityIndex maps let combinedAllocation = mergeQuantityMaps - (mergeQuantityMaps stockAllocation equityCompensationAllocation) - successorExcessAllocation + (mergeQuantityMaps + (mergeQuantityMaps stockAllocation equityCompensationAllocation) + successorExcessAllocation) + conversionAdjustment let netAllocatedByPlan = mergeQuantityMaps combinedAllocation (negateQuantityMap returnedQuantity) pure netAllocatedByPlan @@ -2532,10 +3075,13 @@ stockPlanNetAllocatedQuantityIndexAt eventIndex maps asOf = do stockAllocation <- stockIssuancePlanAllocationIndexAt eventIndex successorIndex maps asOf equityCompensationAllocation <- equityCompensationPlanAllocationIndexAt successorIndex maps asOf successorExcessAllocation <- stockPlanSuccessorExcessAllocationIndexAt eventIndex successorIndex maps asOf + conversionAdjustment <- stockPlanConversionAllocationAdjustmentIndexAt eventIndex successorIndex maps asOf returnedQuantity <- stockPlanReturnedQuantityIndexAt maps asOf let combinedAllocation = mergeQuantityMaps - (mergeQuantityMaps stockAllocation equityCompensationAllocation) - successorExcessAllocation + (mergeQuantityMaps + (mergeQuantityMaps stockAllocation equityCompensationAllocation) + successorExcessAllocation) + conversionAdjustment pure (mergeQuantityMaps combinedAllocation (negateQuantityMap returnedQuantity)) validateStockPlanAllocationReferences : CapTableMaps -> Map Text Decimal -> Update () @@ -2845,6 +3391,9 @@ validateAuthorizedShareCeilings : validateAuthorizedShareCeilings maps adjustmentIndex issuerCid = do eventIndex <- stockClassShareEventIndex maps validateStockBalanceSuccessors eventIndex maps + validateStockTransferResultIssuances maps + validateStockConsolidationResultIssuances eventIndex maps + validateEquityCompensationTransferSuccessorContinuity maps stockClassIndex <- stockClassDataIndex maps stockClasses <- stockClassDataList maps lotsByStockClass <- stockClassShareLotsByStockClass maps stockClassIndex From 6f6124e28acd2462a72cacab2071a4c719b43e85 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 15:20:37 -0400 Subject: [PATCH 38/62] test: cover edited transfer successor continuity --- .../TestStockPlanReservedShares.daml | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index b758ebce..45dbdab7 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -1085,6 +1085,33 @@ testStockPlanReservedShares_StockTransferQuantityIncreaseConsumesPlanHeadroom = edits = [] deletes = [] +testStockPlanReservedShares_StockTransferExactContinuityRejectsEditedSuccessorQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockPlanId = "PLAN_STOCK_TRANSFER_SUCCESSOR_EDIT" + (capTableIssued, stockClassId, stakeholderId) <- createPlanAndStockIssuance issuer cap_table stockPlanId 300.0 100.0 + let sourceSecurityId = "SEC-" <> stockPlanId + let resultSecurityId = "SEC-STOCK-TRANSFER-SUCCESSOR-EDIT" + let resultIssuance = (defaultStockIssuance "TX_STOCK_TRANSFER_SUCCESSOR_EDIT_RESULT" resultSecurityId stakeholderId stockClassId) with + stock_plan_id = None + quantity = 100.0 + + transferred <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (planStockTransfer "TX_STOCK_TRANSFER_SUCCESSOR_EDIT" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + + -- The plan still has 200 shares of headroom, so this must fail because the + -- edited successor no longer equals the transfer's recorded 100-share output. + submitMustFail issuer do + exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockIssuance (resultIssuance with quantity = 200.0)] + deletes = [] + testStockPlanReservedShares_ConvertedAllocationUsesDestinationClassSplit = script do TestOcp{issuer, cap_table} <- setupTestOcp let sourceStockClassId = "SC_PLAN_CONVERSION_SOURCE" From 61d8f2613a374d936eb47773962bf341be62f9c1 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 16:27:43 -0400 Subject: [PATCH 39/62] fix: enforce successor lifecycle continuity --- .../TestEquityCompensationCancellation.daml | 42 +- .../TestStockClassAuthorizedShares.daml | 429 ++++++++++++++++-- .../TestStockPlanReservedShares.daml | 292 ++++++++++++ .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 296 ++++++++++-- 6 files changed, 982 insertions(+), 85 deletions(-) diff --git a/Test/daml/OpenCapTable/TestEquityCompensationCancellation.daml b/Test/daml/OpenCapTable/TestEquityCompensationCancellation.daml index 555578cd..fe63b090 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationCancellation.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationCancellation.daml @@ -90,17 +90,43 @@ testEquityCompensationCancellation_PartialWithOptionals = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp capTableCid <- addDefaultStakeholder issuer cap_table let securityId = "SEC-EC-PARTIAL" + let balanceSecurityId = "SEC-EC-PARTIAL-BAL" + let cancellationDate = DT.time (DA.date 2024 Jun 30) 0 0 0 result <- createEquityCompensationIssuance issuer capTableCid securityId 500000.0 _ <- submit issuer do exerciseCmd result.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 - security_id = securityId - quantity = 150000.0 - reason_text = "Partial forfeiture due to early departure" - balance_security_id = Some "SEC-EC-PARTIAL-BAL" - comments = ["Partial cancellation per separation agreement", "Remaining vested options: 350,000"]] + creates = + [ CT.OcfCreateEquityCompensationCancellation EquityCompensationCancellationOcfData with + id = "TX_EC_CANCEL_PARTIAL" + date = cancellationDate + security_id = securityId + quantity = 150000.0 + reason_text = "Partial forfeiture due to early departure" + balance_security_id = Some balanceSecurityId + comments = ["Partial cancellation per separation agreement", "Remaining vested options: 350,000"] + , CT.OcfCreateEquityCompensationIssuance EquityCompensationIssuanceOcfData with + id = "TX_EC_CANCEL_PARTIAL_BALANCE" + date = cancellationDate + security_id = balanceSecurityId + custom_id = "GRANT-001-BALANCE" + stakeholder_id = "SH-1" + board_approval_date = None + stockholder_approval_date = None + consideration_text = None + security_law_exemptions = [] + stock_plan_id = None + stock_class_id = Some "COMMON" + compensation_type = OcfCompensationTypeOptionNSO + quantity = 350000.0 + exercise_price = Some (OcfMonetary with amount = 0.10, currency = "USD") + base_price = None + early_exercisable = Some False + vesting_terms_id = None + vestings = [] + expiration_date = None + termination_exercise_windows = [] + comments = [] + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 0bc75ee4..6ec6a1af 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -122,13 +122,13 @@ stockClassConversionRatioAdjustment adjustmentId stockClassId numerator = rounding_type = OcfRoundingNormal comments = [] -warrantFixedExerciseTrigger : Text -> Text -> Decimal -> OcfConversionTrigger -warrantFixedExerciseTrigger triggerId stockClassId convertsToQuantity = +warrantExerciseTriggerWith : Text -> OcfWarrantConversionMechanism -> Optional Text -> OcfConversionTrigger +warrantExerciseTriggerWith triggerId conversionMechanism stockClassId = OcfConversionTrigger with conversion_right = OcfRightWarrant (OcfWarrantConversionRight with - conversion_mechanism = OcfWarrantMechanismFixedAmount with converts_to_quantity = convertsToQuantity + conversion_mechanism = conversionMechanism converts_to_future_round = None - converts_to_stock_class_id = Some stockClassId + converts_to_stock_class_id = stockClassId type_ = "WARRANT_CONVERSION_RIGHT") end_date = None nickname = None @@ -139,22 +139,19 @@ warrantFixedExerciseTrigger triggerId stockClassId convertsToQuantity = trigger_id = triggerId type_ = OcfTriggerTypeTypeElectiveAtWill +warrantFixedExerciseTrigger : Text -> Text -> Decimal -> OcfConversionTrigger +warrantFixedExerciseTrigger triggerId stockClassId convertsToQuantity = + warrantExerciseTriggerWith + triggerId + (OcfWarrantMechanismFixedAmount with converts_to_quantity = convertsToQuantity) + (Some stockClassId) + warrantCustomExerciseTrigger : Text -> Text -> OcfConversionTrigger warrantCustomExerciseTrigger triggerId stockClassId = - OcfConversionTrigger with - conversion_right = OcfRightWarrant (OcfWarrantConversionRight with - conversion_mechanism = OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant output" - 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 + warrantExerciseTriggerWith + triggerId + (OcfWarrantMechanismCustom with custom_conversion_description = "Custom warrant output") + (Some stockClassId) warrantIssuance : Text -> Text -> Text -> Text -> Decimal -> WarrantIssuanceOcfData warrantIssuance issuanceId securityId stakeholderId stockClassId convertsToQuantity = @@ -663,31 +660,28 @@ testStockTransferResultMustRemainInSourceStockClass = script do edits = [] deletes = [] -testStockTransferResultQuantityNormalizesInterveningSplit = script do +testStockTransferResultDateMustMatchTransferDate = script do TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_TRANSFER_SPLIT_NORMALIZED" 1_000.0) - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_TRANSFER_SPLIT_NORMALIZED" "Transfer Split Holder" + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_TRANSFER_RESULT_DATE" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_TRANSFER_RESULT_DATE" "Transfer Result Date Holder" let sourceDate = DT.time (DA.date 2024 Jan 01) 0 0 0 let transferDate = DT.time (DA.date 2024 Jan 02) 0 0 0 - let splitDate = DT.time (DA.date 2024 Jan 03) 0 0 0 - let resultDate = DT.time (DA.date 2024 Jan 04) 0 0 0 - capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_TRANSFER_SPLIT_SOURCE" "SEC_TRANSFER_SPLIT_SOURCE" stakeholderId stockClassId) with + let resultDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_TRANSFER_RESULT_DATE_SOURCE" "SEC_TRANSFER_RESULT_DATE_SOURCE" stakeholderId stockClassId) with date = sourceDate quantity = 100.0) - _ <- submit issuer do + submitMustFail issuer do exerciseCmd capTableIssued CT.UpdateCapTable with creates = - [ CT.OcfCreateStockTransfer ((stockTransfer "TX_TRANSFER_SPLIT_NORMALIZED" "SEC_TRANSFER_SPLIT_SOURCE" 100.0 ["SEC_TRANSFER_SPLIT_RESULT"]) with date = transferDate) - , CT.OcfCreateStockClassSplit ((stockClassSplit "TX_TRANSFER_INTERVENING_SPLIT" stockClassId 2.0 1.0) with date = splitDate) - , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_TRANSFER_SPLIT_RESULT" "SEC_TRANSFER_SPLIT_RESULT" stakeholderId stockClassId) with + [ CT.OcfCreateStockTransfer ((stockTransfer "TX_TRANSFER_RESULT_DATE" "SEC_TRANSFER_RESULT_DATE_SOURCE" 100.0 ["SEC_TRANSFER_RESULT_DATE_RESULT"]) with date = transferDate) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_TRANSFER_RESULT_DATE_RESULT" "SEC_TRANSFER_RESULT_DATE_RESULT" stakeholderId stockClassId) with date = resultDate - quantity = 200.0) + quantity = 100.0) ] edits = [] deletes = [] - pure () testStockReissuanceWithResultingIssuanceDoesNotDoubleCountForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -706,6 +700,84 @@ testStockReissuanceWithResultingIssuanceDoesNotDoubleCountForCeiling = script do deletes = [] pure () +testStockReissuanceResultQuantityMustMatchSourceQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REISSUANCE_QUANTITY" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_REISSUANCE_QUANTITY" "Reissuance Quantity Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_REISSUANCE_QUANTITY_SOURCE" "SEC_REISSUANCE_QUANTITY_SOURCE" stakeholderId stockClassId) with quantity = 100.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance (stockReissuance "TX_REISSUANCE_QUANTITY" "SEC_REISSUANCE_QUANTITY_SOURCE" ["SEC_REISSUANCE_QUANTITY_RESULT"]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_REISSUANCE_QUANTITY_RESULT" "SEC_REISSUANCE_QUANTITY_RESULT" stakeholderId stockClassId) with quantity = 10.0) + ] + edits = [] + deletes = [] + +testStockReissuanceResultMustRemainInSourceStockClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithSource, sourceStockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REISSUANCE_CLASS_SOURCE" 1_000.0) + (capTableWithResult, resultStockClassId) <- setupStockClassWith issuer capTableWithSource (limitedStockClass "SC_REISSUANCE_CLASS_RESULT" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithResult "SH_REISSUANCE_CLASS" "Reissuance Class Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_REISSUANCE_CLASS_SOURCE" "SEC_REISSUANCE_CLASS_SOURCE" stakeholderId sourceStockClassId) with quantity = 100.0) + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance (stockReissuance "TX_REISSUANCE_CLASS" "SEC_REISSUANCE_CLASS_SOURCE" ["SEC_REISSUANCE_CLASS_RESULT"]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_REISSUANCE_CLASS_RESULT" "SEC_REISSUANCE_CLASS_RESULT" stakeholderId resultStockClassId) with quantity = 100.0) + ] + edits = [] + deletes = [] + +testStockReissuanceResultDateMustMatchReissuanceDate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REISSUANCE_DATE" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_REISSUANCE_DATE" "Reissuance Date Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_REISSUANCE_DATE_SOURCE" "SEC_REISSUANCE_DATE_SOURCE" stakeholderId stockClassId) with quantity = 100.0) + let reissuanceDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let resultDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance ((stockReissuance "TX_REISSUANCE_DATE" "SEC_REISSUANCE_DATE_SOURCE" ["SEC_REISSUANCE_DATE_RESULT"]) with date = reissuanceDate) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_REISSUANCE_DATE_RESULT" "SEC_REISSUANCE_DATE_RESULT" stakeholderId stockClassId) with + date = resultDate + quantity = 100.0) + ] + edits = [] + deletes = [] + +testStockReissuanceSameTimestampSplitUsesAdjustedSourceQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REISSUANCE_SPLIT" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_REISSUANCE_SPLIT" "Reissuance Split Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_REISSUANCE_SPLIT_SOURCE" "SEC_REISSUANCE_SPLIT_SOURCE" stakeholderId stockClassId) with quantity = 50.0) + let eventDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassSplit ((stockClassSplit "TX_REISSUANCE_SPLIT_EVENT" stockClassId 2.0 1.0) with date = eventDate) + , CT.OcfCreateStockReissuance ((stockReissuance "TX_REISSUANCE_SPLIT" "SEC_REISSUANCE_SPLIT_SOURCE" ["SEC_REISSUANCE_SPLIT_RESULT"]) with date = eventDate) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_REISSUANCE_SPLIT_RESULT" "SEC_REISSUANCE_SPLIT_RESULT" stakeholderId stockClassId) with + date = eventDate + quantity = 100.0) + ] + edits = [] + deletes = [] + pure () + testStockConsolidationWithResultingIssuanceDoesNotDoubleCountForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -741,6 +813,27 @@ testStockConsolidationResultQuantityMustEqualSourceTotal = script do edits = [] deletes = [] +testStockConsolidationResultDateMustMatchConsolidationDate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONSOLIDATION_DATE" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONSOLIDATION_DATE" "Consolidation Date Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_CONSOLIDATION_DATE_SOURCE" "SEC_CONSOLIDATION_DATE_SOURCE" stakeholderId stockClassId) with quantity = 100.0) + let consolidationDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let resultDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + + submitMustFail issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation ((stockConsolidation "TX_CONSOLIDATION_DATE" ["SEC_CONSOLIDATION_DATE_SOURCE"] "SEC_CONSOLIDATION_DATE_RESULT") with date = consolidationDate) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CONSOLIDATION_DATE_RESULT" "SEC_CONSOLIDATION_DATE_RESULT" stakeholderId stockClassId) with + date = resultDate + quantity = 100.0) + ] + edits = [] + deletes = [] + testStockConsolidationSourcesMustMatchResultStockClass = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -1081,6 +1174,34 @@ testStockConversionWithNonRatioRightFailsForCeiling = script do edits = [] deletes = [] +testStockConversionSkipsNonComputableRightWhenRatioRightMatchesResult = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_MIXED_COMMON" 100.0) + let preferredClassId = "SC_CONVERSION_MIXED_SOURCE" + let ppsRight = (stockClassConversionRightTo commonClassId) with + conversion_mechanism = OcfConversionMechanismPpsBasedConversion + ratio = None + reference_share_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + let preferredClass = (limitedStockClass preferredClassId 100.0) with + conversion_rights = [ppsRight, stockClassConversionRightTo commonClassId] + (capTableWithPreferred, _) <- setupStockClassWith issuer capTableWithCommon preferredClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_MIXED" "Conversion Mixed Rights Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_CONVERSION_MIXED_SOURCE" "SEC_CONVERSION_MIXED_SOURCE" stakeholderId preferredClassId) with quantity = 50.0) + let conversion = stockConversion "TX_CONVERSION_MIXED" "SEC_CONVERSION_MIXED_SOURCE" 50.0 + let resultIssuance = (defaultStockIssuance "TX_CONVERSION_MIXED_RESULT" "TX_CONVERSION_MIXED_RESULT" stakeholderId commonClassId) with quantity = 50.0 + + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConversion conversion + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + pure () + testWarrantExerciseCannotExceedStockClassAuthorizedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -1116,6 +1237,85 @@ testFixedWarrantPartialExerciseUsesExerciseQuantityForCeiling = script do deletes = [] pure () +testFixedWarrantExerciseScalesDeclaredShareOutput = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_FIXED_SCALE" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_FIXED_SCALE" "Warrant Fixed Scale Holder" + let securityId = "WSEC_FIXED_SCALE" + let issuance = (warrantIssuance "TX_WARRANT_FIXED_SCALE_ISSUED" securityId stakeholderId stockClassId 100.0) with + exercise_triggers = [warrantFixedExerciseTrigger "TRIGGER_FIXED_SCALE" stockClassId 1_000.0] + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + + let exerciseDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let exercise = (warrantExercise "TX_WARRANT_FIXED_SCALE_EXERCISE" securityId 50.0) with + date = exerciseDate + trigger_id = "TRIGGER_FIXED_SCALE" + let resultIssuance = (defaultStockIssuance "TX_WARRANT_FIXED_SCALE_RESULT" "TX_WARRANT_FIXED_SCALE_EXERCISE_RESULT" stakeholderId stockClassId) with + date = exerciseDate + quantity = 500.0 + _ <- submit issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise exercise + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + pure () + +testFixedWarrantFullExerciseScalesToRemainingBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_FIXED_REMAINDER" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_FIXED_REMAINDER" "Warrant Fixed Remainder Holder" + let securityId = "WSEC_FIXED_REMAINDER" + let issuance = (warrantIssuance "TX_WARRANT_FIXED_REMAINDER_ISSUED" securityId stakeholderId stockClassId 100.0) with + exercise_triggers = [warrantFixedExerciseTrigger "TRIGGER_FIXED_REMAINDER" stockClassId 1_000.0] + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + + let partialDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let partialExercise = (warrantExercise "TX_WARRANT_FIXED_REMAINDER_PARTIAL" securityId 50.0) with + date = partialDate + trigger_id = "TRIGGER_FIXED_REMAINDER" + let partialResult = (defaultStockIssuance "TX_WARRANT_FIXED_REMAINDER_PARTIAL_RESULT" "TX_WARRANT_FIXED_REMAINDER_PARTIAL_RESULT" stakeholderId stockClassId) with + date = partialDate + quantity = 500.0 + partiallyExercised <- submit issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise partialExercise + , CT.OcfCreateStockIssuance partialResult + ] + edits = [] + deletes = [] + + let finalDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let finalExercise = (warrantExercise "TX_WARRANT_FIXED_REMAINDER_FINAL" securityId 0.0) with + date = finalDate + trigger_id = "TRIGGER_FIXED_REMAINDER" + quantity = None + let finalResult = (defaultStockIssuance "TX_WARRANT_FIXED_REMAINDER_FINAL_RESULT" "TX_WARRANT_FIXED_REMAINDER_FINAL_RESULT" stakeholderId stockClassId) with + date = finalDate + quantity = 500.0 + _ <- submit issuer do + exerciseCmd partiallyExercised.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise finalExercise + , CT.OcfCreateStockIssuance finalResult + ] + edits = [] + deletes = [] + pure () + testWarrantExerciseWithUnknownOutputQuantityFailsForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -1159,6 +1359,73 @@ testNonFixedWarrantExerciseRequiresResultingStockIssuanceForCeiling = script do edits = [] deletes = [] +testNonFixedWarrantExerciseUsesRecordedStockQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_NON_FIXED_RESULT" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_NON_FIXED_RESULT" "Warrant Non Fixed Result Holder" + let securityId = "WSEC_NON_FIXED_RESULT" + let issuance = (warrantIssuance "TX_WARRANT_NON_FIXED_RESULT_ISSUED" securityId stakeholderId stockClassId 100.0) with + exercise_triggers = [warrantCustomExerciseTrigger "TRIGGER_NON_FIXED_RESULT" stockClassId] + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + + let exerciseDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let exercise = (warrantExercise "TX_WARRANT_NON_FIXED_RESULT_EXERCISE" securityId 100.0) with + date = exerciseDate + trigger_id = "TRIGGER_NON_FIXED_RESULT" + let resultIssuance = (defaultStockIssuance "TX_WARRANT_NON_FIXED_RESULT" "TX_WARRANT_NON_FIXED_RESULT_EXERCISE_RESULT" stakeholderId stockClassId) with + date = exerciseDate + quantity = 25.0 + _ <- submit issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise exercise + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + pure () + +testWarrantExerciseWithUnspecifiedTargetClassUsesRecordedStock = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_UNSPECIFIED_TARGET" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_UNSPECIFIED_TARGET" "Warrant Unspecified Target Holder" + let securityId = "WSEC_UNSPECIFIED_TARGET" + let issuance = (warrantIssuance "TX_WARRANT_UNSPECIFIED_TARGET_ISSUED" securityId stakeholderId stockClassId 100.0) with + exercise_triggers = + [ warrantExerciseTriggerWith + "TRIGGER_UNSPECIFIED_TARGET" + (OcfWarrantMechanismFixedAmount with converts_to_quantity = 100.0) + None + ] + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + + let exerciseDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let exercise = (warrantExercise "TX_WARRANT_UNSPECIFIED_TARGET_EXERCISE" securityId 100.0) with + date = exerciseDate + trigger_id = "TRIGGER_UNSPECIFIED_TARGET" + let resultIssuance = (defaultStockIssuance "TX_WARRANT_UNSPECIFIED_TARGET_RESULT" "TX_WARRANT_UNSPECIFIED_TARGET_EXERCISE_RESULT" stakeholderId stockClassId) with + date = exerciseDate + quantity = 100.0 + _ <- submit issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise exercise + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + pure () + testWarrantExerciseResultStockClassMustMatchTrigger = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithExpected, expectedClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_RESULT_EXPECTED" 1_000.0) @@ -1245,6 +1512,48 @@ testClasslessEquityCompensationExerciseUsesSinglePlanClassForCeiling = script do edits = [] deletes = [] +testClasslessEquityCompensationMaterializedExerciseMustUseSinglePlanClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithExpected, expectedStockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CLASSLESS_MATERIALIZED_EXERCISE" 1_000.0) + (capTableWithWrong, wrongStockClassId) <- setupStockClassWith issuer capTableWithExpected (limitedStockClass "SC_CLASSLESS_MATERIALIZED_EXERCISE_WRONG" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithWrong "SH_CLASSLESS_MATERIALIZED_EXERCISE" "Classless Materialized Exercise Holder" + let stockPlanId = "PLAN_CLASSLESS_MATERIALIZED_EXERCISE" + let securityId = "ECSEC_CLASSLESS_MATERIALIZED_EXERCISE" + let plan = (defaultStockPlan stockPlanId expectedStockClassId) with initial_shares_reserved = 100.0 + let issuance = (equityCompensationIssuance "TX_CLASSLESS_MATERIALIZED_EXERCISE_GRANTED" securityId stakeholderId expectedStockClassId 50.0) with + stock_class_id = None + stock_plan_id = Some stockPlanId + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + + let wrongExercise = equityCompensationExercise "TX_CLASSLESS_MATERIALIZED_EXERCISE_WRONG" securityId 50.0 + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationExercise wrongExercise + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CLASSLESS_MATERIALIZED_EXERCISE_WRONG_RESULT" "TX_CLASSLESS_MATERIALIZED_EXERCISE_WRONG_RESULT" stakeholderId wrongStockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + + let validExercise = equityCompensationExercise "TX_CLASSLESS_MATERIALIZED_EXERCISE_VALID" securityId 50.0 + _ <- submit issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationExercise validExercise + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CLASSLESS_MATERIALIZED_EXERCISE_VALID_RESULT" "TX_CLASSLESS_MATERIALIZED_EXERCISE_VALID_RESULT" stakeholderId expectedStockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + pure () + testClasslessEquityCompensationExerciseRejectsAmbiguousPlanClass = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -1289,6 +1598,16 @@ testClasslessCashSettledSarExerciseAllowsNoStockOutput = script do edits = [] deletes = [] + let stockExercise = equityCompensationExercise "TX_CASH_SETTLED_SAR_STOCK_EXERCISE" "ECSEC_CASH_SETTLED_SAR" 50.0 + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationExercise stockExercise + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CASH_SETTLED_SAR_STOCK_RESULT" "TX_CASH_SETTLED_SAR_STOCK_EXERCISE_RESULT" stakeholderId stockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateEquityCompensationExercise ((equityCompensationExercise "TX_CASH_SETTLED_SAR_EXERCISE" "ECSEC_CASH_SETTLED_SAR" 50.0) with resulting_security_ids = [])] @@ -1325,6 +1644,56 @@ testClasslessEquityCompensationReleaseUsesSinglePlanClassForCeiling = script do edits = [] deletes = [] +testClasslessEquityCompensationMaterializedReleaseMustUseSinglePlanClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithExpected, expectedStockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CLASSLESS_MATERIALIZED_RELEASE" 1_000.0) + (capTableWithWrong, wrongStockClassId) <- setupStockClassWith issuer capTableWithExpected (limitedStockClass "SC_CLASSLESS_MATERIALIZED_RELEASE_WRONG" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithWrong "SH_CLASSLESS_MATERIALIZED_RELEASE" "Classless Materialized Release Holder" + let stockPlanId = "PLAN_CLASSLESS_MATERIALIZED_RELEASE" + let securityId = "ECSEC_CLASSLESS_MATERIALIZED_RELEASE" + let plan = (defaultStockPlan stockPlanId expectedStockClassId) with initial_shares_reserved = 100.0 + let issuance = (equityCompensationIssuance "TX_CLASSLESS_MATERIALIZED_RELEASE_GRANTED" securityId stakeholderId expectedStockClassId 50.0) with + compensation_type = OcfCompensationTypeRSU + exercise_price = None + stock_class_id = None + stock_plan_id = Some stockPlanId + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + edits = [] + deletes = [] + + let releaseDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let settlementDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let wrongRelease = equityCompensationRelease "TX_CLASSLESS_MATERIALIZED_RELEASE_WRONG" securityId releaseDate settlementDate 50.0 + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationRelease wrongRelease + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CLASSLESS_MATERIALIZED_RELEASE_WRONG_RESULT" "TX_CLASSLESS_MATERIALIZED_RELEASE_WRONG_RESULT" stakeholderId wrongStockClassId) with + date = settlementDate + quantity = 50.0) + ] + edits = [] + deletes = [] + + let validRelease = equityCompensationRelease "TX_CLASSLESS_MATERIALIZED_RELEASE_VALID" securityId releaseDate settlementDate 50.0 + _ <- submit issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationRelease validRelease + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CLASSLESS_MATERIALIZED_RELEASE_VALID_RESULT" "TX_CLASSLESS_MATERIALIZED_RELEASE_VALID_RESULT" stakeholderId expectedStockClassId) with + date = settlementDate + quantity = 50.0) + ] + edits = [] + deletes = [] + pure () + testClasslessEquityCompensationReleaseRejectsAmbiguousPlanClass = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index 45dbdab7..2c57c72d 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -968,6 +968,37 @@ testStockPlanReservedShares_AdjustmentCannotDropBelowAllocatedShares = script do edits = [] deletes = [] +testStockPlanReservedShares_AdjustmentCannotReferenceMissingPlan = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateStockPlanPoolAdjustment (stockPlanPoolAdjustment "TX_POOL_ORPHAN" "PLAN_POOL_ORPHAN" 100.0)] + edits = [] + deletes = [] + +testStockPlanReservedShares_DeletePlanCannotOrphanPoolAdjustment = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, _) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_DELETE_POOL_ADJUSTMENT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let adjustment = stockPlanPoolAdjustment "TX_DELETE_POOL_ADJUSTMENT" stockPlanId 200.0 + + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockPlanPoolAdjustment adjustment + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockPlan stockPlanId] + testStockPlanReservedShares_DeleteAdjustmentCannotInvalidateAllocatedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table @@ -1396,6 +1427,202 @@ testStockPlanReservedShares_BalanceSuccessorOnlyReturnsCancelledShares = script edits = [] deletes = [] +testStockPlanReservedShares_EquityCancellationRequiresBalanceIssuance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_CANCEL_MISSING_BALANCE" + let sourceSecurityId = "SEC_EQUITY_CANCEL_MISSING_BALANCE_SOURCE" + let balanceSecurityId = "SEC_EQUITY_CANCEL_MISSING_BALANCE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_CANCEL_MISSING_BALANCE_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let cancellation = (equityCompensationCancellation "TX_EQUITY_CANCEL_MISSING_BALANCE" sourceSecurityId 40.0) with + balance_security_id = Some balanceSecurityId + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationCancellation cancellation] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityCancellationRejectsOverstatedBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_CANCEL_OVERSTATED_BALANCE" + let sourceSecurityId = "SEC_EQUITY_CANCEL_OVERSTATED_BALANCE_SOURCE" + let balanceSecurityId = "SEC_EQUITY_CANCEL_OVERSTATED_BALANCE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 200.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_CANCEL_OVERSTATED_BALANCE_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let cancellation = (equityCompensationCancellation "TX_EQUITY_CANCEL_OVERSTATED_BALANCE" sourceSecurityId 40.0) with + balance_security_id = Some balanceSecurityId + let balanceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_CANCEL_OVERSTATED_BALANCE_RESULT" balanceSecurityId stakeholderId stockClassId stockPlanId 100.0 + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation cancellation + , CT.OcfCreateEquityCompensationIssuance balanceIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityCancellationRejectsLaterBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_CANCEL_LATER_BALANCE" + let sourceSecurityId = "SEC_EQUITY_CANCEL_LATER_BALANCE_SOURCE" + let balanceSecurityId = "SEC_EQUITY_CANCEL_LATER_BALANCE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_CANCEL_LATER_BALANCE_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let cancellationDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let balanceDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let cancellation = (equityCompensationCancellation "TX_EQUITY_CANCEL_LATER_BALANCE" sourceSecurityId 40.0) with + date = cancellationDate + balance_security_id = Some balanceSecurityId + let balanceIssuance = (stockPlanEquityCompensationIssuance "TX_EQUITY_CANCEL_LATER_BALANCE_RESULT" balanceSecurityId stakeholderId stockClassId stockPlanId 60.0) with date = balanceDate + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation cancellation + , CT.OcfCreateEquityCompensationIssuance balanceIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityCancellationRejectsBalanceClassMismatch = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithSource, sourceStockClassId) <- setupStockClassWith issuer cap_table (defaultStockClass "SC_EQUITY_CANCEL_BALANCE_SOURCE") + (capTableWithWrong, wrongStockClassId) <- setupStockClassWith issuer capTableWithSource (defaultStockClass "SC_EQUITY_CANCEL_BALANCE_WRONG") + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithWrong "SH_EQUITY_CANCEL_BALANCE_CLASS" "Equity Cancellation Balance Class Holder" + let stockPlanId = "PLAN_EQUITY_CANCEL_BALANCE_CLASS" + let sourceSecurityId = "SEC_EQUITY_CANCEL_BALANCE_CLASS_SOURCE" + let balanceSecurityId = "SEC_EQUITY_CANCEL_BALANCE_CLASS" + let plan = (defaultStockPlan stockPlanId sourceStockClassId) with + initial_shares_reserved = 100.0 + stock_class_ids = [sourceStockClassId, wrongStockClassId] + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_CANCEL_BALANCE_CLASS_SOURCE" sourceSecurityId stakeholderId sourceStockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let cancellation = (equityCompensationCancellation "TX_EQUITY_CANCEL_BALANCE_CLASS" sourceSecurityId 40.0) with + balance_security_id = Some balanceSecurityId + let balanceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_CANCEL_BALANCE_CLASS_RESULT" balanceSecurityId stakeholderId wrongStockClassId stockPlanId 60.0 + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation cancellation + , CT.OcfCreateEquityCompensationIssuance balanceIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_EquityCancellationExactBalanceReplacesSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_CANCEL_EXACT_BALANCE" + let sourceSecurityId = "SEC_EQUITY_CANCEL_EXACT_BALANCE_SOURCE" + let balanceSecurityId = "SEC_EQUITY_CANCEL_EXACT_BALANCE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_CANCEL_EXACT_BALANCE_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let cancellationDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let exerciseDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let cancellation = (equityCompensationCancellation "TX_EQUITY_CANCEL_EXACT_BALANCE" sourceSecurityId 40.0) with + date = cancellationDate + balance_security_id = Some balanceSecurityId + let balanceIssuance = (stockPlanEquityCompensationIssuance "TX_EQUITY_CANCEL_EXACT_BALANCE_RESULT" balanceSecurityId stakeholderId stockClassId stockPlanId 60.0) with + date = cancellationDate + stock_plan_id = None + cancelled <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation cancellation + , CT.OcfCreateEquityCompensationIssuance balanceIssuance + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd cancelled.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise ((planEquityCompensationExercise "TX_EQUITY_CANCEL_EXACT_BALANCE_SOURCE_EXERCISE" sourceSecurityId 1.0) with date = exerciseDate)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd cancelled.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise ((planEquityCompensationExercise "TX_EQUITY_CANCEL_EXACT_BALANCE_OVER_EXERCISE" balanceSecurityId 61.0) with date = exerciseDate)] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd cancelled.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise ((planEquityCompensationExercise "TX_EQUITY_CANCEL_EXACT_BALANCE_RESULT_EXERCISE" balanceSecurityId 60.0) with date = exerciseDate)] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_EquityCancellationRejectsSourceAsBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_CANCEL_SELF_BALANCE" + let sourceSecurityId = "SEC_EQUITY_CANCEL_SELF_BALANCE_SOURCE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_CANCEL_SELF_BALANCE_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let cancellation = (equityCompensationCancellation "TX_EQUITY_CANCEL_SELF_BALANCE" sourceSecurityId 40.0) with + balance_security_id = Some sourceSecurityId + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationCancellation cancellation] + edits = [] + deletes = [] + testStockPlanReservedShares_EquityTransferSuccessorDoesNotDoubleAllocate = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table @@ -1484,6 +1711,36 @@ testStockPlanReservedShares_EquityTransferRejectsOverstatedResult = script do edits = [] deletes = [] +testStockPlanReservedShares_EquityTransferRejectsLaterResult = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_TRANSFER_LATER_RESULT" + let sourceSecurityId = "SEC_EQUITY_TRANSFER_LATER_RESULT_SOURCE" + let resultSecurityId = "SEC_EQUITY_TRANSFER_LATER_RESULT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_LATER_RESULT_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let transferDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let resultDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let transfer = (planEquityCompensationTransfer "TX_EQUITY_TRANSFER_LATER_RESULT" sourceSecurityId resultSecurityId 100.0) with date = transferDate + let resultIssuance = (stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_LATER_RESULT" resultSecurityId stakeholderId stockClassId stockPlanId 100.0) with date = resultDate + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer transfer + , CT.OcfCreateEquityCompensationIssuance resultIssuance + ] + edits = [] + deletes = [] + testStockPlanReservedShares_EquityTransferRejectsOverstatedBalance = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table @@ -1516,6 +1773,41 @@ testStockPlanReservedShares_EquityTransferRejectsOverstatedBalance = script do edits = [] deletes = [] +testStockPlanReservedShares_EquityTransferRejectsLaterBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_EQUITY_TRANSFER_LATER_BALANCE" + let sourceSecurityId = "SEC_EQUITY_TRANSFER_LATER_BALANCE_SOURCE" + let resultSecurityId = "SEC_EQUITY_TRANSFER_LATER_BALANCE_RESULT" + let balanceSecurityId = "SEC_EQUITY_TRANSFER_LATER_BALANCE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_LATER_BALANCE_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let transferDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let balanceDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let transfer = ((planEquityCompensationTransfer "TX_EQUITY_TRANSFER_LATER_BALANCE" sourceSecurityId resultSecurityId 40.0) with + date = transferDate + balance_security_id = Some balanceSecurityId) + let resultIssuance = (stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_LATER_BALANCE_RESULT" resultSecurityId stakeholderId stockClassId stockPlanId 40.0) with date = transferDate + let balanceIssuance = (stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_LATER_BALANCE" balanceSecurityId stakeholderId stockClassId stockPlanId 60.0) with date = balanceDate + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer transfer + , CT.OcfCreateEquityCompensationIssuance resultIssuance + , CT.OcfCreateEquityCompensationIssuance balanceIssuance + ] + edits = [] + deletes = [] + testStockPlanReservedShares_EquityTransferAcceptsExactBalance = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 7e5e0f58..2b00babe 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:355ace808e4b4388e6e5c402273f6776af80141283d3703a2e7dc8d154954288 -size 2823348 +oid sha256:fe483d1fed7a9fd1e62ca8b2a3d29cdd7daed719d271ddea6111decc1031b8f6 +size 2844083 diff --git a/dars/dars.lock b/dars/dars.lock index 4d0c0d1a..0ea3375c 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "355ace808e4b4388e6e5c402273f6776af80141283d3703a2e7dc8d154954288", - "size": 2823348, + "sha256": "fe483d1fed7a9fd1e62ca8b2a3d29cdd7daed719d271ddea6111decc1031b8f6", + "size": 2844083, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-10T13:44:14Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 7213da5e..93ed81b3 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -462,17 +462,110 @@ warrantExerciseTrigger issuanceData triggerId = None issuanceData.exercise_triggers -warrantExerciseQuantity : WarrantIssuanceOcfData -> WarrantExerciseOcfData -> OcfWarrantConversionRight -> Decimal -warrantExerciseQuantity issuanceData exerciseData conversionRight = +warrantSecurityQuantityEvents : CapTableMaps -> WarrantIssuanceOcfData -> Update [StockClassShareEvent] +warrantSecurityQuantityEvents maps issuanceData = do + 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.warrant_transfers) + + 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) + transferEvents + (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 ((StockClassShareDelta with + event_date = exercise.exercise_data.date + event_priority = 1 + quantity_delta = 0.0 - quantity) + :: events) + None -> + pure ((StockClassShareRetraction with + event_date = exercise.exercise_data.date + event_priority = 3) + :: events) + else pure events) + cancellationEvents + (Map.values maps.warrant_exercises) + + 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) + exerciseEvents + (Map.values maps.warrant_retractions) + +warrantSecurityQuantityBeforeTerminal : + CapTableMaps -> + WarrantIssuanceOcfData -> + Time -> + Update (Optional Decimal) +warrantSecurityQuantityBeforeTerminal maps issuanceData eventDate = do + events <- warrantSecurityQuantityEvents maps issuanceData + let initialEvents = case issuanceData.quantity of + Some quantity -> + (StockClassShareDelta with + event_date = issuanceData.date + event_priority = 0 + quantity_delta = quantity) + :: events + None -> events + let initialQuantity = case issuanceData.quantity of + Some _ -> Some 0.0 + None -> None + pure (foldl + applyStockClassShareEventOptional + initialQuantity + (filter + (\event -> stockClassShareEventSortKey event < (eventDate, 2, 1)) + (sortOn stockClassShareEventSortKey initialEvents))) + +warrantExerciseQuantity : CapTableMaps -> WarrantIssuanceOcfData -> WarrantExerciseOcfData -> OcfWarrantConversionRight -> Update (Optional Decimal) +warrantExerciseQuantity maps issuanceData exerciseData conversionRight = case conversionRight.conversion_mechanism of - OcfWarrantMechanismFixedAmount with converts_to_quantity -> fromOptional converts_to_quantity exerciseData.quantity - _ -> fromOptional (fromOptional 0.0 issuanceData.quantity) exerciseData.quantity + OcfWarrantMechanismFixedAmount with converts_to_quantity -> do + exercisedQuantityOpt <- case exerciseData.quantity of + Some exercisedQuantity -> pure (Some exercisedQuantity) + None -> warrantSecurityQuantityBeforeTerminal maps issuanceData exerciseData.date + case (issuanceData.quantity, exercisedQuantityOpt) of + (Some issuedQuantity, Some exercisedQuantity) -> + if issuedQuantity > 0.0 + then pure (Some (converts_to_quantity * exercisedQuantity / issuedQuantity)) + else pure None + _ -> pure None + _ -> pure None warrantExerciseOutputShareLot : + CapTableMaps -> WarrantIssuanceOcfData -> WarrantExerciseOcfData -> Update (Optional StockClassShareLot) -warrantExerciseOutputShareLot issuanceData exerciseData = +warrantExerciseOutputShareLot maps issuanceData exerciseData = case warrantExerciseTrigger issuanceData exerciseData.trigger_id of Some trigger -> case trigger.conversion_right of @@ -480,12 +573,18 @@ warrantExerciseOutputShareLot issuanceData exerciseData = case conversionRight.converts_to_stock_class_id of Some stockClassId -> case conversionRight.conversion_mechanism of - OcfWarrantMechanismFixedAmount _ -> - pure (Some (StockClassShareLot with - stock_class_id = stockClassId - security_id = None - event_date = exerciseData.date - quantity = warrantExerciseQuantity issuanceData exerciseData conversionRight)) + OcfWarrantMechanismFixedAmount _ -> do + quantityOpt <- warrantExerciseQuantity maps issuanceData exerciseData conversionRight + case quantityOpt of + Some quantity -> + pure (Some (StockClassShareLot with + stock_class_id = stockClassId + security_id = None + event_date = exerciseData.date + quantity = quantity)) + None -> do + assertMsg ("Warrant exercise output cannot be reconstructed because its fixed conversion quantity is not computable: " <> exerciseData.id) False + pure None _ -> do assertMsg ("Warrant exercise output cannot be reconstructed for a non-fixed conversion mechanism; a resulting stock issuance is required: " <> exerciseData.id) False pure None @@ -647,11 +746,9 @@ validateWarrantExerciseResultIssuances : CapTableMaps -> WarrantIssuanceOcfData validateWarrantExerciseResultIssuances maps issuanceData exerciseData = case warrantExerciseTrigger issuanceData exerciseData.trigger_id of Some trigger -> case trigger.conversion_right of - OcfRightWarrant conversionRight -> - let expectedQuantityOpt = case conversionRight.conversion_mechanism of - OcfWarrantMechanismFixedAmount _ -> Some (warrantExerciseQuantity issuanceData exerciseData conversionRight) - _ -> None - in validateStockResultIssuances + OcfRightWarrant conversionRight -> do + expectedQuantityOpt <- warrantExerciseQuantity maps issuanceData exerciseData conversionRight + validateStockResultIssuances maps ("Warrant exercise " <> exerciseData.id) exerciseData.date @@ -662,24 +759,36 @@ validateWarrantExerciseResultIssuances maps issuanceData exerciseData = None -> assertMsg ("Warrant exercise trigger not found: " <> exerciseData.trigger_id) False validateEquityCompensationExerciseResultIssuances : CapTableMaps -> EquityCompensationIssuanceOcfData -> EquityCompensationExerciseOcfData -> Update () -validateEquityCompensationExerciseResultIssuances maps issuanceData exerciseData = - validateStockResultIssuances - maps - ("Equity compensation exercise " <> exerciseData.id) - exerciseData.date - issuanceData.stock_class_id - (Some exerciseData.quantity) - exerciseData.resulting_security_ids +validateEquityCompensationExerciseResultIssuances maps issuanceData exerciseData = do + let label = "Equity compensation exercise " <> exerciseData.id + case issuanceData.compensation_type of + OcfCompensationTypeCSAR -> + assertMsg (label <> " cannot produce stock for cash-settled stock appreciation rights") False + _ -> do + expectedStockClassId <- equityCompensationOutputStockClassId maps label issuanceData + validateStockResultIssuances + maps + label + exerciseData.date + expectedStockClassId + (Some exerciseData.quantity) + exerciseData.resulting_security_ids validateEquityCompensationReleaseResultIssuances : CapTableMaps -> EquityCompensationIssuanceOcfData -> EquityCompensationReleaseOcfData -> Update () -validateEquityCompensationReleaseResultIssuances maps issuanceData releaseData = - validateStockResultIssuances - maps - ("Equity compensation release " <> releaseData.id) - releaseData.settlement_date - issuanceData.stock_class_id - (Some releaseData.quantity) - releaseData.resulting_security_ids +validateEquityCompensationReleaseResultIssuances maps issuanceData releaseData = do + let label = "Equity compensation release " <> releaseData.id + case issuanceData.compensation_type of + OcfCompensationTypeCSAR -> + assertMsg (label <> " cannot produce stock for cash-settled stock appreciation rights") False + _ -> do + expectedStockClassId <- equityCompensationOutputStockClassId maps label issuanceData + validateStockResultIssuances + maps + label + releaseData.settlement_date + expectedStockClassId + (Some releaseData.quantity) + releaseData.resulting_security_ids addOptionalStockClassShareLot : Optional StockClassShareLot -> @@ -743,7 +852,7 @@ stockClassShareLotsByStockClass maps stockClassIndex = do else case Map.lookup exercise.exercise_data.security_id maps.warrant_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid - lotOpt <- warrantExerciseOutputShareLot issuance.issuance_data exercise.exercise_data + lotOpt <- warrantExerciseOutputShareLot maps issuance.issuance_data exercise.exercise_data pure (addOptionalStockClassShareLot lotOpt index) None -> do assertMsg ("Warrant exercise references missing warrant issuance: " <> exercise.exercise_data.security_id) False @@ -1448,15 +1557,19 @@ equityCompensationIssuanceQuantityTotal : CapTableMaps -> Text -> Optional Text -> + Time -> (Time, Int, Int) -> [Text] -> Update Decimal -equityCompensationIssuanceQuantityTotal maps label expectedStockClassId targetSortKey securityIds = +equityCompensationIssuanceQuantityTotal maps label expectedStockClassId expectedDate targetSortKey securityIds = foldlA (\total securityId -> case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid + assertMsg + (label <> " successor date mismatch for " <> securityId <> ": expected " <> show expectedDate <> ", found " <> show issuance.issuance_data.date) + (issuance.issuance_data.date == expectedDate) assertMsg (label <> " successor stock class mismatch for " <> securityId <> ": expected " <> show expectedStockClassId <> ", found " <> show issuance.issuance_data.stock_class_id) (issuance.issuance_data.stock_class_id == expectedStockClassId) @@ -1468,13 +1581,16 @@ equityCompensationIssuanceQuantityTotal maps label expectedStockClassId targetSo 0.0 securityIds -stockIssuanceQuantityTotal : CapTableMaps -> Text -> Text -> (Time, Int, Int) -> [Text] -> Update Decimal -stockIssuanceQuantityTotal maps label expectedStockClassId targetSortKey securityIds = +stockIssuanceQuantityTotal : CapTableMaps -> Text -> Text -> Time -> (Time, Int, Int) -> [Text] -> Update Decimal +stockIssuanceQuantityTotal maps label expectedStockClassId expectedDate targetSortKey securityIds = foldlA (\total securityId -> case Map.lookup securityId maps.stock_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid + assertMsg + (label <> " successor date mismatch for " <> securityId <> ": expected " <> show expectedDate <> ", found " <> show issuance.issuance_data.date) + (issuance.issuance_data.date == expectedDate) assertMsg (label <> " successor stock class mismatch for " <> securityId <> ": expected " <> expectedStockClassId <> ", found " <> issuance.issuance_data.stock_class_id) (issuance.issuance_data.stock_class_id == expectedStockClassId) @@ -1608,6 +1724,7 @@ validateStockTransferResultIssuances maps = maps label sourceIssuance.issuance_data.stock_class_id + transfer.transfer_data.date targetSortKey transfer.transfer_data.resulting_security_ids assertMsg @@ -1617,6 +1734,36 @@ validateStockTransferResultIssuances maps = else pure ()) (Map.values maps.stock_transfers) +validateStockReissuanceResultIssuances : StockClassShareEventIndex -> CapTableMaps -> Update () +validateStockReissuanceResultIssuances eventIndex maps = + mapA_ + (\reissuanceCid -> do + reissuance <- fetch reissuanceCid + let label = "Stock reissuance " <> reissuance.reissuance_data.id + assertNoPartialResultingStockIssuanceCoverage label maps reissuance.reissuance_data.resulting_security_ids + if allResultingStockIssuancesExist maps reissuance.reissuance_data.resulting_security_ids + then case Map.lookup reissuance.reissuance_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + let targetSortKey = (reissuance.reissuance_data.date, 3, 0) + sourceQuantity <- stockSecurityQuantityBeforeEvent + eventIndex + sourceIssuance.issuance_data + targetSortKey + resultingQuantity <- stockIssuanceQuantityTotal + maps + label + sourceIssuance.issuance_data.stock_class_id + reissuance.reissuance_data.date + targetSortKey + reissuance.reissuance_data.resulting_security_ids + assertMsg + (label <> " resulting quantity mismatch: expected " <> show sourceQuantity <> ", found " <> show resultingQuantity) + (resultingQuantity == sourceQuantity) + None -> assertMsg (label <> " source stock issuance not found: " <> reissuance.reissuance_data.security_id) False + else pure ()) + (Map.values maps.stock_reissuances) + validateStockConsolidationResultIssuances : StockClassShareEventIndex -> CapTableMaps -> Update () validateStockConsolidationResultIssuances eventIndex maps = mapA_ @@ -1626,6 +1773,9 @@ validateStockConsolidationResultIssuances eventIndex maps = case Map.lookup consolidation.consolidation_data.resulting_security_id maps.stock_issuances_by_security_id of Some resultIssuanceCid -> do resultIssuance <- fetch resultIssuanceCid + assertMsg + (label <> " result date mismatch for " <> consolidation.consolidation_data.resulting_security_id <> ": expected " <> show consolidation.consolidation_data.date <> ", found " <> show resultIssuance.issuance_data.date) + (resultIssuance.issuance_data.date == consolidation.consolidation_data.date) let targetSortKey = (consolidation.consolidation_data.date, 3, 3) sourceQuantity <- foldlA (\total sourceSecurityId -> @@ -2647,10 +2797,11 @@ equityCompensationSecurityEvents maps issuanceData = do (\events cancellationCid -> do cancellation <- fetch cancellationCid if cancellation.cancellation_data.security_id == issuanceData.security_id - then pure ((StockClassShareDelta with - event_date = cancellation.cancellation_data.date - event_priority = 1 - quantity_delta = 0.0 - cancellation.cancellation_data.quantity) :: events) + && equityCompensationBalanceSecurityIssuanceExists maps cancellation.cancellation_data.balance_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.equity_compensation_cancellations) @@ -2757,14 +2908,18 @@ validateEquityCompensationBalanceSuccessorQuantity : CapTableMaps -> Text -> Optional Text -> + Time -> (Time, Int, Int) -> Decimal -> Text -> Update () -validateEquityCompensationBalanceSuccessorQuantity maps label expectedStockClassId targetSortKey expectedQuantity securityId = +validateEquityCompensationBalanceSuccessorQuantity maps label expectedStockClassId expectedDate targetSortKey expectedQuantity securityId = case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid + assertMsg + (label <> " balance date mismatch for " <> securityId <> ": expected " <> show expectedDate <> ", found " <> show issuance.issuance_data.date) + (issuance.issuance_data.date == expectedDate) assertMsg (label <> " balance stock class mismatch for " <> securityId <> ": expected " <> show expectedStockClassId <> ", found " <> show issuance.issuance_data.stock_class_id) (issuance.issuance_data.stock_class_id == expectedStockClassId) @@ -2775,6 +2930,42 @@ validateEquityCompensationBalanceSuccessorQuantity maps label expectedStockClass None -> assertMsg (label <> " balance equity compensation issuance not found: " <> securityId) False +validateEquityCompensationCancellationSuccessorContinuity : CapTableMaps -> Update () +validateEquityCompensationCancellationSuccessorContinuity maps = + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + case cancellation.cancellation_data.balance_security_id of + Some balanceSecurityId -> do + let label = "Equity compensation cancellation " <> cancellation.cancellation_data.id + assertMsg + (label <> " balance security must differ from source security " <> cancellation.cancellation_data.security_id) + (balanceSecurityId /= cancellation.cancellation_data.security_id) + case Map.lookup cancellation.cancellation_data.security_id maps.equity_compensation_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + let terminalSortKey = (cancellation.cancellation_data.date, 2, 1) + currentQuantity <- equityCompensationSecurityQuantityBeforeEvent + maps + sourceIssuance.issuance_data + terminalSortKey + let expectedBalanceQuantity = currentQuantity - cancellation.cancellation_data.quantity + assertMsg + (label <> " exceeds the current source quantity for " <> sourceIssuance.issuance_data.security_id) + (expectedBalanceQuantity >= 0.0) + validateEquityCompensationBalanceSuccessorQuantity + maps + label + sourceIssuance.issuance_data.stock_class_id + cancellation.cancellation_data.date + terminalSortKey + expectedBalanceQuantity + balanceSecurityId + None -> + assertMsg (label <> " source equity compensation issuance not found: " <> cancellation.cancellation_data.security_id) False + None -> pure ()) + (Map.values maps.equity_compensation_cancellations) + validateEquityCompensationTransferSuccessorContinuity : CapTableMaps -> Update () validateEquityCompensationTransferSuccessorContinuity maps = mapA_ @@ -2798,6 +2989,7 @@ validateEquityCompensationTransferSuccessorContinuity maps = maps label sourceIssuance.issuance_data.stock_class_id + transfer.transfer_data.date targetSortKey transfer.transfer_data.resulting_security_ids assertMsg @@ -2818,6 +3010,7 @@ validateEquityCompensationTransferSuccessorContinuity maps = maps label sourceIssuance.issuance_data.stock_class_id + transfer.transfer_data.date terminalSortKey expectedBalanceQuantity balanceSecurityId @@ -2828,11 +3021,15 @@ validateEquityCompensationTransferSuccessorContinuity maps = (Map.values maps.equity_compensation_transfers) validateStockPlanEquityCompensationSourceQuantities : CapTableMaps -> Update () -validateStockPlanEquityCompensationSourceQuantities maps = +validateStockPlanEquityCompensationSourceQuantities maps = do + successorIndex <- stockPlanSuccessorIndex maps mapA_ (\issuanceCid -> do issuance <- fetch issuanceCid - case issuance.issuance_data.stock_plan_id of + case effectivePlanForSecurity + successorIndex.equity_compensation_successors_by_security_id + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id of Some _ -> do hasExercise <- foldlA (\found exerciseCid -> do @@ -3093,6 +3290,16 @@ validateStockPlanAllocationReferences maps netAllocatedQuantityIndex = (isSome (Map.lookup stockPlanId maps.stock_plans))) (Map.toList netAllocatedQuantityIndex) +validateStockPlanPoolAdjustmentReferences : CapTableMaps -> Update () +validateStockPlanPoolAdjustmentReferences maps = + mapA_ + (\adjustmentCid -> do + adjustment <- fetch adjustmentCid + assertMsg + ("Stock plan pool adjustment references missing stock plan: " <> adjustment.adjustment_data.stock_plan_id) + (isSome (Map.lookup adjustment.adjustment_data.stock_plan_id maps.stock_plans))) + (Map.values maps.stock_plan_pool_adjustments) + assertStockPlanIncludesClass : Text -> CapTableMaps -> Text -> Text -> Update () assertStockPlanIncludesClass label maps stockPlanId stockClassId = case Map.lookup stockPlanId maps.stock_plans of @@ -3374,6 +3581,7 @@ validateStockPlanReservedSharesAtDates eventIndex maps adjustmentHistory = do validateStockPlanPoolCeilings : StockClassShareEventIndex -> CapTableMaps -> Update () validateStockPlanPoolCeilings eventIndex maps = do + validateStockPlanPoolAdjustmentReferences maps adjustmentIndex <- stockPlanPoolAdjustmentIndex maps adjustmentHistory <- stockPlanPoolAdjustmentHistoryIndex maps validateStockPlanAllocationClasses maps @@ -3392,7 +3600,9 @@ validateAuthorizedShareCeilings maps adjustmentIndex issuerCid = do eventIndex <- stockClassShareEventIndex maps validateStockBalanceSuccessors eventIndex maps validateStockTransferResultIssuances maps + validateStockReissuanceResultIssuances eventIndex maps validateStockConsolidationResultIssuances eventIndex maps + validateEquityCompensationCancellationSuccessorContinuity maps validateEquityCompensationTransferSuccessorContinuity maps stockClassIndex <- stockClassDataIndex maps stockClasses <- stockClassDataList maps From a74f716436fd5d6faf2aa9b42ccf5d9e96b707dd Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 16:43:59 -0400 Subject: [PATCH 40/62] test: clarify stock issuance optional coverage --- Test/daml/OpenCapTable/TestStockIssuance.daml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockIssuance.daml b/Test/daml/OpenCapTable/TestStockIssuance.daml index 5a66cb93..4779e0af 100644 --- a/Test/daml/OpenCapTable/TestStockIssuance.daml +++ b/Test/daml/OpenCapTable/TestStockIssuance.daml @@ -42,7 +42,7 @@ testCreateAndDeleteStockIssuance = script do deletes = [CT.OcfDeleteStockIssuance "TX_STOCK_ISSUANCE_1"] pure () --- Test creating a stock issuance with all optional fields populated +-- Test creating a stock issuance with optional fields that do not require a stock plan populated testStockIssuance_OptionalsSome = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -50,7 +50,7 @@ testStockIssuance_OptionalsSome = script do (cap_table', classId) <- setupStockClass issuer cap_table (cap_table'', stakeholderId) <- setupStakeholderWithId issuer cap_table' "SH-3" "Test Holder" - -- Create Stock Issuance with all optional fields populated + -- Keep stock_plan_id empty here; plan-backed stock issuance is covered by stock-plan tests. let issuance = (defaultStockIssuance "TX_STOCK_ISS_SOME" "SEC-3" stakeholderId classId) with custom_id = "CERT-3" date = DT.time (DA.date 2024 Feb 01) 0 0 0 From 77a49659596b30227857ac3958b662cadeaa5834 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 17:31:13 -0400 Subject: [PATCH 41/62] fix: enforce lifecycle identity and terminal balances --- .../TestEquityCompensationRelease.daml | 91 +- .../OpenCapTable/TestLifecycleInvariants.daml | 473 +++++++++++ .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 782 ++++++++++++++++-- 5 files changed, 1279 insertions(+), 75 deletions(-) create mode 100644 Test/daml/OpenCapTable/TestLifecycleInvariants.daml diff --git a/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml b/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml index 36f71b7d..91061f05 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml @@ -1,35 +1,72 @@ 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 (addPrerequisites, addEquityCompensationIssuance) +import OpenCapTable.TestHelpers (addPrerequisites, defaultStockIssuance) 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 + 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 = None + expiration_date = None + stock_class_id = Some "SC_COMMON" + 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 <- addPrerequisites 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 - creates = [CT.OcfCreateEquityCompensationRelease EquityCompensationReleaseOcfData with - id = "TX_EC_RELEASE_1" - date = DT.time (DA.date 2024 Apr 01) 0 0 0 - security_id = "SEC_RSU_001" - settlement_date = DT.time (DA.date 2024 Apr 03) 0 0 0 - release_price = OcfMonetary with amount = 45.50; currency = "USD" - quantity = 5000.0 - resulting_security_ids = ["SEC_STOCK_101"] - consideration_text = Some "RSU cliff vest - 25% of total grant" - comments = ["First anniversary cliff vest for Employee A"]] + creates = + [ CT.OcfCreateStockIssuance ((defaultStockIssuance "STOCK-ISS-EC-RELEASE-1" "SEC_STOCK_101" "SH-1" "SC_COMMON") with + date = DT.time (DA.date 2024 Apr 03) 0 0 0 + quantity = 5000.0) + , CT.OcfCreateEquityCompensationRelease EquityCompensationReleaseOcfData with + id = "TX_EC_RELEASE_1" + date = DT.time (DA.date 2024 Apr 01) 0 0 0 + security_id = "SEC_RSU_001" + settlement_date = DT.time (DA.date 2024 Apr 03) 0 0 0 + release_price = OcfMonetary with amount = 45.50; currency = "USD" + quantity = 5000.0 + resulting_security_ids = ["SEC_STOCK_101"] + consideration_text = Some "RSU cliff vest - 25% of total grant" + comments = ["First anniversary cliff vest for Employee A"] + ] edits = [] deletes = [] pure () @@ -40,20 +77,28 @@ testCreateAndArchiveEquityCompensationReleaseQuarterly = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp -- Add stakeholder and equity compensation issuance before releasing cap_table <- addPrerequisites 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 - creates = [CT.OcfCreateEquityCompensationRelease EquityCompensationReleaseOcfData with - id = "TX_EC_RELEASE_2" - date = DT.time (DA.date 2024 Jul 01) 0 0 0 - security_id = "SEC_RSU_002" - settlement_date = DT.time (DA.date 2024 Jul 05) 0 0 0 - release_price = OcfMonetary with amount = 52.00; currency = "USD" - quantity = 1250.0 - resulting_security_ids = ["SEC_STOCK_102", "SEC_STOCK_103"] - consideration_text = None - comments = ["Q3 2024 quarterly vest", "Net settlement after tax withholding"]] + creates = + [ CT.OcfCreateStockIssuance ((defaultStockIssuance "STOCK-ISS-EC-RELEASE-2A" "SEC_STOCK_102" "SH-1" "SC_COMMON") with + date = DT.time (DA.date 2024 Jul 05) 0 0 0 + quantity = 625.0) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "STOCK-ISS-EC-RELEASE-2B" "SEC_STOCK_103" "SH-1" "SC_COMMON") with + date = DT.time (DA.date 2024 Jul 05) 0 0 0 + quantity = 625.0) + , CT.OcfCreateEquityCompensationRelease EquityCompensationReleaseOcfData with + id = "TX_EC_RELEASE_2" + date = DT.time (DA.date 2024 Jul 01) 0 0 0 + security_id = "SEC_RSU_002" + settlement_date = DT.time (DA.date 2024 Jul 05) 0 0 0 + release_price = OcfMonetary with amount = 52.00; currency = "USD" + quantity = 1250.0 + resulting_security_ids = ["SEC_STOCK_102", "SEC_STOCK_103"] + consideration_text = None + comments = ["Q3 2024 quarterly vest", "Net settlement after tax withholding"] + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestLifecycleInvariants.daml b/Test/daml/OpenCapTable/TestLifecycleInvariants.daml new file mode 100644 index 00000000..94a4a6ca --- /dev/null +++ b/Test/daml/OpenCapTable/TestLifecycleInvariants.daml @@ -0,0 +1,473 @@ +module OpenCapTable.TestLifecycleInvariants where + +import Fairmint.OpenCapTable.OCF.EquityCompensationCancellation +import Fairmint.OpenCapTable.OCF.EquityCompensationExercise +import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance +import Fairmint.OpenCapTable.OCF.EquityCompensationRelease +import Fairmint.OpenCapTable.OCF.EquityCompensationTransfer +import Fairmint.OpenCapTable.OCF.StockConsolidation +import Fairmint.OpenCapTable.OCF.StockReissuance +import Fairmint.OpenCapTable.OCF.StockTransfer +import Fairmint.OpenCapTable.OCF.WarrantExercise +import Fairmint.OpenCapTable.OCF.WarrantIssuance +import Fairmint.OpenCapTable.Types.Conversion +import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import Fairmint.OpenCapTable.Types.Stock (OcfQuantitySourceType(..)) +import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) +import qualified Fairmint.OpenCapTable.CapTable as CT +import OpenCapTable.Setup +import OpenCapTable.TestHelpers +import Daml.Script + +lifecycleWarrantTrigger : Text -> Text -> OcfConversionTrigger +lifecycleWarrantTrigger triggerId stockClassId = + OcfConversionTrigger with + conversion_right = OcfRightWarrant (OcfWarrantConversionRight with + conversion_mechanism = OcfWarrantMechanismFixedAmount with converts_to_quantity = 100.0 + converts_to_future_round = None + converts_to_stock_class_id = 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 + +lifecycleWarrantIssuance : Text -> Text -> Text -> Text -> WarrantIssuanceOcfData +lifecycleWarrantIssuance issuanceId securityId stakeholderId stockClassId = + WarrantIssuanceOcfData with + id = issuanceId + custom_id = "WR-" <> issuanceId + date = defaultTestDate + purchase_price = OcfMonetary with amount = 1.0, currency = "USD" + security_id = securityId + stakeholder_id = stakeholderId + comments = [] + exercise_triggers = [lifecycleWarrantTrigger "TRIGGER_FIXED" stockClassId] + security_law_exemptions = [] + vestings = [] + board_approval_date = None + consideration_text = None + exercise_price = None + quantity = Some 100.0 + quantity_source = Some OcfQuantityUnspecified + stockholder_approval_date = None + vesting_terms_id = None + warrant_expiration_date = None + +lifecycleWarrantExercise : Text -> Text -> Optional Decimal -> [Text] -> WarrantExerciseOcfData +lifecycleWarrantExercise exerciseId securityId quantity resultingSecurityIds = + WarrantExerciseOcfData with + id = exerciseId + date = defaultTestDate + security_id = securityId + trigger_id = "TRIGGER_FIXED" + comments = [] + resulting_security_ids = resultingSecurityIds + consideration_text = None + quantity + +lifecycleStockTransfer : Text -> Text -> Decimal -> Optional Text -> [Text] -> StockTransferOcfData +lifecycleStockTransfer transferId securityId quantity balanceSecurityId resultingSecurityIds = + StockTransferOcfData with + id = transferId + date = defaultTestDate + security_id = securityId + quantity + resulting_security_ids = resultingSecurityIds + balance_security_id = balanceSecurityId + consideration_text = None + comments = [] + +lifecycleStockReissuance : Text -> Text -> [Text] -> StockReissuanceOcfData +lifecycleStockReissuance reissuanceId securityId resultingSecurityIds = + StockReissuanceOcfData with + id = reissuanceId + date = defaultTestDate + security_id = securityId + resulting_security_ids = resultingSecurityIds + split_transaction_id = None + reason_text = Some "Lifecycle replacement" + comments = [] + +lifecycleStockConsolidation : Text -> [Text] -> Text -> StockConsolidationOcfData +lifecycleStockConsolidation consolidationId securityIds resultingSecurityId = + StockConsolidationOcfData with + id = consolidationId + date = defaultTestDate + security_ids = securityIds + resulting_security_id = resultingSecurityId + reason_text = Some "Lifecycle consolidation" + comments = [] + +lifecycleEquityIssuance : Text -> Text -> Text -> Text -> Decimal -> EquityCompensationIssuanceOcfData +lifecycleEquityIssuance issuanceId securityId stakeholderId stockClassId quantity = + EquityCompensationIssuanceOcfData with + id = issuanceId + custom_id = "EC-" <> issuanceId + date = defaultTestDate + compensation_type = OcfCompensationTypeOptionNSO + 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 = Some stockClassId + stock_plan_id = None + stockholder_approval_date = None + vesting_terms_id = None + +lifecycleEquityCancellation : Text -> Text -> Decimal -> Text -> EquityCompensationCancellationOcfData +lifecycleEquityCancellation cancellationId securityId quantity balanceSecurityId = + EquityCompensationCancellationOcfData with + id = cancellationId + date = defaultTestDate + security_id = securityId + quantity + balance_security_id = Some balanceSecurityId + reason_text = "Lifecycle cancellation" + comments = [] + +lifecycleEquityTransfer : Text -> Text -> Decimal -> Optional Text -> [Text] -> EquityCompensationTransferOcfData +lifecycleEquityTransfer transferId securityId quantity balanceSecurityId resultingSecurityIds = + EquityCompensationTransferOcfData with + id = transferId + date = defaultTestDate + security_id = securityId + quantity + resulting_security_ids = resultingSecurityIds + balance_security_id = balanceSecurityId + consideration_text = None + comments = [] + +lifecycleEquityExercise : Text -> Text -> Decimal -> [Text] -> EquityCompensationExerciseOcfData +lifecycleEquityExercise exerciseId securityId quantity resultingSecurityIds = + EquityCompensationExerciseOcfData with + id = exerciseId + date = defaultTestDate + security_id = securityId + quantity + resulting_security_ids = resultingSecurityIds + consideration_text = None + comments = [] + +lifecycleEquityRelease : Text -> Text -> Decimal -> [Text] -> EquityCompensationReleaseOcfData +lifecycleEquityRelease releaseId securityId quantity resultingSecurityIds = + EquityCompensationReleaseOcfData with + id = releaseId + date = defaultTestDate + security_id = securityId + settlement_date = defaultTestDate + release_price = OcfMonetary with amount = 1.0, currency = "USD" + quantity + resulting_security_ids = resultingSecurityIds + consideration_text = None + comments = [] + +testWarrantExerciseCannotExceedRemainingQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_REMAINING" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_REMAINING" "Warrant Remaining Holder" + let securityId = "WSEC_REMAINING" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_WARRANT_REMAINING_ISSUE" securityId stakeholderId stockClassId)] + edits = [] + deletes = [] + + firstExercise <- submit issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_WARRANT_REMAINING_FIRST" securityId (Some 60.0) ["SEC_WARRANT_REMAINING_FIRST"]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_WARRANT_REMAINING_FIRST_RESULT" "SEC_WARRANT_REMAINING_FIRST" stakeholderId stockClassId) with quantity = 60.0) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd firstExercise.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_WARRANT_REMAINING_SECOND" securityId (Some 50.0) ["SEC_WARRANT_REMAINING_SECOND"]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_WARRANT_REMAINING_SECOND_RESULT" "SEC_WARRANT_REMAINING_SECOND" stakeholderId stockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + +testTwoFullWarrantExercisesAtSameTimestampFail = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_DOUBLE_FULL" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_DOUBLE_FULL" "Warrant Double Full Holder" + let securityId = "WSEC_DOUBLE_FULL" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_WARRANT_DOUBLE_FULL_ISSUE" securityId stakeholderId stockClassId)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_WARRANT_DOUBLE_FULL_A" securityId None ["SEC_WARRANT_DOUBLE_FULL_A"]) + , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_WARRANT_DOUBLE_FULL_RESULT_A" "SEC_WARRANT_DOUBLE_FULL_A" stakeholderId stockClassId) + , CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_WARRANT_DOUBLE_FULL_B" securityId None ["SEC_WARRANT_DOUBLE_FULL_B"]) + , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_WARRANT_DOUBLE_FULL_RESULT_B" "SEC_WARRANT_DOUBLE_FULL_B" stakeholderId stockClassId) + ] + edits = [] + deletes = [] + +testTwoStockReissuancesAtSameTimestampFail = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REISSUE_DOUBLE" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_REISSUE_DOUBLE" "Reissuance Double Holder" + let sourceSecurityId = "SEC_REISSUE_DOUBLE_SOURCE" + issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_REISSUE_DOUBLE_SOURCE" sourceSecurityId stakeholderId stockClassId) + + submitMustFail issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_REISSUE_DOUBLE_A" sourceSecurityId ["SEC_REISSUE_DOUBLE_A"]) + , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_REISSUE_DOUBLE_RESULT_A" "SEC_REISSUE_DOUBLE_A" stakeholderId stockClassId) + , CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_REISSUE_DOUBLE_B" sourceSecurityId ["SEC_REISSUE_DOUBLE_B"]) + , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_REISSUE_DOUBLE_RESULT_B" "SEC_REISSUE_DOUBLE_B" stakeholderId stockClassId) + ] + edits = [] + deletes = [] + +testMixedStockTerminalEventsAtSameTimestampFail = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_TERMINAL_MIXED" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_TERMINAL_MIXED" "Mixed Terminal Holder" + let sourceSecurityId = "SEC_TERMINAL_MIXED_SOURCE" + issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_TERMINAL_MIXED_SOURCE" sourceSecurityId stakeholderId stockClassId) + + submitMustFail issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_TERMINAL_MIXED_REISSUE" sourceSecurityId ["SEC_TERMINAL_MIXED_REISSUE"]) + , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_TERMINAL_MIXED_REISSUE_RESULT" "SEC_TERMINAL_MIXED_REISSUE" stakeholderId stockClassId) + , CT.OcfCreateStockConsolidation (lifecycleStockConsolidation "TX_TERMINAL_MIXED_CONSOLIDATE" [sourceSecurityId] "SEC_TERMINAL_MIXED_CONSOLIDATE") + , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_TERMINAL_MIXED_CONSOLIDATE_RESULT" "SEC_TERMINAL_MIXED_CONSOLIDATE" stakeholderId stockClassId) + ] + edits = [] + deletes = [] + +testRepeatedEquityTerminalEventsAtSameTimestampFail = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_DOUBLE_TERMINAL" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_DOUBLE_TERMINAL" "Equity Double Terminal Holder" + let sourceSecurityId = "ESEC_DOUBLE_TERMINAL_SOURCE" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_DOUBLE_TERMINAL_SOURCE" sourceSecurityId stakeholderId stockClassId 100.0)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation (lifecycleEquityCancellation "TX_EQUITY_DOUBLE_TERMINAL_A" sourceSecurityId 50.0 "ESEC_DOUBLE_TERMINAL_A") + , CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_DOUBLE_TERMINAL_RESULT_A" "ESEC_DOUBLE_TERMINAL_A" stakeholderId stockClassId 50.0) + , CT.OcfCreateEquityCompensationCancellation (lifecycleEquityCancellation "TX_EQUITY_DOUBLE_TERMINAL_B" sourceSecurityId 50.0 "ESEC_DOUBLE_TERMINAL_B") + , CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_DOUBLE_TERMINAL_RESULT_B" "ESEC_DOUBLE_TERMINAL_B" stakeholderId stockClassId 50.0) + ] + edits = [] + deletes = [] + +testBalanceSecurityCannotAlsoBeResultingSecurity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_BALANCE_RESULT_ALIAS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_BALANCE_RESULT_ALIAS" "Balance Result Alias Holder" + let sourceSecurityId = "SEC_BALANCE_RESULT_ALIAS_SOURCE" + let aliasSecurityId = "SEC_BALANCE_RESULT_ALIAS" + issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_BALANCE_RESULT_ALIAS_SOURCE" sourceSecurityId stakeholderId stockClassId) + + submitMustFail issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (lifecycleStockTransfer "TX_BALANCE_RESULT_ALIAS" sourceSecurityId 50.0 (Some aliasSecurityId) [aliasSecurityId]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_BALANCE_RESULT_ALIAS_OUTPUT" aliasSecurityId stakeholderId stockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + +testStockReissuanceCannotReuseSourceAsResult = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REISSUE_SOURCE_ALIAS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_REISSUE_SOURCE_ALIAS" "Reissuance Source Alias Holder" + let sourceSecurityId = "SEC_REISSUE_SOURCE_ALIAS" + issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_REISSUE_SOURCE_ALIAS_SOURCE" sourceSecurityId stakeholderId stockClassId) + + submitMustFail issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = [CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_REISSUE_SOURCE_ALIAS" sourceSecurityId [sourceSecurityId])] + edits = [] + deletes = [] + +testDuplicateResultIdsFail = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_DUPLICATE_RESULTS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_DUPLICATE_RESULTS" "Duplicate Results Holder" + let sourceSecurityId = "SEC_DUPLICATE_RESULTS_SOURCE" + let resultSecurityId = "SEC_DUPLICATE_RESULTS_RESULT" + issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_DUPLICATE_RESULTS_SOURCE" sourceSecurityId stakeholderId stockClassId) + + submitMustFail issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_DUPLICATE_RESULTS" sourceSecurityId [resultSecurityId, resultSecurityId]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_DUPLICATE_RESULTS_OUTPUT" resultSecurityId stakeholderId stockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + +testSuccessorSecurityCannotBeReusedAcrossTransactions = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_SHARED_SUCCESSOR" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_SHARED_SUCCESSOR" "Shared Successor Holder" + let firstSourceId = "SEC_SHARED_SUCCESSOR_SOURCE_A" + let secondSourceId = "SEC_SHARED_SUCCESSOR_SOURCE_B" + let resultSecurityId = "SEC_SHARED_SUCCESSOR_RESULT" + firstIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_SHARED_SUCCESSOR_SOURCE_A" firstSourceId stakeholderId stockClassId) with quantity = 50.0) + secondIssued <- createStockIssuanceWith issuer firstIssued ((defaultStockIssuance "TX_SHARED_SUCCESSOR_SOURCE_B" secondSourceId stakeholderId stockClassId) with quantity = 50.0) + + submitMustFail issuer do + exerciseCmd secondIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (lifecycleStockTransfer "TX_SHARED_SUCCESSOR_TRANSFER" firstSourceId 50.0 None [resultSecurityId]) + , CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_SHARED_SUCCESSOR_REISSUE" secondSourceId [resultSecurityId]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_SHARED_SUCCESSOR_RESULT" resultSecurityId stakeholderId stockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + +testCrossTypeSourceResultAliasFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CROSS_TYPE_ALIAS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CROSS_TYPE_ALIAS" "Cross Type Alias Holder" + let sharedSecurityId = "SEC_CROSS_TYPE_ALIAS" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_CROSS_TYPE_ALIAS_WARRANT" sharedSecurityId stakeholderId stockClassId)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_CROSS_TYPE_ALIAS_EXERCISE" sharedSecurityId (Some 100.0) [sharedSecurityId]) + , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_CROSS_TYPE_ALIAS_RESULT" sharedSecurityId stakeholderId stockClassId) + ] + edits = [] + deletes = [] + +testStockBalanceSuccessorIncludesPriorUnmaterializedReduction = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_STOCK_UNMATERIALIZED_HISTORY" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_STOCK_UNMATERIALIZED_HISTORY" "Stock Unmaterialized History Holder" + let sourceSecurityId = "SEC_STOCK_UNMATERIALIZED_HISTORY_SOURCE" + issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_STOCK_UNMATERIALIZED_HISTORY_SOURCE" sourceSecurityId stakeholderId stockClassId) + transferred <- submit issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = [CT.OcfCreateStockTransfer (lifecycleStockTransfer "TX_STOCK_UNMATERIALIZED_HISTORY_TRANSFER" sourceSecurityId 30.0 None ["SEC_STOCK_UNMATERIALIZED_HISTORY_MISSING"])] + edits = [] + deletes = [] + + let balanceSecurityId = "SEC_STOCK_UNMATERIALIZED_HISTORY_BALANCE" + let cancellation = (stockCancellation "TX_STOCK_UNMATERIALIZED_HISTORY_CANCEL" sourceSecurityId 20.0) with + balance_security_id = Some balanceSecurityId + submitMustFail issuer do + exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_STOCK_UNMATERIALIZED_HISTORY_BALANCE" balanceSecurityId stakeholderId stockClassId) with quantity = 80.0) + ] + edits = [] + deletes = [] + +testEquityBalanceSuccessorIncludesPriorUnmaterializedReduction = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_UNMATERIALIZED_HISTORY" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_UNMATERIALIZED_HISTORY" "Equity Unmaterialized History Holder" + let sourceSecurityId = "ESEC_UNMATERIALIZED_HISTORY_SOURCE" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_UNMATERIALIZED_HISTORY_SOURCE" sourceSecurityId stakeholderId stockClassId 100.0)] + edits = [] + deletes = [] + transferred <- submit issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationTransfer (lifecycleEquityTransfer "TX_EQUITY_UNMATERIALIZED_HISTORY_TRANSFER" sourceSecurityId 30.0 None ["ESEC_UNMATERIALIZED_HISTORY_MISSING"])] + edits = [] + deletes = [] + + let balanceSecurityId = "ESEC_UNMATERIALIZED_HISTORY_BALANCE" + submitMustFail issuer do + exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation (lifecycleEquityCancellation "TX_EQUITY_UNMATERIALIZED_HISTORY_CANCEL" sourceSecurityId 20.0 balanceSecurityId) + , CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_UNMATERIALIZED_HISTORY_BALANCE" balanceSecurityId stakeholderId stockClassId 80.0) + ] + edits = [] + deletes = [] + +testUnmaterializedWarrantExerciseResultCannotAliasSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_UNMATERIALIZED_ALIAS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_UNMATERIALIZED_ALIAS" "Warrant Unmaterialized Alias Holder" + let securityId = "WSEC_UNMATERIALIZED_ALIAS" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_WARRANT_UNMATERIALIZED_ALIAS_SOURCE" securityId stakeholderId stockClassId)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_WARRANT_UNMATERIALIZED_ALIAS_EXERCISE" securityId (Some 50.0) [securityId])] + edits = [] + deletes = [] + +testUnmaterializedEquityExerciseResultCannotAliasSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_EXERCISE_UNMATERIALIZED_ALIAS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_EXERCISE_UNMATERIALIZED_ALIAS" "Equity Exercise Alias Holder" + let securityId = "ESEC_EXERCISE_UNMATERIALIZED_ALIAS" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_EXERCISE_UNMATERIALIZED_ALIAS_SOURCE" securityId stakeholderId stockClassId 100.0)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise (lifecycleEquityExercise "TX_EQUITY_EXERCISE_UNMATERIALIZED_ALIAS" securityId 50.0 [securityId])] + edits = [] + deletes = [] + +testUnmaterializedEquityReleaseResultCannotAliasSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_RELEASE_UNMATERIALIZED_ALIAS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_RELEASE_UNMATERIALIZED_ALIAS" "Equity Release Alias Holder" + let securityId = "ESEC_RELEASE_UNMATERIALIZED_ALIAS" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_RELEASE_UNMATERIALIZED_ALIAS_SOURCE" securityId stakeholderId stockClassId 100.0)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationRelease (lifecycleEquityRelease "TX_EQUITY_RELEASE_UNMATERIALIZED_ALIAS" securityId 50.0 [securityId])] + edits = [] + deletes = [] diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 2b00babe..9b45c179 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe483d1fed7a9fd1e62ca8b2a3d29cdd7daed719d271ddea6111decc1031b8f6 -size 2844083 +oid sha256:00951f0333bb010ce94089c0d511794397ad0cc139ef5b70a6b5b6b81f2789f0 +size 2914422 diff --git a/dars/dars.lock b/dars/dars.lock index 0ea3375c..9fc1322f 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "fe483d1fed7a9fd1e62ca8b2a3d29cdd7daed719d271ddea6111decc1031b8f6", - "size": 2844083, + "sha256": "00951f0333bb010ce94089c0d511794397ad0cc139ef5b70a6b5b6b81f2789f0", + "size": 2914422, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-10T13:44:14Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 93ed81b3..04738098 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -12,7 +12,7 @@ module Fairmint.OpenCapTable.CapTable where import DA.Map (Map) import qualified DA.Map as Map -import DA.List (sortOn) +import DA.List (sortOn, unique) import DA.Action (foldlA, when) import DA.Foldable (mapA_) import DA.Optional (fromOptional, isNone, isSome) @@ -101,6 +101,39 @@ toMaps ct = CapTableMaps with stock_issuances_by_security_id = ct.stock_issuances_by_security_id warrant_issuances_by_security_id = ct.warrant_issuances_by_security_id +securityIdsInFinalMaps : CapTableMaps -> Update [Text] +securityIdsInFinalMaps maps = do + convertibleSecurityIds <- foldlA + (\securityIds issuanceCid -> do + issuance <- fetch issuanceCid + pure (issuance.issuance_data.security_id :: securityIds)) + [] + (Map.values maps.convertible_issuances) + equityCompensationSecurityIds <- foldlA + (\securityIds issuanceCid -> do + issuance <- fetch issuanceCid + pure (issuance.issuance_data.security_id :: securityIds)) + [] + (Map.values maps.equity_compensation_issuances) + stockSecurityIds <- foldlA + (\securityIds issuanceCid -> do + issuance <- fetch issuanceCid + pure (issuance.issuance_data.security_id :: securityIds)) + [] + (Map.values maps.stock_issuances) + warrantSecurityIds <- foldlA + (\securityIds issuanceCid -> do + issuance <- fetch issuanceCid + pure (issuance.issuance_data.security_id :: securityIds)) + [] + (Map.values maps.warrant_issuances) + pure (convertibleSecurityIds ++ equityCompensationSecurityIds ++ stockSecurityIds ++ warrantSecurityIds) + +validateFinalSecurityIdUniqueness : CapTableMaps -> Update () +validateFinalSecurityIdUniqueness maps = do + securityIds <- securityIdsInFinalMaps maps + assertMsg "Security IDs must be globally unique" (unique securityIds) + data AuthorizedSharesCeiling = AuthorizedSharesFinite Decimal | AuthorizedSharesUnlimited @@ -147,6 +180,24 @@ data StockClassShareEventIndex = StockClassShareEventIndex with type StockSuccessorEdge = (Text, Time, [Text]) type StockSuccessorEdgeIndex = Map Text [(Time, [Text])] +data SecurityQuantityEvent + = SecurityQuantityDelta with + event_date: Time + event_priority: Int + quantity_delta: Decimal + | SecurityQuantityTerminalDelta with + 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 + deriving (Eq, Show) + data StockPlanSuccessorIndex = StockPlanSuccessorIndex with stock_successors_by_security_id: Map Text (Time, Optional Text) equity_compensation_successors_by_security_id: Map Text (Time, Optional Text) @@ -462,16 +513,103 @@ warrantExerciseTrigger issuanceData triggerId = None issuanceData.exercise_triggers -warrantSecurityQuantityEvents : CapTableMaps -> WarrantIssuanceOcfData -> Update [StockClassShareEvent] +securityQuantityReductionEvent : Time -> Decimal -> Optional Text -> SecurityQuantityEvent +securityQuantityReductionEvent eventDate quantity balanceSecurityId = + case balanceSecurityId of + Some _ -> + SecurityQuantityTerminalDelta with + event_date = eventDate + event_priority = 2 + quantity_delta = 0.0 - quantity + None -> + SecurityQuantityDelta with + event_date = eventDate + event_priority = 1 + quantity_delta = 0.0 - quantity + +securityQuantityEventSortKey : SecurityQuantityEvent -> (Time, Int, Int) +securityQuantityEventSortKey event = case event of + 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 () +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 + 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 + _ <- foldlA + (validateSecurityQuantityBalanceEvent securityId) + initialQuantity + (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)) + +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 (stockSecurityReductionEvents - transfer.transfer_data.date - transfer.transfer_data.quantity - transfer.transfer_data.balance_security_id ++ events) + then 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) @@ -480,12 +618,9 @@ warrantSecurityQuantityEvents maps issuanceData = do (\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) + then pure (securityQuantityReductionEvent cancellation.cancellation_data.date cancellation.cancellation_data.quantity cancellation.cancellation_data.balance_security_id :: events) else pure events) - transferEvents + [] (Map.values maps.warrant_cancellations) exerciseEvents <- foldlA @@ -494,32 +629,59 @@ warrantSecurityQuantityEvents maps issuanceData = do if exercise.exercise_data.security_id == issuanceData.security_id then case exercise.exercise_data.quantity of Some quantity -> - pure ((StockClassShareDelta with + pure ((SecurityQuantityDelta with event_date = exercise.exercise_data.date event_priority = 1 quantity_delta = 0.0 - quantity) :: events) None -> - pure ((StockClassShareRetraction with + pure ((SecurityQuantityRetraction with event_date = exercise.exercise_data.date event_priority = 3) :: events) else pure events) - cancellationEvents + [] (Map.values maps.warrant_exercises) - foldlA + retractionEvents <- foldlA (\events retractionCid -> do retraction <- fetch retractionCid if retraction.retraction_data.security_id == issuanceData.security_id - then pure ((StockClassShareRetraction with + then pure ((SecurityQuantityRetraction with event_date = retraction.retraction_data.date event_priority = 3) :: events) else pure events) - exerciseEvents + [] (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 -> do + assertSecurityQuantityEventsOnOrAfter issuanceData.security_id issuanceData.date events + 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) + warrantSecurityQuantityBeforeTerminal : CapTableMaps -> WarrantIssuanceOcfData -> @@ -527,23 +689,20 @@ warrantSecurityQuantityBeforeTerminal : Update (Optional Decimal) warrantSecurityQuantityBeforeTerminal maps issuanceData eventDate = do events <- warrantSecurityQuantityEvents maps issuanceData - let initialEvents = case issuanceData.quantity of - Some quantity -> - (StockClassShareDelta with + 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 - None -> events - let initialQuantity = case issuanceData.quantity of - Some _ -> Some 0.0 - None -> None - pure (foldl - applyStockClassShareEventOptional - initialQuantity - (filter - (\event -> stockClassShareEventSortKey event < (eventDate, 2, 1)) - (sortOn stockClassShareEventSortKey initialEvents))) + ) + (eventDate, 2, 1) + None -> + securityQuantityBeforeEvent issuanceData.security_id None events (eventDate, 2, 1) warrantExerciseQuantity : CapTableMaps -> WarrantIssuanceOcfData -> WarrantExerciseOcfData -> OcfWarrantConversionRight -> Update (Optional Decimal) warrantExerciseQuantity maps issuanceData exerciseData conversionRight = @@ -1483,29 +1642,166 @@ stockClassShareLotQuantityAt eventIndex lot asOf = (filter (stockClassShareEventOnOrBefore asOf) (stockClassShareLotEvents eventIndex lot))) else Some 0.0 +stockSecurityShareEvents : StockClassShareEventIndex -> CapTableMaps -> StockIssuanceOcfData -> Update [StockClassShareEvent] +stockSecurityShareEvents eventIndex 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) + + let splitCutoffSortKey = stockClassShareLotSplitCutoffSortKey eventIndex (stockIssuanceShareLot issuanceData) + splitEvents <- foldlA + (\events splitCid -> do + split <- fetch splitCid + let splitSortKey = (split.split_data.date, 2, 2) + if split.split_data.stock_class_id == issuanceData.stock_class_id && splitSortKey >= splitCutoffSortKey + 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 ++ transferEvents ++ conversionEvents ++ retractionEvents ++ reissuanceEvents ++ consolidationEvents ++ splitEvents)) + +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 + 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 + assertMsg + ("Stock security balance cannot be retired for " <> securityId <> " because it has no remaining balance") + (current > 0.0) + pure 0.0 + +validateStockSecurityBalance : StockClassShareEventIndex -> CapTableMaps -> StockIssuanceOcfData -> Update () +validateStockSecurityBalance eventIndex maps issuanceData = do + events <- stockSecurityShareEvents eventIndex maps issuanceData + _ <- foldlA + (validateStockSecurityBalanceEvent issuanceData.security_id) + 0.0 + (sortOn stockClassShareEventSortKey events) + pure () + +validateStockSecurityBalances : StockClassShareEventIndex -> CapTableMaps -> Update () +validateStockSecurityBalances eventIndex maps = + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + validateStockSecurityBalance eventIndex maps issuance.issuance_data) + (Map.values maps.stock_issuances) + stockSecurityQuantityBeforeEvent : StockClassShareEventIndex -> + CapTableMaps -> StockIssuanceOcfData -> (Time, Int, Int) -> Update Decimal -stockSecurityQuantityBeforeEvent eventIndex issuanceData targetSortKey = - let lot = stockIssuanceShareLot issuanceData - priorEvents = filter - (\event -> stockClassShareEventSortKey event < targetSortKey) - (sortOn stockClassShareEventSortKey (stockClassShareLotEvents eventIndex lot)) - in case foldl applyStockClassShareEventOptional (Some 0.0) priorEvents of - Some quantity -> pure quantity - None -> do - assertMsg ("Stock security quantity history is invalid for " <> issuanceData.security_id <> " before " <> show targetSortKey) False - pure 0.0 +stockSecurityQuantityBeforeEvent eventIndex maps issuanceData targetSortKey = do + events <- stockSecurityShareEvents eventIndex maps issuanceData + foldlA + (validateStockSecurityBalanceEvent issuanceData.security_id) + 0.0 + (filter + (\event -> stockClassShareEventSortKey event < targetSortKey) + (sortOn stockClassShareEventSortKey events)) stockSecurityQuantityBeforeTerminal : StockClassShareEventIndex -> + CapTableMaps -> StockIssuanceOcfData -> Time -> Update Decimal -stockSecurityQuantityBeforeTerminal eventIndex issuanceData eventDate = - stockSecurityQuantityBeforeEvent eventIndex issuanceData (eventDate, 2, 1) +stockSecurityQuantityBeforeTerminal eventIndex maps issuanceData eventDate = + stockSecurityQuantityBeforeEvent eventIndex maps issuanceData (eventDate, 2, 1) stockReductionSortKey : Time -> Optional Text -> (Time, Int, Int) stockReductionSortKey eventDate balanceSecurityId = @@ -1638,7 +1934,7 @@ validateStockTerminalBalanceSuccessor : validateStockTerminalBalanceSuccessor eventIndex maps label sourceIssuanceData eventDate reductionQuantity balanceSecurityId = case balanceSecurityId of Some securityId -> do - currentQuantity <- stockSecurityQuantityBeforeTerminal eventIndex sourceIssuanceData eventDate + currentQuantity <- stockSecurityQuantityBeforeTerminal eventIndex maps sourceIssuanceData eventDate let expectedQuantity = currentQuantity - reductionQuantity assertMsg (label <> " exceeds the current source quantity for " <> sourceIssuanceData.security_id) @@ -1748,6 +2044,7 @@ validateStockReissuanceResultIssuances eventIndex maps = let targetSortKey = (reissuance.reissuance_data.date, 3, 0) sourceQuantity <- stockSecurityQuantityBeforeEvent eventIndex + maps sourceIssuance.issuance_data targetSortKey resultingQuantity <- stockIssuanceQuantityTotal @@ -1787,6 +2084,7 @@ validateStockConsolidationResultIssuances eventIndex maps = (sourceIssuance.issuance_data.stock_class_id == resultIssuance.issuance_data.stock_class_id) quantity <- stockSecurityQuantityBeforeEvent eventIndex + maps sourceIssuance.issuance_data targetSortKey pure (total + quantity) @@ -2409,6 +2707,7 @@ stockPlanInheritedSuccessorExcessLots eventIndex successorIndex maps = do Some stockPlanId -> do sourceQuantity <- stockSecurityQuantityBeforeEvent eventIndex + maps sourceIssuance.issuance_data (reissuance.reissuance_data.date, 3, 3) (_, successorQuantity) <- recordedInheritedStockIssuanceSummary @@ -2457,6 +2756,7 @@ stockPlanConversionAllocationTransitions eventIndex successorIndex maps = do sourceBasisQuantity <- case conversion.conversion_data.balance_security_id of Some _ -> stockSecurityQuantityBeforeTerminal eventIndex + maps sourceIssuance.issuance_data conversion.conversion_data.date None -> pure conversion.conversion_data.quantity_converted @@ -2869,6 +3169,106 @@ equityCompensationSecurityEvents maps issuanceData = do pure (retractionEvents ++ splitEvents) +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.settlement_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) + + stockClassIds <- equityCompensationPlanStockClassIds maps issuanceData + splitEvents <- foldlA + (\events splitCid -> do + split <- fetch splitCid + if split.split_data.stock_class_id `elem` stockClassIds && 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) + case (issuanceData.stock_class_id, stockClassIds, splitEvents) of + (None, _ :: _ :: _, _ :: _) -> do + assertMsg + ("Equity compensation issuance without stock_class_id cannot be split-adjusted across multiple stock-plan classes: " <> issuanceData.id) + False + _ -> pure () + + pure (transferEvents ++ cancellationEvents ++ exerciseEvents ++ releaseEvents ++ retractionEvents ++ splitEvents) + +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) + equityCompensationCurrentQuantity : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update (Optional Decimal) equityCompensationCurrentQuantity maps issuanceData = do events <- equityCompensationSecurityEvents maps issuanceData @@ -2894,11 +3294,17 @@ equityCompensationSecurityQuantityBeforeEvent : (Time, Int, Int) -> Update Decimal equityCompensationSecurityQuantityBeforeEvent maps issuanceData targetSortKey = do - events <- equityCompensationSecurityEvents maps issuanceData - let priorEvents = filter - (\event -> stockClassShareEventSortKey event < targetSortKey) - (sortOn stockClassShareEventSortKey events) - case foldl applyStockClassShareEventOptional (Some issuanceData.quantity) priorEvents of + events <- equityCompensationSecurityQuantityEvents maps issuanceData + quantityOpt <- securityQuantityBeforeEvent + issuanceData.security_id + (Some 0.0) + ((SecurityQuantityDelta with + event_date = issuanceData.date + event_priority = 0 + quantity_delta = issuanceData.quantity) + :: events) + targetSortKey + case quantityOpt of Some quantity -> pure quantity None -> do assertMsg ("Equity compensation quantity history is invalid for " <> issuanceData.security_id <> " before " <> show targetSortKey) False @@ -3591,6 +3997,281 @@ validateStockPlanPoolCeilings eventIndex maps = do validateStockPlanReservedShares eventIndex maps adjustmentIndex validateStockPlanReservedSharesAtDates eventIndex maps adjustmentHistory +assertSuccessorSecurityIdDistinct : Text -> Text -> Text -> Update () +assertSuccessorSecurityIdDistinct label sourceSecurityId successorSecurityId = + assertMsg + (label <> " must use a new successor security ID instead of reusing " <> sourceSecurityId) + (successorSecurityId /= sourceSecurityId) + +validateSuccessorSecurityIds : Text -> Text -> Optional Text -> [Text] -> Update () +validateSuccessorSecurityIds label sourceSecurityId balanceSecurityId resultingSecurityIds = do + mapA_ (assertSuccessorSecurityIdDistinct label sourceSecurityId) resultingSecurityIds + case balanceSecurityId of + Some balanceId -> do + assertSuccessorSecurityIdDistinct label sourceSecurityId balanceId + assertMsg + (label <> " cannot use its balance security as a resulting security: " <> balanceId) + (balanceId `notElem` resultingSecurityIds) + None -> pure () + +successorSecurityIdsFor : Optional Text -> [Text] -> [Text] +successorSecurityIdsFor balanceSecurityId resultingSecurityIds = + case balanceSecurityId of + Some balanceId -> balanceId :: resultingSecurityIds + None -> resultingSecurityIds + +validateGlobalSuccessorSecurityIdUniqueness : CapTableMaps -> Update () +validateGlobalSuccessorSecurityIdUniqueness maps = do + stockTransferIds <- foldlA + (\securityIds transferCid -> do + transfer <- fetch transferCid + pure (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids ++ securityIds)) + [] + (Map.values maps.stock_transfers) + stockCancellationIds <- foldlA + (\securityIds cancellationCid -> do + cancellation <- fetch cancellationCid + pure (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [] ++ securityIds)) + stockTransferIds + (Map.values maps.stock_cancellations) + stockRepurchaseIds <- foldlA + (\securityIds repurchaseCid -> do + repurchase <- fetch repurchaseCid + pure (successorSecurityIdsFor repurchase.repurchase_data.balance_security_id [] ++ securityIds)) + stockCancellationIds + (Map.values maps.stock_repurchases) + stockConversionIds <- foldlA + (\securityIds conversionCid -> do + conversion <- fetch conversionCid + pure (successorSecurityIdsFor conversion.conversion_data.balance_security_id conversion.conversion_data.resulting_security_ids ++ securityIds)) + stockRepurchaseIds + (Map.values maps.stock_conversions) + stockReissuanceIds <- foldlA + (\securityIds reissuanceCid -> do + reissuance <- fetch reissuanceCid + pure (reissuance.reissuance_data.resulting_security_ids ++ securityIds)) + stockConversionIds + (Map.values maps.stock_reissuances) + stockConsolidationIds <- foldlA + (\securityIds consolidationCid -> do + consolidation <- fetch consolidationCid + pure (consolidation.consolidation_data.resulting_security_id :: securityIds)) + stockReissuanceIds + (Map.values maps.stock_consolidations) + warrantTransferIds <- foldlA + (\securityIds transferCid -> do + transfer <- fetch transferCid + pure (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids ++ securityIds)) + stockConsolidationIds + (Map.values maps.warrant_transfers) + warrantCancellationIds <- foldlA + (\securityIds cancellationCid -> do + cancellation <- fetch cancellationCid + pure (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [] ++ securityIds)) + warrantTransferIds + (Map.values maps.warrant_cancellations) + warrantExerciseIds <- foldlA + (\securityIds exerciseCid -> do + exercise <- fetch exerciseCid + pure (exercise.exercise_data.resulting_security_ids ++ securityIds)) + warrantCancellationIds + (Map.values maps.warrant_exercises) + convertibleTransferIds <- foldlA + (\securityIds transferCid -> do + transfer <- fetch transferCid + pure (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids ++ securityIds)) + warrantExerciseIds + (Map.values maps.convertible_transfers) + convertibleCancellationIds <- foldlA + (\securityIds cancellationCid -> do + cancellation <- fetch cancellationCid + pure (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [] ++ securityIds)) + convertibleTransferIds + (Map.values maps.convertible_cancellations) + convertibleConversionIds <- foldlA + (\securityIds conversionCid -> do + conversion <- fetch conversionCid + pure (successorSecurityIdsFor conversion.conversion_data.balance_security_id conversion.conversion_data.resulting_security_ids ++ securityIds)) + convertibleCancellationIds + (Map.values maps.convertible_conversions) + equityCompensationTransferIds <- foldlA + (\securityIds transferCid -> do + transfer <- fetch transferCid + pure (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids ++ securityIds)) + convertibleConversionIds + (Map.values maps.equity_compensation_transfers) + equityCompensationCancellationIds <- foldlA + (\securityIds cancellationCid -> do + cancellation <- fetch cancellationCid + pure (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [] ++ securityIds)) + equityCompensationTransferIds + (Map.values maps.equity_compensation_cancellations) + equityCompensationExerciseIds <- foldlA + (\securityIds exerciseCid -> do + exercise <- fetch exerciseCid + pure (exercise.exercise_data.resulting_security_ids ++ securityIds)) + equityCompensationCancellationIds + (Map.values maps.equity_compensation_exercises) + allSuccessorIds <- foldlA + (\securityIds releaseCid -> do + release <- fetch releaseCid + pure (release.release_data.resulting_security_ids ++ securityIds)) + equityCompensationExerciseIds + (Map.values maps.equity_compensation_releases) + assertMsg + "Each successor security ID may be produced by only one lifecycle transaction" + (unique allSuccessorIds) + +validateSuccessorSecurityIdContinuity : CapTableMaps -> Update () +validateSuccessorSecurityIdContinuity maps = do + validateGlobalSuccessorSecurityIdUniqueness maps + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + validateSuccessorSecurityIds + ("Stock transfer " <> transfer.transfer_data.id) + transfer.transfer_data.security_id + transfer.transfer_data.balance_security_id + transfer.transfer_data.resulting_security_ids) + (Map.values maps.stock_transfers) + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + validateSuccessorSecurityIds + ("Stock cancellation " <> cancellation.cancellation_data.id) + cancellation.cancellation_data.security_id + cancellation.cancellation_data.balance_security_id + []) + (Map.values maps.stock_cancellations) + mapA_ + (\repurchaseCid -> do + repurchase <- fetch repurchaseCid + validateSuccessorSecurityIds + ("Stock repurchase " <> repurchase.repurchase_data.id) + repurchase.repurchase_data.security_id + repurchase.repurchase_data.balance_security_id + []) + (Map.values maps.stock_repurchases) + mapA_ + (\conversionCid -> do + conversion <- fetch conversionCid + validateSuccessorSecurityIds + ("Stock conversion " <> conversion.conversion_data.id) + conversion.conversion_data.security_id + conversion.conversion_data.balance_security_id + conversion.conversion_data.resulting_security_ids) + (Map.values maps.stock_conversions) + mapA_ + (\reissuanceCid -> do + reissuance <- fetch reissuanceCid + validateSuccessorSecurityIds + ("Stock reissuance " <> reissuance.reissuance_data.id) + reissuance.reissuance_data.security_id + None + reissuance.reissuance_data.resulting_security_ids) + (Map.values maps.stock_reissuances) + mapA_ + (\consolidationCid -> do + consolidation <- fetch consolidationCid + mapA_ + (\sourceSecurityId -> + assertSuccessorSecurityIdDistinct + ("Stock consolidation " <> consolidation.consolidation_data.id) + sourceSecurityId + consolidation.consolidation_data.resulting_security_id) + consolidation.consolidation_data.security_ids) + (Map.values maps.stock_consolidations) + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + validateSuccessorSecurityIds + ("Warrant transfer " <> transfer.transfer_data.id) + transfer.transfer_data.security_id + transfer.transfer_data.balance_security_id + transfer.transfer_data.resulting_security_ids) + (Map.values maps.warrant_transfers) + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + validateSuccessorSecurityIds + ("Warrant cancellation " <> cancellation.cancellation_data.id) + cancellation.cancellation_data.security_id + cancellation.cancellation_data.balance_security_id + []) + (Map.values maps.warrant_cancellations) + mapA_ + (\exerciseCid -> do + exercise <- fetch exerciseCid + validateSuccessorSecurityIds + ("Warrant exercise " <> exercise.exercise_data.id) + exercise.exercise_data.security_id + None + exercise.exercise_data.resulting_security_ids) + (Map.values maps.warrant_exercises) + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + validateSuccessorSecurityIds + ("Convertible transfer " <> transfer.transfer_data.id) + transfer.transfer_data.security_id + transfer.transfer_data.balance_security_id + transfer.transfer_data.resulting_security_ids) + (Map.values maps.convertible_transfers) + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + validateSuccessorSecurityIds + ("Convertible cancellation " <> cancellation.cancellation_data.id) + cancellation.cancellation_data.security_id + cancellation.cancellation_data.balance_security_id + []) + (Map.values maps.convertible_cancellations) + mapA_ + (\conversionCid -> do + conversion <- fetch conversionCid + validateSuccessorSecurityIds + ("Convertible conversion " <> conversion.conversion_data.id) + conversion.conversion_data.security_id + conversion.conversion_data.balance_security_id + conversion.conversion_data.resulting_security_ids) + (Map.values maps.convertible_conversions) + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + validateSuccessorSecurityIds + ("Equity compensation transfer " <> transfer.transfer_data.id) + transfer.transfer_data.security_id + transfer.transfer_data.balance_security_id + transfer.transfer_data.resulting_security_ids) + (Map.values maps.equity_compensation_transfers) + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + validateSuccessorSecurityIds + ("Equity compensation cancellation " <> cancellation.cancellation_data.id) + cancellation.cancellation_data.security_id + cancellation.cancellation_data.balance_security_id + []) + (Map.values maps.equity_compensation_cancellations) + mapA_ + (\exerciseCid -> do + exercise <- fetch exerciseCid + validateSuccessorSecurityIds + ("Equity compensation exercise " <> exercise.exercise_data.id) + exercise.exercise_data.security_id + None + exercise.exercise_data.resulting_security_ids) + (Map.values maps.equity_compensation_exercises) + mapA_ + (\releaseCid -> do + release <- fetch releaseCid + validateSuccessorSecurityIds + ("Equity compensation release " <> release.release_data.id) + release.release_data.security_id + None + release.release_data.resulting_security_ids) + (Map.values maps.equity_compensation_releases) + validateAuthorizedShareCeilings : CapTableMaps -> Map Text StockClassAuthorizedSharesAdjustmentOcfData -> @@ -3598,6 +4279,11 @@ validateAuthorizedShareCeilings : Update () validateAuthorizedShareCeilings maps adjustmentIndex issuerCid = do eventIndex <- stockClassShareEventIndex maps + validateFinalSecurityIdUniqueness maps + validateSuccessorSecurityIdContinuity maps + validateStockSecurityBalances eventIndex maps + validateWarrantSecurityBalances maps + validateEquityCompensationSecurityBalances maps validateStockBalanceSuccessors eventIndex maps validateStockTransferResultIssuances maps validateStockReissuanceResultIssuances eventIndex maps From 15fe66a45c94c7905c16049c82a31ea7b2f24df1 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 17:43:08 -0400 Subject: [PATCH 42/62] Revert "fix: enforce lifecycle identity and terminal balances" This reverts commit 77a49659596b30227857ac3958b662cadeaa5834. --- .../TestEquityCompensationRelease.daml | 91 +- .../OpenCapTable/TestLifecycleInvariants.daml | 473 ----------- .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 782 ++---------------- 5 files changed, 75 insertions(+), 1279 deletions(-) delete mode 100644 Test/daml/OpenCapTable/TestLifecycleInvariants.daml diff --git a/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml b/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml index 91061f05..36f71b7d 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml @@ -1,72 +1,35 @@ 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 (addPrerequisites, defaultStockIssuance) +import OpenCapTable.TestHelpers (addPrerequisites, addEquityCompensationIssuance) 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 - 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 = None - expiration_date = None - stock_class_id = Some "SC_COMMON" - 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 <- addPrerequisites issuer cap_table - cap_table <- addEquityCompensationReleaseIssuance issuer cap_table "EC-ISS-RSU-1" "SEC_RSU_001" "SH-1" 20000.0 + cap_table <- addEquityCompensationIssuance issuer cap_table "EC-ISS-RSU-1" "SEC_RSU_001" "SH-1" -- RSU vesting and release _ <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with - creates = - [ CT.OcfCreateStockIssuance ((defaultStockIssuance "STOCK-ISS-EC-RELEASE-1" "SEC_STOCK_101" "SH-1" "SC_COMMON") with - date = DT.time (DA.date 2024 Apr 03) 0 0 0 - quantity = 5000.0) - , CT.OcfCreateEquityCompensationRelease EquityCompensationReleaseOcfData with - id = "TX_EC_RELEASE_1" - date = DT.time (DA.date 2024 Apr 01) 0 0 0 - security_id = "SEC_RSU_001" - settlement_date = DT.time (DA.date 2024 Apr 03) 0 0 0 - release_price = OcfMonetary with amount = 45.50; currency = "USD" - quantity = 5000.0 - resulting_security_ids = ["SEC_STOCK_101"] - consideration_text = Some "RSU cliff vest - 25% of total grant" - comments = ["First anniversary cliff vest for Employee A"] - ] + creates = [CT.OcfCreateEquityCompensationRelease EquityCompensationReleaseOcfData with + id = "TX_EC_RELEASE_1" + date = DT.time (DA.date 2024 Apr 01) 0 0 0 + security_id = "SEC_RSU_001" + settlement_date = DT.time (DA.date 2024 Apr 03) 0 0 0 + release_price = OcfMonetary with amount = 45.50; currency = "USD" + quantity = 5000.0 + resulting_security_ids = ["SEC_STOCK_101"] + consideration_text = Some "RSU cliff vest - 25% of total grant" + comments = ["First anniversary cliff vest for Employee A"]] edits = [] deletes = [] pure () @@ -77,28 +40,20 @@ testCreateAndArchiveEquityCompensationReleaseQuarterly = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp -- Add stakeholder and equity compensation issuance before releasing cap_table <- addPrerequisites issuer cap_table - cap_table <- addEquityCompensationReleaseIssuance issuer cap_table "EC-ISS-RSU-2" "SEC_RSU_002" "SH-1" 5000.0 + cap_table <- addEquityCompensationIssuance issuer cap_table "EC-ISS-RSU-2" "SEC_RSU_002" "SH-1" -- Quarterly RSU vest with tax withholding _ <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with - creates = - [ CT.OcfCreateStockIssuance ((defaultStockIssuance "STOCK-ISS-EC-RELEASE-2A" "SEC_STOCK_102" "SH-1" "SC_COMMON") with - date = DT.time (DA.date 2024 Jul 05) 0 0 0 - quantity = 625.0) - , CT.OcfCreateStockIssuance ((defaultStockIssuance "STOCK-ISS-EC-RELEASE-2B" "SEC_STOCK_103" "SH-1" "SC_COMMON") with - date = DT.time (DA.date 2024 Jul 05) 0 0 0 - quantity = 625.0) - , CT.OcfCreateEquityCompensationRelease EquityCompensationReleaseOcfData with - id = "TX_EC_RELEASE_2" - date = DT.time (DA.date 2024 Jul 01) 0 0 0 - security_id = "SEC_RSU_002" - settlement_date = DT.time (DA.date 2024 Jul 05) 0 0 0 - release_price = OcfMonetary with amount = 52.00; currency = "USD" - quantity = 1250.0 - resulting_security_ids = ["SEC_STOCK_102", "SEC_STOCK_103"] - consideration_text = None - comments = ["Q3 2024 quarterly vest", "Net settlement after tax withholding"] - ] + creates = [CT.OcfCreateEquityCompensationRelease EquityCompensationReleaseOcfData with + id = "TX_EC_RELEASE_2" + date = DT.time (DA.date 2024 Jul 01) 0 0 0 + security_id = "SEC_RSU_002" + settlement_date = DT.time (DA.date 2024 Jul 05) 0 0 0 + release_price = OcfMonetary with amount = 52.00; currency = "USD" + quantity = 1250.0 + resulting_security_ids = ["SEC_STOCK_102", "SEC_STOCK_103"] + consideration_text = None + comments = ["Q3 2024 quarterly vest", "Net settlement after tax withholding"]] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestLifecycleInvariants.daml b/Test/daml/OpenCapTable/TestLifecycleInvariants.daml deleted file mode 100644 index 94a4a6ca..00000000 --- a/Test/daml/OpenCapTable/TestLifecycleInvariants.daml +++ /dev/null @@ -1,473 +0,0 @@ -module OpenCapTable.TestLifecycleInvariants where - -import Fairmint.OpenCapTable.OCF.EquityCompensationCancellation -import Fairmint.OpenCapTable.OCF.EquityCompensationExercise -import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance -import Fairmint.OpenCapTable.OCF.EquityCompensationRelease -import Fairmint.OpenCapTable.OCF.EquityCompensationTransfer -import Fairmint.OpenCapTable.OCF.StockConsolidation -import Fairmint.OpenCapTable.OCF.StockReissuance -import Fairmint.OpenCapTable.OCF.StockTransfer -import Fairmint.OpenCapTable.OCF.WarrantExercise -import Fairmint.OpenCapTable.OCF.WarrantIssuance -import Fairmint.OpenCapTable.Types.Conversion -import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) -import Fairmint.OpenCapTable.Types.Stock (OcfQuantitySourceType(..)) -import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) -import qualified Fairmint.OpenCapTable.CapTable as CT -import OpenCapTable.Setup -import OpenCapTable.TestHelpers -import Daml.Script - -lifecycleWarrantTrigger : Text -> Text -> OcfConversionTrigger -lifecycleWarrantTrigger triggerId stockClassId = - OcfConversionTrigger with - conversion_right = OcfRightWarrant (OcfWarrantConversionRight with - conversion_mechanism = OcfWarrantMechanismFixedAmount with converts_to_quantity = 100.0 - converts_to_future_round = None - converts_to_stock_class_id = 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 - -lifecycleWarrantIssuance : Text -> Text -> Text -> Text -> WarrantIssuanceOcfData -lifecycleWarrantIssuance issuanceId securityId stakeholderId stockClassId = - WarrantIssuanceOcfData with - id = issuanceId - custom_id = "WR-" <> issuanceId - date = defaultTestDate - purchase_price = OcfMonetary with amount = 1.0, currency = "USD" - security_id = securityId - stakeholder_id = stakeholderId - comments = [] - exercise_triggers = [lifecycleWarrantTrigger "TRIGGER_FIXED" stockClassId] - security_law_exemptions = [] - vestings = [] - board_approval_date = None - consideration_text = None - exercise_price = None - quantity = Some 100.0 - quantity_source = Some OcfQuantityUnspecified - stockholder_approval_date = None - vesting_terms_id = None - warrant_expiration_date = None - -lifecycleWarrantExercise : Text -> Text -> Optional Decimal -> [Text] -> WarrantExerciseOcfData -lifecycleWarrantExercise exerciseId securityId quantity resultingSecurityIds = - WarrantExerciseOcfData with - id = exerciseId - date = defaultTestDate - security_id = securityId - trigger_id = "TRIGGER_FIXED" - comments = [] - resulting_security_ids = resultingSecurityIds - consideration_text = None - quantity - -lifecycleStockTransfer : Text -> Text -> Decimal -> Optional Text -> [Text] -> StockTransferOcfData -lifecycleStockTransfer transferId securityId quantity balanceSecurityId resultingSecurityIds = - StockTransferOcfData with - id = transferId - date = defaultTestDate - security_id = securityId - quantity - resulting_security_ids = resultingSecurityIds - balance_security_id = balanceSecurityId - consideration_text = None - comments = [] - -lifecycleStockReissuance : Text -> Text -> [Text] -> StockReissuanceOcfData -lifecycleStockReissuance reissuanceId securityId resultingSecurityIds = - StockReissuanceOcfData with - id = reissuanceId - date = defaultTestDate - security_id = securityId - resulting_security_ids = resultingSecurityIds - split_transaction_id = None - reason_text = Some "Lifecycle replacement" - comments = [] - -lifecycleStockConsolidation : Text -> [Text] -> Text -> StockConsolidationOcfData -lifecycleStockConsolidation consolidationId securityIds resultingSecurityId = - StockConsolidationOcfData with - id = consolidationId - date = defaultTestDate - security_ids = securityIds - resulting_security_id = resultingSecurityId - reason_text = Some "Lifecycle consolidation" - comments = [] - -lifecycleEquityIssuance : Text -> Text -> Text -> Text -> Decimal -> EquityCompensationIssuanceOcfData -lifecycleEquityIssuance issuanceId securityId stakeholderId stockClassId quantity = - EquityCompensationIssuanceOcfData with - id = issuanceId - custom_id = "EC-" <> issuanceId - date = defaultTestDate - compensation_type = OcfCompensationTypeOptionNSO - 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 = Some stockClassId - stock_plan_id = None - stockholder_approval_date = None - vesting_terms_id = None - -lifecycleEquityCancellation : Text -> Text -> Decimal -> Text -> EquityCompensationCancellationOcfData -lifecycleEquityCancellation cancellationId securityId quantity balanceSecurityId = - EquityCompensationCancellationOcfData with - id = cancellationId - date = defaultTestDate - security_id = securityId - quantity - balance_security_id = Some balanceSecurityId - reason_text = "Lifecycle cancellation" - comments = [] - -lifecycleEquityTransfer : Text -> Text -> Decimal -> Optional Text -> [Text] -> EquityCompensationTransferOcfData -lifecycleEquityTransfer transferId securityId quantity balanceSecurityId resultingSecurityIds = - EquityCompensationTransferOcfData with - id = transferId - date = defaultTestDate - security_id = securityId - quantity - resulting_security_ids = resultingSecurityIds - balance_security_id = balanceSecurityId - consideration_text = None - comments = [] - -lifecycleEquityExercise : Text -> Text -> Decimal -> [Text] -> EquityCompensationExerciseOcfData -lifecycleEquityExercise exerciseId securityId quantity resultingSecurityIds = - EquityCompensationExerciseOcfData with - id = exerciseId - date = defaultTestDate - security_id = securityId - quantity - resulting_security_ids = resultingSecurityIds - consideration_text = None - comments = [] - -lifecycleEquityRelease : Text -> Text -> Decimal -> [Text] -> EquityCompensationReleaseOcfData -lifecycleEquityRelease releaseId securityId quantity resultingSecurityIds = - EquityCompensationReleaseOcfData with - id = releaseId - date = defaultTestDate - security_id = securityId - settlement_date = defaultTestDate - release_price = OcfMonetary with amount = 1.0, currency = "USD" - quantity - resulting_security_ids = resultingSecurityIds - consideration_text = None - comments = [] - -testWarrantExerciseCannotExceedRemainingQuantity = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_REMAINING" 1_000.0) - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_REMAINING" "Warrant Remaining Holder" - let securityId = "WSEC_REMAINING" - issued <- submit issuer do - exerciseCmd capTableReady CT.UpdateCapTable with - creates = [CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_WARRANT_REMAINING_ISSUE" securityId stakeholderId stockClassId)] - edits = [] - deletes = [] - - firstExercise <- submit issuer do - exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with - creates = - [ CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_WARRANT_REMAINING_FIRST" securityId (Some 60.0) ["SEC_WARRANT_REMAINING_FIRST"]) - , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_WARRANT_REMAINING_FIRST_RESULT" "SEC_WARRANT_REMAINING_FIRST" stakeholderId stockClassId) with quantity = 60.0) - ] - edits = [] - deletes = [] - - submitMustFail issuer do - exerciseCmd firstExercise.updatedCapTableCid CT.UpdateCapTable with - creates = - [ CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_WARRANT_REMAINING_SECOND" securityId (Some 50.0) ["SEC_WARRANT_REMAINING_SECOND"]) - , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_WARRANT_REMAINING_SECOND_RESULT" "SEC_WARRANT_REMAINING_SECOND" stakeholderId stockClassId) with quantity = 50.0) - ] - edits = [] - deletes = [] - -testTwoFullWarrantExercisesAtSameTimestampFail = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_DOUBLE_FULL" 1_000.0) - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_DOUBLE_FULL" "Warrant Double Full Holder" - let securityId = "WSEC_DOUBLE_FULL" - issued <- submit issuer do - exerciseCmd capTableReady CT.UpdateCapTable with - creates = [CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_WARRANT_DOUBLE_FULL_ISSUE" securityId stakeholderId stockClassId)] - edits = [] - deletes = [] - - submitMustFail issuer do - exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with - creates = - [ CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_WARRANT_DOUBLE_FULL_A" securityId None ["SEC_WARRANT_DOUBLE_FULL_A"]) - , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_WARRANT_DOUBLE_FULL_RESULT_A" "SEC_WARRANT_DOUBLE_FULL_A" stakeholderId stockClassId) - , CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_WARRANT_DOUBLE_FULL_B" securityId None ["SEC_WARRANT_DOUBLE_FULL_B"]) - , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_WARRANT_DOUBLE_FULL_RESULT_B" "SEC_WARRANT_DOUBLE_FULL_B" stakeholderId stockClassId) - ] - edits = [] - deletes = [] - -testTwoStockReissuancesAtSameTimestampFail = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REISSUE_DOUBLE" 1_000.0) - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_REISSUE_DOUBLE" "Reissuance Double Holder" - let sourceSecurityId = "SEC_REISSUE_DOUBLE_SOURCE" - issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_REISSUE_DOUBLE_SOURCE" sourceSecurityId stakeholderId stockClassId) - - submitMustFail issuer do - exerciseCmd issued CT.UpdateCapTable with - creates = - [ CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_REISSUE_DOUBLE_A" sourceSecurityId ["SEC_REISSUE_DOUBLE_A"]) - , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_REISSUE_DOUBLE_RESULT_A" "SEC_REISSUE_DOUBLE_A" stakeholderId stockClassId) - , CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_REISSUE_DOUBLE_B" sourceSecurityId ["SEC_REISSUE_DOUBLE_B"]) - , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_REISSUE_DOUBLE_RESULT_B" "SEC_REISSUE_DOUBLE_B" stakeholderId stockClassId) - ] - edits = [] - deletes = [] - -testMixedStockTerminalEventsAtSameTimestampFail = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_TERMINAL_MIXED" 1_000.0) - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_TERMINAL_MIXED" "Mixed Terminal Holder" - let sourceSecurityId = "SEC_TERMINAL_MIXED_SOURCE" - issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_TERMINAL_MIXED_SOURCE" sourceSecurityId stakeholderId stockClassId) - - submitMustFail issuer do - exerciseCmd issued CT.UpdateCapTable with - creates = - [ CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_TERMINAL_MIXED_REISSUE" sourceSecurityId ["SEC_TERMINAL_MIXED_REISSUE"]) - , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_TERMINAL_MIXED_REISSUE_RESULT" "SEC_TERMINAL_MIXED_REISSUE" stakeholderId stockClassId) - , CT.OcfCreateStockConsolidation (lifecycleStockConsolidation "TX_TERMINAL_MIXED_CONSOLIDATE" [sourceSecurityId] "SEC_TERMINAL_MIXED_CONSOLIDATE") - , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_TERMINAL_MIXED_CONSOLIDATE_RESULT" "SEC_TERMINAL_MIXED_CONSOLIDATE" stakeholderId stockClassId) - ] - edits = [] - deletes = [] - -testRepeatedEquityTerminalEventsAtSameTimestampFail = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_DOUBLE_TERMINAL" 1_000.0) - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_DOUBLE_TERMINAL" "Equity Double Terminal Holder" - let sourceSecurityId = "ESEC_DOUBLE_TERMINAL_SOURCE" - issued <- submit issuer do - exerciseCmd capTableReady CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_DOUBLE_TERMINAL_SOURCE" sourceSecurityId stakeholderId stockClassId 100.0)] - edits = [] - deletes = [] - - submitMustFail issuer do - exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with - creates = - [ CT.OcfCreateEquityCompensationCancellation (lifecycleEquityCancellation "TX_EQUITY_DOUBLE_TERMINAL_A" sourceSecurityId 50.0 "ESEC_DOUBLE_TERMINAL_A") - , CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_DOUBLE_TERMINAL_RESULT_A" "ESEC_DOUBLE_TERMINAL_A" stakeholderId stockClassId 50.0) - , CT.OcfCreateEquityCompensationCancellation (lifecycleEquityCancellation "TX_EQUITY_DOUBLE_TERMINAL_B" sourceSecurityId 50.0 "ESEC_DOUBLE_TERMINAL_B") - , CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_DOUBLE_TERMINAL_RESULT_B" "ESEC_DOUBLE_TERMINAL_B" stakeholderId stockClassId 50.0) - ] - edits = [] - deletes = [] - -testBalanceSecurityCannotAlsoBeResultingSecurity = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_BALANCE_RESULT_ALIAS" 1_000.0) - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_BALANCE_RESULT_ALIAS" "Balance Result Alias Holder" - let sourceSecurityId = "SEC_BALANCE_RESULT_ALIAS_SOURCE" - let aliasSecurityId = "SEC_BALANCE_RESULT_ALIAS" - issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_BALANCE_RESULT_ALIAS_SOURCE" sourceSecurityId stakeholderId stockClassId) - - submitMustFail issuer do - exerciseCmd issued CT.UpdateCapTable with - creates = - [ CT.OcfCreateStockTransfer (lifecycleStockTransfer "TX_BALANCE_RESULT_ALIAS" sourceSecurityId 50.0 (Some aliasSecurityId) [aliasSecurityId]) - , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_BALANCE_RESULT_ALIAS_OUTPUT" aliasSecurityId stakeholderId stockClassId) with quantity = 50.0) - ] - edits = [] - deletes = [] - -testStockReissuanceCannotReuseSourceAsResult = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REISSUE_SOURCE_ALIAS" 1_000.0) - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_REISSUE_SOURCE_ALIAS" "Reissuance Source Alias Holder" - let sourceSecurityId = "SEC_REISSUE_SOURCE_ALIAS" - issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_REISSUE_SOURCE_ALIAS_SOURCE" sourceSecurityId stakeholderId stockClassId) - - submitMustFail issuer do - exerciseCmd issued CT.UpdateCapTable with - creates = [CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_REISSUE_SOURCE_ALIAS" sourceSecurityId [sourceSecurityId])] - edits = [] - deletes = [] - -testDuplicateResultIdsFail = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_DUPLICATE_RESULTS" 1_000.0) - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_DUPLICATE_RESULTS" "Duplicate Results Holder" - let sourceSecurityId = "SEC_DUPLICATE_RESULTS_SOURCE" - let resultSecurityId = "SEC_DUPLICATE_RESULTS_RESULT" - issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_DUPLICATE_RESULTS_SOURCE" sourceSecurityId stakeholderId stockClassId) - - submitMustFail issuer do - exerciseCmd issued CT.UpdateCapTable with - creates = - [ CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_DUPLICATE_RESULTS" sourceSecurityId [resultSecurityId, resultSecurityId]) - , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_DUPLICATE_RESULTS_OUTPUT" resultSecurityId stakeholderId stockClassId) with quantity = 50.0) - ] - edits = [] - deletes = [] - -testSuccessorSecurityCannotBeReusedAcrossTransactions = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_SHARED_SUCCESSOR" 1_000.0) - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_SHARED_SUCCESSOR" "Shared Successor Holder" - let firstSourceId = "SEC_SHARED_SUCCESSOR_SOURCE_A" - let secondSourceId = "SEC_SHARED_SUCCESSOR_SOURCE_B" - let resultSecurityId = "SEC_SHARED_SUCCESSOR_RESULT" - firstIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_SHARED_SUCCESSOR_SOURCE_A" firstSourceId stakeholderId stockClassId) with quantity = 50.0) - secondIssued <- createStockIssuanceWith issuer firstIssued ((defaultStockIssuance "TX_SHARED_SUCCESSOR_SOURCE_B" secondSourceId stakeholderId stockClassId) with quantity = 50.0) - - submitMustFail issuer do - exerciseCmd secondIssued CT.UpdateCapTable with - creates = - [ CT.OcfCreateStockTransfer (lifecycleStockTransfer "TX_SHARED_SUCCESSOR_TRANSFER" firstSourceId 50.0 None [resultSecurityId]) - , CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_SHARED_SUCCESSOR_REISSUE" secondSourceId [resultSecurityId]) - , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_SHARED_SUCCESSOR_RESULT" resultSecurityId stakeholderId stockClassId) with quantity = 50.0) - ] - edits = [] - deletes = [] - -testCrossTypeSourceResultAliasFails = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CROSS_TYPE_ALIAS" 1_000.0) - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CROSS_TYPE_ALIAS" "Cross Type Alias Holder" - let sharedSecurityId = "SEC_CROSS_TYPE_ALIAS" - issued <- submit issuer do - exerciseCmd capTableReady CT.UpdateCapTable with - creates = [CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_CROSS_TYPE_ALIAS_WARRANT" sharedSecurityId stakeholderId stockClassId)] - edits = [] - deletes = [] - - submitMustFail issuer do - exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with - creates = - [ CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_CROSS_TYPE_ALIAS_EXERCISE" sharedSecurityId (Some 100.0) [sharedSecurityId]) - , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_CROSS_TYPE_ALIAS_RESULT" sharedSecurityId stakeholderId stockClassId) - ] - edits = [] - deletes = [] - -testStockBalanceSuccessorIncludesPriorUnmaterializedReduction = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_STOCK_UNMATERIALIZED_HISTORY" 1_000.0) - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_STOCK_UNMATERIALIZED_HISTORY" "Stock Unmaterialized History Holder" - let sourceSecurityId = "SEC_STOCK_UNMATERIALIZED_HISTORY_SOURCE" - issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_STOCK_UNMATERIALIZED_HISTORY_SOURCE" sourceSecurityId stakeholderId stockClassId) - transferred <- submit issuer do - exerciseCmd issued CT.UpdateCapTable with - creates = [CT.OcfCreateStockTransfer (lifecycleStockTransfer "TX_STOCK_UNMATERIALIZED_HISTORY_TRANSFER" sourceSecurityId 30.0 None ["SEC_STOCK_UNMATERIALIZED_HISTORY_MISSING"])] - edits = [] - deletes = [] - - let balanceSecurityId = "SEC_STOCK_UNMATERIALIZED_HISTORY_BALANCE" - let cancellation = (stockCancellation "TX_STOCK_UNMATERIALIZED_HISTORY_CANCEL" sourceSecurityId 20.0) with - balance_security_id = Some balanceSecurityId - submitMustFail issuer do - exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with - creates = - [ CT.OcfCreateStockCancellation cancellation - , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_STOCK_UNMATERIALIZED_HISTORY_BALANCE" balanceSecurityId stakeholderId stockClassId) with quantity = 80.0) - ] - edits = [] - deletes = [] - -testEquityBalanceSuccessorIncludesPriorUnmaterializedReduction = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_UNMATERIALIZED_HISTORY" 1_000.0) - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_UNMATERIALIZED_HISTORY" "Equity Unmaterialized History Holder" - let sourceSecurityId = "ESEC_UNMATERIALIZED_HISTORY_SOURCE" - issued <- submit issuer do - exerciseCmd capTableReady CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_UNMATERIALIZED_HISTORY_SOURCE" sourceSecurityId stakeholderId stockClassId 100.0)] - edits = [] - deletes = [] - transferred <- submit issuer do - exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationTransfer (lifecycleEquityTransfer "TX_EQUITY_UNMATERIALIZED_HISTORY_TRANSFER" sourceSecurityId 30.0 None ["ESEC_UNMATERIALIZED_HISTORY_MISSING"])] - edits = [] - deletes = [] - - let balanceSecurityId = "ESEC_UNMATERIALIZED_HISTORY_BALANCE" - submitMustFail issuer do - exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with - creates = - [ CT.OcfCreateEquityCompensationCancellation (lifecycleEquityCancellation "TX_EQUITY_UNMATERIALIZED_HISTORY_CANCEL" sourceSecurityId 20.0 balanceSecurityId) - , CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_UNMATERIALIZED_HISTORY_BALANCE" balanceSecurityId stakeholderId stockClassId 80.0) - ] - edits = [] - deletes = [] - -testUnmaterializedWarrantExerciseResultCannotAliasSource = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_UNMATERIALIZED_ALIAS" 1_000.0) - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_UNMATERIALIZED_ALIAS" "Warrant Unmaterialized Alias Holder" - let securityId = "WSEC_UNMATERIALIZED_ALIAS" - issued <- submit issuer do - exerciseCmd capTableReady CT.UpdateCapTable with - creates = [CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_WARRANT_UNMATERIALIZED_ALIAS_SOURCE" securityId stakeholderId stockClassId)] - edits = [] - deletes = [] - - submitMustFail issuer do - exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_WARRANT_UNMATERIALIZED_ALIAS_EXERCISE" securityId (Some 50.0) [securityId])] - edits = [] - deletes = [] - -testUnmaterializedEquityExerciseResultCannotAliasSource = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_EXERCISE_UNMATERIALIZED_ALIAS" 1_000.0) - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_EXERCISE_UNMATERIALIZED_ALIAS" "Equity Exercise Alias Holder" - let securityId = "ESEC_EXERCISE_UNMATERIALIZED_ALIAS" - issued <- submit issuer do - exerciseCmd capTableReady CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_EXERCISE_UNMATERIALIZED_ALIAS_SOURCE" securityId stakeholderId stockClassId 100.0)] - edits = [] - deletes = [] - - submitMustFail issuer do - exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationExercise (lifecycleEquityExercise "TX_EQUITY_EXERCISE_UNMATERIALIZED_ALIAS" securityId 50.0 [securityId])] - edits = [] - deletes = [] - -testUnmaterializedEquityReleaseResultCannotAliasSource = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_RELEASE_UNMATERIALIZED_ALIAS" 1_000.0) - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_RELEASE_UNMATERIALIZED_ALIAS" "Equity Release Alias Holder" - let securityId = "ESEC_RELEASE_UNMATERIALIZED_ALIAS" - issued <- submit issuer do - exerciseCmd capTableReady CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_RELEASE_UNMATERIALIZED_ALIAS_SOURCE" securityId stakeholderId stockClassId 100.0)] - edits = [] - deletes = [] - - submitMustFail issuer do - exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateEquityCompensationRelease (lifecycleEquityRelease "TX_EQUITY_RELEASE_UNMATERIALIZED_ALIAS" securityId 50.0 [securityId])] - edits = [] - deletes = [] diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 9b45c179..2b00babe 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00951f0333bb010ce94089c0d511794397ad0cc139ef5b70a6b5b6b81f2789f0 -size 2914422 +oid sha256:fe483d1fed7a9fd1e62ca8b2a3d29cdd7daed719d271ddea6111decc1031b8f6 +size 2844083 diff --git a/dars/dars.lock b/dars/dars.lock index 9fc1322f..0ea3375c 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "00951f0333bb010ce94089c0d511794397ad0cc139ef5b70a6b5b6b81f2789f0", - "size": 2914422, + "sha256": "fe483d1fed7a9fd1e62ca8b2a3d29cdd7daed719d271ddea6111decc1031b8f6", + "size": 2844083, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-10T13:44:14Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 04738098..93ed81b3 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -12,7 +12,7 @@ module Fairmint.OpenCapTable.CapTable where import DA.Map (Map) import qualified DA.Map as Map -import DA.List (sortOn, unique) +import DA.List (sortOn) import DA.Action (foldlA, when) import DA.Foldable (mapA_) import DA.Optional (fromOptional, isNone, isSome) @@ -101,39 +101,6 @@ toMaps ct = CapTableMaps with stock_issuances_by_security_id = ct.stock_issuances_by_security_id warrant_issuances_by_security_id = ct.warrant_issuances_by_security_id -securityIdsInFinalMaps : CapTableMaps -> Update [Text] -securityIdsInFinalMaps maps = do - convertibleSecurityIds <- foldlA - (\securityIds issuanceCid -> do - issuance <- fetch issuanceCid - pure (issuance.issuance_data.security_id :: securityIds)) - [] - (Map.values maps.convertible_issuances) - equityCompensationSecurityIds <- foldlA - (\securityIds issuanceCid -> do - issuance <- fetch issuanceCid - pure (issuance.issuance_data.security_id :: securityIds)) - [] - (Map.values maps.equity_compensation_issuances) - stockSecurityIds <- foldlA - (\securityIds issuanceCid -> do - issuance <- fetch issuanceCid - pure (issuance.issuance_data.security_id :: securityIds)) - [] - (Map.values maps.stock_issuances) - warrantSecurityIds <- foldlA - (\securityIds issuanceCid -> do - issuance <- fetch issuanceCid - pure (issuance.issuance_data.security_id :: securityIds)) - [] - (Map.values maps.warrant_issuances) - pure (convertibleSecurityIds ++ equityCompensationSecurityIds ++ stockSecurityIds ++ warrantSecurityIds) - -validateFinalSecurityIdUniqueness : CapTableMaps -> Update () -validateFinalSecurityIdUniqueness maps = do - securityIds <- securityIdsInFinalMaps maps - assertMsg "Security IDs must be globally unique" (unique securityIds) - data AuthorizedSharesCeiling = AuthorizedSharesFinite Decimal | AuthorizedSharesUnlimited @@ -180,24 +147,6 @@ data StockClassShareEventIndex = StockClassShareEventIndex with type StockSuccessorEdge = (Text, Time, [Text]) type StockSuccessorEdgeIndex = Map Text [(Time, [Text])] -data SecurityQuantityEvent - = SecurityQuantityDelta with - event_date: Time - event_priority: Int - quantity_delta: Decimal - | SecurityQuantityTerminalDelta with - 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 - deriving (Eq, Show) - data StockPlanSuccessorIndex = StockPlanSuccessorIndex with stock_successors_by_security_id: Map Text (Time, Optional Text) equity_compensation_successors_by_security_id: Map Text (Time, Optional Text) @@ -513,103 +462,16 @@ warrantExerciseTrigger issuanceData triggerId = None issuanceData.exercise_triggers -securityQuantityReductionEvent : Time -> Decimal -> Optional Text -> SecurityQuantityEvent -securityQuantityReductionEvent eventDate quantity balanceSecurityId = - case balanceSecurityId of - Some _ -> - SecurityQuantityTerminalDelta with - event_date = eventDate - event_priority = 2 - quantity_delta = 0.0 - quantity - None -> - SecurityQuantityDelta with - event_date = eventDate - event_priority = 1 - quantity_delta = 0.0 - quantity - -securityQuantityEventSortKey : SecurityQuantityEvent -> (Time, Int, Int) -securityQuantityEventSortKey event = case event of - 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 () -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 - 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 - _ <- foldlA - (validateSecurityQuantityBalanceEvent securityId) - initialQuantity - (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)) - -warrantSecurityQuantityEvents : CapTableMaps -> WarrantIssuanceOcfData -> Update [SecurityQuantityEvent] +warrantSecurityQuantityEvents : CapTableMaps -> WarrantIssuanceOcfData -> Update [StockClassShareEvent] warrantSecurityQuantityEvents 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) + then pure (stockSecurityReductionEvents + transfer.transfer_data.date + transfer.transfer_data.quantity + transfer.transfer_data.balance_security_id ++ events) else pure events) [] (Map.values maps.warrant_transfers) @@ -618,9 +480,12 @@ warrantSecurityQuantityEvents maps issuanceData = do (\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) + 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.warrant_cancellations) exerciseEvents <- foldlA @@ -629,59 +494,32 @@ warrantSecurityQuantityEvents maps issuanceData = do if exercise.exercise_data.security_id == issuanceData.security_id then case exercise.exercise_data.quantity of Some quantity -> - pure ((SecurityQuantityDelta with + pure ((StockClassShareDelta with event_date = exercise.exercise_data.date event_priority = 1 quantity_delta = 0.0 - quantity) :: events) None -> - pure ((SecurityQuantityRetraction with + pure ((StockClassShareRetraction with event_date = exercise.exercise_data.date event_priority = 3) :: events) else pure events) - [] + cancellationEvents (Map.values maps.warrant_exercises) - retractionEvents <- foldlA + foldlA (\events retractionCid -> do retraction <- fetch retractionCid if retraction.retraction_data.security_id == issuanceData.security_id - then pure ((SecurityQuantityRetraction with + then pure ((StockClassShareRetraction with event_date = retraction.retraction_data.date event_priority = 3) :: events) else pure events) - [] + exerciseEvents (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 -> do - assertSecurityQuantityEventsOnOrAfter issuanceData.security_id issuanceData.date events - 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) - warrantSecurityQuantityBeforeTerminal : CapTableMaps -> WarrantIssuanceOcfData -> @@ -689,20 +527,23 @@ warrantSecurityQuantityBeforeTerminal : Update (Optional Decimal) warrantSecurityQuantityBeforeTerminal maps issuanceData eventDate = do events <- warrantSecurityQuantityEvents maps issuanceData - case issuanceData.quantity of - Some quantity -> - securityQuantityBeforeEvent - issuanceData.security_id - (Some 0.0) - ((SecurityQuantityDelta with + let initialEvents = case issuanceData.quantity of + Some quantity -> + (StockClassShareDelta with event_date = issuanceData.date event_priority = 0 quantity_delta = quantity) :: events - ) - (eventDate, 2, 1) - None -> - securityQuantityBeforeEvent issuanceData.security_id None events (eventDate, 2, 1) + None -> events + let initialQuantity = case issuanceData.quantity of + Some _ -> Some 0.0 + None -> None + pure (foldl + applyStockClassShareEventOptional + initialQuantity + (filter + (\event -> stockClassShareEventSortKey event < (eventDate, 2, 1)) + (sortOn stockClassShareEventSortKey initialEvents))) warrantExerciseQuantity : CapTableMaps -> WarrantIssuanceOcfData -> WarrantExerciseOcfData -> OcfWarrantConversionRight -> Update (Optional Decimal) warrantExerciseQuantity maps issuanceData exerciseData conversionRight = @@ -1642,166 +1483,29 @@ stockClassShareLotQuantityAt eventIndex lot asOf = (filter (stockClassShareEventOnOrBefore asOf) (stockClassShareLotEvents eventIndex lot))) else Some 0.0 -stockSecurityShareEvents : StockClassShareEventIndex -> CapTableMaps -> StockIssuanceOcfData -> Update [StockClassShareEvent] -stockSecurityShareEvents eventIndex 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) - - let splitCutoffSortKey = stockClassShareLotSplitCutoffSortKey eventIndex (stockIssuanceShareLot issuanceData) - splitEvents <- foldlA - (\events splitCid -> do - split <- fetch splitCid - let splitSortKey = (split.split_data.date, 2, 2) - if split.split_data.stock_class_id == issuanceData.stock_class_id && splitSortKey >= splitCutoffSortKey - 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 ++ transferEvents ++ conversionEvents ++ retractionEvents ++ reissuanceEvents ++ consolidationEvents ++ splitEvents)) - -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 - 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 - assertMsg - ("Stock security balance cannot be retired for " <> securityId <> " because it has no remaining balance") - (current > 0.0) - pure 0.0 - -validateStockSecurityBalance : StockClassShareEventIndex -> CapTableMaps -> StockIssuanceOcfData -> Update () -validateStockSecurityBalance eventIndex maps issuanceData = do - events <- stockSecurityShareEvents eventIndex maps issuanceData - _ <- foldlA - (validateStockSecurityBalanceEvent issuanceData.security_id) - 0.0 - (sortOn stockClassShareEventSortKey events) - pure () - -validateStockSecurityBalances : StockClassShareEventIndex -> CapTableMaps -> Update () -validateStockSecurityBalances eventIndex maps = - mapA_ - (\issuanceCid -> do - issuance <- fetch issuanceCid - validateStockSecurityBalance eventIndex maps issuance.issuance_data) - (Map.values maps.stock_issuances) - stockSecurityQuantityBeforeEvent : StockClassShareEventIndex -> - CapTableMaps -> StockIssuanceOcfData -> (Time, Int, Int) -> Update Decimal -stockSecurityQuantityBeforeEvent eventIndex maps issuanceData targetSortKey = do - events <- stockSecurityShareEvents eventIndex maps issuanceData - foldlA - (validateStockSecurityBalanceEvent issuanceData.security_id) - 0.0 - (filter - (\event -> stockClassShareEventSortKey event < targetSortKey) - (sortOn stockClassShareEventSortKey events)) +stockSecurityQuantityBeforeEvent eventIndex issuanceData targetSortKey = + let lot = stockIssuanceShareLot issuanceData + priorEvents = filter + (\event -> stockClassShareEventSortKey event < targetSortKey) + (sortOn stockClassShareEventSortKey (stockClassShareLotEvents eventIndex lot)) + in case foldl applyStockClassShareEventOptional (Some 0.0) priorEvents of + Some quantity -> pure quantity + None -> do + assertMsg ("Stock security quantity history is invalid for " <> issuanceData.security_id <> " before " <> show targetSortKey) False + pure 0.0 stockSecurityQuantityBeforeTerminal : StockClassShareEventIndex -> - CapTableMaps -> StockIssuanceOcfData -> Time -> Update Decimal -stockSecurityQuantityBeforeTerminal eventIndex maps issuanceData eventDate = - stockSecurityQuantityBeforeEvent eventIndex maps issuanceData (eventDate, 2, 1) +stockSecurityQuantityBeforeTerminal eventIndex issuanceData eventDate = + stockSecurityQuantityBeforeEvent eventIndex issuanceData (eventDate, 2, 1) stockReductionSortKey : Time -> Optional Text -> (Time, Int, Int) stockReductionSortKey eventDate balanceSecurityId = @@ -1934,7 +1638,7 @@ validateStockTerminalBalanceSuccessor : validateStockTerminalBalanceSuccessor eventIndex maps label sourceIssuanceData eventDate reductionQuantity balanceSecurityId = case balanceSecurityId of Some securityId -> do - currentQuantity <- stockSecurityQuantityBeforeTerminal eventIndex maps sourceIssuanceData eventDate + currentQuantity <- stockSecurityQuantityBeforeTerminal eventIndex sourceIssuanceData eventDate let expectedQuantity = currentQuantity - reductionQuantity assertMsg (label <> " exceeds the current source quantity for " <> sourceIssuanceData.security_id) @@ -2044,7 +1748,6 @@ validateStockReissuanceResultIssuances eventIndex maps = let targetSortKey = (reissuance.reissuance_data.date, 3, 0) sourceQuantity <- stockSecurityQuantityBeforeEvent eventIndex - maps sourceIssuance.issuance_data targetSortKey resultingQuantity <- stockIssuanceQuantityTotal @@ -2084,7 +1787,6 @@ validateStockConsolidationResultIssuances eventIndex maps = (sourceIssuance.issuance_data.stock_class_id == resultIssuance.issuance_data.stock_class_id) quantity <- stockSecurityQuantityBeforeEvent eventIndex - maps sourceIssuance.issuance_data targetSortKey pure (total + quantity) @@ -2707,7 +2409,6 @@ stockPlanInheritedSuccessorExcessLots eventIndex successorIndex maps = do Some stockPlanId -> do sourceQuantity <- stockSecurityQuantityBeforeEvent eventIndex - maps sourceIssuance.issuance_data (reissuance.reissuance_data.date, 3, 3) (_, successorQuantity) <- recordedInheritedStockIssuanceSummary @@ -2756,7 +2457,6 @@ stockPlanConversionAllocationTransitions eventIndex successorIndex maps = do sourceBasisQuantity <- case conversion.conversion_data.balance_security_id of Some _ -> stockSecurityQuantityBeforeTerminal eventIndex - maps sourceIssuance.issuance_data conversion.conversion_data.date None -> pure conversion.conversion_data.quantity_converted @@ -3169,106 +2869,6 @@ equityCompensationSecurityEvents maps issuanceData = do pure (retractionEvents ++ splitEvents) -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.settlement_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) - - stockClassIds <- equityCompensationPlanStockClassIds maps issuanceData - splitEvents <- foldlA - (\events splitCid -> do - split <- fetch splitCid - if split.split_data.stock_class_id `elem` stockClassIds && 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) - case (issuanceData.stock_class_id, stockClassIds, splitEvents) of - (None, _ :: _ :: _, _ :: _) -> do - assertMsg - ("Equity compensation issuance without stock_class_id cannot be split-adjusted across multiple stock-plan classes: " <> issuanceData.id) - False - _ -> pure () - - pure (transferEvents ++ cancellationEvents ++ exerciseEvents ++ releaseEvents ++ retractionEvents ++ splitEvents) - -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) - equityCompensationCurrentQuantity : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update (Optional Decimal) equityCompensationCurrentQuantity maps issuanceData = do events <- equityCompensationSecurityEvents maps issuanceData @@ -3294,17 +2894,11 @@ equityCompensationSecurityQuantityBeforeEvent : (Time, Int, Int) -> Update Decimal equityCompensationSecurityQuantityBeforeEvent maps issuanceData targetSortKey = do - events <- equityCompensationSecurityQuantityEvents maps issuanceData - quantityOpt <- securityQuantityBeforeEvent - issuanceData.security_id - (Some 0.0) - ((SecurityQuantityDelta with - event_date = issuanceData.date - event_priority = 0 - quantity_delta = issuanceData.quantity) - :: events) - targetSortKey - case quantityOpt of + events <- equityCompensationSecurityEvents maps issuanceData + let priorEvents = filter + (\event -> stockClassShareEventSortKey event < targetSortKey) + (sortOn stockClassShareEventSortKey events) + case foldl applyStockClassShareEventOptional (Some issuanceData.quantity) priorEvents of Some quantity -> pure quantity None -> do assertMsg ("Equity compensation quantity history is invalid for " <> issuanceData.security_id <> " before " <> show targetSortKey) False @@ -3997,281 +3591,6 @@ validateStockPlanPoolCeilings eventIndex maps = do validateStockPlanReservedShares eventIndex maps adjustmentIndex validateStockPlanReservedSharesAtDates eventIndex maps adjustmentHistory -assertSuccessorSecurityIdDistinct : Text -> Text -> Text -> Update () -assertSuccessorSecurityIdDistinct label sourceSecurityId successorSecurityId = - assertMsg - (label <> " must use a new successor security ID instead of reusing " <> sourceSecurityId) - (successorSecurityId /= sourceSecurityId) - -validateSuccessorSecurityIds : Text -> Text -> Optional Text -> [Text] -> Update () -validateSuccessorSecurityIds label sourceSecurityId balanceSecurityId resultingSecurityIds = do - mapA_ (assertSuccessorSecurityIdDistinct label sourceSecurityId) resultingSecurityIds - case balanceSecurityId of - Some balanceId -> do - assertSuccessorSecurityIdDistinct label sourceSecurityId balanceId - assertMsg - (label <> " cannot use its balance security as a resulting security: " <> balanceId) - (balanceId `notElem` resultingSecurityIds) - None -> pure () - -successorSecurityIdsFor : Optional Text -> [Text] -> [Text] -successorSecurityIdsFor balanceSecurityId resultingSecurityIds = - case balanceSecurityId of - Some balanceId -> balanceId :: resultingSecurityIds - None -> resultingSecurityIds - -validateGlobalSuccessorSecurityIdUniqueness : CapTableMaps -> Update () -validateGlobalSuccessorSecurityIdUniqueness maps = do - stockTransferIds <- foldlA - (\securityIds transferCid -> do - transfer <- fetch transferCid - pure (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids ++ securityIds)) - [] - (Map.values maps.stock_transfers) - stockCancellationIds <- foldlA - (\securityIds cancellationCid -> do - cancellation <- fetch cancellationCid - pure (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [] ++ securityIds)) - stockTransferIds - (Map.values maps.stock_cancellations) - stockRepurchaseIds <- foldlA - (\securityIds repurchaseCid -> do - repurchase <- fetch repurchaseCid - pure (successorSecurityIdsFor repurchase.repurchase_data.balance_security_id [] ++ securityIds)) - stockCancellationIds - (Map.values maps.stock_repurchases) - stockConversionIds <- foldlA - (\securityIds conversionCid -> do - conversion <- fetch conversionCid - pure (successorSecurityIdsFor conversion.conversion_data.balance_security_id conversion.conversion_data.resulting_security_ids ++ securityIds)) - stockRepurchaseIds - (Map.values maps.stock_conversions) - stockReissuanceIds <- foldlA - (\securityIds reissuanceCid -> do - reissuance <- fetch reissuanceCid - pure (reissuance.reissuance_data.resulting_security_ids ++ securityIds)) - stockConversionIds - (Map.values maps.stock_reissuances) - stockConsolidationIds <- foldlA - (\securityIds consolidationCid -> do - consolidation <- fetch consolidationCid - pure (consolidation.consolidation_data.resulting_security_id :: securityIds)) - stockReissuanceIds - (Map.values maps.stock_consolidations) - warrantTransferIds <- foldlA - (\securityIds transferCid -> do - transfer <- fetch transferCid - pure (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids ++ securityIds)) - stockConsolidationIds - (Map.values maps.warrant_transfers) - warrantCancellationIds <- foldlA - (\securityIds cancellationCid -> do - cancellation <- fetch cancellationCid - pure (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [] ++ securityIds)) - warrantTransferIds - (Map.values maps.warrant_cancellations) - warrantExerciseIds <- foldlA - (\securityIds exerciseCid -> do - exercise <- fetch exerciseCid - pure (exercise.exercise_data.resulting_security_ids ++ securityIds)) - warrantCancellationIds - (Map.values maps.warrant_exercises) - convertibleTransferIds <- foldlA - (\securityIds transferCid -> do - transfer <- fetch transferCid - pure (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids ++ securityIds)) - warrantExerciseIds - (Map.values maps.convertible_transfers) - convertibleCancellationIds <- foldlA - (\securityIds cancellationCid -> do - cancellation <- fetch cancellationCid - pure (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [] ++ securityIds)) - convertibleTransferIds - (Map.values maps.convertible_cancellations) - convertibleConversionIds <- foldlA - (\securityIds conversionCid -> do - conversion <- fetch conversionCid - pure (successorSecurityIdsFor conversion.conversion_data.balance_security_id conversion.conversion_data.resulting_security_ids ++ securityIds)) - convertibleCancellationIds - (Map.values maps.convertible_conversions) - equityCompensationTransferIds <- foldlA - (\securityIds transferCid -> do - transfer <- fetch transferCid - pure (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids ++ securityIds)) - convertibleConversionIds - (Map.values maps.equity_compensation_transfers) - equityCompensationCancellationIds <- foldlA - (\securityIds cancellationCid -> do - cancellation <- fetch cancellationCid - pure (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [] ++ securityIds)) - equityCompensationTransferIds - (Map.values maps.equity_compensation_cancellations) - equityCompensationExerciseIds <- foldlA - (\securityIds exerciseCid -> do - exercise <- fetch exerciseCid - pure (exercise.exercise_data.resulting_security_ids ++ securityIds)) - equityCompensationCancellationIds - (Map.values maps.equity_compensation_exercises) - allSuccessorIds <- foldlA - (\securityIds releaseCid -> do - release <- fetch releaseCid - pure (release.release_data.resulting_security_ids ++ securityIds)) - equityCompensationExerciseIds - (Map.values maps.equity_compensation_releases) - assertMsg - "Each successor security ID may be produced by only one lifecycle transaction" - (unique allSuccessorIds) - -validateSuccessorSecurityIdContinuity : CapTableMaps -> Update () -validateSuccessorSecurityIdContinuity maps = do - validateGlobalSuccessorSecurityIdUniqueness maps - mapA_ - (\transferCid -> do - transfer <- fetch transferCid - validateSuccessorSecurityIds - ("Stock transfer " <> transfer.transfer_data.id) - transfer.transfer_data.security_id - transfer.transfer_data.balance_security_id - transfer.transfer_data.resulting_security_ids) - (Map.values maps.stock_transfers) - mapA_ - (\cancellationCid -> do - cancellation <- fetch cancellationCid - validateSuccessorSecurityIds - ("Stock cancellation " <> cancellation.cancellation_data.id) - cancellation.cancellation_data.security_id - cancellation.cancellation_data.balance_security_id - []) - (Map.values maps.stock_cancellations) - mapA_ - (\repurchaseCid -> do - repurchase <- fetch repurchaseCid - validateSuccessorSecurityIds - ("Stock repurchase " <> repurchase.repurchase_data.id) - repurchase.repurchase_data.security_id - repurchase.repurchase_data.balance_security_id - []) - (Map.values maps.stock_repurchases) - mapA_ - (\conversionCid -> do - conversion <- fetch conversionCid - validateSuccessorSecurityIds - ("Stock conversion " <> conversion.conversion_data.id) - conversion.conversion_data.security_id - conversion.conversion_data.balance_security_id - conversion.conversion_data.resulting_security_ids) - (Map.values maps.stock_conversions) - mapA_ - (\reissuanceCid -> do - reissuance <- fetch reissuanceCid - validateSuccessorSecurityIds - ("Stock reissuance " <> reissuance.reissuance_data.id) - reissuance.reissuance_data.security_id - None - reissuance.reissuance_data.resulting_security_ids) - (Map.values maps.stock_reissuances) - mapA_ - (\consolidationCid -> do - consolidation <- fetch consolidationCid - mapA_ - (\sourceSecurityId -> - assertSuccessorSecurityIdDistinct - ("Stock consolidation " <> consolidation.consolidation_data.id) - sourceSecurityId - consolidation.consolidation_data.resulting_security_id) - consolidation.consolidation_data.security_ids) - (Map.values maps.stock_consolidations) - mapA_ - (\transferCid -> do - transfer <- fetch transferCid - validateSuccessorSecurityIds - ("Warrant transfer " <> transfer.transfer_data.id) - transfer.transfer_data.security_id - transfer.transfer_data.balance_security_id - transfer.transfer_data.resulting_security_ids) - (Map.values maps.warrant_transfers) - mapA_ - (\cancellationCid -> do - cancellation <- fetch cancellationCid - validateSuccessorSecurityIds - ("Warrant cancellation " <> cancellation.cancellation_data.id) - cancellation.cancellation_data.security_id - cancellation.cancellation_data.balance_security_id - []) - (Map.values maps.warrant_cancellations) - mapA_ - (\exerciseCid -> do - exercise <- fetch exerciseCid - validateSuccessorSecurityIds - ("Warrant exercise " <> exercise.exercise_data.id) - exercise.exercise_data.security_id - None - exercise.exercise_data.resulting_security_ids) - (Map.values maps.warrant_exercises) - mapA_ - (\transferCid -> do - transfer <- fetch transferCid - validateSuccessorSecurityIds - ("Convertible transfer " <> transfer.transfer_data.id) - transfer.transfer_data.security_id - transfer.transfer_data.balance_security_id - transfer.transfer_data.resulting_security_ids) - (Map.values maps.convertible_transfers) - mapA_ - (\cancellationCid -> do - cancellation <- fetch cancellationCid - validateSuccessorSecurityIds - ("Convertible cancellation " <> cancellation.cancellation_data.id) - cancellation.cancellation_data.security_id - cancellation.cancellation_data.balance_security_id - []) - (Map.values maps.convertible_cancellations) - mapA_ - (\conversionCid -> do - conversion <- fetch conversionCid - validateSuccessorSecurityIds - ("Convertible conversion " <> conversion.conversion_data.id) - conversion.conversion_data.security_id - conversion.conversion_data.balance_security_id - conversion.conversion_data.resulting_security_ids) - (Map.values maps.convertible_conversions) - mapA_ - (\transferCid -> do - transfer <- fetch transferCid - validateSuccessorSecurityIds - ("Equity compensation transfer " <> transfer.transfer_data.id) - transfer.transfer_data.security_id - transfer.transfer_data.balance_security_id - transfer.transfer_data.resulting_security_ids) - (Map.values maps.equity_compensation_transfers) - mapA_ - (\cancellationCid -> do - cancellation <- fetch cancellationCid - validateSuccessorSecurityIds - ("Equity compensation cancellation " <> cancellation.cancellation_data.id) - cancellation.cancellation_data.security_id - cancellation.cancellation_data.balance_security_id - []) - (Map.values maps.equity_compensation_cancellations) - mapA_ - (\exerciseCid -> do - exercise <- fetch exerciseCid - validateSuccessorSecurityIds - ("Equity compensation exercise " <> exercise.exercise_data.id) - exercise.exercise_data.security_id - None - exercise.exercise_data.resulting_security_ids) - (Map.values maps.equity_compensation_exercises) - mapA_ - (\releaseCid -> do - release <- fetch releaseCid - validateSuccessorSecurityIds - ("Equity compensation release " <> release.release_data.id) - release.release_data.security_id - None - release.release_data.resulting_security_ids) - (Map.values maps.equity_compensation_releases) - validateAuthorizedShareCeilings : CapTableMaps -> Map Text StockClassAuthorizedSharesAdjustmentOcfData -> @@ -4279,11 +3598,6 @@ validateAuthorizedShareCeilings : Update () validateAuthorizedShareCeilings maps adjustmentIndex issuerCid = do eventIndex <- stockClassShareEventIndex maps - validateFinalSecurityIdUniqueness maps - validateSuccessorSecurityIdContinuity maps - validateStockSecurityBalances eventIndex maps - validateWarrantSecurityBalances maps - validateEquityCompensationSecurityBalances maps validateStockBalanceSuccessors eventIndex maps validateStockTransferResultIssuances maps validateStockReissuanceResultIssuances eventIndex maps From 69491fa825da9c7912059af764752df16f86d1e1 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 17:45:02 -0400 Subject: [PATCH 43/62] Reapply "fix: enforce lifecycle identity and terminal balances" This reverts commit 15fe66a45c94c7905c16049c82a31ea7b2f24df1. --- .../TestEquityCompensationRelease.daml | 91 +- .../OpenCapTable/TestLifecycleInvariants.daml | 473 +++++++++++ .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 782 ++++++++++++++++-- 5 files changed, 1279 insertions(+), 75 deletions(-) create mode 100644 Test/daml/OpenCapTable/TestLifecycleInvariants.daml diff --git a/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml b/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml index 36f71b7d..91061f05 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml @@ -1,35 +1,72 @@ 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 (addPrerequisites, addEquityCompensationIssuance) +import OpenCapTable.TestHelpers (addPrerequisites, defaultStockIssuance) 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 + 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 = None + expiration_date = None + stock_class_id = Some "SC_COMMON" + 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 <- addPrerequisites 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 - creates = [CT.OcfCreateEquityCompensationRelease EquityCompensationReleaseOcfData with - id = "TX_EC_RELEASE_1" - date = DT.time (DA.date 2024 Apr 01) 0 0 0 - security_id = "SEC_RSU_001" - settlement_date = DT.time (DA.date 2024 Apr 03) 0 0 0 - release_price = OcfMonetary with amount = 45.50; currency = "USD" - quantity = 5000.0 - resulting_security_ids = ["SEC_STOCK_101"] - consideration_text = Some "RSU cliff vest - 25% of total grant" - comments = ["First anniversary cliff vest for Employee A"]] + creates = + [ CT.OcfCreateStockIssuance ((defaultStockIssuance "STOCK-ISS-EC-RELEASE-1" "SEC_STOCK_101" "SH-1" "SC_COMMON") with + date = DT.time (DA.date 2024 Apr 03) 0 0 0 + quantity = 5000.0) + , CT.OcfCreateEquityCompensationRelease EquityCompensationReleaseOcfData with + id = "TX_EC_RELEASE_1" + date = DT.time (DA.date 2024 Apr 01) 0 0 0 + security_id = "SEC_RSU_001" + settlement_date = DT.time (DA.date 2024 Apr 03) 0 0 0 + release_price = OcfMonetary with amount = 45.50; currency = "USD" + quantity = 5000.0 + resulting_security_ids = ["SEC_STOCK_101"] + consideration_text = Some "RSU cliff vest - 25% of total grant" + comments = ["First anniversary cliff vest for Employee A"] + ] edits = [] deletes = [] pure () @@ -40,20 +77,28 @@ testCreateAndArchiveEquityCompensationReleaseQuarterly = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp -- Add stakeholder and equity compensation issuance before releasing cap_table <- addPrerequisites 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 - creates = [CT.OcfCreateEquityCompensationRelease EquityCompensationReleaseOcfData with - id = "TX_EC_RELEASE_2" - date = DT.time (DA.date 2024 Jul 01) 0 0 0 - security_id = "SEC_RSU_002" - settlement_date = DT.time (DA.date 2024 Jul 05) 0 0 0 - release_price = OcfMonetary with amount = 52.00; currency = "USD" - quantity = 1250.0 - resulting_security_ids = ["SEC_STOCK_102", "SEC_STOCK_103"] - consideration_text = None - comments = ["Q3 2024 quarterly vest", "Net settlement after tax withholding"]] + creates = + [ CT.OcfCreateStockIssuance ((defaultStockIssuance "STOCK-ISS-EC-RELEASE-2A" "SEC_STOCK_102" "SH-1" "SC_COMMON") with + date = DT.time (DA.date 2024 Jul 05) 0 0 0 + quantity = 625.0) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "STOCK-ISS-EC-RELEASE-2B" "SEC_STOCK_103" "SH-1" "SC_COMMON") with + date = DT.time (DA.date 2024 Jul 05) 0 0 0 + quantity = 625.0) + , CT.OcfCreateEquityCompensationRelease EquityCompensationReleaseOcfData with + id = "TX_EC_RELEASE_2" + date = DT.time (DA.date 2024 Jul 01) 0 0 0 + security_id = "SEC_RSU_002" + settlement_date = DT.time (DA.date 2024 Jul 05) 0 0 0 + release_price = OcfMonetary with amount = 52.00; currency = "USD" + quantity = 1250.0 + resulting_security_ids = ["SEC_STOCK_102", "SEC_STOCK_103"] + consideration_text = None + comments = ["Q3 2024 quarterly vest", "Net settlement after tax withholding"] + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestLifecycleInvariants.daml b/Test/daml/OpenCapTable/TestLifecycleInvariants.daml new file mode 100644 index 00000000..94a4a6ca --- /dev/null +++ b/Test/daml/OpenCapTable/TestLifecycleInvariants.daml @@ -0,0 +1,473 @@ +module OpenCapTable.TestLifecycleInvariants where + +import Fairmint.OpenCapTable.OCF.EquityCompensationCancellation +import Fairmint.OpenCapTable.OCF.EquityCompensationExercise +import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance +import Fairmint.OpenCapTable.OCF.EquityCompensationRelease +import Fairmint.OpenCapTable.OCF.EquityCompensationTransfer +import Fairmint.OpenCapTable.OCF.StockConsolidation +import Fairmint.OpenCapTable.OCF.StockReissuance +import Fairmint.OpenCapTable.OCF.StockTransfer +import Fairmint.OpenCapTable.OCF.WarrantExercise +import Fairmint.OpenCapTable.OCF.WarrantIssuance +import Fairmint.OpenCapTable.Types.Conversion +import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) +import Fairmint.OpenCapTable.Types.Stock (OcfQuantitySourceType(..)) +import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) +import qualified Fairmint.OpenCapTable.CapTable as CT +import OpenCapTable.Setup +import OpenCapTable.TestHelpers +import Daml.Script + +lifecycleWarrantTrigger : Text -> Text -> OcfConversionTrigger +lifecycleWarrantTrigger triggerId stockClassId = + OcfConversionTrigger with + conversion_right = OcfRightWarrant (OcfWarrantConversionRight with + conversion_mechanism = OcfWarrantMechanismFixedAmount with converts_to_quantity = 100.0 + converts_to_future_round = None + converts_to_stock_class_id = 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 + +lifecycleWarrantIssuance : Text -> Text -> Text -> Text -> WarrantIssuanceOcfData +lifecycleWarrantIssuance issuanceId securityId stakeholderId stockClassId = + WarrantIssuanceOcfData with + id = issuanceId + custom_id = "WR-" <> issuanceId + date = defaultTestDate + purchase_price = OcfMonetary with amount = 1.0, currency = "USD" + security_id = securityId + stakeholder_id = stakeholderId + comments = [] + exercise_triggers = [lifecycleWarrantTrigger "TRIGGER_FIXED" stockClassId] + security_law_exemptions = [] + vestings = [] + board_approval_date = None + consideration_text = None + exercise_price = None + quantity = Some 100.0 + quantity_source = Some OcfQuantityUnspecified + stockholder_approval_date = None + vesting_terms_id = None + warrant_expiration_date = None + +lifecycleWarrantExercise : Text -> Text -> Optional Decimal -> [Text] -> WarrantExerciseOcfData +lifecycleWarrantExercise exerciseId securityId quantity resultingSecurityIds = + WarrantExerciseOcfData with + id = exerciseId + date = defaultTestDate + security_id = securityId + trigger_id = "TRIGGER_FIXED" + comments = [] + resulting_security_ids = resultingSecurityIds + consideration_text = None + quantity + +lifecycleStockTransfer : Text -> Text -> Decimal -> Optional Text -> [Text] -> StockTransferOcfData +lifecycleStockTransfer transferId securityId quantity balanceSecurityId resultingSecurityIds = + StockTransferOcfData with + id = transferId + date = defaultTestDate + security_id = securityId + quantity + resulting_security_ids = resultingSecurityIds + balance_security_id = balanceSecurityId + consideration_text = None + comments = [] + +lifecycleStockReissuance : Text -> Text -> [Text] -> StockReissuanceOcfData +lifecycleStockReissuance reissuanceId securityId resultingSecurityIds = + StockReissuanceOcfData with + id = reissuanceId + date = defaultTestDate + security_id = securityId + resulting_security_ids = resultingSecurityIds + split_transaction_id = None + reason_text = Some "Lifecycle replacement" + comments = [] + +lifecycleStockConsolidation : Text -> [Text] -> Text -> StockConsolidationOcfData +lifecycleStockConsolidation consolidationId securityIds resultingSecurityId = + StockConsolidationOcfData with + id = consolidationId + date = defaultTestDate + security_ids = securityIds + resulting_security_id = resultingSecurityId + reason_text = Some "Lifecycle consolidation" + comments = [] + +lifecycleEquityIssuance : Text -> Text -> Text -> Text -> Decimal -> EquityCompensationIssuanceOcfData +lifecycleEquityIssuance issuanceId securityId stakeholderId stockClassId quantity = + EquityCompensationIssuanceOcfData with + id = issuanceId + custom_id = "EC-" <> issuanceId + date = defaultTestDate + compensation_type = OcfCompensationTypeOptionNSO + 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 = Some stockClassId + stock_plan_id = None + stockholder_approval_date = None + vesting_terms_id = None + +lifecycleEquityCancellation : Text -> Text -> Decimal -> Text -> EquityCompensationCancellationOcfData +lifecycleEquityCancellation cancellationId securityId quantity balanceSecurityId = + EquityCompensationCancellationOcfData with + id = cancellationId + date = defaultTestDate + security_id = securityId + quantity + balance_security_id = Some balanceSecurityId + reason_text = "Lifecycle cancellation" + comments = [] + +lifecycleEquityTransfer : Text -> Text -> Decimal -> Optional Text -> [Text] -> EquityCompensationTransferOcfData +lifecycleEquityTransfer transferId securityId quantity balanceSecurityId resultingSecurityIds = + EquityCompensationTransferOcfData with + id = transferId + date = defaultTestDate + security_id = securityId + quantity + resulting_security_ids = resultingSecurityIds + balance_security_id = balanceSecurityId + consideration_text = None + comments = [] + +lifecycleEquityExercise : Text -> Text -> Decimal -> [Text] -> EquityCompensationExerciseOcfData +lifecycleEquityExercise exerciseId securityId quantity resultingSecurityIds = + EquityCompensationExerciseOcfData with + id = exerciseId + date = defaultTestDate + security_id = securityId + quantity + resulting_security_ids = resultingSecurityIds + consideration_text = None + comments = [] + +lifecycleEquityRelease : Text -> Text -> Decimal -> [Text] -> EquityCompensationReleaseOcfData +lifecycleEquityRelease releaseId securityId quantity resultingSecurityIds = + EquityCompensationReleaseOcfData with + id = releaseId + date = defaultTestDate + security_id = securityId + settlement_date = defaultTestDate + release_price = OcfMonetary with amount = 1.0, currency = "USD" + quantity + resulting_security_ids = resultingSecurityIds + consideration_text = None + comments = [] + +testWarrantExerciseCannotExceedRemainingQuantity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_REMAINING" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_REMAINING" "Warrant Remaining Holder" + let securityId = "WSEC_REMAINING" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_WARRANT_REMAINING_ISSUE" securityId stakeholderId stockClassId)] + edits = [] + deletes = [] + + firstExercise <- submit issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_WARRANT_REMAINING_FIRST" securityId (Some 60.0) ["SEC_WARRANT_REMAINING_FIRST"]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_WARRANT_REMAINING_FIRST_RESULT" "SEC_WARRANT_REMAINING_FIRST" stakeholderId stockClassId) with quantity = 60.0) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd firstExercise.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_WARRANT_REMAINING_SECOND" securityId (Some 50.0) ["SEC_WARRANT_REMAINING_SECOND"]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_WARRANT_REMAINING_SECOND_RESULT" "SEC_WARRANT_REMAINING_SECOND" stakeholderId stockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + +testTwoFullWarrantExercisesAtSameTimestampFail = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_DOUBLE_FULL" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_DOUBLE_FULL" "Warrant Double Full Holder" + let securityId = "WSEC_DOUBLE_FULL" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_WARRANT_DOUBLE_FULL_ISSUE" securityId stakeholderId stockClassId)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_WARRANT_DOUBLE_FULL_A" securityId None ["SEC_WARRANT_DOUBLE_FULL_A"]) + , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_WARRANT_DOUBLE_FULL_RESULT_A" "SEC_WARRANT_DOUBLE_FULL_A" stakeholderId stockClassId) + , CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_WARRANT_DOUBLE_FULL_B" securityId None ["SEC_WARRANT_DOUBLE_FULL_B"]) + , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_WARRANT_DOUBLE_FULL_RESULT_B" "SEC_WARRANT_DOUBLE_FULL_B" stakeholderId stockClassId) + ] + edits = [] + deletes = [] + +testTwoStockReissuancesAtSameTimestampFail = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REISSUE_DOUBLE" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_REISSUE_DOUBLE" "Reissuance Double Holder" + let sourceSecurityId = "SEC_REISSUE_DOUBLE_SOURCE" + issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_REISSUE_DOUBLE_SOURCE" sourceSecurityId stakeholderId stockClassId) + + submitMustFail issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_REISSUE_DOUBLE_A" sourceSecurityId ["SEC_REISSUE_DOUBLE_A"]) + , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_REISSUE_DOUBLE_RESULT_A" "SEC_REISSUE_DOUBLE_A" stakeholderId stockClassId) + , CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_REISSUE_DOUBLE_B" sourceSecurityId ["SEC_REISSUE_DOUBLE_B"]) + , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_REISSUE_DOUBLE_RESULT_B" "SEC_REISSUE_DOUBLE_B" stakeholderId stockClassId) + ] + edits = [] + deletes = [] + +testMixedStockTerminalEventsAtSameTimestampFail = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_TERMINAL_MIXED" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_TERMINAL_MIXED" "Mixed Terminal Holder" + let sourceSecurityId = "SEC_TERMINAL_MIXED_SOURCE" + issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_TERMINAL_MIXED_SOURCE" sourceSecurityId stakeholderId stockClassId) + + submitMustFail issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_TERMINAL_MIXED_REISSUE" sourceSecurityId ["SEC_TERMINAL_MIXED_REISSUE"]) + , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_TERMINAL_MIXED_REISSUE_RESULT" "SEC_TERMINAL_MIXED_REISSUE" stakeholderId stockClassId) + , CT.OcfCreateStockConsolidation (lifecycleStockConsolidation "TX_TERMINAL_MIXED_CONSOLIDATE" [sourceSecurityId] "SEC_TERMINAL_MIXED_CONSOLIDATE") + , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_TERMINAL_MIXED_CONSOLIDATE_RESULT" "SEC_TERMINAL_MIXED_CONSOLIDATE" stakeholderId stockClassId) + ] + edits = [] + deletes = [] + +testRepeatedEquityTerminalEventsAtSameTimestampFail = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_DOUBLE_TERMINAL" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_DOUBLE_TERMINAL" "Equity Double Terminal Holder" + let sourceSecurityId = "ESEC_DOUBLE_TERMINAL_SOURCE" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_DOUBLE_TERMINAL_SOURCE" sourceSecurityId stakeholderId stockClassId 100.0)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation (lifecycleEquityCancellation "TX_EQUITY_DOUBLE_TERMINAL_A" sourceSecurityId 50.0 "ESEC_DOUBLE_TERMINAL_A") + , CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_DOUBLE_TERMINAL_RESULT_A" "ESEC_DOUBLE_TERMINAL_A" stakeholderId stockClassId 50.0) + , CT.OcfCreateEquityCompensationCancellation (lifecycleEquityCancellation "TX_EQUITY_DOUBLE_TERMINAL_B" sourceSecurityId 50.0 "ESEC_DOUBLE_TERMINAL_B") + , CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_DOUBLE_TERMINAL_RESULT_B" "ESEC_DOUBLE_TERMINAL_B" stakeholderId stockClassId 50.0) + ] + edits = [] + deletes = [] + +testBalanceSecurityCannotAlsoBeResultingSecurity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_BALANCE_RESULT_ALIAS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_BALANCE_RESULT_ALIAS" "Balance Result Alias Holder" + let sourceSecurityId = "SEC_BALANCE_RESULT_ALIAS_SOURCE" + let aliasSecurityId = "SEC_BALANCE_RESULT_ALIAS" + issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_BALANCE_RESULT_ALIAS_SOURCE" sourceSecurityId stakeholderId stockClassId) + + submitMustFail issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (lifecycleStockTransfer "TX_BALANCE_RESULT_ALIAS" sourceSecurityId 50.0 (Some aliasSecurityId) [aliasSecurityId]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_BALANCE_RESULT_ALIAS_OUTPUT" aliasSecurityId stakeholderId stockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + +testStockReissuanceCannotReuseSourceAsResult = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_REISSUE_SOURCE_ALIAS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_REISSUE_SOURCE_ALIAS" "Reissuance Source Alias Holder" + let sourceSecurityId = "SEC_REISSUE_SOURCE_ALIAS" + issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_REISSUE_SOURCE_ALIAS_SOURCE" sourceSecurityId stakeholderId stockClassId) + + submitMustFail issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = [CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_REISSUE_SOURCE_ALIAS" sourceSecurityId [sourceSecurityId])] + edits = [] + deletes = [] + +testDuplicateResultIdsFail = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_DUPLICATE_RESULTS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_DUPLICATE_RESULTS" "Duplicate Results Holder" + let sourceSecurityId = "SEC_DUPLICATE_RESULTS_SOURCE" + let resultSecurityId = "SEC_DUPLICATE_RESULTS_RESULT" + issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_DUPLICATE_RESULTS_SOURCE" sourceSecurityId stakeholderId stockClassId) + + submitMustFail issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_DUPLICATE_RESULTS" sourceSecurityId [resultSecurityId, resultSecurityId]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_DUPLICATE_RESULTS_OUTPUT" resultSecurityId stakeholderId stockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + +testSuccessorSecurityCannotBeReusedAcrossTransactions = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_SHARED_SUCCESSOR" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_SHARED_SUCCESSOR" "Shared Successor Holder" + let firstSourceId = "SEC_SHARED_SUCCESSOR_SOURCE_A" + let secondSourceId = "SEC_SHARED_SUCCESSOR_SOURCE_B" + let resultSecurityId = "SEC_SHARED_SUCCESSOR_RESULT" + firstIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_SHARED_SUCCESSOR_SOURCE_A" firstSourceId stakeholderId stockClassId) with quantity = 50.0) + secondIssued <- createStockIssuanceWith issuer firstIssued ((defaultStockIssuance "TX_SHARED_SUCCESSOR_SOURCE_B" secondSourceId stakeholderId stockClassId) with quantity = 50.0) + + submitMustFail issuer do + exerciseCmd secondIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (lifecycleStockTransfer "TX_SHARED_SUCCESSOR_TRANSFER" firstSourceId 50.0 None [resultSecurityId]) + , CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_SHARED_SUCCESSOR_REISSUE" secondSourceId [resultSecurityId]) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_SHARED_SUCCESSOR_RESULT" resultSecurityId stakeholderId stockClassId) with quantity = 50.0) + ] + edits = [] + deletes = [] + +testCrossTypeSourceResultAliasFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CROSS_TYPE_ALIAS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CROSS_TYPE_ALIAS" "Cross Type Alias Holder" + let sharedSecurityId = "SEC_CROSS_TYPE_ALIAS" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_CROSS_TYPE_ALIAS_WARRANT" sharedSecurityId stakeholderId stockClassId)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_CROSS_TYPE_ALIAS_EXERCISE" sharedSecurityId (Some 100.0) [sharedSecurityId]) + , CT.OcfCreateStockIssuance (defaultStockIssuance "TX_CROSS_TYPE_ALIAS_RESULT" sharedSecurityId stakeholderId stockClassId) + ] + edits = [] + deletes = [] + +testStockBalanceSuccessorIncludesPriorUnmaterializedReduction = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_STOCK_UNMATERIALIZED_HISTORY" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_STOCK_UNMATERIALIZED_HISTORY" "Stock Unmaterialized History Holder" + let sourceSecurityId = "SEC_STOCK_UNMATERIALIZED_HISTORY_SOURCE" + issued <- createStockIssuanceWith issuer capTableReady (defaultStockIssuance "TX_STOCK_UNMATERIALIZED_HISTORY_SOURCE" sourceSecurityId stakeholderId stockClassId) + transferred <- submit issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = [CT.OcfCreateStockTransfer (lifecycleStockTransfer "TX_STOCK_UNMATERIALIZED_HISTORY_TRANSFER" sourceSecurityId 30.0 None ["SEC_STOCK_UNMATERIALIZED_HISTORY_MISSING"])] + edits = [] + deletes = [] + + let balanceSecurityId = "SEC_STOCK_UNMATERIALIZED_HISTORY_BALANCE" + let cancellation = (stockCancellation "TX_STOCK_UNMATERIALIZED_HISTORY_CANCEL" sourceSecurityId 20.0) with + balance_security_id = Some balanceSecurityId + submitMustFail issuer do + exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockCancellation cancellation + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_STOCK_UNMATERIALIZED_HISTORY_BALANCE" balanceSecurityId stakeholderId stockClassId) with quantity = 80.0) + ] + edits = [] + deletes = [] + +testEquityBalanceSuccessorIncludesPriorUnmaterializedReduction = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_UNMATERIALIZED_HISTORY" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_UNMATERIALIZED_HISTORY" "Equity Unmaterialized History Holder" + let sourceSecurityId = "ESEC_UNMATERIALIZED_HISTORY_SOURCE" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_UNMATERIALIZED_HISTORY_SOURCE" sourceSecurityId stakeholderId stockClassId 100.0)] + edits = [] + deletes = [] + transferred <- submit issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationTransfer (lifecycleEquityTransfer "TX_EQUITY_UNMATERIALIZED_HISTORY_TRANSFER" sourceSecurityId 30.0 None ["ESEC_UNMATERIALIZED_HISTORY_MISSING"])] + edits = [] + deletes = [] + + let balanceSecurityId = "ESEC_UNMATERIALIZED_HISTORY_BALANCE" + submitMustFail issuer do + exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation (lifecycleEquityCancellation "TX_EQUITY_UNMATERIALIZED_HISTORY_CANCEL" sourceSecurityId 20.0 balanceSecurityId) + , CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_UNMATERIALIZED_HISTORY_BALANCE" balanceSecurityId stakeholderId stockClassId 80.0) + ] + edits = [] + deletes = [] + +testUnmaterializedWarrantExerciseResultCannotAliasSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_WARRANT_UNMATERIALIZED_ALIAS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_WARRANT_UNMATERIALIZED_ALIAS" "Warrant Unmaterialized Alias Holder" + let securityId = "WSEC_UNMATERIALIZED_ALIAS" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_WARRANT_UNMATERIALIZED_ALIAS_SOURCE" securityId stakeholderId stockClassId)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantExercise (lifecycleWarrantExercise "TX_WARRANT_UNMATERIALIZED_ALIAS_EXERCISE" securityId (Some 50.0) [securityId])] + edits = [] + deletes = [] + +testUnmaterializedEquityExerciseResultCannotAliasSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_EXERCISE_UNMATERIALIZED_ALIAS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_EXERCISE_UNMATERIALIZED_ALIAS" "Equity Exercise Alias Holder" + let securityId = "ESEC_EXERCISE_UNMATERIALIZED_ALIAS" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_EXERCISE_UNMATERIALIZED_ALIAS_SOURCE" securityId stakeholderId stockClassId 100.0)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise (lifecycleEquityExercise "TX_EQUITY_EXERCISE_UNMATERIALIZED_ALIAS" securityId 50.0 [securityId])] + edits = [] + deletes = [] + +testUnmaterializedEquityReleaseResultCannotAliasSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EQUITY_RELEASE_UNMATERIALIZED_ALIAS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EQUITY_RELEASE_UNMATERIALIZED_ALIAS" "Equity Release Alias Holder" + let securityId = "ESEC_RELEASE_UNMATERIALIZED_ALIAS" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance (lifecycleEquityIssuance "TX_EQUITY_RELEASE_UNMATERIALIZED_ALIAS_SOURCE" securityId stakeholderId stockClassId 100.0)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationRelease (lifecycleEquityRelease "TX_EQUITY_RELEASE_UNMATERIALIZED_ALIAS" securityId 50.0 [securityId])] + edits = [] + deletes = [] diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 2b00babe..9b45c179 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe483d1fed7a9fd1e62ca8b2a3d29cdd7daed719d271ddea6111decc1031b8f6 -size 2844083 +oid sha256:00951f0333bb010ce94089c0d511794397ad0cc139ef5b70a6b5b6b81f2789f0 +size 2914422 diff --git a/dars/dars.lock b/dars/dars.lock index 0ea3375c..9fc1322f 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "fe483d1fed7a9fd1e62ca8b2a3d29cdd7daed719d271ddea6111decc1031b8f6", - "size": 2844083, + "sha256": "00951f0333bb010ce94089c0d511794397ad0cc139ef5b70a6b5b6b81f2789f0", + "size": 2914422, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-10T13:44:14Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 93ed81b3..04738098 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -12,7 +12,7 @@ module Fairmint.OpenCapTable.CapTable where import DA.Map (Map) import qualified DA.Map as Map -import DA.List (sortOn) +import DA.List (sortOn, unique) import DA.Action (foldlA, when) import DA.Foldable (mapA_) import DA.Optional (fromOptional, isNone, isSome) @@ -101,6 +101,39 @@ toMaps ct = CapTableMaps with stock_issuances_by_security_id = ct.stock_issuances_by_security_id warrant_issuances_by_security_id = ct.warrant_issuances_by_security_id +securityIdsInFinalMaps : CapTableMaps -> Update [Text] +securityIdsInFinalMaps maps = do + convertibleSecurityIds <- foldlA + (\securityIds issuanceCid -> do + issuance <- fetch issuanceCid + pure (issuance.issuance_data.security_id :: securityIds)) + [] + (Map.values maps.convertible_issuances) + equityCompensationSecurityIds <- foldlA + (\securityIds issuanceCid -> do + issuance <- fetch issuanceCid + pure (issuance.issuance_data.security_id :: securityIds)) + [] + (Map.values maps.equity_compensation_issuances) + stockSecurityIds <- foldlA + (\securityIds issuanceCid -> do + issuance <- fetch issuanceCid + pure (issuance.issuance_data.security_id :: securityIds)) + [] + (Map.values maps.stock_issuances) + warrantSecurityIds <- foldlA + (\securityIds issuanceCid -> do + issuance <- fetch issuanceCid + pure (issuance.issuance_data.security_id :: securityIds)) + [] + (Map.values maps.warrant_issuances) + pure (convertibleSecurityIds ++ equityCompensationSecurityIds ++ stockSecurityIds ++ warrantSecurityIds) + +validateFinalSecurityIdUniqueness : CapTableMaps -> Update () +validateFinalSecurityIdUniqueness maps = do + securityIds <- securityIdsInFinalMaps maps + assertMsg "Security IDs must be globally unique" (unique securityIds) + data AuthorizedSharesCeiling = AuthorizedSharesFinite Decimal | AuthorizedSharesUnlimited @@ -147,6 +180,24 @@ data StockClassShareEventIndex = StockClassShareEventIndex with type StockSuccessorEdge = (Text, Time, [Text]) type StockSuccessorEdgeIndex = Map Text [(Time, [Text])] +data SecurityQuantityEvent + = SecurityQuantityDelta with + event_date: Time + event_priority: Int + quantity_delta: Decimal + | SecurityQuantityTerminalDelta with + 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 + deriving (Eq, Show) + data StockPlanSuccessorIndex = StockPlanSuccessorIndex with stock_successors_by_security_id: Map Text (Time, Optional Text) equity_compensation_successors_by_security_id: Map Text (Time, Optional Text) @@ -462,16 +513,103 @@ warrantExerciseTrigger issuanceData triggerId = None issuanceData.exercise_triggers -warrantSecurityQuantityEvents : CapTableMaps -> WarrantIssuanceOcfData -> Update [StockClassShareEvent] +securityQuantityReductionEvent : Time -> Decimal -> Optional Text -> SecurityQuantityEvent +securityQuantityReductionEvent eventDate quantity balanceSecurityId = + case balanceSecurityId of + Some _ -> + SecurityQuantityTerminalDelta with + event_date = eventDate + event_priority = 2 + quantity_delta = 0.0 - quantity + None -> + SecurityQuantityDelta with + event_date = eventDate + event_priority = 1 + quantity_delta = 0.0 - quantity + +securityQuantityEventSortKey : SecurityQuantityEvent -> (Time, Int, Int) +securityQuantityEventSortKey event = case event of + 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 () +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 + 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 + _ <- foldlA + (validateSecurityQuantityBalanceEvent securityId) + initialQuantity + (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)) + +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 (stockSecurityReductionEvents - transfer.transfer_data.date - transfer.transfer_data.quantity - transfer.transfer_data.balance_security_id ++ events) + then 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) @@ -480,12 +618,9 @@ warrantSecurityQuantityEvents maps issuanceData = do (\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) + then pure (securityQuantityReductionEvent cancellation.cancellation_data.date cancellation.cancellation_data.quantity cancellation.cancellation_data.balance_security_id :: events) else pure events) - transferEvents + [] (Map.values maps.warrant_cancellations) exerciseEvents <- foldlA @@ -494,32 +629,59 @@ warrantSecurityQuantityEvents maps issuanceData = do if exercise.exercise_data.security_id == issuanceData.security_id then case exercise.exercise_data.quantity of Some quantity -> - pure ((StockClassShareDelta with + pure ((SecurityQuantityDelta with event_date = exercise.exercise_data.date event_priority = 1 quantity_delta = 0.0 - quantity) :: events) None -> - pure ((StockClassShareRetraction with + pure ((SecurityQuantityRetraction with event_date = exercise.exercise_data.date event_priority = 3) :: events) else pure events) - cancellationEvents + [] (Map.values maps.warrant_exercises) - foldlA + retractionEvents <- foldlA (\events retractionCid -> do retraction <- fetch retractionCid if retraction.retraction_data.security_id == issuanceData.security_id - then pure ((StockClassShareRetraction with + then pure ((SecurityQuantityRetraction with event_date = retraction.retraction_data.date event_priority = 3) :: events) else pure events) - exerciseEvents + [] (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 -> do + assertSecurityQuantityEventsOnOrAfter issuanceData.security_id issuanceData.date events + 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) + warrantSecurityQuantityBeforeTerminal : CapTableMaps -> WarrantIssuanceOcfData -> @@ -527,23 +689,20 @@ warrantSecurityQuantityBeforeTerminal : Update (Optional Decimal) warrantSecurityQuantityBeforeTerminal maps issuanceData eventDate = do events <- warrantSecurityQuantityEvents maps issuanceData - let initialEvents = case issuanceData.quantity of - Some quantity -> - (StockClassShareDelta with + 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 - None -> events - let initialQuantity = case issuanceData.quantity of - Some _ -> Some 0.0 - None -> None - pure (foldl - applyStockClassShareEventOptional - initialQuantity - (filter - (\event -> stockClassShareEventSortKey event < (eventDate, 2, 1)) - (sortOn stockClassShareEventSortKey initialEvents))) + ) + (eventDate, 2, 1) + None -> + securityQuantityBeforeEvent issuanceData.security_id None events (eventDate, 2, 1) warrantExerciseQuantity : CapTableMaps -> WarrantIssuanceOcfData -> WarrantExerciseOcfData -> OcfWarrantConversionRight -> Update (Optional Decimal) warrantExerciseQuantity maps issuanceData exerciseData conversionRight = @@ -1483,29 +1642,166 @@ stockClassShareLotQuantityAt eventIndex lot asOf = (filter (stockClassShareEventOnOrBefore asOf) (stockClassShareLotEvents eventIndex lot))) else Some 0.0 +stockSecurityShareEvents : StockClassShareEventIndex -> CapTableMaps -> StockIssuanceOcfData -> Update [StockClassShareEvent] +stockSecurityShareEvents eventIndex 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) + + let splitCutoffSortKey = stockClassShareLotSplitCutoffSortKey eventIndex (stockIssuanceShareLot issuanceData) + splitEvents <- foldlA + (\events splitCid -> do + split <- fetch splitCid + let splitSortKey = (split.split_data.date, 2, 2) + if split.split_data.stock_class_id == issuanceData.stock_class_id && splitSortKey >= splitCutoffSortKey + 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 ++ transferEvents ++ conversionEvents ++ retractionEvents ++ reissuanceEvents ++ consolidationEvents ++ splitEvents)) + +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 + 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 + assertMsg + ("Stock security balance cannot be retired for " <> securityId <> " because it has no remaining balance") + (current > 0.0) + pure 0.0 + +validateStockSecurityBalance : StockClassShareEventIndex -> CapTableMaps -> StockIssuanceOcfData -> Update () +validateStockSecurityBalance eventIndex maps issuanceData = do + events <- stockSecurityShareEvents eventIndex maps issuanceData + _ <- foldlA + (validateStockSecurityBalanceEvent issuanceData.security_id) + 0.0 + (sortOn stockClassShareEventSortKey events) + pure () + +validateStockSecurityBalances : StockClassShareEventIndex -> CapTableMaps -> Update () +validateStockSecurityBalances eventIndex maps = + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + validateStockSecurityBalance eventIndex maps issuance.issuance_data) + (Map.values maps.stock_issuances) + stockSecurityQuantityBeforeEvent : StockClassShareEventIndex -> + CapTableMaps -> StockIssuanceOcfData -> (Time, Int, Int) -> Update Decimal -stockSecurityQuantityBeforeEvent eventIndex issuanceData targetSortKey = - let lot = stockIssuanceShareLot issuanceData - priorEvents = filter - (\event -> stockClassShareEventSortKey event < targetSortKey) - (sortOn stockClassShareEventSortKey (stockClassShareLotEvents eventIndex lot)) - in case foldl applyStockClassShareEventOptional (Some 0.0) priorEvents of - Some quantity -> pure quantity - None -> do - assertMsg ("Stock security quantity history is invalid for " <> issuanceData.security_id <> " before " <> show targetSortKey) False - pure 0.0 +stockSecurityQuantityBeforeEvent eventIndex maps issuanceData targetSortKey = do + events <- stockSecurityShareEvents eventIndex maps issuanceData + foldlA + (validateStockSecurityBalanceEvent issuanceData.security_id) + 0.0 + (filter + (\event -> stockClassShareEventSortKey event < targetSortKey) + (sortOn stockClassShareEventSortKey events)) stockSecurityQuantityBeforeTerminal : StockClassShareEventIndex -> + CapTableMaps -> StockIssuanceOcfData -> Time -> Update Decimal -stockSecurityQuantityBeforeTerminal eventIndex issuanceData eventDate = - stockSecurityQuantityBeforeEvent eventIndex issuanceData (eventDate, 2, 1) +stockSecurityQuantityBeforeTerminal eventIndex maps issuanceData eventDate = + stockSecurityQuantityBeforeEvent eventIndex maps issuanceData (eventDate, 2, 1) stockReductionSortKey : Time -> Optional Text -> (Time, Int, Int) stockReductionSortKey eventDate balanceSecurityId = @@ -1638,7 +1934,7 @@ validateStockTerminalBalanceSuccessor : validateStockTerminalBalanceSuccessor eventIndex maps label sourceIssuanceData eventDate reductionQuantity balanceSecurityId = case balanceSecurityId of Some securityId -> do - currentQuantity <- stockSecurityQuantityBeforeTerminal eventIndex sourceIssuanceData eventDate + currentQuantity <- stockSecurityQuantityBeforeTerminal eventIndex maps sourceIssuanceData eventDate let expectedQuantity = currentQuantity - reductionQuantity assertMsg (label <> " exceeds the current source quantity for " <> sourceIssuanceData.security_id) @@ -1748,6 +2044,7 @@ validateStockReissuanceResultIssuances eventIndex maps = let targetSortKey = (reissuance.reissuance_data.date, 3, 0) sourceQuantity <- stockSecurityQuantityBeforeEvent eventIndex + maps sourceIssuance.issuance_data targetSortKey resultingQuantity <- stockIssuanceQuantityTotal @@ -1787,6 +2084,7 @@ validateStockConsolidationResultIssuances eventIndex maps = (sourceIssuance.issuance_data.stock_class_id == resultIssuance.issuance_data.stock_class_id) quantity <- stockSecurityQuantityBeforeEvent eventIndex + maps sourceIssuance.issuance_data targetSortKey pure (total + quantity) @@ -2409,6 +2707,7 @@ stockPlanInheritedSuccessorExcessLots eventIndex successorIndex maps = do Some stockPlanId -> do sourceQuantity <- stockSecurityQuantityBeforeEvent eventIndex + maps sourceIssuance.issuance_data (reissuance.reissuance_data.date, 3, 3) (_, successorQuantity) <- recordedInheritedStockIssuanceSummary @@ -2457,6 +2756,7 @@ stockPlanConversionAllocationTransitions eventIndex successorIndex maps = do sourceBasisQuantity <- case conversion.conversion_data.balance_security_id of Some _ -> stockSecurityQuantityBeforeTerminal eventIndex + maps sourceIssuance.issuance_data conversion.conversion_data.date None -> pure conversion.conversion_data.quantity_converted @@ -2869,6 +3169,106 @@ equityCompensationSecurityEvents maps issuanceData = do pure (retractionEvents ++ splitEvents) +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.settlement_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) + + stockClassIds <- equityCompensationPlanStockClassIds maps issuanceData + splitEvents <- foldlA + (\events splitCid -> do + split <- fetch splitCid + if split.split_data.stock_class_id `elem` stockClassIds && 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) + case (issuanceData.stock_class_id, stockClassIds, splitEvents) of + (None, _ :: _ :: _, _ :: _) -> do + assertMsg + ("Equity compensation issuance without stock_class_id cannot be split-adjusted across multiple stock-plan classes: " <> issuanceData.id) + False + _ -> pure () + + pure (transferEvents ++ cancellationEvents ++ exerciseEvents ++ releaseEvents ++ retractionEvents ++ splitEvents) + +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) + equityCompensationCurrentQuantity : CapTableMaps -> EquityCompensationIssuanceOcfData -> Update (Optional Decimal) equityCompensationCurrentQuantity maps issuanceData = do events <- equityCompensationSecurityEvents maps issuanceData @@ -2894,11 +3294,17 @@ equityCompensationSecurityQuantityBeforeEvent : (Time, Int, Int) -> Update Decimal equityCompensationSecurityQuantityBeforeEvent maps issuanceData targetSortKey = do - events <- equityCompensationSecurityEvents maps issuanceData - let priorEvents = filter - (\event -> stockClassShareEventSortKey event < targetSortKey) - (sortOn stockClassShareEventSortKey events) - case foldl applyStockClassShareEventOptional (Some issuanceData.quantity) priorEvents of + events <- equityCompensationSecurityQuantityEvents maps issuanceData + quantityOpt <- securityQuantityBeforeEvent + issuanceData.security_id + (Some 0.0) + ((SecurityQuantityDelta with + event_date = issuanceData.date + event_priority = 0 + quantity_delta = issuanceData.quantity) + :: events) + targetSortKey + case quantityOpt of Some quantity -> pure quantity None -> do assertMsg ("Equity compensation quantity history is invalid for " <> issuanceData.security_id <> " before " <> show targetSortKey) False @@ -3591,6 +3997,281 @@ validateStockPlanPoolCeilings eventIndex maps = do validateStockPlanReservedShares eventIndex maps adjustmentIndex validateStockPlanReservedSharesAtDates eventIndex maps adjustmentHistory +assertSuccessorSecurityIdDistinct : Text -> Text -> Text -> Update () +assertSuccessorSecurityIdDistinct label sourceSecurityId successorSecurityId = + assertMsg + (label <> " must use a new successor security ID instead of reusing " <> sourceSecurityId) + (successorSecurityId /= sourceSecurityId) + +validateSuccessorSecurityIds : Text -> Text -> Optional Text -> [Text] -> Update () +validateSuccessorSecurityIds label sourceSecurityId balanceSecurityId resultingSecurityIds = do + mapA_ (assertSuccessorSecurityIdDistinct label sourceSecurityId) resultingSecurityIds + case balanceSecurityId of + Some balanceId -> do + assertSuccessorSecurityIdDistinct label sourceSecurityId balanceId + assertMsg + (label <> " cannot use its balance security as a resulting security: " <> balanceId) + (balanceId `notElem` resultingSecurityIds) + None -> pure () + +successorSecurityIdsFor : Optional Text -> [Text] -> [Text] +successorSecurityIdsFor balanceSecurityId resultingSecurityIds = + case balanceSecurityId of + Some balanceId -> balanceId :: resultingSecurityIds + None -> resultingSecurityIds + +validateGlobalSuccessorSecurityIdUniqueness : CapTableMaps -> Update () +validateGlobalSuccessorSecurityIdUniqueness maps = do + stockTransferIds <- foldlA + (\securityIds transferCid -> do + transfer <- fetch transferCid + pure (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids ++ securityIds)) + [] + (Map.values maps.stock_transfers) + stockCancellationIds <- foldlA + (\securityIds cancellationCid -> do + cancellation <- fetch cancellationCid + pure (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [] ++ securityIds)) + stockTransferIds + (Map.values maps.stock_cancellations) + stockRepurchaseIds <- foldlA + (\securityIds repurchaseCid -> do + repurchase <- fetch repurchaseCid + pure (successorSecurityIdsFor repurchase.repurchase_data.balance_security_id [] ++ securityIds)) + stockCancellationIds + (Map.values maps.stock_repurchases) + stockConversionIds <- foldlA + (\securityIds conversionCid -> do + conversion <- fetch conversionCid + pure (successorSecurityIdsFor conversion.conversion_data.balance_security_id conversion.conversion_data.resulting_security_ids ++ securityIds)) + stockRepurchaseIds + (Map.values maps.stock_conversions) + stockReissuanceIds <- foldlA + (\securityIds reissuanceCid -> do + reissuance <- fetch reissuanceCid + pure (reissuance.reissuance_data.resulting_security_ids ++ securityIds)) + stockConversionIds + (Map.values maps.stock_reissuances) + stockConsolidationIds <- foldlA + (\securityIds consolidationCid -> do + consolidation <- fetch consolidationCid + pure (consolidation.consolidation_data.resulting_security_id :: securityIds)) + stockReissuanceIds + (Map.values maps.stock_consolidations) + warrantTransferIds <- foldlA + (\securityIds transferCid -> do + transfer <- fetch transferCid + pure (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids ++ securityIds)) + stockConsolidationIds + (Map.values maps.warrant_transfers) + warrantCancellationIds <- foldlA + (\securityIds cancellationCid -> do + cancellation <- fetch cancellationCid + pure (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [] ++ securityIds)) + warrantTransferIds + (Map.values maps.warrant_cancellations) + warrantExerciseIds <- foldlA + (\securityIds exerciseCid -> do + exercise <- fetch exerciseCid + pure (exercise.exercise_data.resulting_security_ids ++ securityIds)) + warrantCancellationIds + (Map.values maps.warrant_exercises) + convertibleTransferIds <- foldlA + (\securityIds transferCid -> do + transfer <- fetch transferCid + pure (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids ++ securityIds)) + warrantExerciseIds + (Map.values maps.convertible_transfers) + convertibleCancellationIds <- foldlA + (\securityIds cancellationCid -> do + cancellation <- fetch cancellationCid + pure (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [] ++ securityIds)) + convertibleTransferIds + (Map.values maps.convertible_cancellations) + convertibleConversionIds <- foldlA + (\securityIds conversionCid -> do + conversion <- fetch conversionCid + pure (successorSecurityIdsFor conversion.conversion_data.balance_security_id conversion.conversion_data.resulting_security_ids ++ securityIds)) + convertibleCancellationIds + (Map.values maps.convertible_conversions) + equityCompensationTransferIds <- foldlA + (\securityIds transferCid -> do + transfer <- fetch transferCid + pure (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids ++ securityIds)) + convertibleConversionIds + (Map.values maps.equity_compensation_transfers) + equityCompensationCancellationIds <- foldlA + (\securityIds cancellationCid -> do + cancellation <- fetch cancellationCid + pure (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [] ++ securityIds)) + equityCompensationTransferIds + (Map.values maps.equity_compensation_cancellations) + equityCompensationExerciseIds <- foldlA + (\securityIds exerciseCid -> do + exercise <- fetch exerciseCid + pure (exercise.exercise_data.resulting_security_ids ++ securityIds)) + equityCompensationCancellationIds + (Map.values maps.equity_compensation_exercises) + allSuccessorIds <- foldlA + (\securityIds releaseCid -> do + release <- fetch releaseCid + pure (release.release_data.resulting_security_ids ++ securityIds)) + equityCompensationExerciseIds + (Map.values maps.equity_compensation_releases) + assertMsg + "Each successor security ID may be produced by only one lifecycle transaction" + (unique allSuccessorIds) + +validateSuccessorSecurityIdContinuity : CapTableMaps -> Update () +validateSuccessorSecurityIdContinuity maps = do + validateGlobalSuccessorSecurityIdUniqueness maps + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + validateSuccessorSecurityIds + ("Stock transfer " <> transfer.transfer_data.id) + transfer.transfer_data.security_id + transfer.transfer_data.balance_security_id + transfer.transfer_data.resulting_security_ids) + (Map.values maps.stock_transfers) + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + validateSuccessorSecurityIds + ("Stock cancellation " <> cancellation.cancellation_data.id) + cancellation.cancellation_data.security_id + cancellation.cancellation_data.balance_security_id + []) + (Map.values maps.stock_cancellations) + mapA_ + (\repurchaseCid -> do + repurchase <- fetch repurchaseCid + validateSuccessorSecurityIds + ("Stock repurchase " <> repurchase.repurchase_data.id) + repurchase.repurchase_data.security_id + repurchase.repurchase_data.balance_security_id + []) + (Map.values maps.stock_repurchases) + mapA_ + (\conversionCid -> do + conversion <- fetch conversionCid + validateSuccessorSecurityIds + ("Stock conversion " <> conversion.conversion_data.id) + conversion.conversion_data.security_id + conversion.conversion_data.balance_security_id + conversion.conversion_data.resulting_security_ids) + (Map.values maps.stock_conversions) + mapA_ + (\reissuanceCid -> do + reissuance <- fetch reissuanceCid + validateSuccessorSecurityIds + ("Stock reissuance " <> reissuance.reissuance_data.id) + reissuance.reissuance_data.security_id + None + reissuance.reissuance_data.resulting_security_ids) + (Map.values maps.stock_reissuances) + mapA_ + (\consolidationCid -> do + consolidation <- fetch consolidationCid + mapA_ + (\sourceSecurityId -> + assertSuccessorSecurityIdDistinct + ("Stock consolidation " <> consolidation.consolidation_data.id) + sourceSecurityId + consolidation.consolidation_data.resulting_security_id) + consolidation.consolidation_data.security_ids) + (Map.values maps.stock_consolidations) + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + validateSuccessorSecurityIds + ("Warrant transfer " <> transfer.transfer_data.id) + transfer.transfer_data.security_id + transfer.transfer_data.balance_security_id + transfer.transfer_data.resulting_security_ids) + (Map.values maps.warrant_transfers) + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + validateSuccessorSecurityIds + ("Warrant cancellation " <> cancellation.cancellation_data.id) + cancellation.cancellation_data.security_id + cancellation.cancellation_data.balance_security_id + []) + (Map.values maps.warrant_cancellations) + mapA_ + (\exerciseCid -> do + exercise <- fetch exerciseCid + validateSuccessorSecurityIds + ("Warrant exercise " <> exercise.exercise_data.id) + exercise.exercise_data.security_id + None + exercise.exercise_data.resulting_security_ids) + (Map.values maps.warrant_exercises) + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + validateSuccessorSecurityIds + ("Convertible transfer " <> transfer.transfer_data.id) + transfer.transfer_data.security_id + transfer.transfer_data.balance_security_id + transfer.transfer_data.resulting_security_ids) + (Map.values maps.convertible_transfers) + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + validateSuccessorSecurityIds + ("Convertible cancellation " <> cancellation.cancellation_data.id) + cancellation.cancellation_data.security_id + cancellation.cancellation_data.balance_security_id + []) + (Map.values maps.convertible_cancellations) + mapA_ + (\conversionCid -> do + conversion <- fetch conversionCid + validateSuccessorSecurityIds + ("Convertible conversion " <> conversion.conversion_data.id) + conversion.conversion_data.security_id + conversion.conversion_data.balance_security_id + conversion.conversion_data.resulting_security_ids) + (Map.values maps.convertible_conversions) + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + validateSuccessorSecurityIds + ("Equity compensation transfer " <> transfer.transfer_data.id) + transfer.transfer_data.security_id + transfer.transfer_data.balance_security_id + transfer.transfer_data.resulting_security_ids) + (Map.values maps.equity_compensation_transfers) + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + validateSuccessorSecurityIds + ("Equity compensation cancellation " <> cancellation.cancellation_data.id) + cancellation.cancellation_data.security_id + cancellation.cancellation_data.balance_security_id + []) + (Map.values maps.equity_compensation_cancellations) + mapA_ + (\exerciseCid -> do + exercise <- fetch exerciseCid + validateSuccessorSecurityIds + ("Equity compensation exercise " <> exercise.exercise_data.id) + exercise.exercise_data.security_id + None + exercise.exercise_data.resulting_security_ids) + (Map.values maps.equity_compensation_exercises) + mapA_ + (\releaseCid -> do + release <- fetch releaseCid + validateSuccessorSecurityIds + ("Equity compensation release " <> release.release_data.id) + release.release_data.security_id + None + release.release_data.resulting_security_ids) + (Map.values maps.equity_compensation_releases) + validateAuthorizedShareCeilings : CapTableMaps -> Map Text StockClassAuthorizedSharesAdjustmentOcfData -> @@ -3598,6 +4279,11 @@ validateAuthorizedShareCeilings : Update () validateAuthorizedShareCeilings maps adjustmentIndex issuerCid = do eventIndex <- stockClassShareEventIndex maps + validateFinalSecurityIdUniqueness maps + validateSuccessorSecurityIdContinuity maps + validateStockSecurityBalances eventIndex maps + validateWarrantSecurityBalances maps + validateEquityCompensationSecurityBalances maps validateStockBalanceSuccessors eventIndex maps validateStockTransferResultIssuances maps validateStockReissuanceResultIssuances eventIndex maps From 22d0e24fa0bd146dfc9e58cd5584c9692f004321 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 17:45:14 -0400 Subject: [PATCH 44/62] chore: remove obsolete codegen metadata --- scripts/codegen/generate-captable.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/codegen/generate-captable.ts b/scripts/codegen/generate-captable.ts index ed84e12e..ccd9d1b0 100644 --- a/scripts/codegen/generate-captable.ts +++ b/scripts/codegen/generate-captable.ts @@ -51,7 +51,6 @@ interface TypeDef { map_field: string; tier: number; validations: Validation[]; - validates_stock_class_initial_authorized_shares: boolean; // For issuance types, the name of the security_id index map (e.g., 'stock_issuances_by_security_id') security_id_index_map: string | null; } @@ -220,7 +219,6 @@ function discoverTypes(config: Config): TypeDef[] { error: `${toTitleCase(fieldSpec)} not found`, }; }), - validates_stock_class_initial_authorized_shares: name === 'StockClass', security_id_index_map: SECURITY_ID_INDEX_MAPS[name] ?? null, }; From 92d779392667ea9724194310718a305a37c8628f Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 17:49:36 -0400 Subject: [PATCH 45/62] test: make reviewed quantities explicit --- Test/daml/OpenCapTable/TestEquityCompensationRelease.daml | 2 +- Test/daml/OpenCapTable/TestLifecycleInvariants.daml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml b/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml index 91061f05..84c3815a 100644 --- a/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml +++ b/Test/daml/OpenCapTable/TestEquityCompensationRelease.daml @@ -21,7 +21,7 @@ addEquityCompensationReleaseIssuance issuer capTableCid issuanceId securityId st custom_id = "EC-" <> issuanceId date = DT.time (DA.date 2024 Jan 01) 0 0 0 compensation_type = OcfCompensationTypeRSU - quantity + quantity = quantity security_id = securityId stakeholder_id = stakeholderId comments = [] diff --git a/Test/daml/OpenCapTable/TestLifecycleInvariants.daml b/Test/daml/OpenCapTable/TestLifecycleInvariants.daml index 94a4a6ca..6596cbc8 100644 --- a/Test/daml/OpenCapTable/TestLifecycleInvariants.daml +++ b/Test/daml/OpenCapTable/TestLifecycleInvariants.daml @@ -68,7 +68,7 @@ lifecycleWarrantExercise exerciseId securityId quantity resultingSecurityIds = comments = [] resulting_security_ids = resultingSecurityIds consideration_text = None - quantity + quantity = quantity lifecycleStockTransfer : Text -> Text -> Decimal -> Optional Text -> [Text] -> StockTransferOcfData lifecycleStockTransfer transferId securityId quantity balanceSecurityId resultingSecurityIds = From ae8a70588ba6f3d70a1c4914ef62d5add5495f29 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 18:06:58 -0400 Subject: [PATCH 46/62] fix: reject invalid successor security identities --- .../OpenCapTable/OCF/StockConsolidation.daml | 3 +- .../OpenCapTable/Types/Validation.daml | 9 + .../OpenCapTable/TestLifecycleInvariants.daml | 21 +++ .../OpenCapTable/TestStockConsolidation.daml | 17 ++ .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- .../codegen/templates/CapTable.daml.template | 163 +++++++++++++++--- 7 files changed, 190 insertions(+), 33 deletions(-) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConsolidation.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConsolidation.daml index ca6c29b4..43dff6b0 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConsolidation.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockConsolidation.daml @@ -6,7 +6,7 @@ module Fairmint.OpenCapTable.OCF.StockConsolidation where -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/objects/transactions/consolidation/StockConsolidation.schema.json import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) +import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray, validateUniqueStringsArray) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -62,4 +62,5 @@ validateStockConsolidationOcfData d = -- Arrays element validation (security_ids required per Consolidation base schema) validateNonEmptyStringsArray d.comments && validateNonEmptyStringsArray d.security_ids && + validateUniqueStringsArray d.security_ids && not (null d.security_ids) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml index dcda2c04..ef67ff9a 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml @@ -15,6 +15,15 @@ import Fairmint.Shared.TypeHelpers (validateRequiredText) validateNonEmptyStringsArray : [Text] -> Bool validateNonEmptyStringsArray xs = all validateRequiredText xs +hasDuplicateTexts : [Text] -> Bool +hasDuplicateTexts texts = case texts of + [] -> False + text :: rest -> text `elem` rest || hasDuplicateTexts rest + +-- Ensures no element appears more than once; empty arrays are allowed +validateUniqueStringsArray : [Text] -> Bool +validateUniqueStringsArray xs = not (hasDuplicateTexts xs) + -- Country code -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/types/CountryCode.schema.json validCountryCodes : [Text] diff --git a/Test/daml/OpenCapTable/TestLifecycleInvariants.daml b/Test/daml/OpenCapTable/TestLifecycleInvariants.daml index 6596cbc8..90b82c85 100644 --- a/Test/daml/OpenCapTable/TestLifecycleInvariants.daml +++ b/Test/daml/OpenCapTable/TestLifecycleInvariants.daml @@ -371,6 +371,27 @@ testCrossTypeSourceResultAliasFails = script do edits = [] deletes = [] +testUnmaterializedStockResultCannotAliasLiveWarrant = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_LIVE_CROSS_TYPE_ALIAS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_LIVE_CROSS_TYPE_ALIAS" "Live Cross Type Alias Holder" + let stockSecurityId = "SEC_LIVE_CROSS_TYPE_ALIAS_STOCK" + let warrantSecurityId = "SEC_LIVE_CROSS_TYPE_ALIAS_WARRANT" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_LIVE_CROSS_TYPE_ALIAS_STOCK" stockSecurityId stakeholderId stockClassId) with quantity = 100.0) + , CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_LIVE_CROSS_TYPE_ALIAS_WARRANT" warrantSecurityId stakeholderId stockClassId) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockTransfer (lifecycleStockTransfer "TX_LIVE_CROSS_TYPE_ALIAS_TRANSFER" stockSecurityId 50.0 None [warrantSecurityId])] + edits = [] + deletes = [] + testStockBalanceSuccessorIncludesPriorUnmaterializedReduction = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_STOCK_UNMATERIALIZED_HISTORY" 1_000.0) diff --git a/Test/daml/OpenCapTable/TestStockConsolidation.daml b/Test/daml/OpenCapTable/TestStockConsolidation.daml index 4598bf2c..dfce1464 100644 --- a/Test/daml/OpenCapTable/TestStockConsolidation.daml +++ b/Test/daml/OpenCapTable/TestStockConsolidation.daml @@ -59,6 +59,23 @@ testStockConsolidation_TwoSecurities = script do deletes = [] pure () +testStockConsolidation_DuplicateSecurityIdsRejected = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + result <- createStockIssuance issuer capTableCid "SSEC-DUP-CONSOLIDATE" 100.0 + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockConsolidation StockConsolidationOcfData with + id = "TX_STOCK_CONSOLIDATION_DUP" + date = DT.time (DA.date 2025 Apr 15) 0 0 0 + security_ids = ["SSEC-DUP-CONSOLIDATE", "SSEC-DUP-CONSOLIDATE"] + resulting_security_id = "SSEC-DUP-CONSOLIDATED" + comments = [] + reason_text = Some "Duplicate source security IDs are invalid"] + edits = [] + deletes = [] + pure () + -- Test 2: Consolidation of multiple securities with comments testStockConsolidation_MultipleSecurities = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 9b45c179..2707c22c 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00951f0333bb010ce94089c0d511794397ad0cc139ef5b70a6b5b6b81f2789f0 -size 2914422 +oid sha256:11ba467d2cf5778d86bb140b32736c9326dd2dcd034c6bccae154eee682f825c +size 2923110 diff --git a/dars/dars.lock b/dars/dars.lock index 9fc1322f..ecdcd9fa 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,10 +33,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "00951f0333bb010ce94089c0d511794397ad0cc139ef5b70a6b5b6b81f2789f0", - "size": 2914422, + "sha256": "11ba467d2cf5778d86bb140b32736c9326dd2dcd034c6bccae154eee682f825c", + "size": 2923110, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-10T13:44:14Z", + "uploadedAt": "2026-07-10T22:04:37Z", "networks": [] } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 04738098..baafd1bc 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -4014,6 +4014,15 @@ validateSuccessorSecurityIds label sourceSecurityId balanceSecurityId resultingS (balanceId `notElem` resultingSecurityIds) None -> pure () +validateSuccessorSecurityIdTypes : Text -> [Text] -> [Text] -> Update () +validateSuccessorSecurityIdTypes label unexpectedIssuanceIds successorSecurityIds = + mapA_ + (\successorSecurityId -> + assertMsg + (label <> " successor security ID collides with a live security of another type: " <> successorSecurityId) + (successorSecurityId `notElem` unexpectedIssuanceIds)) + successorSecurityIds + successorSecurityIdsFor : Optional Text -> [Text] -> [Text] successorSecurityIdsFor balanceSecurityId resultingSecurityIds = case balanceSecurityId of @@ -4125,150 +4134,250 @@ validateGlobalSuccessorSecurityIdUniqueness maps = do validateSuccessorSecurityIdContinuity : CapTableMaps -> Update () validateSuccessorSecurityIdContinuity maps = do validateGlobalSuccessorSecurityIdUniqueness maps + let stockUnexpectedIssuanceIds = + Map.keys maps.warrant_issuances_by_security_id + ++ Map.keys maps.convertible_issuances_by_security_id + ++ Map.keys maps.equity_compensation_issuances_by_security_id + let warrantUnexpectedIssuanceIds = + Map.keys maps.stock_issuances_by_security_id + ++ Map.keys maps.convertible_issuances_by_security_id + ++ Map.keys maps.equity_compensation_issuances_by_security_id + let convertibleUnexpectedIssuanceIds = + Map.keys maps.stock_issuances_by_security_id + ++ Map.keys maps.warrant_issuances_by_security_id + ++ Map.keys maps.equity_compensation_issuances_by_security_id + let equityCompensationUnexpectedIssuanceIds = + Map.keys maps.stock_issuances_by_security_id + ++ Map.keys maps.warrant_issuances_by_security_id + ++ Map.keys maps.convertible_issuances_by_security_id mapA_ (\transferCid -> do transfer <- fetch transferCid + let label = "Stock transfer " <> transfer.transfer_data.id validateSuccessorSecurityIds - ("Stock transfer " <> transfer.transfer_data.id) + label transfer.transfer_data.security_id transfer.transfer_data.balance_security_id - transfer.transfer_data.resulting_security_ids) + transfer.transfer_data.resulting_security_ids + validateSuccessorSecurityIdTypes + label + stockUnexpectedIssuanceIds + (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids)) (Map.values maps.stock_transfers) mapA_ (\cancellationCid -> do cancellation <- fetch cancellationCid + let label = "Stock cancellation " <> cancellation.cancellation_data.id validateSuccessorSecurityIds - ("Stock cancellation " <> cancellation.cancellation_data.id) + label cancellation.cancellation_data.security_id cancellation.cancellation_data.balance_security_id - []) + [] + validateSuccessorSecurityIdTypes + label + stockUnexpectedIssuanceIds + (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [])) (Map.values maps.stock_cancellations) mapA_ (\repurchaseCid -> do repurchase <- fetch repurchaseCid + let label = "Stock repurchase " <> repurchase.repurchase_data.id validateSuccessorSecurityIds - ("Stock repurchase " <> repurchase.repurchase_data.id) + label repurchase.repurchase_data.security_id repurchase.repurchase_data.balance_security_id - []) + [] + validateSuccessorSecurityIdTypes + label + stockUnexpectedIssuanceIds + (successorSecurityIdsFor repurchase.repurchase_data.balance_security_id [])) (Map.values maps.stock_repurchases) mapA_ (\conversionCid -> do conversion <- fetch conversionCid + let label = "Stock conversion " <> conversion.conversion_data.id validateSuccessorSecurityIds - ("Stock conversion " <> conversion.conversion_data.id) + label conversion.conversion_data.security_id conversion.conversion_data.balance_security_id - conversion.conversion_data.resulting_security_ids) + conversion.conversion_data.resulting_security_ids + validateSuccessorSecurityIdTypes + label + stockUnexpectedIssuanceIds + (successorSecurityIdsFor conversion.conversion_data.balance_security_id conversion.conversion_data.resulting_security_ids)) (Map.values maps.stock_conversions) mapA_ (\reissuanceCid -> do reissuance <- fetch reissuanceCid + let label = "Stock reissuance " <> reissuance.reissuance_data.id validateSuccessorSecurityIds - ("Stock reissuance " <> reissuance.reissuance_data.id) + label reissuance.reissuance_data.security_id None + reissuance.reissuance_data.resulting_security_ids + validateSuccessorSecurityIdTypes + label + stockUnexpectedIssuanceIds reissuance.reissuance_data.resulting_security_ids) (Map.values maps.stock_reissuances) mapA_ (\consolidationCid -> do consolidation <- fetch consolidationCid + let label = "Stock consolidation " <> consolidation.consolidation_data.id mapA_ (\sourceSecurityId -> assertSuccessorSecurityIdDistinct - ("Stock consolidation " <> consolidation.consolidation_data.id) + label sourceSecurityId consolidation.consolidation_data.resulting_security_id) - consolidation.consolidation_data.security_ids) + consolidation.consolidation_data.security_ids + validateSuccessorSecurityIdTypes + label + stockUnexpectedIssuanceIds + [consolidation.consolidation_data.resulting_security_id]) (Map.values maps.stock_consolidations) mapA_ (\transferCid -> do transfer <- fetch transferCid + let label = "Warrant transfer " <> transfer.transfer_data.id validateSuccessorSecurityIds - ("Warrant transfer " <> transfer.transfer_data.id) + label transfer.transfer_data.security_id transfer.transfer_data.balance_security_id - transfer.transfer_data.resulting_security_ids) + transfer.transfer_data.resulting_security_ids + validateSuccessorSecurityIdTypes + label + warrantUnexpectedIssuanceIds + (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids)) (Map.values maps.warrant_transfers) mapA_ (\cancellationCid -> do cancellation <- fetch cancellationCid + let label = "Warrant cancellation " <> cancellation.cancellation_data.id validateSuccessorSecurityIds - ("Warrant cancellation " <> cancellation.cancellation_data.id) + label cancellation.cancellation_data.security_id cancellation.cancellation_data.balance_security_id - []) + [] + validateSuccessorSecurityIdTypes + label + warrantUnexpectedIssuanceIds + (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [])) (Map.values maps.warrant_cancellations) mapA_ (\exerciseCid -> do exercise <- fetch exerciseCid + let label = "Warrant exercise " <> exercise.exercise_data.id validateSuccessorSecurityIds - ("Warrant exercise " <> exercise.exercise_data.id) + label exercise.exercise_data.security_id None + exercise.exercise_data.resulting_security_ids + validateSuccessorSecurityIdTypes + label + stockUnexpectedIssuanceIds exercise.exercise_data.resulting_security_ids) (Map.values maps.warrant_exercises) mapA_ (\transferCid -> do transfer <- fetch transferCid + let label = "Convertible transfer " <> transfer.transfer_data.id validateSuccessorSecurityIds - ("Convertible transfer " <> transfer.transfer_data.id) + label transfer.transfer_data.security_id transfer.transfer_data.balance_security_id - transfer.transfer_data.resulting_security_ids) + transfer.transfer_data.resulting_security_ids + validateSuccessorSecurityIdTypes + label + convertibleUnexpectedIssuanceIds + (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids)) (Map.values maps.convertible_transfers) mapA_ (\cancellationCid -> do cancellation <- fetch cancellationCid + let label = "Convertible cancellation " <> cancellation.cancellation_data.id validateSuccessorSecurityIds - ("Convertible cancellation " <> cancellation.cancellation_data.id) + label cancellation.cancellation_data.security_id cancellation.cancellation_data.balance_security_id - []) + [] + validateSuccessorSecurityIdTypes + label + convertibleUnexpectedIssuanceIds + (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [])) (Map.values maps.convertible_cancellations) mapA_ (\conversionCid -> do conversion <- fetch conversionCid + let label = "Convertible conversion " <> conversion.conversion_data.id validateSuccessorSecurityIds - ("Convertible conversion " <> conversion.conversion_data.id) + label conversion.conversion_data.security_id conversion.conversion_data.balance_security_id + conversion.conversion_data.resulting_security_ids + validateSuccessorSecurityIdTypes + label + convertibleUnexpectedIssuanceIds + (successorSecurityIdsFor conversion.conversion_data.balance_security_id []) + validateSuccessorSecurityIdTypes + label + stockUnexpectedIssuanceIds conversion.conversion_data.resulting_security_ids) (Map.values maps.convertible_conversions) mapA_ (\transferCid -> do transfer <- fetch transferCid + let label = "Equity compensation transfer " <> transfer.transfer_data.id validateSuccessorSecurityIds - ("Equity compensation transfer " <> transfer.transfer_data.id) + label transfer.transfer_data.security_id transfer.transfer_data.balance_security_id - transfer.transfer_data.resulting_security_ids) + transfer.transfer_data.resulting_security_ids + validateSuccessorSecurityIdTypes + label + equityCompensationUnexpectedIssuanceIds + (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids)) (Map.values maps.equity_compensation_transfers) mapA_ (\cancellationCid -> do cancellation <- fetch cancellationCid + let label = "Equity compensation cancellation " <> cancellation.cancellation_data.id validateSuccessorSecurityIds - ("Equity compensation cancellation " <> cancellation.cancellation_data.id) + label cancellation.cancellation_data.security_id cancellation.cancellation_data.balance_security_id - []) + [] + validateSuccessorSecurityIdTypes + label + equityCompensationUnexpectedIssuanceIds + (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id [])) (Map.values maps.equity_compensation_cancellations) mapA_ (\exerciseCid -> do exercise <- fetch exerciseCid + let label = "Equity compensation exercise " <> exercise.exercise_data.id validateSuccessorSecurityIds - ("Equity compensation exercise " <> exercise.exercise_data.id) + label exercise.exercise_data.security_id None + exercise.exercise_data.resulting_security_ids + validateSuccessorSecurityIdTypes + label + stockUnexpectedIssuanceIds exercise.exercise_data.resulting_security_ids) (Map.values maps.equity_compensation_exercises) mapA_ (\releaseCid -> do release <- fetch releaseCid + let label = "Equity compensation release " <> release.release_data.id validateSuccessorSecurityIds - ("Equity compensation release " <> release.release_data.id) + label release.release_data.security_id None + release.release_data.resulting_security_ids + validateSuccessorSecurityIdTypes + label + stockUnexpectedIssuanceIds release.release_data.resulting_security_ids) (Map.values maps.equity_compensation_releases) From 984fc1950dd88da5ead886bcff1d0668554961a0 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 18:24:43 -0400 Subject: [PATCH 47/62] fix: preserve lifecycle accounting across sparse outputs --- .../OpenCapTable/TestLifecycleInvariants.daml | 94 ++++++++++++++++ .../TestStockPlanReservedShares.daml | 46 ++++++++ .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- .../codegen/templates/CapTable.daml.template | 105 ++++++++++++------ 5 files changed, 219 insertions(+), 36 deletions(-) diff --git a/Test/daml/OpenCapTable/TestLifecycleInvariants.daml b/Test/daml/OpenCapTable/TestLifecycleInvariants.daml index 90b82c85..4e55399e 100644 --- a/Test/daml/OpenCapTable/TestLifecycleInvariants.daml +++ b/Test/daml/OpenCapTable/TestLifecycleInvariants.daml @@ -7,6 +7,7 @@ import Fairmint.OpenCapTable.OCF.EquityCompensationRelease import Fairmint.OpenCapTable.OCF.EquityCompensationTransfer import Fairmint.OpenCapTable.OCF.StockConsolidation import Fairmint.OpenCapTable.OCF.StockReissuance +import Fairmint.OpenCapTable.OCF.StockRetraction import Fairmint.OpenCapTable.OCF.StockTransfer import Fairmint.OpenCapTable.OCF.WarrantExercise import Fairmint.OpenCapTable.OCF.WarrantIssuance @@ -392,6 +393,99 @@ testUnmaterializedStockResultCannotAliasLiveWarrant = script do edits = [] deletes = [] +testUnmaterializedPartialTransferWithBalancePreservesShareCount = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_UNMATERIALIZED_TRANSFER_BALANCE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_UNMATERIALIZED_TRANSFER_BALANCE" "Unmaterialized Transfer Holder" + let sourceSecurityId = "SEC_UNMATERIALIZED_TRANSFER_SOURCE" + let balanceSecurityId = "SEC_UNMATERIALIZED_TRANSFER_BALANCE" + issued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_UNMATERIALIZED_TRANSFER_SOURCE" sourceSecurityId stakeholderId stockClassId) with quantity = 100.0) + let balanceIssuance = (defaultStockIssuance "TX_UNMATERIALIZED_TRANSFER_BALANCE" balanceSecurityId stakeholderId stockClassId) with quantity = 60.0 + transferred <- submit issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer (lifecycleStockTransfer "TX_UNMATERIALIZED_TRANSFER" sourceSecurityId 40.0 (Some balanceSecurityId) ["SEC_UNMATERIALIZED_TRANSFER_MISSING_RESULT"]) + , CT.OcfCreateStockIssuance balanceIssuance + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_UNMATERIALIZED_TRANSFER_EXCESS" "SEC_UNMATERIALIZED_TRANSFER_EXCESS" stakeholderId stockClassId) with quantity = 1.0)] + edits = [] + deletes = [] + +testUnmaterializedReissuanceCannotOutliveSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_UNMATERIALIZED_REISSUANCE_SOURCE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_UNMATERIALIZED_REISSUANCE_SOURCE" "Unmaterialized Reissuance Holder" + let sourceTransactionId = "TX_UNMATERIALIZED_REISSUANCE_SOURCE" + let sourceSecurityId = "SEC_UNMATERIALIZED_REISSUANCE_SOURCE" + issued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance sourceTransactionId sourceSecurityId stakeholderId stockClassId) with quantity = 100.0) + reissued <- submit issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = [CT.OcfCreateStockReissuance (lifecycleStockReissuance "TX_UNMATERIALIZED_REISSUANCE" sourceSecurityId ["SEC_UNMATERIALIZED_REISSUANCE_MISSING_RESULT"])] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd reissued.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockIssuance sourceTransactionId] + +testUnmaterializedConsolidationCannotOutliveSource = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_UNMATERIALIZED_CONSOLIDATION_SOURCE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_UNMATERIALIZED_CONSOLIDATION_SOURCE" "Unmaterialized Consolidation Holder" + let sourceTransactionId = "TX_UNMATERIALIZED_CONSOLIDATION_SOURCE" + let sourceSecurityId = "SEC_UNMATERIALIZED_CONSOLIDATION_SOURCE" + issued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance sourceTransactionId sourceSecurityId stakeholderId stockClassId) with quantity = 100.0) + consolidated <- submit issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = [CT.OcfCreateStockConsolidation (lifecycleStockConsolidation "TX_UNMATERIALIZED_CONSOLIDATION" [sourceSecurityId] "SEC_UNMATERIALIZED_CONSOLIDATION_MISSING_RESULT")] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd consolidated.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteStockIssuance sourceTransactionId] + +testSameSecurityIdCorrectionRetainsRetractionHistory = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CORRECTED_RETRACTED_SECURITY" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CORRECTED_RETRACTED_SECURITY" "Corrected Retracted Holder" + let originalTransactionId = "TX_CORRECTED_RETRACTED_ORIGINAL" + let securityId = "SEC_CORRECTED_RETRACTED" + issued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance originalTransactionId securityId stakeholderId stockClassId) with quantity = 100.0) + retracted <- submit issuer do + exerciseCmd issued CT.UpdateCapTable with + creates = [CT.OcfCreateStockRetraction StockRetractionOcfData with + id = "TX_CORRECTED_RETRACTED_RETRACTION" + date = defaultTestDate + security_id = securityId + reason_text = "Corrected security remains retracted" + comments = []] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd retracted.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CORRECTED_RETRACTED_REPLACEMENT" securityId stakeholderId stockClassId) with quantity = 100.0) + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_CORRECTED_RETRACTED_NEW_SECURITY" "SEC_CORRECTED_RETRACTED_NEW_SECURITY" stakeholderId stockClassId) with quantity = 100.0) + ] + edits = [] + deletes = [CT.OcfDeleteStockIssuance originalTransactionId] + pure () + testStockBalanceSuccessorIncludesPriorUnmaterializedReduction = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_STOCK_UNMATERIALIZED_HISTORY" 1_000.0) diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index 2c57c72d..dc22ea61 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -1186,6 +1186,52 @@ testStockPlanReservedShares_ConvertedAllocationUsesDestinationClassSplit = scrip edits = [] deletes = [] +testStockPlanReservedShares_ConvertedClassOutsidePlanTracksSplitDate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let sourceStockClassId = "SC_PLAN_CONVERSION_EXTERNAL_SOURCE" + let destinationStockClassId = "SC_PLAN_CONVERSION_EXTERNAL_DESTINATION" + let stockPlanId = "PLAN_CONVERSION_EXTERNAL_DESTINATION_SPLIT" + let sourceSecurityId = "SEC_PLAN_CONVERSION_EXTERNAL_SOURCE" + let resultSecurityId = "SEC_PLAN_CONVERSION_EXTERNAL_RESULT" + let conversionDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let splitDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let adjustmentDate = DT.time (DA.date 2024 Jan 04) 0 0 0 + (capTableWithDestination, _) <- setupStockClassWith issuer cap_table (defaultStockClass destinationStockClassId) + let sourceStockClass = (defaultStockClass sourceStockClassId) with + conversion_rights = [stockClassConversionRightTo destinationStockClassId] + (capTableWithSource, _) <- setupStockClassWith issuer capTableWithDestination sourceStockClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithSource "SH_PLAN_CONVERSION_EXTERNAL" "External Conversion Holder" + let plan = (defaultStockPlan stockPlanId sourceStockClassId) with + initial_shares_reserved = 150.0 + stock_class_ids = [sourceStockClassId] + let sourceIssuance = (defaultStockIssuance "TX_PLAN_CONVERSION_EXTERNAL_SOURCE" sourceSecurityId stakeholderId sourceStockClassId) with + stock_plan_id = Some stockPlanId + quantity = 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance sourceIssuance + ] + edits = [] + deletes = [] + let resultIssuance = (defaultStockIssuance "TX_PLAN_CONVERSION_EXTERNAL_RESULT" resultSecurityId stakeholderId destinationStockClassId) with + date = conversionDate + stock_plan_id = None + quantity = 100.0 + let adjustment = (stockPlanPoolAdjustment "TX_PLAN_CONVERSION_EXTERNAL_ADJUSTMENT" stockPlanId 200.0) with date = adjustmentDate + + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConversion (planStockConversion "TX_PLAN_CONVERSION_EXTERNAL" sourceSecurityId resultSecurityId conversionDate 100.0) + , CT.OcfCreateStockIssuance resultIssuance + , CT.OcfCreateStockClassSplit ((stockClassSplit "TX_PLAN_CONVERSION_EXTERNAL_SPLIT" destinationStockClassId 2.0 1.0) with date = splitDate) + , CT.OcfCreateStockPlanPoolAdjustment adjustment + ] + edits = [] + deletes = [] + testStockPlanReservedShares_ConvertedAllocationFitsAdjustedPool = script do TestOcp{issuer, cap_table} <- setupTestOcp let sourceStockClassId = "SC_PLAN_CONVERSION_SOURCE_ALLOWED" diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 2707c22c..b2b0e6f0 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11ba467d2cf5778d86bb140b32736c9326dd2dcd034c6bccae154eee682f825c -size 2923110 +oid sha256:3df9dc47f3f32ce07b2259cab092c5919bb1d016b827aea727bf26dbbe5a660e +size 2927228 diff --git a/dars/dars.lock b/dars/dars.lock index ecdcd9fa..346a0f85 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,10 +33,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "11ba467d2cf5778d86bb140b32736c9326dd2dcd034c6bccae154eee682f825c", - "size": 2923110, + "sha256": "3df9dc47f3f32ce07b2259cab092c5919bb1d016b827aea727bf26dbbe5a660e", + "size": 2927228, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-10T22:04:37Z", + "uploadedAt": "2026-07-10T22:21:21Z", "networks": [] } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index baafd1bc..f8a3cff0 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -970,6 +970,29 @@ stockClassShareLotsByStockClass maps stockClassIndex = do Map.empty (Map.values maps.stock_issuances) + stockTransferLots <- foldlA + (\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) + stockIssuanceLots + (Map.values maps.stock_transfers) + stockConversionLots <- foldlA (\index conversionCid -> do conversion <- fetch conversionCid @@ -992,7 +1015,7 @@ stockClassShareLotsByStockClass maps stockClassIndex = do None -> do assertMsg ("Stock conversion references missing source stock issuance: " <> conversion.conversion_data.security_id) False pure index) - stockIssuanceLots + stockTransferLots (Map.values maps.stock_conversions) warrantExerciseLots <- foldlA @@ -1426,10 +1449,7 @@ stockClassShareEventIndex maps = do transfer <- fetch transferCid assertNoPartialResultingStockIssuanceCoverage ("Stock transfer " <> transfer.transfer_data.id) maps transfer.transfer_data.resulting_security_ids let events = stockSecurityReductionEvents transfer.transfer_data.date transfer.transfer_data.quantity transfer.transfer_data.balance_security_id - if allResultingStockIssuancesExist maps transfer.transfer_data.resulting_security_ids - && stockBalanceSecurityIssuanceExists maps transfer.transfer_data.balance_security_id - then pure (addStockClassShareEvents transfer.transfer_data.security_id events index) - else pure index) + pure (addStockClassShareEvents transfer.transfer_data.security_id events index)) conversionEventsBySecurityId (Map.values maps.stock_transfers) @@ -2037,28 +2057,29 @@ validateStockReissuanceResultIssuances eventIndex maps = reissuance <- fetch reissuanceCid let label = "Stock reissuance " <> reissuance.reissuance_data.id assertNoPartialResultingStockIssuanceCoverage label maps reissuance.reissuance_data.resulting_security_ids - if allResultingStockIssuancesExist maps reissuance.reissuance_data.resulting_security_ids - then case Map.lookup reissuance.reissuance_data.security_id maps.stock_issuances_by_security_id of - Some sourceIssuanceCid -> do - sourceIssuance <- fetch sourceIssuanceCid - let targetSortKey = (reissuance.reissuance_data.date, 3, 0) - sourceQuantity <- stockSecurityQuantityBeforeEvent - eventIndex - maps - sourceIssuance.issuance_data - targetSortKey - resultingQuantity <- stockIssuanceQuantityTotal - maps - label - sourceIssuance.issuance_data.stock_class_id - reissuance.reissuance_data.date - targetSortKey - reissuance.reissuance_data.resulting_security_ids - assertMsg - (label <> " resulting quantity mismatch: expected " <> show sourceQuantity <> ", found " <> show resultingQuantity) - (resultingQuantity == sourceQuantity) - None -> assertMsg (label <> " source stock issuance not found: " <> reissuance.reissuance_data.security_id) False - else pure ()) + case Map.lookup reissuance.reissuance_data.security_id maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> + if allResultingStockIssuancesExist maps reissuance.reissuance_data.resulting_security_ids + then do + sourceIssuance <- fetch sourceIssuanceCid + let targetSortKey = (reissuance.reissuance_data.date, 3, 0) + sourceQuantity <- stockSecurityQuantityBeforeEvent + eventIndex + maps + sourceIssuance.issuance_data + targetSortKey + resultingQuantity <- stockIssuanceQuantityTotal + maps + label + sourceIssuance.issuance_data.stock_class_id + reissuance.reissuance_data.date + targetSortKey + reissuance.reissuance_data.resulting_security_ids + assertMsg + (label <> " resulting quantity mismatch: expected " <> show sourceQuantity <> ", found " <> show resultingQuantity) + (resultingQuantity == sourceQuantity) + else pure () + None -> assertMsg (label <> " source stock issuance not found: " <> reissuance.reissuance_data.security_id) False) (Map.values maps.stock_reissuances) validateStockConsolidationResultIssuances : StockClassShareEventIndex -> CapTableMaps -> Update () @@ -2067,6 +2088,12 @@ validateStockConsolidationResultIssuances eventIndex maps = (\consolidationCid -> do consolidation <- fetch consolidationCid let label = "Stock consolidation " <> consolidation.consolidation_data.id + mapA_ + (\sourceSecurityId -> + assertMsg + (label <> " source stock issuance not found: " <> sourceSecurityId) + (isSome (Map.lookup sourceSecurityId maps.stock_issuances_by_security_id))) + consolidation.consolidation_data.security_ids case Map.lookup consolidation.consolidation_data.resulting_security_id maps.stock_issuances_by_security_id of Some resultIssuanceCid -> do resultIssuance <- fetch resultIssuanceCid @@ -3866,9 +3893,24 @@ addStockClassSplitDateToStockPlans splitData maps index = index (Map.values maps.stock_plans) -stockPlanValidationDateIndex : CapTableMaps -> Update (Map Text [Time]) -stockPlanValidationDateIndex maps = do +addStockClassSplitDateToConvertedStockPlans : + [(Text, StockClassShareLot, [StockClassShareLot])] -> + StockClassSplitOcfData -> + Map Text [Time] -> + Map Text [Time] +addStockClassSplitDateToConvertedStockPlans transitions splitData index = + foldl + (\dates (stockPlanId, _, outputLots) -> + if any (\outputLot -> outputLot.stock_class_id == splitData.stock_class_id) outputLots + then addStockPlanDate stockPlanId splitData.date dates + else dates) + index + transitions + +stockPlanValidationDateIndex : StockClassShareEventIndex -> CapTableMaps -> Update (Map Text [Time]) +stockPlanValidationDateIndex eventIndex maps = do successorIndex <- stockPlanSuccessorIndex maps + conversionTransitions <- stockPlanConversionAllocationTransitions eventIndex successorIndex maps planDates <- foldlA (\index stockPlanCid -> do stockPlan <- fetch stockPlanCid @@ -3953,7 +3995,8 @@ stockPlanValidationDateIndex maps = do foldlA (\index splitCid -> do split <- fetch splitCid - addStockClassSplitDateToStockPlans split.split_data maps index) + planSplitDates <- addStockClassSplitDateToStockPlans split.split_data maps index + pure (addStockClassSplitDateToConvertedStockPlans conversionTransitions split.split_data planSplitDates)) equityCompensationRetractionDates (Map.values maps.stock_class_splits) @@ -3963,7 +4006,7 @@ validateStockPlanReservedSharesAtDates : Map Text [StockPlanPoolAdjustmentOcfData] -> Update () validateStockPlanReservedSharesAtDates eventIndex maps adjustmentHistory = do - validationDates <- stockPlanValidationDateIndex maps + validationDates <- stockPlanValidationDateIndex eventIndex maps mapA_ (\stockPlanCid -> do stockPlan <- fetch stockPlanCid From b9a776ecf69d2ac69637731cdd69fbb26144aedf Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 18:49:08 -0400 Subject: [PATCH 48/62] perf: bound duplicate security validation --- .../daml/Fairmint/OpenCapTable/Types/Validation.daml | 9 +++++++-- dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 +++--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml index ef67ff9a..016dc20f 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Validation.daml @@ -8,6 +8,7 @@ module Fairmint.OpenCapTable.Types.Validation where import qualified DA.Text as Text import qualified DA.Crypto.Text as CryptoText +import qualified DA.Set as Set import Fairmint.Shared.TypeHelpers (validateRequiredText) -- Shared Text array validator @@ -16,9 +17,13 @@ validateNonEmptyStringsArray : [Text] -> Bool validateNonEmptyStringsArray xs = all validateRequiredText xs hasDuplicateTexts : [Text] -> Bool -hasDuplicateTexts texts = case texts of +hasDuplicateTexts = hasDuplicateTextsWith Set.empty + +hasDuplicateTextsWith : Set.Set Text -> [Text] -> Bool +hasDuplicateTextsWith seen remaining = case remaining of [] -> False - text :: rest -> text `elem` rest || hasDuplicateTexts rest + text :: rest -> + text `Set.member` seen || hasDuplicateTextsWith (Set.insert text seen) rest -- Ensures no element appears more than once; empty arrays are allowed validateUniqueStringsArray : [Text] -> Bool diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index b2b0e6f0..0fc2e8ee 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3df9dc47f3f32ce07b2259cab092c5919bb1d016b827aea727bf26dbbe5a660e -size 2927228 +oid sha256:f466f746de059af163ada17d68525767648f8eb35f4638192148a0b5f14bfcc2 +size 2929026 diff --git a/dars/dars.lock b/dars/dars.lock index 346a0f85..330a9b75 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,10 +33,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "3df9dc47f3f32ce07b2259cab092c5919bb1d016b827aea727bf26dbbe5a660e", - "size": 2927228, + "sha256": "f466f746de059af163ada17d68525767648f8eb35f4638192148a0b5f14bfcc2", + "size": 2929026, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-10T22:21:21Z", + "uploadedAt": "2026-07-10T22:47:06.144Z", "networks": [] } } From 9106512eec515bbcfc2d106481f240e06ad17e16 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 19:01:31 -0400 Subject: [PATCH 49/62] fix: preserve stock plan class continuity --- .../TestStockPlanReservedShares.daml | 89 +++++++++++++++++++ .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- .../codegen/templates/CapTable.daml.template | 58 +++++++++--- 4 files changed, 141 insertions(+), 16 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index dc22ea61..fe355565 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -928,6 +928,35 @@ testStockPlanReservedShares_ReturnToPoolCanRolloverToDifferentPlan = script do deletes = [] pure () +testStockPlanReservedShares_ReturnToPoolRejectsTargetPlanOutsideSecurityClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let sourceStockClassId = "SC_RETURN_CLASS_SOURCE" + let targetStockClassId = "SC_RETURN_CLASS_TARGET" + let sourcePlanId = "PLAN_RETURN_CLASS_SOURCE" + let targetPlanId = "PLAN_RETURN_CLASS_TARGET" + (capTableWithSourceClass, _) <- setupStockClassWith issuer cap_table (defaultStockClass sourceStockClassId) + (capTableWithTargetClass, _) <- setupStockClassWith issuer capTableWithSourceClass (defaultStockClass targetStockClassId) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithTargetClass "SH_RETURN_CLASS_MISMATCH" "Return Class Holder" + let sourcePlan = (defaultStockPlan sourcePlanId sourceStockClassId) with initial_shares_reserved = 100.0 + let targetPlan = (defaultStockPlan targetPlanId targetStockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = stockPlanEquityCompensationIssuance "TX_RETURN_CLASS_SOURCE" "SEC_RETURN_CLASS_SOURCE" stakeholderId sourceStockClassId sourcePlanId 100.0 + let cancellation = equityCompensationCancellation "TX_RETURN_CLASS_CANCEL" sourceIssuance.security_id 100.0 + let returnToPool = stockPlanReturnToPool "TX_RETURN_CLASS_POOL" sourceIssuance.security_id targetPlanId 100.0 + let targetIssuance = stockPlanEquityCompensationIssuance "TX_RETURN_CLASS_TARGET" "SEC_RETURN_CLASS_TARGET" stakeholderId targetStockClassId targetPlanId 200.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan sourcePlan + , CT.OcfCreateStockPlan targetPlan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + , CT.OcfCreateEquityCompensationCancellation cancellation + , CT.OcfCreateStockPlanReturnToPool returnToPool + , CT.OcfCreateEquityCompensationIssuance targetIssuance + ] + edits = [] + deletes = [] + testStockPlanReservedShares_ReturnCannotCreateNegativeHistoricalAllocation = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table @@ -1669,6 +1698,37 @@ testStockPlanReservedShares_EquityCancellationRejectsSourceAsBalance = script do edits = [] deletes = [] +testStockPlanReservedShares_ClasslessEquityCancellationBalanceUsesPlanClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_CLASSLESS_EQUITY_CANCEL_BALANCE" + let sourceSecurityId = "SEC_CLASSLESS_EQUITY_CANCEL_SOURCE" + let balanceSecurityId = "SEC_CLASSLESS_EQUITY_CANCEL_BALANCE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (stockPlanEquityCompensationIssuance "TX_CLASSLESS_EQUITY_CANCEL_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0) with + stock_class_id = None + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let cancellation = (equityCompensationCancellation "TX_CLASSLESS_EQUITY_CANCEL" sourceSecurityId 40.0) with + balance_security_id = Some balanceSecurityId + let balanceIssuance = stockPlanEquityCompensationIssuance "TX_CLASSLESS_EQUITY_CANCEL_BALANCE" balanceSecurityId stakeholderId stockClassId stockPlanId 60.0 + _ <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation cancellation + , CT.OcfCreateEquityCompensationIssuance balanceIssuance + ] + edits = [] + deletes = [] + pure () + testStockPlanReservedShares_EquityTransferSuccessorDoesNotDoubleAllocate = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table @@ -1697,6 +1757,35 @@ testStockPlanReservedShares_EquityTransferSuccessorDoesNotDoubleAllocate = scrip deletes = [] pure () +testStockPlanReservedShares_ClasslessEquityTransferResultUsesPlanClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_CLASSLESS_EQUITY_TRANSFER" + let sourceSecurityId = "SEC_CLASSLESS_EQUITY_TRANSFER_SOURCE" + let resultSecurityId = "SEC_CLASSLESS_EQUITY_TRANSFER_RESULT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (stockPlanEquityCompensationIssuance "TX_CLASSLESS_EQUITY_TRANSFER_SOURCE" sourceSecurityId stakeholderId stockClassId stockPlanId 100.0) with + stock_class_id = None + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let resultIssuance = stockPlanEquityCompensationIssuance "TX_CLASSLESS_EQUITY_TRANSFER_RESULT" resultSecurityId stakeholderId stockClassId stockPlanId 100.0 + _ <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer (planEquityCompensationTransfer "TX_CLASSLESS_EQUITY_TRANSFER" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance resultIssuance + ] + edits = [] + deletes = [] + pure () + testStockPlanReservedShares_EquityTransferReducesSourceQuantity = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 0fc2e8ee..fa15f0fe 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f466f746de059af163ada17d68525767648f8eb35f4638192148a0b5f14bfcc2 -size 2929026 +oid sha256:f66af56fa2e6fcaed8bbab4adaa008ba9e2d190ba35cdefb9fccc634135228c5 +size 2932120 diff --git a/dars/dars.lock b/dars/dars.lock index 330a9b75..5ec40104 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,10 +33,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "f466f746de059af163ada17d68525767648f8eb35f4638192148a0b5f14bfcc2", - "size": 2929026, + "sha256": "f66af56fa2e6fcaed8bbab4adaa008ba9e2d190ba35cdefb9fccc634135228c5", + "size": 2932120, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-10T22:47:06.144Z", + "uploadedAt": "2026-07-10T23:00:56.353Z", "networks": [] } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index f8a3cff0..2e5d137d 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -1869,15 +1869,32 @@ equityCompensationIssuanceQuantityAtSortKey maps issuanceData targetSortKey = stockClassQuantityAtSortKey maps stockClassId issuanceData.date issuanceData.quantity targetSortKey None -> pure issuanceData.quantity +equityCompensationSuccessorStockClassIds : + CapTableMaps -> + EquityCompensationIssuanceOcfData -> + Update [Optional Text] +equityCompensationSuccessorStockClassIds maps issuanceData = + case issuanceData.stock_class_id of + Some stockClassId -> pure [Some stockClassId] + None -> case issuanceData.stock_plan_id of + Some stockPlanId -> case Map.lookup stockPlanId maps.stock_plans of + Some stockPlanCid -> do + stockPlan <- fetch stockPlanCid + pure $ case stockPlan.plan_data.stock_class_ids of + [stockClassId] -> [None, Some stockClassId] + _ -> [None] + None -> pure [None] + None -> pure [None] + equityCompensationIssuanceQuantityTotal : CapTableMaps -> Text -> - Optional Text -> + [Optional Text] -> Time -> (Time, Int, Int) -> [Text] -> Update Decimal -equityCompensationIssuanceQuantityTotal maps label expectedStockClassId expectedDate targetSortKey securityIds = +equityCompensationIssuanceQuantityTotal maps label expectedStockClassIds expectedDate targetSortKey securityIds = foldlA (\total securityId -> case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of @@ -1887,8 +1904,8 @@ equityCompensationIssuanceQuantityTotal maps label expectedStockClassId expected (label <> " successor date mismatch for " <> securityId <> ": expected " <> show expectedDate <> ", found " <> show issuance.issuance_data.date) (issuance.issuance_data.date == expectedDate) assertMsg - (label <> " successor stock class mismatch for " <> securityId <> ": expected " <> show expectedStockClassId <> ", found " <> show issuance.issuance_data.stock_class_id) - (issuance.issuance_data.stock_class_id == expectedStockClassId) + (label <> " successor stock class mismatch for " <> securityId <> ": expected one of " <> show expectedStockClassIds <> ", found " <> show issuance.issuance_data.stock_class_id) + (issuance.issuance_data.stock_class_id `elem` expectedStockClassIds) quantity <- equityCompensationIssuanceQuantityAtSortKey maps issuance.issuance_data targetSortKey pure (total + quantity) None -> do @@ -3340,13 +3357,13 @@ equityCompensationSecurityQuantityBeforeEvent maps issuanceData targetSortKey = validateEquityCompensationBalanceSuccessorQuantity : CapTableMaps -> Text -> - Optional Text -> + [Optional Text] -> Time -> (Time, Int, Int) -> Decimal -> Text -> Update () -validateEquityCompensationBalanceSuccessorQuantity maps label expectedStockClassId expectedDate targetSortKey expectedQuantity securityId = +validateEquityCompensationBalanceSuccessorQuantity maps label expectedStockClassIds expectedDate targetSortKey expectedQuantity securityId = case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid @@ -3354,8 +3371,8 @@ validateEquityCompensationBalanceSuccessorQuantity maps label expectedStockClass (label <> " balance date mismatch for " <> securityId <> ": expected " <> show expectedDate <> ", found " <> show issuance.issuance_data.date) (issuance.issuance_data.date == expectedDate) assertMsg - (label <> " balance stock class mismatch for " <> securityId <> ": expected " <> show expectedStockClassId <> ", found " <> show issuance.issuance_data.stock_class_id) - (issuance.issuance_data.stock_class_id == expectedStockClassId) + (label <> " balance stock class mismatch for " <> securityId <> ": expected one of " <> show expectedStockClassIds <> ", found " <> show issuance.issuance_data.stock_class_id) + (issuance.issuance_data.stock_class_id `elem` expectedStockClassIds) actualQuantity <- equityCompensationIssuanceQuantityAtSortKey maps issuance.issuance_data targetSortKey assertMsg (label <> " balance quantity mismatch for " <> securityId <> ": expected " <> show expectedQuantity <> ", found " <> show actualQuantity) @@ -3386,10 +3403,11 @@ validateEquityCompensationCancellationSuccessorContinuity maps = assertMsg (label <> " exceeds the current source quantity for " <> sourceIssuance.issuance_data.security_id) (expectedBalanceQuantity >= 0.0) + expectedStockClassIds <- equityCompensationSuccessorStockClassIds maps sourceIssuance.issuance_data validateEquityCompensationBalanceSuccessorQuantity maps label - sourceIssuance.issuance_data.stock_class_id + expectedStockClassIds cancellation.cancellation_data.date terminalSortKey expectedBalanceQuantity @@ -3418,10 +3436,11 @@ validateEquityCompensationTransferSuccessorContinuity maps = Some sourceIssuanceCid -> do sourceIssuance <- fetch sourceIssuanceCid let targetSortKey = stockReductionSortKey transfer.transfer_data.date transfer.transfer_data.balance_security_id + expectedStockClassIds <- equityCompensationSuccessorStockClassIds maps sourceIssuance.issuance_data resultingQuantity <- equityCompensationIssuanceQuantityTotal maps label - sourceIssuance.issuance_data.stock_class_id + expectedStockClassIds transfer.transfer_data.date targetSortKey transfer.transfer_data.resulting_security_ids @@ -3442,7 +3461,7 @@ validateEquityCompensationTransferSuccessorContinuity maps = validateEquityCompensationBalanceSuccessorQuantity maps label - sourceIssuance.issuance_data.stock_class_id + expectedStockClassIds transfer.transfer_data.date terminalSortKey expectedBalanceQuantity @@ -3606,6 +3625,22 @@ validateStockPlanReturnToPoolQuantities maps = do (returnedQuantity <= securityAllocation)) (Map.values maps.stock_plan_return_to_pools) +validateStockPlanReturnToPoolClasses : CapTableMaps -> Update () +validateStockPlanReturnToPoolClasses maps = + mapA_ + (\returnCid -> do + returnToPool <- fetch returnCid + stockClassIdOpt <- stockPlanReturnToPoolStockClassId maps returnToPool.return_data.security_id + case stockClassIdOpt of + Some stockClassId -> + assertStockPlanIncludesClass + ("Stock plan return-to-pool " <> returnToPool.return_data.id) + maps + returnToPool.return_data.stock_plan_id + stockClassId + None -> pure ()) + (Map.values maps.stock_plan_return_to_pools) + stockPlanNetAllocatedQuantityIndex : StockClassShareEventIndex -> CapTableMaps -> Update (Map Text Decimal) stockPlanNetAllocatedQuantityIndex eventIndex maps = do successorIndex <- stockPlanSuccessorIndex maps @@ -4036,6 +4071,7 @@ validateStockPlanPoolCeilings eventIndex maps = do validateStockPlanAllocationClasses maps validateStockPlanAllocationEffectiveDates maps validateStockPlanEquityCompensationSourceQuantities maps + validateStockPlanReturnToPoolClasses maps validateStockPlanReturnToPoolQuantities maps validateStockPlanReservedShares eventIndex maps adjustmentIndex validateStockPlanReservedSharesAtDates eventIndex maps adjustmentHistory From c03cf7277fb3c09a748a6a507194f33f0e6fc122 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 19:39:45 -0400 Subject: [PATCH 50/62] fix: close stock plan attribution gaps --- .../TestStockPlanReservedShares.daml | 244 ++++++++++++++++++ .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- .../codegen/templates/CapTable.daml.template | 59 ++++- 4 files changed, 303 insertions(+), 10 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index fe355565..5ee087f3 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -2185,3 +2185,247 @@ testStockPlanReservedShares_CumulativeReturnsAreSplitAdjusted = script do ] edits = [] deletes = [] + +testStockPlanReservedShares_ClasslessSuccessorRemainsValidWhenPlanAddsClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_CLASSLESS_SUCCESSOR_PLAN_EDIT_FIRST" + (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_CLASSLESS_SUCCESSOR_PLAN_EDIT_SECOND" + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_CLASSLESS_SUCCESSOR_PLAN_EDIT" "Classless Successor Plan Edit Holder" + let stockPlanId = "PLAN_CLASSLESS_SUCCESSOR_PLAN_EDIT" + let sourceSecurityId = "SEC_CLASSLESS_SUCCESSOR_PLAN_EDIT_SOURCE" + let resultSecurityId = "SEC_CLASSLESS_SUCCESSOR_PLAN_EDIT_RESULT" + let plan = (defaultStockPlan stockPlanId firstStockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_SUCCESSOR_PLAN_EDIT_SOURCE" + sourceSecurityId + stakeholderId + firstStockClassId + stockPlanId + 100.0) with stock_class_id = None + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let resultIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_SUCCESSOR_PLAN_EDIT_RESULT" + resultSecurityId + stakeholderId + firstStockClassId + stockPlanId + 100.0) with stock_plan_id = None + transferred <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer + (planEquityCompensationTransfer "TX_CLASSLESS_SUCCESSOR_PLAN_EDIT_TRANSFER" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance resultIssuance + ] + edits = [] + deletes = [] + + let editedPlan = plan with stock_class_ids = [firstStockClassId, secondStockClassId] + _ <- submit issuer do + exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockPlan editedPlan] + deletes = [] + pure () + +testStockPlanReservedShares_ClasslessSuccessorRejectsClassOutsidePlan = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithPlanClass, planStockClassId) <- setupStockClassWithId issuer cap_table "SC_CLASSLESS_SUCCESSOR_IN_PLAN" + (capTableWithClasses, outsideStockClassId) <- setupStockClassWithId issuer capTableWithPlanClass "SC_CLASSLESS_SUCCESSOR_OUTSIDE_PLAN" + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_CLASSLESS_SUCCESSOR_OUTSIDE_PLAN" "Classless Successor Outside Plan Holder" + let stockPlanId = "PLAN_CLASSLESS_SUCCESSOR_OUTSIDE_PLAN" + let sourceSecurityId = "SEC_CLASSLESS_SUCCESSOR_OUTSIDE_PLAN_SOURCE" + let resultSecurityId = "SEC_CLASSLESS_SUCCESSOR_OUTSIDE_PLAN_RESULT" + let plan = (defaultStockPlan stockPlanId planStockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_SUCCESSOR_OUTSIDE_PLAN_SOURCE" + sourceSecurityId + stakeholderId + planStockClassId + stockPlanId + 100.0) with stock_class_id = None + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let resultIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_SUCCESSOR_OUTSIDE_PLAN_RESULT" + resultSecurityId + stakeholderId + outsideStockClassId + stockPlanId + 100.0) with stock_plan_id = None + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer + (planEquityCompensationTransfer "TX_CLASSLESS_SUCCESSOR_OUTSIDE_PLAN_TRANSFER" sourceSecurityId resultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance resultIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_AmbiguousClasslessReturnStaysInSourcePlan = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_AMBIGUOUS_CLASSLESS_RETURN_FIRST" + (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_AMBIGUOUS_CLASSLESS_RETURN_SECOND" + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_AMBIGUOUS_CLASSLESS_RETURN" "Ambiguous Classless Return Holder" + let stockPlanId = "PLAN_AMBIGUOUS_CLASSLESS_RETURN" + let securityId = "SEC_AMBIGUOUS_CLASSLESS_RETURN" + let plan = (defaultStockPlan stockPlanId firstStockClassId) with + initial_shares_reserved = 100.0 + stock_class_ids = [firstStockClassId, secondStockClassId] + let issuance = (stockPlanEquityCompensationIssuance + "TX_AMBIGUOUS_CLASSLESS_RETURN_SOURCE" + securityId + stakeholderId + firstStockClassId + stockPlanId + 100.0) with stock_class_id = None + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance issuance + , CT.OcfCreateEquityCompensationCancellation + (equityCompensationCancellation "TX_AMBIGUOUS_CLASSLESS_RETURN_CANCEL" securityId 100.0) + , CT.OcfCreateStockPlanReturnToPool + (stockPlanReturnToPool "TX_AMBIGUOUS_CLASSLESS_RETURN_POOL" securityId stockPlanId 100.0) + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_AmbiguousClasslessReturnRejectsDifferentPlan = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_AMBIGUOUS_CLASSLESS_ROLLOVER_FIRST" + (capTableWithSecondClass, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_AMBIGUOUS_CLASSLESS_ROLLOVER_SECOND" + (capTableWithClasses, targetStockClassId) <- setupStockClassWithId issuer capTableWithSecondClass "SC_AMBIGUOUS_CLASSLESS_ROLLOVER_TARGET" + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_AMBIGUOUS_CLASSLESS_ROLLOVER" "Ambiguous Classless Rollover Holder" + let sourcePlanId = "PLAN_AMBIGUOUS_CLASSLESS_ROLLOVER_SOURCE" + let targetPlanId = "PLAN_AMBIGUOUS_CLASSLESS_ROLLOVER_TARGET" + let sourceSecurityId = "SEC_AMBIGUOUS_CLASSLESS_ROLLOVER_SOURCE" + let sourcePlan = (defaultStockPlan sourcePlanId firstStockClassId) with + initial_shares_reserved = 100.0 + stock_class_ids = [firstStockClassId, secondStockClassId] + let targetPlan = (defaultStockPlan targetPlanId targetStockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (stockPlanEquityCompensationIssuance + "TX_AMBIGUOUS_CLASSLESS_ROLLOVER_SOURCE" + sourceSecurityId + stakeholderId + firstStockClassId + sourcePlanId + 100.0) with stock_class_id = None + let targetIssuance = stockPlanEquityCompensationIssuance + "TX_AMBIGUOUS_CLASSLESS_ROLLOVER_TARGET" + "SEC_AMBIGUOUS_CLASSLESS_ROLLOVER_TARGET" + stakeholderId + targetStockClassId + targetPlanId + 200.0 + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan sourcePlan + , CT.OcfCreateStockPlan targetPlan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + , CT.OcfCreateEquityCompensationCancellation + (equityCompensationCancellation "TX_AMBIGUOUS_CLASSLESS_ROLLOVER_CANCEL" sourceSecurityId 100.0) + , CT.OcfCreateStockPlanReturnToPool + (stockPlanReturnToPool "TX_AMBIGUOUS_CLASSLESS_ROLLOVER_POOL" sourceSecurityId targetPlanId 100.0) + , CT.OcfCreateEquityCompensationIssuance targetIssuance + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ConsolidationAllowsSamePlanSources = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_CONSOLIDATION_SAME_PLAN" + let firstSourceSecurityId = "SEC_CONSOLIDATION_SAME_PLAN_FIRST" + let secondSourceSecurityId = "SEC_CONSOLIDATION_SAME_PLAN_SECOND" + let resultSecurityId = "SEC_CONSOLIDATION_SAME_PLAN_RESULT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let firstSourceIssuance = (defaultStockIssuance "TX_CONSOLIDATION_SAME_PLAN_FIRST" firstSourceSecurityId stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 40.0 + let secondSourceIssuance = (defaultStockIssuance "TX_CONSOLIDATION_SAME_PLAN_SECOND" secondSourceSecurityId stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 60.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance firstSourceIssuance + , CT.OcfCreateStockIssuance secondSourceIssuance + ] + edits = [] + deletes = [] + + let consolidationDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let resultIssuance = (defaultStockIssuance "TX_CONSOLIDATION_SAME_PLAN_RESULT" resultSecurityId stakeholderId stockClassId) with + date = consolidationDate + stock_plan_id = None + quantity = 100.0 + _ <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation + (planStockConsolidation "TX_CONSOLIDATION_SAME_PLAN" [firstSourceSecurityId, secondSourceSecurityId] resultSecurityId consolidationDate) + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_ConsolidationRejectsMixedPlanSources = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_CONSOLIDATION_MIXED_SOURCES" + let planSourceSecurityId = "SEC_CONSOLIDATION_MIXED_PLAN_SOURCE" + let nonPlanSourceSecurityId = "SEC_CONSOLIDATION_MIXED_NON_PLAN_SOURCE" + let resultSecurityId = "SEC_CONSOLIDATION_MIXED_RESULT" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let planSourceIssuance = (defaultStockIssuance "TX_CONSOLIDATION_MIXED_PLAN_SOURCE" planSourceSecurityId stakeholderId stockClassId) with + stock_plan_id = Some stockPlanId + quantity = 50.0 + let nonPlanSourceIssuance = (defaultStockIssuance "TX_CONSOLIDATION_MIXED_NON_PLAN_SOURCE" nonPlanSourceSecurityId stakeholderId stockClassId) with quantity = 50.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockIssuance planSourceIssuance + , CT.OcfCreateStockIssuance nonPlanSourceIssuance + ] + edits = [] + deletes = [] + + let consolidationDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let resultIssuance = (defaultStockIssuance "TX_CONSOLIDATION_MIXED_RESULT" resultSecurityId stakeholderId stockClassId) with + date = consolidationDate + stock_plan_id = None + quantity = 100.0 + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation + (planStockConsolidation "TX_CONSOLIDATION_MIXED_SOURCES" [planSourceSecurityId, nonPlanSourceSecurityId] resultSecurityId consolidationDate) + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index fa15f0fe..aa38cb8f 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f66af56fa2e6fcaed8bbab4adaa008ba9e2d190ba35cdefb9fccc634135228c5 -size 2932120 +oid sha256:43d20d6a470a4d61a0e04221ab093f78b03c3808f5d65b65c3c5b54d19495aed +size 2936842 diff --git a/dars/dars.lock b/dars/dars.lock index 5ec40104..360980ab 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,10 +33,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "f66af56fa2e6fcaed8bbab4adaa008ba9e2d190ba35cdefb9fccc634135228c5", - "size": 2932120, + "sha256": "43d20d6a470a4d61a0e04221ab093f78b03c3808f5d65b65c3c5b54d19495aed", + "size": 2936842, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-10T23:00:56.353Z", + "uploadedAt": "2026-07-10T23:38:38.084Z", "networks": [] } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 2e5d137d..d6365a63 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -1880,9 +1880,7 @@ equityCompensationSuccessorStockClassIds maps issuanceData = Some stockPlanId -> case Map.lookup stockPlanId maps.stock_plans of Some stockPlanCid -> do stockPlan <- fetch stockPlanCid - pure $ case stockPlan.plan_data.stock_class_ids of - [stockClassId] -> [None, Some stockClassId] - _ -> [None] + pure (None :: map Some stockPlan.plan_data.stock_class_ids) None -> pure [None] None -> pure [None] @@ -2624,6 +2622,26 @@ stabilizeStockPlanSuccessors maps sourcePlans currentIndex = do then pure currentIndex else stabilizeStockPlanSuccessors maps sourcePlans nextIndex +validateStockConsolidationPlanSources : + CapTableMaps -> + Map Text Text -> + Map Text (Time, Optional Text) -> + Update () +validateStockConsolidationPlanSources maps sourcePlans successorIndex = + mapA_ + (\consolidationCid -> do + consolidation <- fetch consolidationCid + let sourcePlanIds = map + (declaredPlanForSecurity sourcePlans successorIndex) + consolidation.consolidation_data.security_ids + case sourcePlanIds of + [] -> pure () + firstPlanId :: remainingPlanIds -> + assertMsg + ("Stock consolidation " <> consolidation.consolidation_data.id <> " cannot combine sources with different stock-plan attribution") + (all (== firstPlanId) remainingPlanIds)) + (Map.values maps.stock_consolidations) + stockPlanSuccessorIndex : CapTableMaps -> Update StockPlanSuccessorIndex stockPlanSuccessorIndex maps = do equityDeclaredPlans <- foldlA @@ -2647,6 +2665,7 @@ stockPlanSuccessorIndex maps = do (Map.values maps.stock_issuances) let stockSourcePlans = mergePlanIndexes stockDeclaredPlans equityEffectivePlans stockSuccessors <- stabilizeStockPlanSuccessors maps stockSourcePlans Map.empty + validateStockConsolidationPlanSources maps stockSourcePlans stockSuccessors pure StockPlanSuccessorIndex with stock_successors_by_security_id = stockSuccessors @@ -3612,6 +3631,26 @@ stockPlanSecurityReturnableQuantityAt successorIndex maps returnData asOf = do assertMsg ("Stock plan return to pool security not found: " <> returnData.security_id) False pure 0.0 +stockPlanSecurityEffectivePlanId : StockPlanSuccessorIndex -> CapTableMaps -> Text -> Update (Optional Text) +stockPlanSecurityEffectivePlanId successorIndex maps securityId = + case (Map.lookup securityId maps.stock_issuances_by_security_id, Map.lookup securityId maps.equity_compensation_issuances_by_security_id) of + (Some stockCid, None) -> do + issuance <- fetch stockCid + pure (effectivePlanForSecurity + successorIndex.stock_successors_by_security_id + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id) + (None, Some equityCompensationCid) -> do + issuance <- fetch equityCompensationCid + pure (effectivePlanForSecurity + successorIndex.equity_compensation_successors_by_security_id + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id) + (Some _, Some _) -> do + assertMsg ("Stock plan return to pool security id is ambiguous: " <> securityId) False + pure None + (None, None) -> pure None + validateStockPlanReturnToPoolQuantities : CapTableMaps -> Update () validateStockPlanReturnToPoolQuantities maps = do successorIndex <- stockPlanSuccessorIndex maps @@ -3626,11 +3665,13 @@ validateStockPlanReturnToPoolQuantities maps = do (Map.values maps.stock_plan_return_to_pools) validateStockPlanReturnToPoolClasses : CapTableMaps -> Update () -validateStockPlanReturnToPoolClasses maps = +validateStockPlanReturnToPoolClasses maps = do + successorIndex <- stockPlanSuccessorIndex maps mapA_ (\returnCid -> do returnToPool <- fetch returnCid stockClassIdOpt <- stockPlanReturnToPoolStockClassId maps returnToPool.return_data.security_id + sourcePlanIdOpt <- stockPlanSecurityEffectivePlanId successorIndex maps returnToPool.return_data.security_id case stockClassIdOpt of Some stockClassId -> assertStockPlanIncludesClass @@ -3638,7 +3679,15 @@ validateStockPlanReturnToPoolClasses maps = maps returnToPool.return_data.stock_plan_id stockClassId - None -> pure ()) + None -> case sourcePlanIdOpt of + Some sourcePlanId -> + assertMsg + ("Stock plan return-to-pool " <> returnToPool.return_data.id <> " cannot roll an ambiguous classless security into a different stock plan") + (returnToPool.return_data.stock_plan_id == sourcePlanId) + None -> + assertMsg + ("Stock plan return-to-pool " <> returnToPool.return_data.id <> " cannot determine the source stock plan") + False) (Map.values maps.stock_plan_return_to_pools) stockPlanNetAllocatedQuantityIndex : StockClassShareEventIndex -> CapTableMaps -> Update (Map Text Decimal) From bf668fa4443dd5c58b8939a99b3f94b0383a321a Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 20:13:26 -0400 Subject: [PATCH 51/62] fix: require explicit stock plan continuity --- .../TestStockPlanReservedShares.daml | 186 +++++++++++++++--- .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- .../codegen/templates/CapTable.daml.template | 39 +++- 4 files changed, 198 insertions(+), 37 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index 5ee087f3..19fa4758 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -2186,7 +2186,7 @@ testStockPlanReservedShares_CumulativeReturnsAreSplitAdjusted = script do edits = [] deletes = [] -testStockPlanReservedShares_ClasslessSuccessorRemainsValidWhenPlanAddsClass = script do +testStockPlanReservedShares_PlanAddClassRequiresSourceDisambiguation = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_CLASSLESS_SUCCESSOR_PLAN_EDIT_FIRST" (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_CLASSLESS_SUCCESSOR_PLAN_EDIT_SECOND" @@ -2229,11 +2229,21 @@ testStockPlanReservedShares_ClasslessSuccessorRemainsValidWhenPlanAddsClass = sc deletes = [] let editedPlan = plan with stock_class_ids = [firstStockClassId, secondStockClassId] - _ <- submit issuer do + submitMustFail issuer do exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with creates = [] edits = [CT.OcfEditStockPlan editedPlan] deletes = [] + + let disambiguatedSourceIssuance = sourceIssuance with stock_class_id = Some firstStockClassId + _ <- submit issuer do + exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = + [ CT.OcfEditStockPlan editedPlan + , CT.OcfEditEquityCompensationIssuance disambiguatedSourceIssuance + ] + deletes = [] pure () testStockPlanReservedShares_ClasslessSuccessorRejectsClassOutsidePlan = script do @@ -2278,6 +2288,71 @@ testStockPlanReservedShares_ClasslessSuccessorRejectsClassOutsidePlan = script d edits = [] deletes = [] +testStockPlanReservedShares_MultiClassClasslessSourceRequiresClasslessSuccessor = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_MULTICLASS_CLASSLESS_SUCCESSOR_FIRST" + (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_MULTICLASS_CLASSLESS_SUCCESSOR_SECOND" + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_MULTICLASS_CLASSLESS_SUCCESSOR" "Multi-Class Classless Successor Holder" + let stockPlanId = "PLAN_MULTICLASS_CLASSLESS_SUCCESSOR" + let sourceSecurityId = "SEC_MULTICLASS_CLASSLESS_SUCCESSOR_SOURCE" + let classedResultSecurityId = "SEC_MULTICLASS_CLASSLESS_SUCCESSOR_CLASSED_RESULT" + let classlessResultSecurityId = "SEC_MULTICLASS_CLASSLESS_SUCCESSOR_CLASSLESS_RESULT" + let plan = (defaultStockPlan stockPlanId firstStockClassId) with + initial_shares_reserved = 100.0 + stock_class_ids = [firstStockClassId, secondStockClassId] + let sourceIssuance = (stockPlanEquityCompensationIssuance + "TX_MULTICLASS_CLASSLESS_SUCCESSOR_SOURCE" + sourceSecurityId + stakeholderId + firstStockClassId + stockPlanId + 100.0) with stock_class_id = None + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let classedResultIssuance = (stockPlanEquityCompensationIssuance + "TX_MULTICLASS_CLASSLESS_SUCCESSOR_CLASSED_RESULT" + classedResultSecurityId + stakeholderId + firstStockClassId + stockPlanId + 100.0) with stock_plan_id = None + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer + (planEquityCompensationTransfer "TX_MULTICLASS_CLASSLESS_SUCCESSOR_CLASSED_TRANSFER" sourceSecurityId classedResultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance classedResultIssuance + ] + edits = [] + deletes = [] + + let classlessResultIssuance = (stockPlanEquityCompensationIssuance + "TX_MULTICLASS_CLASSLESS_SUCCESSOR_CLASSLESS_RESULT" + classlessResultSecurityId + stakeholderId + firstStockClassId + stockPlanId + 100.0) with + stock_class_id = None + stock_plan_id = None + _ <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer + (planEquityCompensationTransfer "TX_MULTICLASS_CLASSLESS_SUCCESSOR_CLASSLESS_TRANSFER" sourceSecurityId classlessResultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance classlessResultIssuance + ] + edits = [] + deletes = [] + pure () + testStockPlanReservedShares_AmbiguousClasslessReturnStaysInSourcePlan = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_AMBIGUOUS_CLASSLESS_RETURN_FIRST" @@ -2310,48 +2385,69 @@ testStockPlanReservedShares_AmbiguousClasslessReturnStaysInSourcePlan = script d deletes = [] pure () -testStockPlanReservedShares_AmbiguousClasslessReturnRejectsDifferentPlan = script do +testStockPlanReservedShares_ClasslessCrossPlanReturnRequiresRecordedClass = script do TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_AMBIGUOUS_CLASSLESS_ROLLOVER_FIRST" - (capTableWithSecondClass, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_AMBIGUOUS_CLASSLESS_ROLLOVER_SECOND" - (capTableWithClasses, targetStockClassId) <- setupStockClassWithId issuer capTableWithSecondClass "SC_AMBIGUOUS_CLASSLESS_ROLLOVER_TARGET" - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_AMBIGUOUS_CLASSLESS_ROLLOVER" "Ambiguous Classless Rollover Holder" - let sourcePlanId = "PLAN_AMBIGUOUS_CLASSLESS_ROLLOVER_SOURCE" - let targetPlanId = "PLAN_AMBIGUOUS_CLASSLESS_ROLLOVER_TARGET" - let sourceSecurityId = "SEC_AMBIGUOUS_CLASSLESS_ROLLOVER_SOURCE" - let sourcePlan = (defaultStockPlan sourcePlanId firstStockClassId) with - initial_shares_reserved = 100.0 - stock_class_ids = [firstStockClassId, secondStockClassId] - let targetPlan = (defaultStockPlan targetPlanId targetStockClassId) with initial_shares_reserved = 100.0 + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourcePlanId = "PLAN_CLASSLESS_ROLLOVER_SOURCE" + let targetPlanId = "PLAN_CLASSLESS_ROLLOVER_TARGET" + let sourceSecurityId = "SEC_CLASSLESS_ROLLOVER_SOURCE" + let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 100.0 + let targetPlan = (defaultStockPlan targetPlanId stockClassId) with initial_shares_reserved = 100.0 let sourceIssuance = (stockPlanEquityCompensationIssuance - "TX_AMBIGUOUS_CLASSLESS_ROLLOVER_SOURCE" + "TX_CLASSLESS_ROLLOVER_SOURCE" sourceSecurityId stakeholderId - firstStockClassId + stockClassId sourcePlanId 100.0) with stock_class_id = None let targetIssuance = stockPlanEquityCompensationIssuance - "TX_AMBIGUOUS_CLASSLESS_ROLLOVER_TARGET" - "SEC_AMBIGUOUS_CLASSLESS_ROLLOVER_TARGET" + "TX_CLASSLESS_ROLLOVER_TARGET" + "SEC_CLASSLESS_ROLLOVER_TARGET" stakeholderId - targetStockClassId + stockClassId targetPlanId 200.0 - - submitMustFail issuer do + source <- submit issuer do exerciseCmd capTableReady CT.UpdateCapTable with creates = [ CT.OcfCreateStockPlan sourcePlan , CT.OcfCreateStockPlan targetPlan , CT.OcfCreateEquityCompensationIssuance sourceIssuance - , CT.OcfCreateEquityCompensationCancellation - (equityCompensationCancellation "TX_AMBIGUOUS_CLASSLESS_ROLLOVER_CANCEL" sourceSecurityId 100.0) + ] + edits = [] + deletes = [] + + let cancellation = equityCompensationCancellation "TX_CLASSLESS_ROLLOVER_CANCEL" sourceSecurityId 100.0 + let returnToPool = stockPlanReturnToPool "TX_CLASSLESS_ROLLOVER_POOL" sourceSecurityId targetPlanId 100.0 + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation cancellation + , CT.OcfCreateStockPlanReturnToPool returnToPool + , CT.OcfCreateEquityCompensationIssuance targetIssuance + ] + edits = [] + deletes = [] + + let disambiguatedSourceIssuance = sourceIssuance with stock_class_id = Some stockClassId + disambiguated <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditEquityCompensationIssuance disambiguatedSourceIssuance] + deletes = [] + + _ <- submit issuer do + exerciseCmd disambiguated.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation + cancellation , CT.OcfCreateStockPlanReturnToPool - (stockPlanReturnToPool "TX_AMBIGUOUS_CLASSLESS_ROLLOVER_POOL" sourceSecurityId targetPlanId 100.0) + returnToPool , CT.OcfCreateEquityCompensationIssuance targetIssuance ] edits = [] deletes = [] + pure () testStockPlanReservedShares_ConsolidationAllowsSamePlanSources = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -2393,6 +2489,48 @@ testStockPlanReservedShares_ConsolidationAllowsSamePlanSources = script do deletes = [] pure () +testStockPlanReservedShares_ConsolidationRejectsDifferentResultPlan = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourcePlanId = "PLAN_CONSOLIDATION_RESULT_SOURCE" + let resultPlanId = "PLAN_CONSOLIDATION_RESULT_TARGET" + let firstSourceSecurityId = "SEC_CONSOLIDATION_RESULT_FIRST" + let secondSourceSecurityId = "SEC_CONSOLIDATION_RESULT_SECOND" + let resultSecurityId = "SEC_CONSOLIDATION_RESULT_DIFFERENT_PLAN" + let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 100.0 + let resultPlan = (defaultStockPlan resultPlanId stockClassId) with initial_shares_reserved = 100.0 + let firstSourceIssuance = (defaultStockIssuance "TX_CONSOLIDATION_RESULT_FIRST" firstSourceSecurityId stakeholderId stockClassId) with + stock_plan_id = Some sourcePlanId + quantity = 40.0 + let secondSourceIssuance = (defaultStockIssuance "TX_CONSOLIDATION_RESULT_SECOND" secondSourceSecurityId stakeholderId stockClassId) with + stock_plan_id = Some sourcePlanId + quantity = 60.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan sourcePlan + , CT.OcfCreateStockPlan resultPlan + , CT.OcfCreateStockIssuance firstSourceIssuance + , CT.OcfCreateStockIssuance secondSourceIssuance + ] + edits = [] + deletes = [] + + let consolidationDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let resultIssuance = (defaultStockIssuance "TX_CONSOLIDATION_RESULT_DIFFERENT_PLAN" resultSecurityId stakeholderId stockClassId) with + date = consolidationDate + stock_plan_id = Some resultPlanId + quantity = 100.0 + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockConsolidation + (planStockConsolidation "TX_CONSOLIDATION_RESULT_DIFFERENT_PLAN" [firstSourceSecurityId, secondSourceSecurityId] resultSecurityId consolidationDate) + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + testStockPlanReservedShares_ConsolidationRejectsMixedPlanSources = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index aa38cb8f..171acb39 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:43d20d6a470a4d61a0e04221ab093f78b03c3808f5d65b65c3c5b54d19495aed -size 2936842 +oid sha256:9c213eb74ed1b0c5db3586e2a421393c913c0f302f9b706be2bf0a862df76172 +size 2939365 diff --git a/dars/dars.lock b/dars/dars.lock index 360980ab..8b0ed819 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,10 +33,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "43d20d6a470a4d61a0e04221ab093f78b03c3808f5d65b65c3c5b54d19495aed", - "size": 2936842, + "sha256": "9c213eb74ed1b0c5db3586e2a421393c913c0f302f9b706be2bf0a862df76172", + "size": 2939365, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-10T23:38:38.084Z", + "uploadedAt": "2026-07-11T00:12:43.932Z", "networks": [] } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index d6365a63..3544bc76 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -1880,7 +1880,9 @@ equityCompensationSuccessorStockClassIds maps issuanceData = Some stockPlanId -> case Map.lookup stockPlanId maps.stock_plans of Some stockPlanCid -> do stockPlan <- fetch stockPlanCid - pure (None :: map Some stockPlan.plan_data.stock_class_ids) + pure $ case stockPlan.plan_data.stock_class_ids of + [stockClassId] -> [None, Some stockClassId] + _ -> [None] None -> pure [None] None -> pure [None] @@ -2622,12 +2624,12 @@ stabilizeStockPlanSuccessors maps sourcePlans currentIndex = do then pure currentIndex else stabilizeStockPlanSuccessors maps sourcePlans nextIndex -validateStockConsolidationPlanSources : +validateStockConsolidationPlanContinuity : CapTableMaps -> Map Text Text -> Map Text (Time, Optional Text) -> Update () -validateStockConsolidationPlanSources maps sourcePlans successorIndex = +validateStockConsolidationPlanContinuity maps sourcePlans successorIndex = mapA_ (\consolidationCid -> do consolidation <- fetch consolidationCid @@ -2636,10 +2638,17 @@ validateStockConsolidationPlanSources maps sourcePlans successorIndex = consolidation.consolidation_data.security_ids case sourcePlanIds of [] -> pure () - firstPlanId :: remainingPlanIds -> + firstPlanId :: remainingPlanIds -> do assertMsg ("Stock consolidation " <> consolidation.consolidation_data.id <> " cannot combine sources with different stock-plan attribution") - (all (== firstPlanId) remainingPlanIds)) + (all (== firstPlanId) remainingPlanIds) + assertMsg + ("Stock consolidation " <> consolidation.consolidation_data.id <> " resulting security must preserve source stock-plan attribution") + (declaredPlanForSecurity + sourcePlans + successorIndex + consolidation.consolidation_data.resulting_security_id + == firstPlanId)) (Map.values maps.stock_consolidations) stockPlanSuccessorIndex : CapTableMaps -> Update StockPlanSuccessorIndex @@ -2665,7 +2674,7 @@ stockPlanSuccessorIndex maps = do (Map.values maps.stock_issuances) let stockSourcePlans = mergePlanIndexes stockDeclaredPlans equityEffectivePlans stockSuccessors <- stabilizeStockPlanSuccessors maps stockSourcePlans Map.empty - validateStockConsolidationPlanSources maps stockSourcePlans stockSuccessors + validateStockConsolidationPlanContinuity maps stockSourcePlans stockSuccessors pure StockPlanSuccessorIndex with stock_successors_by_security_id = stockSuccessors @@ -3651,6 +3660,20 @@ stockPlanSecurityEffectivePlanId successorIndex maps securityId = pure None (None, None) -> pure None +stockPlanSecurityRecordedStockClassId : CapTableMaps -> Text -> Update (Optional Text) +stockPlanSecurityRecordedStockClassId maps securityId = + case (Map.lookup securityId maps.stock_issuances_by_security_id, Map.lookup securityId maps.equity_compensation_issuances_by_security_id) of + (Some stockCid, None) -> do + issuance <- fetch stockCid + pure (Some issuance.issuance_data.stock_class_id) + (None, Some equityCompensationCid) -> do + issuance <- fetch equityCompensationCid + pure issuance.issuance_data.stock_class_id + (Some _, Some _) -> do + assertMsg ("Stock plan return to pool security id is ambiguous: " <> securityId) False + pure None + (None, None) -> pure None + validateStockPlanReturnToPoolQuantities : CapTableMaps -> Update () validateStockPlanReturnToPoolQuantities maps = do successorIndex <- stockPlanSuccessorIndex maps @@ -3670,7 +3693,7 @@ validateStockPlanReturnToPoolClasses maps = do mapA_ (\returnCid -> do returnToPool <- fetch returnCid - stockClassIdOpt <- stockPlanReturnToPoolStockClassId maps returnToPool.return_data.security_id + stockClassIdOpt <- stockPlanSecurityRecordedStockClassId maps returnToPool.return_data.security_id sourcePlanIdOpt <- stockPlanSecurityEffectivePlanId successorIndex maps returnToPool.return_data.security_id case stockClassIdOpt of Some stockClassId -> @@ -3682,7 +3705,7 @@ validateStockPlanReturnToPoolClasses maps = do None -> case sourcePlanIdOpt of Some sourcePlanId -> assertMsg - ("Stock plan return-to-pool " <> returnToPool.return_data.id <> " cannot roll an ambiguous classless security into a different stock plan") + ("Stock plan return-to-pool " <> returnToPool.return_data.id <> " cannot roll a security without a recorded stock class into a different stock plan") (returnToPool.return_data.stock_plan_id == sourcePlanId) None -> assertMsg From 4db6ca5a36f8814fbff93ff7588865d263a61b7c Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 20:34:59 -0400 Subject: [PATCH 52/62] fix: preserve historical stock plan attribution --- .../OpenCapTable/TestLifecycleInvariants.daml | 80 ++++++ .../TestStockPlanReservedShares.daml | 256 +++++++++++++++-- .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 260 ++++++++++++++++-- 5 files changed, 549 insertions(+), 55 deletions(-) diff --git a/Test/daml/OpenCapTable/TestLifecycleInvariants.daml b/Test/daml/OpenCapTable/TestLifecycleInvariants.daml index 4e55399e..2f706e49 100644 --- a/Test/daml/OpenCapTable/TestLifecycleInvariants.daml +++ b/Test/daml/OpenCapTable/TestLifecycleInvariants.daml @@ -18,6 +18,7 @@ import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup import OpenCapTable.TestHelpers +import qualified DA.Map as Map import Daml.Script lifecycleWarrantTrigger : Text -> Text -> OcfConversionTrigger @@ -285,6 +286,85 @@ testRepeatedEquityTerminalEventsAtSameTimestampFail = script do edits = [] deletes = [] +testSecurityIssuanceDuplicateAcrossTypesFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_GLOBAL_SECURITY_ID" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_GLOBAL_SECURITY_ID" "Global Security ID Holder" + let sharedSecurityId = "SEC_GLOBAL_SECURITY_ID" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance (defaultStockIssuance "TX_GLOBAL_SECURITY_ID_STOCK" sharedSecurityId stakeholderId stockClassId)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_GLOBAL_SECURITY_ID_WARRANT" sharedSecurityId stakeholderId stockClassId)] + edits = [] + deletes = [] + +testSecurityIssuanceDuplicateAcrossTypesInSameBatchFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_BATCH_SECURITY_ID" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_BATCH_SECURITY_ID" "Batch Security ID Holder" + let sharedSecurityId = "SEC_BATCH_SECURITY_ID" + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance (defaultStockIssuance "TX_BATCH_SECURITY_ID_STOCK" sharedSecurityId stakeholderId stockClassId) + , CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_BATCH_SECURITY_ID_WARRANT" sharedSecurityId stakeholderId stockClassId) + ] + edits = [] + deletes = [] + +testSecurityIssuanceEditCannotReuseAnotherTypeSecurityId = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EDIT_SECURITY_ID" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EDIT_SECURITY_ID" "Edit Security ID Holder" + let stockSecurityId = "SEC_EDIT_SECURITY_ID_STOCK" + let warrantSecurityId = "SEC_EDIT_SECURITY_ID_WARRANT" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance (defaultStockIssuance "TX_EDIT_SECURITY_ID_STOCK" stockSecurityId stakeholderId stockClassId) + , CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_EDIT_SECURITY_ID_WARRANT" warrantSecurityId stakeholderId stockClassId) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockIssuance (defaultStockIssuance "TX_EDIT_SECURITY_ID_STOCK" warrantSecurityId stakeholderId stockClassId)] + deletes = [] + +testFinalSecurityIdUniquenessRejectsStaleLegacyIndex = script do + TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_LEGACY_SECURITY_ID" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_LEGACY_SECURITY_ID" "Legacy Security ID Holder" + let sharedSecurityId = "SEC_LEGACY_SECURITY_ID" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance (defaultStockIssuance "TX_LEGACY_SECURITY_ID_STOCK" sharedSecurityId stakeholderId stockClassId)] + edits = [] + deletes = [] + capTableDataOpt <- queryContractId issuer issued.updatedCapTableCid + let Some capTableData = capTableDataOpt + + duplicateWarrantCid <- submit (actAs issuer <> actAs system_operator) $ createCmd $ WarrantIssuance with + context = ctx + issuance_data = lifecycleWarrantIssuance "TX_LEGACY_SECURITY_ID_WARRANT" sharedSecurityId stakeholderId stockClassId + let corruptedCapTable = capTableData with + warrant_issuances = Map.insert "TX_LEGACY_SECURITY_ID_WARRANT" duplicateWarrantCid capTableData.warrant_issuances + corruptedCapTableCid <- submit (actAs issuer <> actAs system_operator) $ createCmd corruptedCapTable + + submitMustFail issuer do + exerciseCmd corruptedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [] + testBalanceSecurityCannotAlsoBeResultingSecurity = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_BALANCE_RESULT_ALIAS" 1_000.0) diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index 19fa4758..dd3183f8 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -2186,7 +2186,7 @@ testStockPlanReservedShares_CumulativeReturnsAreSplitAdjusted = script do edits = [] deletes = [] -testStockPlanReservedShares_PlanAddClassRequiresSourceDisambiguation = script do +testStockPlanReservedShares_ClasslessSuccessorRemainsValidWhenPlanAddsClass = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_CLASSLESS_SUCCESSOR_PLAN_EDIT_FIRST" (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_CLASSLESS_SUCCESSOR_PLAN_EDIT_SECOND" @@ -2229,20 +2229,186 @@ testStockPlanReservedShares_PlanAddClassRequiresSourceDisambiguation = script do deletes = [] let editedPlan = plan with stock_class_ids = [firstStockClassId, secondStockClassId] - submitMustFail issuer do + planEdited <- submit issuer do exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with creates = [] edits = [CT.OcfEditStockPlan editedPlan] deletes = [] - let disambiguatedSourceIssuance = sourceIssuance with stock_class_id = Some firstStockClassId - _ <- submit issuer do - exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + let reclassifiedResult = resultIssuance with stock_class_id = Some secondStockClassId + submitMustFail issuer do + exerciseCmd planEdited.updatedCapTableCid CT.UpdateCapTable with creates = [] - edits = - [ CT.OcfEditStockPlan editedPlan - , CT.OcfEditEquityCompensationIssuance disambiguatedSourceIssuance + edits = [CT.OcfEditEquityCompensationIssuance reclassifiedResult] + deletes = [] + pure () + +testStockPlanReservedShares_ClasslessSuccessorCannotUseNewlyAddedPlanClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_CLASSLESS_NEW_CLASS_FIRST" + (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_CLASSLESS_NEW_CLASS_SECOND" + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_CLASSLESS_NEW_CLASS" "Classless New Class Holder" + let stockPlanId = "PLAN_CLASSLESS_NEW_CLASS" + let sourceSecurityId = "SEC_CLASSLESS_NEW_CLASS_SOURCE" + let newClassResultSecurityId = "SEC_CLASSLESS_NEW_CLASS_RESULT" + let priorClassResultSecurityId = "SEC_CLASSLESS_PRIOR_CLASS_RESULT" + let plan = (defaultStockPlan stockPlanId firstStockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_NEW_CLASS_SOURCE" + sourceSecurityId + stakeholderId + firstStockClassId + stockPlanId + 100.0) with stock_class_id = None + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let editedPlan = plan with stock_class_ids = [firstStockClassId, secondStockClassId] + let newClassResultIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_NEW_CLASS_RESULT" + newClassResultSecurityId + stakeholderId + secondStockClassId + stockPlanId + 100.0) with stock_plan_id = None + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer + (planEquityCompensationTransfer "TX_CLASSLESS_NEW_CLASS_TRANSFER" sourceSecurityId newClassResultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance newClassResultIssuance + ] + edits = [CT.OcfEditStockPlan editedPlan] + deletes = [] + + let priorClassResultIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_PRIOR_CLASS_RESULT" + priorClassResultSecurityId + stakeholderId + firstStockClassId + stockPlanId + 100.0) with stock_plan_id = None + _ <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer + (planEquityCompensationTransfer "TX_CLASSLESS_PRIOR_CLASS_TRANSFER" sourceSecurityId priorClassResultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance priorClassResultIssuance + ] + edits = [CT.OcfEditStockPlan editedPlan] + deletes = [] + pure () + +testStockPlanReservedShares_ClasslessTransferCannotAdoptExistingNewClassSecurity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_CLASSLESS_ADOPT_FIRST" + (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_CLASSLESS_ADOPT_SECOND" + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_CLASSLESS_ADOPT" "Classless Adopt Holder" + let stockPlanId = "PLAN_CLASSLESS_ADOPT" + let sourceSecurityId = "SEC_CLASSLESS_ADOPT_SOURCE" + let existingSecurityId = "SEC_CLASSLESS_ADOPT_EXISTING" + let plan = (defaultStockPlan stockPlanId firstStockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_ADOPT_SOURCE" + sourceSecurityId + stakeholderId + firstStockClassId + stockPlanId + 100.0) with stock_class_id = None + let existingIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_ADOPT_EXISTING" + existingSecurityId + stakeholderId + secondStockClassId + stockPlanId + 100.0) with stock_plan_id = None + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + , CT.OcfCreateEquityCompensationIssuance existingIssuance + ] + edits = [] + deletes = [] + + let editedPlan = plan with stock_class_ids = [firstStockClassId, secondStockClassId] + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer + (planEquityCompensationTransfer "TX_CLASSLESS_ADOPT_TRANSFER" sourceSecurityId existingSecurityId 100.0) + ] + edits = [CT.OcfEditStockPlan editedPlan] + deletes = [] + +testStockPlanReservedShares_ClasslessCancellationCannotUseNewlyAddedPlanClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_CLASSLESS_CANCEL_NEW_CLASS_FIRST" + (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_CLASSLESS_CANCEL_NEW_CLASS_SECOND" + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_CLASSLESS_CANCEL_NEW_CLASS" "Classless Cancellation New Class Holder" + let stockPlanId = "PLAN_CLASSLESS_CANCEL_NEW_CLASS" + let sourceSecurityId = "SEC_CLASSLESS_CANCEL_NEW_CLASS_SOURCE" + let newClassBalanceSecurityId = "SEC_CLASSLESS_CANCEL_NEW_CLASS_BALANCE" + let priorClassBalanceSecurityId = "SEC_CLASSLESS_CANCEL_PRIOR_CLASS_BALANCE" + let plan = (defaultStockPlan stockPlanId firstStockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_CANCEL_NEW_CLASS_SOURCE" + sourceSecurityId + stakeholderId + firstStockClassId + stockPlanId + 100.0) with stock_class_id = None + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance ] + edits = [] + deletes = [] + + let editedPlan = plan with stock_class_ids = [firstStockClassId, secondStockClassId] + let newClassCancellation = (equityCompensationCancellation "TX_CLASSLESS_CANCEL_NEW_CLASS" sourceSecurityId 40.0) with + balance_security_id = Some newClassBalanceSecurityId + let newClassBalanceIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_CANCEL_NEW_CLASS_BALANCE" + newClassBalanceSecurityId + stakeholderId + secondStockClassId + stockPlanId + 60.0) with stock_plan_id = None + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation newClassCancellation + , CT.OcfCreateEquityCompensationIssuance newClassBalanceIssuance + ] + edits = [CT.OcfEditStockPlan editedPlan] + deletes = [] + + let priorClassCancellation = (equityCompensationCancellation "TX_CLASSLESS_CANCEL_PRIOR_CLASS" sourceSecurityId 40.0) with + balance_security_id = Some priorClassBalanceSecurityId + let priorClassBalanceIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_CANCEL_PRIOR_CLASS_BALANCE" + priorClassBalanceSecurityId + stakeholderId + firstStockClassId + stockPlanId + 60.0) with stock_plan_id = None + _ <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation priorClassCancellation + , CT.OcfCreateEquityCompensationIssuance priorClassBalanceIssuance + ] + edits = [CT.OcfEditStockPlan editedPlan] deletes = [] pure () @@ -2385,28 +2551,30 @@ testStockPlanReservedShares_AmbiguousClasslessReturnStaysInSourcePlan = script d deletes = [] pure () -testStockPlanReservedShares_ClasslessCrossPlanReturnRequiresRecordedClass = script do +testStockPlanReservedShares_ClasslessRolloverCannotBackdateNewPlanClass = script do TestOcp{issuer, cap_table} <- setupTestOcp - (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_CLASSLESS_ROLLOVER_BACKDATE_FIRST" + (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_CLASSLESS_ROLLOVER_BACKDATE_SECOND" + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_CLASSLESS_ROLLOVER_BACKDATE" "Classless Rollover Backdate Holder" let sourcePlanId = "PLAN_CLASSLESS_ROLLOVER_SOURCE" let targetPlanId = "PLAN_CLASSLESS_ROLLOVER_TARGET" let sourceSecurityId = "SEC_CLASSLESS_ROLLOVER_SOURCE" - let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 100.0 - let targetPlan = (defaultStockPlan targetPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourcePlan = (defaultStockPlan sourcePlanId firstStockClassId) with initial_shares_reserved = 100.0 + let targetPlan = (defaultStockPlan targetPlanId secondStockClassId) with initial_shares_reserved = 100.0 let sourceIssuance = (stockPlanEquityCompensationIssuance "TX_CLASSLESS_ROLLOVER_SOURCE" sourceSecurityId stakeholderId - stockClassId + firstStockClassId sourcePlanId 100.0) with stock_class_id = None let targetIssuance = stockPlanEquityCompensationIssuance "TX_CLASSLESS_ROLLOVER_TARGET" "SEC_CLASSLESS_ROLLOVER_TARGET" stakeholderId - stockClassId + secondStockClassId targetPlanId - 200.0 + 100.0 source <- submit issuer do exerciseCmd capTableReady CT.UpdateCapTable with creates = @@ -2419,6 +2587,7 @@ testStockPlanReservedShares_ClasslessCrossPlanReturnRequiresRecordedClass = scri let cancellation = equityCompensationCancellation "TX_CLASSLESS_ROLLOVER_CANCEL" sourceSecurityId 100.0 let returnToPool = stockPlanReturnToPool "TX_CLASSLESS_ROLLOVER_POOL" sourceSecurityId targetPlanId 100.0 + let editedSourcePlan = sourcePlan with stock_class_ids = [firstStockClassId, secondStockClassId] submitMustFail issuer do exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with creates = @@ -2426,25 +2595,58 @@ testStockPlanReservedShares_ClasslessCrossPlanReturnRequiresRecordedClass = scri , CT.OcfCreateStockPlanReturnToPool returnToPool , CT.OcfCreateEquityCompensationIssuance targetIssuance ] + edits = [CT.OcfEditStockPlan editedSourcePlan] + deletes = [] + +testStockPlanReservedShares_ClasslessRolloverRemainsValidWhenSourcePlanAddsClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_CLASSLESS_ROLLOVER_FIRST" + (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_CLASSLESS_ROLLOVER_SECOND" + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_CLASSLESS_ROLLOVER_PLAN_EDIT" "Classless Rollover Plan Edit Holder" + let sourcePlanId = "PLAN_CLASSLESS_ROLLOVER_SOURCE" + let targetPlanId = "PLAN_CLASSLESS_ROLLOVER_TARGET" + let sourceSecurityId = "SEC_CLASSLESS_ROLLOVER_SOURCE" + let sourcePlan = (defaultStockPlan sourcePlanId firstStockClassId) with initial_shares_reserved = 100.0 + let targetPlan = (defaultStockPlan targetPlanId firstStockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_ROLLOVER_SOURCE" + sourceSecurityId + stakeholderId + firstStockClassId + sourcePlanId + 100.0) with stock_class_id = None + let targetIssuance = stockPlanEquityCompensationIssuance + "TX_CLASSLESS_ROLLOVER_TARGET" + "SEC_CLASSLESS_ROLLOVER_TARGET" + stakeholderId + firstStockClassId + targetPlanId + 200.0 + rolledOver <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan sourcePlan + , CT.OcfCreateStockPlan targetPlan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + , CT.OcfCreateEquityCompensationCancellation + (equityCompensationCancellation "TX_CLASSLESS_ROLLOVER_CANCEL" sourceSecurityId 100.0) + , CT.OcfCreateStockPlanReturnToPool + (stockPlanReturnToPool "TX_CLASSLESS_ROLLOVER_POOL" sourceSecurityId targetPlanId 100.0) + , CT.OcfCreateEquityCompensationIssuance targetIssuance + ] edits = [] deletes = [] - let disambiguatedSourceIssuance = sourceIssuance with stock_class_id = Some stockClassId - disambiguated <- submit issuer do - exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + let editedSourcePlan = sourcePlan with stock_class_ids = [firstStockClassId, secondStockClassId] + planEdited <- submit issuer do + exerciseCmd rolledOver.updatedCapTableCid CT.UpdateCapTable with creates = [] - edits = [CT.OcfEditEquityCompensationIssuance disambiguatedSourceIssuance] + edits = [CT.OcfEditStockPlan editedSourcePlan] deletes = [] _ <- submit issuer do - exerciseCmd disambiguated.updatedCapTableCid CT.UpdateCapTable with - creates = - [ CT.OcfCreateEquityCompensationCancellation - cancellation - , CT.OcfCreateStockPlanReturnToPool - returnToPool - , CT.OcfCreateEquityCompensationIssuance targetIssuance - ] + exerciseCmd planEdited.updatedCapTableCid CT.UpdateCapTable with + creates = [] edits = [] deletes = [] pure () diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 171acb39..4fcd03b2 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c213eb74ed1b0c5db3586e2a421393c913c0f302f9b706be2bf0a862df76172 -size 2939365 +oid sha256:3fe4c144a17cb43a4aae05a225279be3e09a36770f588283a0031439a35fa8a4 +size 2963807 diff --git a/dars/dars.lock b/dars/dars.lock index 8b0ed819..3a0373c5 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "9c213eb74ed1b0c5db3586e2a421393c913c0f302f9b706be2bf0a862df76172", - "size": 2939365, + "sha256": "3fe4c144a17cb43a4aae05a225279be3e09a36770f588283a0031439a35fa8a4", + "size": 2963807, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-11T00:12:43.932Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 3544bc76..13a1d786 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -89,6 +89,12 @@ data CapTableMaps = CapTableMaps with warrant_issuances_by_security_id: Map Text (ContractId WarrantIssuance) deriving (Eq, Show) +data StockPlanValidationSnapshot = StockPlanValidationSnapshot with + classless_equity_successors: Map Text (Text, Text, Optional Text) + stock_plan_classes: Map Text [Text] + stock_plan_return_to_pool_cids: Map Text (ContractId StockPlanReturnToPool) + deriving (Eq, Show) + -- | Extract maps from CapTable for processing toMaps : CapTable -> CapTableMaps toMaps ct = CapTableMaps with @@ -1880,12 +1886,160 @@ equityCompensationSuccessorStockClassIds maps issuanceData = Some stockPlanId -> case Map.lookup stockPlanId maps.stock_plans of Some stockPlanCid -> do stockPlan <- fetch stockPlanCid - pure $ case stockPlan.plan_data.stock_class_ids of - [stockClassId] -> [None, Some stockClassId] - _ -> [None] + pure (None :: map Some stockPlan.plan_data.stock_class_ids) None -> pure [None] None -> pure [None] +addClasslessEquitySuccessorHistory : + CapTableMaps -> + Map Text (Text, Text, Optional Text) -> + EquityCompensationIssuanceOcfData -> + [Text] -> + Update (Map Text (Text, Text, Optional Text)) +addClasslessEquitySuccessorHistory maps history sourceIssuanceData successorSecurityIds = + case (sourceIssuanceData.stock_class_id, sourceIssuanceData.stock_plan_id) of + (None, Some stockPlanId) -> + foldlA + (\updatedHistory successorSecurityId -> + case Map.lookup successorSecurityId maps.equity_compensation_issuances_by_security_id of + Some successorCid -> do + successor <- fetch successorCid + pure (Map.insert + successorSecurityId + (sourceIssuanceData.security_id, stockPlanId, successor.issuance_data.stock_class_id) + updatedHistory) + None -> pure updatedHistory) + history + successorSecurityIds + _ -> pure history + +classlessEquitySuccessorHistoryIndex : CapTableMaps -> Update (Map Text (Text, Text, Optional Text)) +classlessEquitySuccessorHistoryIndex maps = do + transferHistory <- foldlA + (\history transferCid -> do + transfer <- fetch transferCid + case Map.lookup transfer.transfer_data.security_id maps.equity_compensation_issuances_by_security_id of + Some sourceCid -> do + source <- fetch sourceCid + addClasslessEquitySuccessorHistory + maps + history + source.issuance_data + (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids) + None -> pure history) + Map.empty + (Map.values maps.equity_compensation_transfers) + foldlA + (\history cancellationCid -> do + cancellation <- fetch cancellationCid + case (cancellation.cancellation_data.balance_security_id, Map.lookup cancellation.cancellation_data.security_id maps.equity_compensation_issuances_by_security_id) of + (Some balanceSecurityId, Some sourceCid) -> do + source <- fetch sourceCid + addClasslessEquitySuccessorHistory maps history source.issuance_data [balanceSecurityId] + _ -> pure history) + transferHistory + (Map.values maps.equity_compensation_cancellations) + +stockPlanClassIndex : CapTableMaps -> Update (Map Text [Text]) +stockPlanClassIndex maps = + foldlA + (\index stockPlanCid -> do + stockPlan <- fetch stockPlanCid + pure (Map.insert stockPlan.plan_data.id stockPlan.plan_data.stock_class_ids index)) + Map.empty + (Map.values maps.stock_plans) + +stockPlanValidationSnapshot : CapTableMaps -> Update StockPlanValidationSnapshot +stockPlanValidationSnapshot maps = do + classlessEquitySuccessors <- classlessEquitySuccessorHistoryIndex maps + stockPlanClasses <- stockPlanClassIndex maps + pure StockPlanValidationSnapshot with + classless_equity_successors = classlessEquitySuccessors + stock_plan_classes = stockPlanClasses + stock_plan_return_to_pool_cids = maps.stock_plan_return_to_pools + +validateNewClasslessEquitySuccessorStockClass : + StockPlanValidationSnapshot -> + CapTableMaps -> + Text -> + Text -> + Text -> + Update () +validateNewClasslessEquitySuccessorStockClass snapshot maps label stockPlanId stockClassId = + case Map.lookup stockPlanId maps.stock_plans of + Some stockPlanCid -> do + stockPlan <- fetch stockPlanCid + let isCurrentSingleton = stockPlan.plan_data.stock_class_ids == [stockClassId] + let isPriorSingleton = + Map.lookup stockPlanId snapshot.stock_plan_classes == Some [stockClassId] + && stockClassId `elem` stockPlan.plan_data.stock_class_ids + assertMsg + (label <> " cannot assign stock class " <> stockClassId <> " without an unambiguous current or pre-update plan class") + (isCurrentSingleton || isPriorSingleton) + None -> assertMsg (label <> " references missing stock plan " <> stockPlanId) False + +validateClasslessEquitySuccessorStockClass : + StockPlanValidationSnapshot -> + CapTableMaps -> + Text -> + EquityCompensationIssuanceOcfData -> + Text -> + Update () +validateClasslessEquitySuccessorStockClass snapshot maps label sourceIssuanceData successorSecurityId = + case (sourceIssuanceData.stock_class_id, sourceIssuanceData.stock_plan_id) of + (None, Some stockPlanId) -> + case Map.lookup successorSecurityId maps.equity_compensation_issuances_by_security_id of + Some successorCid -> do + successor <- fetch successorCid + case successor.issuance_data.stock_class_id of + Some stockClassId -> + case Map.lookup successorSecurityId snapshot.classless_equity_successors of + Some (priorSourceSecurityId, priorStockPlanId, Some priorStockClassId) -> + if priorSourceSecurityId == sourceIssuanceData.security_id + && priorStockPlanId == stockPlanId + && priorStockClassId == stockClassId + then pure () + else validateNewClasslessEquitySuccessorStockClass snapshot maps label stockPlanId stockClassId + _ -> validateNewClasslessEquitySuccessorStockClass snapshot maps label stockPlanId stockClassId + None -> pure () + None -> pure () + _ -> pure () + +validateClasslessEquitySuccessorStockClassHistory : + StockPlanValidationSnapshot -> + CapTableMaps -> + Update () +validateClasslessEquitySuccessorStockClassHistory snapshot maps = do + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + case Map.lookup transfer.transfer_data.security_id maps.equity_compensation_issuances_by_security_id of + Some sourceCid -> do + source <- fetch sourceCid + mapA_ + (validateClasslessEquitySuccessorStockClass + snapshot + maps + ("Equity compensation transfer " <> transfer.transfer_data.id) + source.issuance_data) + (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids) + None -> pure ()) + (Map.values maps.equity_compensation_transfers) + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + case (cancellation.cancellation_data.balance_security_id, Map.lookup cancellation.cancellation_data.security_id maps.equity_compensation_issuances_by_security_id) of + (Some balanceSecurityId, Some sourceCid) -> do + source <- fetch sourceCid + validateClasslessEquitySuccessorStockClass + snapshot + maps + ("Equity compensation cancellation " <> cancellation.cancellation_data.id) + source.issuance_data + balanceSecurityId + _ -> pure ()) + (Map.values maps.equity_compensation_cancellations) + equityCompensationIssuanceQuantityTotal : CapTableMaps -> Text -> @@ -3065,26 +3219,43 @@ equityCompensationPlanAllocationIndex successorIndex maps = Map.empty (Map.values maps.equity_compensation_issuances) -stockPlanReturnToPoolStockClassId : CapTableMaps -> Text -> Update (Optional Text) -stockPlanReturnToPoolStockClassId maps securityId = - case (Map.lookup securityId maps.stock_issuances_by_security_id, Map.lookup securityId maps.equity_compensation_issuances_by_security_id) of +stockPlanClassIds : CapTableMaps -> Text -> Update [Text] +stockPlanClassIds maps stockPlanId = + case Map.lookup stockPlanId maps.stock_plans of + Some stockPlanCid -> do + stockPlan <- fetch stockPlanCid + pure stockPlan.plan_data.stock_class_ids + None -> pure [] + +stockPlanReturnToPoolStockClassId : CapTableMaps -> StockPlanReturnToPoolOcfData -> Update (Optional Text) +stockPlanReturnToPoolStockClassId maps returnData = + case (Map.lookup returnData.security_id maps.stock_issuances_by_security_id, Map.lookup returnData.security_id maps.equity_compensation_issuances_by_security_id) of (Some stockCid, None) -> do issuance <- fetch stockCid pure (Some issuance.issuance_data.stock_class_id) (None, Some equityCompensationCid) -> do issuance <- fetch equityCompensationCid - stockClassIds <- equityCompensationPlanStockClassIds maps issuance.issuance_data - case stockClassIds of - [stockClassId] -> pure (Some stockClassId) - _ -> pure None + case issuance.issuance_data.stock_class_id of + Some stockClassId -> pure (Some stockClassId) + None -> case issuance.issuance_data.stock_plan_id of + Some sourcePlanId -> do + sourceClassIds <- stockPlanClassIds maps sourcePlanId + case sourceClassIds of + [stockClassId] -> pure (Some stockClassId) + _ -> do + targetClassIds <- stockPlanClassIds maps returnData.stock_plan_id + case filter (`elem` targetClassIds) sourceClassIds of + [stockClassId] -> pure (Some stockClassId) + _ -> pure None + None -> pure None (Some _, Some _) -> do - assertMsg ("Stock plan return-to-pool security id is ambiguous: " <> securityId) False + assertMsg ("Stock plan return-to-pool security id is ambiguous: " <> returnData.security_id) False pure None (None, None) -> pure None stockPlanReturnToPoolEvents : CapTableMaps -> StockPlanReturnToPoolOcfData -> Update [StockClassShareEvent] stockPlanReturnToPoolEvents maps returnData = do - stockClassIdOpt <- stockPlanReturnToPoolStockClassId maps returnData.security_id + stockClassIdOpt <- stockPlanReturnToPoolStockClassId maps returnData splitEvents <- case stockClassIdOpt of Some stockClassId -> foldlA @@ -3674,6 +3845,28 @@ stockPlanSecurityRecordedStockClassId maps securityId = pure None (None, None) -> pure None +classlessStockPlanSourceClassId : + StockPlanValidationSnapshot -> + CapTableMaps -> + Text -> + Update (Optional Text) +classlessStockPlanSourceClassId snapshot maps sourcePlanId = do + currentClassIds <- stockPlanClassIds maps sourcePlanId + pure $ case currentClassIds of + [stockClassId] -> Some stockClassId + _ -> case Map.lookup sourcePlanId snapshot.stock_plan_classes of + Some [stockClassId] -> + if stockClassId `elem` currentClassIds then Some stockClassId else None + _ -> None + +stockPlanReturnToPoolWasUnchanged : + StockPlanValidationSnapshot -> + StockPlanReturnToPoolOcfData -> + ContractId StockPlanReturnToPool -> + Bool +stockPlanReturnToPoolWasUnchanged snapshot returnData returnCid = + Map.lookup returnData.id snapshot.stock_plan_return_to_pool_cids == Some returnCid + validateStockPlanReturnToPoolQuantities : CapTableMaps -> Update () validateStockPlanReturnToPoolQuantities maps = do successorIndex <- stockPlanSuccessorIndex maps @@ -3687,15 +3880,15 @@ validateStockPlanReturnToPoolQuantities maps = do (returnedQuantity <= securityAllocation)) (Map.values maps.stock_plan_return_to_pools) -validateStockPlanReturnToPoolClasses : CapTableMaps -> Update () -validateStockPlanReturnToPoolClasses maps = do +validateStockPlanReturnToPoolClasses : StockPlanValidationSnapshot -> CapTableMaps -> Update () +validateStockPlanReturnToPoolClasses snapshot maps = do successorIndex <- stockPlanSuccessorIndex maps mapA_ (\returnCid -> do returnToPool <- fetch returnCid - stockClassIdOpt <- stockPlanSecurityRecordedStockClassId maps returnToPool.return_data.security_id + recordedStockClassIdOpt <- stockPlanSecurityRecordedStockClassId maps returnToPool.return_data.security_id sourcePlanIdOpt <- stockPlanSecurityEffectivePlanId successorIndex maps returnToPool.return_data.security_id - case stockClassIdOpt of + case recordedStockClassIdOpt of Some stockClassId -> assertStockPlanIncludesClass ("Stock plan return-to-pool " <> returnToPool.return_data.id) @@ -3704,9 +3897,24 @@ validateStockPlanReturnToPoolClasses maps = do stockClassId None -> case sourcePlanIdOpt of Some sourcePlanId -> - assertMsg - ("Stock plan return-to-pool " <> returnToPool.return_data.id <> " cannot roll a security without a recorded stock class into a different stock plan") - (returnToPool.return_data.stock_plan_id == sourcePlanId) + if returnToPool.return_data.stock_plan_id == sourcePlanId + then pure () + else do + inferredStockClassIdOpt <- + if stockPlanReturnToPoolWasUnchanged snapshot returnToPool.return_data returnCid + then stockPlanReturnToPoolStockClassId maps returnToPool.return_data + else classlessStockPlanSourceClassId snapshot maps sourcePlanId + case inferredStockClassIdOpt of + Some stockClassId -> + assertStockPlanIncludesClass + ("Stock plan return-to-pool " <> returnToPool.return_data.id) + maps + returnToPool.return_data.stock_plan_id + stockClassId + None -> + assertMsg + ("Stock plan return-to-pool " <> returnToPool.return_data.id <> " cannot roll a classless security into a different plan without an unambiguous historical class") + False None -> assertMsg ("Stock plan return-to-pool " <> returnToPool.return_data.id <> " cannot determine the source stock plan") @@ -4135,15 +4343,16 @@ validateStockPlanReservedSharesAtDates eventIndex maps adjustmentHistory = do (fromOptional [] (Map.lookup stockPlan.plan_data.id validationDates))) (Map.values maps.stock_plans) -validateStockPlanPoolCeilings : StockClassShareEventIndex -> CapTableMaps -> Update () -validateStockPlanPoolCeilings eventIndex maps = do +validateStockPlanPoolCeilings : StockPlanValidationSnapshot -> StockClassShareEventIndex -> CapTableMaps -> Update () +validateStockPlanPoolCeilings snapshot eventIndex maps = do validateStockPlanPoolAdjustmentReferences maps adjustmentIndex <- stockPlanPoolAdjustmentIndex maps adjustmentHistory <- stockPlanPoolAdjustmentHistoryIndex maps + validateClasslessEquitySuccessorStockClassHistory snapshot maps validateStockPlanAllocationClasses maps validateStockPlanAllocationEffectiveDates maps validateStockPlanEquityCompensationSourceQuantities maps - validateStockPlanReturnToPoolClasses maps + validateStockPlanReturnToPoolClasses snapshot maps validateStockPlanReturnToPoolQuantities maps validateStockPlanReservedShares eventIndex maps adjustmentIndex validateStockPlanReservedSharesAtDates eventIndex maps adjustmentHistory @@ -4533,11 +4742,12 @@ validateSuccessorSecurityIdContinuity maps = do (Map.values maps.equity_compensation_releases) validateAuthorizedShareCeilings : + StockPlanValidationSnapshot -> CapTableMaps -> Map Text StockClassAuthorizedSharesAdjustmentOcfData -> ContractId Issuer -> Update () -validateAuthorizedShareCeilings maps adjustmentIndex issuerCid = do +validateAuthorizedShareCeilings stockPlanSnapshot maps adjustmentIndex issuerCid = do eventIndex <- stockClassShareEventIndex maps validateFinalSecurityIdUniqueness maps validateSuccessorSecurityIdContinuity maps @@ -4563,7 +4773,7 @@ validateAuthorizedShareCeilings maps adjustmentIndex issuerCid = do validateStockClassAuthorizedSharesAtDates eventIndex stockClasses lotsByStockClass stockClassAdjustmentHistory stockClassValidationDates validateIssuerAuthorizedShares stockClasses maps adjustmentIndex issuerCid validateIssuerAuthorizedSharesAtDates stockClasses maps stockClassAdjustmentHistory issuerAdjustmentHistory issuerCid - validateStockPlanPoolCeilings eventIndex maps + validateStockPlanPoolCeilings stockPlanSnapshot eventIndex maps -- | Get the processing tier for a create operation getCreateTier : OcfCreateData -> Int @@ -4644,6 +4854,7 @@ template CapTable do -- Start with current maps let initialMaps = toMaps this + initialStockPlanValidation <- stockPlanValidationSnapshot initialMaps -- Process deletes first to free unique constraints (e.g., security_id) -- This allows object replacements (delete old + create new with same security_id) @@ -4700,6 +4911,7 @@ template CapTable authorizedShareAdjustmentIndex <- stockClassAuthorizedAdjustmentIndex finalMaps validateAuthorizedShareCeilings + initialStockPlanValidation finalMaps authorizedShareAdjustmentIndex newIssuerCid From 1e654b9790bd38c1004e41b7e33c745b2430a003 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 20:42:30 -0400 Subject: [PATCH 53/62] fix: enforce inherited stock plan continuity --- .../TestStockPlanReservedShares.daml | 124 ++++++++++++++++++ .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- .../codegen/templates/CapTable.daml.template | 46 +++++++ 4 files changed, 175 insertions(+), 5 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index 19fa4758..8e6b5b0c 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -2567,3 +2567,127 @@ testStockPlanReservedShares_ConsolidationRejectsMixedPlanSources = script do ] edits = [] deletes = [] + +testStockPlanReservedShares_EquitySuccessorPlanMustMatchInheritedPlan = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourcePlanId = "PLAN_EQUITY_SUCCESSOR_SOURCE" + let targetPlanId = "PLAN_EQUITY_SUCCESSOR_TARGET" + let planSourceSecurityId = "SEC_EQUITY_SUCCESSOR_PLAN_SOURCE" + let nonPlanSourceSecurityId = "SEC_EQUITY_SUCCESSOR_NON_PLAN_SOURCE" + let mismatchedResultSecurityId = "SEC_EQUITY_SUCCESSOR_MISMATCHED_RESULT" + let allowedResultSecurityId = "SEC_EQUITY_SUCCESSOR_ALLOWED_RESULT" + let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 100.0 + let targetPlan = (defaultStockPlan targetPlanId stockClassId) with initial_shares_reserved = 100.0 + let planSourceIssuance = stockPlanEquityCompensationIssuance + "TX_EQUITY_SUCCESSOR_PLAN_SOURCE" + planSourceSecurityId + stakeholderId + stockClassId + sourcePlanId + 100.0 + let nonPlanSourceIssuance = (stockPlanEquityCompensationIssuance + "TX_EQUITY_SUCCESSOR_NON_PLAN_SOURCE" + nonPlanSourceSecurityId + stakeholderId + stockClassId + targetPlanId + 100.0) with stock_plan_id = None + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan sourcePlan + , CT.OcfCreateStockPlan targetPlan + , CT.OcfCreateEquityCompensationIssuance planSourceIssuance + , CT.OcfCreateEquityCompensationIssuance nonPlanSourceIssuance + ] + edits = [] + deletes = [] + + let mismatchedResultIssuance = stockPlanEquityCompensationIssuance + "TX_EQUITY_SUCCESSOR_MISMATCHED_RESULT" + mismatchedResultSecurityId + stakeholderId + stockClassId + targetPlanId + 100.0 + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer + (planEquityCompensationTransfer "TX_EQUITY_SUCCESSOR_MISMATCHED_TRANSFER" planSourceSecurityId mismatchedResultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance mismatchedResultIssuance + ] + edits = [] + deletes = [] + + let allowedResultIssuance = stockPlanEquityCompensationIssuance + "TX_EQUITY_SUCCESSOR_ALLOWED_RESULT" + allowedResultSecurityId + stakeholderId + stockClassId + targetPlanId + 100.0 + _ <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer + (planEquityCompensationTransfer "TX_EQUITY_SUCCESSOR_ALLOWED_TRANSFER" nonPlanSourceSecurityId allowedResultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance allowedResultIssuance + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_StockSuccessorPlanMustMatchInheritedPlan = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourcePlanId = "PLAN_STOCK_SUCCESSOR_SOURCE" + let targetPlanId = "PLAN_STOCK_SUCCESSOR_TARGET" + let planSourceSecurityId = "SEC_STOCK_SUCCESSOR_PLAN_SOURCE" + let nonPlanSourceSecurityId = "SEC_STOCK_SUCCESSOR_NON_PLAN_SOURCE" + let mismatchedResultSecurityId = "SEC_STOCK_SUCCESSOR_MISMATCHED_RESULT" + let allowedResultSecurityId = "SEC_STOCK_SUCCESSOR_ALLOWED_RESULT" + let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 100.0 + let targetPlan = (defaultStockPlan targetPlanId stockClassId) with initial_shares_reserved = 100.0 + let planSourceIssuance = (defaultStockIssuance "TX_STOCK_SUCCESSOR_PLAN_SOURCE" planSourceSecurityId stakeholderId stockClassId) with + stock_plan_id = Some sourcePlanId + quantity = 100.0 + let nonPlanSourceIssuance = (defaultStockIssuance "TX_STOCK_SUCCESSOR_NON_PLAN_SOURCE" nonPlanSourceSecurityId stakeholderId stockClassId) with quantity = 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan sourcePlan + , CT.OcfCreateStockPlan targetPlan + , CT.OcfCreateStockIssuance planSourceIssuance + , CT.OcfCreateStockIssuance nonPlanSourceIssuance + ] + edits = [] + deletes = [] + + let mismatchedResultIssuance = (defaultStockIssuance "TX_STOCK_SUCCESSOR_MISMATCHED_RESULT" mismatchedResultSecurityId stakeholderId stockClassId) with + stock_plan_id = Some targetPlanId + quantity = 100.0 + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer + (planStockTransfer "TX_STOCK_SUCCESSOR_MISMATCHED_TRANSFER" planSourceSecurityId mismatchedResultSecurityId 100.0) + , CT.OcfCreateStockIssuance mismatchedResultIssuance + ] + edits = [] + deletes = [] + + let allowedResultIssuance = (defaultStockIssuance "TX_STOCK_SUCCESSOR_ALLOWED_RESULT" allowedResultSecurityId stakeholderId stockClassId) with + stock_plan_id = Some targetPlanId + quantity = 100.0 + _ <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer + (planStockTransfer "TX_STOCK_SUCCESSOR_ALLOWED_TRANSFER" nonPlanSourceSecurityId allowedResultSecurityId 100.0) + , CT.OcfCreateStockIssuance allowedResultIssuance + ] + edits = [] + deletes = [] + pure () diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 171acb39..2c6fb78d 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c213eb74ed1b0c5db3586e2a421393c913c0f302f9b706be2bf0a862df76172 -size 2939365 +oid sha256:3a9080210d0812062c029b54c625fc80624eb310770b2625ae5e4445f59d8264 +size 2942894 diff --git a/dars/dars.lock b/dars/dars.lock index 8b0ed819..a8d415ce 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,10 +33,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "9c213eb74ed1b0c5db3586e2a421393c913c0f302f9b706be2bf0a862df76172", - "size": 2939365, + "sha256": "3a9080210d0812062c029b54c625fc80624eb310770b2625ae5e4445f59d8264", + "size": 2942894, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T00:12:43.932Z", + "uploadedAt": "2026-07-11T00:41:54.852Z", "networks": [] } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 3544bc76..b42462f9 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -2512,6 +2512,50 @@ effectivePlanIndex declaredPlans successorIndex = declaredPlans (Map.toList successorIndex) +validateRecordedSuccessorPlanId : + Text -> + Map Text (Time, Optional Text) -> + Text -> + Optional Text -> + Update () +validateRecordedSuccessorPlanId label successorIndex securityId recordedPlanIdOpt = + case (Map.lookup securityId successorIndex, recordedPlanIdOpt) of + (Some (_, inheritedPlanIdOpt), Some recordedPlanId) -> + assertMsg + (label <> " " <> securityId <> " records stock plan " <> recordedPlanId <> " but inherits " <> show inheritedPlanIdOpt) + (inheritedPlanIdOpt == Some recordedPlanId) + _ -> pure () + +validateEquityCompensationSuccessorPlanContinuity : + CapTableMaps -> + Map Text (Time, Optional Text) -> + Update () +validateEquityCompensationSuccessorPlanContinuity maps successorIndex = + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + validateRecordedSuccessorPlanId + "Equity compensation successor" + successorIndex + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id) + (Map.values maps.equity_compensation_issuances) + +validateStockSuccessorPlanContinuity : + CapTableMaps -> + Map Text (Time, Optional Text) -> + Update () +validateStockSuccessorPlanContinuity maps successorIndex = + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + validateRecordedSuccessorPlanId + "Stock successor" + successorIndex + issuance.issuance_data.security_id + issuance.issuance_data.stock_plan_id) + (Map.values maps.stock_issuances) + mergePlanIndexes : Map Text Text -> Map Text Text -> Map Text Text mergePlanIndexes base additions = foldl @@ -2662,6 +2706,7 @@ stockPlanSuccessorIndex maps = do Map.empty (Map.values maps.equity_compensation_issuances) equitySuccessors <- stabilizeEquityPlanSuccessors maps equityDeclaredPlans Map.empty + validateEquityCompensationSuccessorPlanContinuity maps equitySuccessors let equityEffectivePlans = effectivePlanIndex equityDeclaredPlans equitySuccessors stockDeclaredPlans <- foldlA @@ -2674,6 +2719,7 @@ stockPlanSuccessorIndex maps = do (Map.values maps.stock_issuances) let stockSourcePlans = mergePlanIndexes stockDeclaredPlans equityEffectivePlans stockSuccessors <- stabilizeStockPlanSuccessors maps stockSourcePlans Map.empty + validateStockSuccessorPlanContinuity maps stockSuccessors validateStockConsolidationPlanContinuity maps stockSourcePlans stockSuccessors pure StockPlanSuccessorIndex with From 4d039eaec0a2e1e2723474f8b887ca88985045be Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 20:59:28 -0400 Subject: [PATCH 54/62] fix: reconcile stock plan validation history --- .../OpenCapTable/TestLifecycleInvariants.daml | 80 ++++ .../TestStockPlanReservedShares.daml | 353 ++++++++++++++++-- .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 260 +++++++++++-- 5 files changed, 646 insertions(+), 55 deletions(-) diff --git a/Test/daml/OpenCapTable/TestLifecycleInvariants.daml b/Test/daml/OpenCapTable/TestLifecycleInvariants.daml index 4e55399e..2f706e49 100644 --- a/Test/daml/OpenCapTable/TestLifecycleInvariants.daml +++ b/Test/daml/OpenCapTable/TestLifecycleInvariants.daml @@ -18,6 +18,7 @@ import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup import OpenCapTable.TestHelpers +import qualified DA.Map as Map import Daml.Script lifecycleWarrantTrigger : Text -> Text -> OcfConversionTrigger @@ -285,6 +286,85 @@ testRepeatedEquityTerminalEventsAtSameTimestampFail = script do edits = [] deletes = [] +testSecurityIssuanceDuplicateAcrossTypesFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_GLOBAL_SECURITY_ID" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_GLOBAL_SECURITY_ID" "Global Security ID Holder" + let sharedSecurityId = "SEC_GLOBAL_SECURITY_ID" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance (defaultStockIssuance "TX_GLOBAL_SECURITY_ID_STOCK" sharedSecurityId stakeholderId stockClassId)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_GLOBAL_SECURITY_ID_WARRANT" sharedSecurityId stakeholderId stockClassId)] + edits = [] + deletes = [] + +testSecurityIssuanceDuplicateAcrossTypesInSameBatchFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_BATCH_SECURITY_ID" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_BATCH_SECURITY_ID" "Batch Security ID Holder" + let sharedSecurityId = "SEC_BATCH_SECURITY_ID" + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance (defaultStockIssuance "TX_BATCH_SECURITY_ID_STOCK" sharedSecurityId stakeholderId stockClassId) + , CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_BATCH_SECURITY_ID_WARRANT" sharedSecurityId stakeholderId stockClassId) + ] + edits = [] + deletes = [] + +testSecurityIssuanceEditCannotReuseAnotherTypeSecurityId = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EDIT_SECURITY_ID" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EDIT_SECURITY_ID" "Edit Security ID Holder" + let stockSecurityId = "SEC_EDIT_SECURITY_ID_STOCK" + let warrantSecurityId = "SEC_EDIT_SECURITY_ID_WARRANT" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance (defaultStockIssuance "TX_EDIT_SECURITY_ID_STOCK" stockSecurityId stakeholderId stockClassId) + , CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_EDIT_SECURITY_ID_WARRANT" warrantSecurityId stakeholderId stockClassId) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockIssuance (defaultStockIssuance "TX_EDIT_SECURITY_ID_STOCK" warrantSecurityId stakeholderId stockClassId)] + deletes = [] + +testFinalSecurityIdUniquenessRejectsStaleLegacyIndex = script do + TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_LEGACY_SECURITY_ID" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_LEGACY_SECURITY_ID" "Legacy Security ID Holder" + let sharedSecurityId = "SEC_LEGACY_SECURITY_ID" + issued <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance (defaultStockIssuance "TX_LEGACY_SECURITY_ID_STOCK" sharedSecurityId stakeholderId stockClassId)] + edits = [] + deletes = [] + capTableDataOpt <- queryContractId issuer issued.updatedCapTableCid + let Some capTableData = capTableDataOpt + + duplicateWarrantCid <- submit (actAs issuer <> actAs system_operator) $ createCmd $ WarrantIssuance with + context = ctx + issuance_data = lifecycleWarrantIssuance "TX_LEGACY_SECURITY_ID_WARRANT" sharedSecurityId stakeholderId stockClassId + let corruptedCapTable = capTableData with + warrant_issuances = Map.insert "TX_LEGACY_SECURITY_ID_WARRANT" duplicateWarrantCid capTableData.warrant_issuances + corruptedCapTableCid <- submit (actAs issuer <> actAs system_operator) $ createCmd corruptedCapTable + + submitMustFail issuer do + exerciseCmd corruptedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [] + testBalanceSecurityCannotAlsoBeResultingSecurity = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_BALANCE_RESULT_ALIAS" 1_000.0) diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index 8e6b5b0c..30a3f18f 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -2186,7 +2186,7 @@ testStockPlanReservedShares_CumulativeReturnsAreSplitAdjusted = script do edits = [] deletes = [] -testStockPlanReservedShares_PlanAddClassRequiresSourceDisambiguation = script do +testStockPlanReservedShares_ClasslessSuccessorRemainsValidWhenPlanAddsClass = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_CLASSLESS_SUCCESSOR_PLAN_EDIT_FIRST" (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_CLASSLESS_SUCCESSOR_PLAN_EDIT_SECOND" @@ -2229,20 +2229,186 @@ testStockPlanReservedShares_PlanAddClassRequiresSourceDisambiguation = script do deletes = [] let editedPlan = plan with stock_class_ids = [firstStockClassId, secondStockClassId] - submitMustFail issuer do + planEdited <- submit issuer do exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with creates = [] edits = [CT.OcfEditStockPlan editedPlan] deletes = [] - let disambiguatedSourceIssuance = sourceIssuance with stock_class_id = Some firstStockClassId - _ <- submit issuer do - exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + let reclassifiedResult = resultIssuance with stock_class_id = Some secondStockClassId + submitMustFail issuer do + exerciseCmd planEdited.updatedCapTableCid CT.UpdateCapTable with creates = [] - edits = - [ CT.OcfEditStockPlan editedPlan - , CT.OcfEditEquityCompensationIssuance disambiguatedSourceIssuance + edits = [CT.OcfEditEquityCompensationIssuance reclassifiedResult] + deletes = [] + pure () + +testStockPlanReservedShares_ClasslessSuccessorCannotUseNewlyAddedPlanClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_CLASSLESS_NEW_CLASS_FIRST" + (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_CLASSLESS_NEW_CLASS_SECOND" + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_CLASSLESS_NEW_CLASS" "Classless New Class Holder" + let stockPlanId = "PLAN_CLASSLESS_NEW_CLASS" + let sourceSecurityId = "SEC_CLASSLESS_NEW_CLASS_SOURCE" + let newClassResultSecurityId = "SEC_CLASSLESS_NEW_CLASS_RESULT" + let priorClassResultSecurityId = "SEC_CLASSLESS_PRIOR_CLASS_RESULT" + let plan = (defaultStockPlan stockPlanId firstStockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_NEW_CLASS_SOURCE" + sourceSecurityId + stakeholderId + firstStockClassId + stockPlanId + 100.0) with stock_class_id = None + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let editedPlan = plan with stock_class_ids = [firstStockClassId, secondStockClassId] + let newClassResultIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_NEW_CLASS_RESULT" + newClassResultSecurityId + stakeholderId + secondStockClassId + stockPlanId + 100.0) with stock_plan_id = None + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer + (planEquityCompensationTransfer "TX_CLASSLESS_NEW_CLASS_TRANSFER" sourceSecurityId newClassResultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance newClassResultIssuance + ] + edits = [CT.OcfEditStockPlan editedPlan] + deletes = [] + + let priorClassResultIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_PRIOR_CLASS_RESULT" + priorClassResultSecurityId + stakeholderId + firstStockClassId + stockPlanId + 100.0) with stock_plan_id = None + _ <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer + (planEquityCompensationTransfer "TX_CLASSLESS_PRIOR_CLASS_TRANSFER" sourceSecurityId priorClassResultSecurityId 100.0) + , CT.OcfCreateEquityCompensationIssuance priorClassResultIssuance + ] + edits = [CT.OcfEditStockPlan editedPlan] + deletes = [] + pure () + +testStockPlanReservedShares_ClasslessTransferCannotAdoptExistingNewClassSecurity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_CLASSLESS_ADOPT_FIRST" + (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_CLASSLESS_ADOPT_SECOND" + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_CLASSLESS_ADOPT" "Classless Adopt Holder" + let stockPlanId = "PLAN_CLASSLESS_ADOPT" + let sourceSecurityId = "SEC_CLASSLESS_ADOPT_SOURCE" + let existingSecurityId = "SEC_CLASSLESS_ADOPT_EXISTING" + let plan = (defaultStockPlan stockPlanId firstStockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_ADOPT_SOURCE" + sourceSecurityId + stakeholderId + firstStockClassId + stockPlanId + 100.0) with stock_class_id = None + let existingIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_ADOPT_EXISTING" + existingSecurityId + stakeholderId + secondStockClassId + stockPlanId + 100.0) with stock_plan_id = None + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + , CT.OcfCreateEquityCompensationIssuance existingIssuance + ] + edits = [] + deletes = [] + + let editedPlan = plan with stock_class_ids = [firstStockClassId, secondStockClassId] + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer + (planEquityCompensationTransfer "TX_CLASSLESS_ADOPT_TRANSFER" sourceSecurityId existingSecurityId 100.0) + ] + edits = [CT.OcfEditStockPlan editedPlan] + deletes = [] + +testStockPlanReservedShares_ClasslessCancellationCannotUseNewlyAddedPlanClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_CLASSLESS_CANCEL_NEW_CLASS_FIRST" + (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_CLASSLESS_CANCEL_NEW_CLASS_SECOND" + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_CLASSLESS_CANCEL_NEW_CLASS" "Classless Cancellation New Class Holder" + let stockPlanId = "PLAN_CLASSLESS_CANCEL_NEW_CLASS" + let sourceSecurityId = "SEC_CLASSLESS_CANCEL_NEW_CLASS_SOURCE" + let newClassBalanceSecurityId = "SEC_CLASSLESS_CANCEL_NEW_CLASS_BALANCE" + let priorClassBalanceSecurityId = "SEC_CLASSLESS_CANCEL_PRIOR_CLASS_BALANCE" + let plan = (defaultStockPlan stockPlanId firstStockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_CANCEL_NEW_CLASS_SOURCE" + sourceSecurityId + stakeholderId + firstStockClassId + stockPlanId + 100.0) with stock_class_id = None + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let editedPlan = plan with stock_class_ids = [firstStockClassId, secondStockClassId] + let newClassCancellation = (equityCompensationCancellation "TX_CLASSLESS_CANCEL_NEW_CLASS" sourceSecurityId 40.0) with + balance_security_id = Some newClassBalanceSecurityId + let newClassBalanceIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_CANCEL_NEW_CLASS_BALANCE" + newClassBalanceSecurityId + stakeholderId + secondStockClassId + stockPlanId + 60.0) with stock_plan_id = None + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation newClassCancellation + , CT.OcfCreateEquityCompensationIssuance newClassBalanceIssuance + ] + edits = [CT.OcfEditStockPlan editedPlan] + deletes = [] + + let priorClassCancellation = (equityCompensationCancellation "TX_CLASSLESS_CANCEL_PRIOR_CLASS" sourceSecurityId 40.0) with + balance_security_id = Some priorClassBalanceSecurityId + let priorClassBalanceIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_CANCEL_PRIOR_CLASS_BALANCE" + priorClassBalanceSecurityId + stakeholderId + firstStockClassId + stockPlanId + 60.0) with stock_plan_id = None + _ <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation priorClassCancellation + , CT.OcfCreateEquityCompensationIssuance priorClassBalanceIssuance ] + edits = [CT.OcfEditStockPlan editedPlan] deletes = [] pure () @@ -2385,28 +2551,30 @@ testStockPlanReservedShares_AmbiguousClasslessReturnStaysInSourcePlan = script d deletes = [] pure () -testStockPlanReservedShares_ClasslessCrossPlanReturnRequiresRecordedClass = script do +testStockPlanReservedShares_ClasslessRolloverCannotBackdateNewPlanClass = script do TestOcp{issuer, cap_table} <- setupTestOcp - (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_CLASSLESS_ROLLOVER_BACKDATE_FIRST" + (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_CLASSLESS_ROLLOVER_BACKDATE_SECOND" + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_CLASSLESS_ROLLOVER_BACKDATE" "Classless Rollover Backdate Holder" let sourcePlanId = "PLAN_CLASSLESS_ROLLOVER_SOURCE" let targetPlanId = "PLAN_CLASSLESS_ROLLOVER_TARGET" let sourceSecurityId = "SEC_CLASSLESS_ROLLOVER_SOURCE" - let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 100.0 - let targetPlan = (defaultStockPlan targetPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourcePlan = (defaultStockPlan sourcePlanId firstStockClassId) with initial_shares_reserved = 100.0 + let targetPlan = (defaultStockPlan targetPlanId secondStockClassId) with initial_shares_reserved = 100.0 let sourceIssuance = (stockPlanEquityCompensationIssuance "TX_CLASSLESS_ROLLOVER_SOURCE" sourceSecurityId stakeholderId - stockClassId + firstStockClassId sourcePlanId 100.0) with stock_class_id = None let targetIssuance = stockPlanEquityCompensationIssuance "TX_CLASSLESS_ROLLOVER_TARGET" "SEC_CLASSLESS_ROLLOVER_TARGET" stakeholderId - stockClassId + secondStockClassId targetPlanId - 200.0 + 100.0 source <- submit issuer do exerciseCmd capTableReady CT.UpdateCapTable with creates = @@ -2419,6 +2587,7 @@ testStockPlanReservedShares_ClasslessCrossPlanReturnRequiresRecordedClass = scri let cancellation = equityCompensationCancellation "TX_CLASSLESS_ROLLOVER_CANCEL" sourceSecurityId 100.0 let returnToPool = stockPlanReturnToPool "TX_CLASSLESS_ROLLOVER_POOL" sourceSecurityId targetPlanId 100.0 + let editedSourcePlan = sourcePlan with stock_class_ids = [firstStockClassId, secondStockClassId] submitMustFail issuer do exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with creates = @@ -2426,25 +2595,58 @@ testStockPlanReservedShares_ClasslessCrossPlanReturnRequiresRecordedClass = scri , CT.OcfCreateStockPlanReturnToPool returnToPool , CT.OcfCreateEquityCompensationIssuance targetIssuance ] + edits = [CT.OcfEditStockPlan editedSourcePlan] + deletes = [] + +testStockPlanReservedShares_ClasslessRolloverRemainsValidWhenSourcePlanAddsClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_CLASSLESS_ROLLOVER_FIRST" + (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_CLASSLESS_ROLLOVER_SECOND" + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_CLASSLESS_ROLLOVER_PLAN_EDIT" "Classless Rollover Plan Edit Holder" + let sourcePlanId = "PLAN_CLASSLESS_ROLLOVER_SOURCE" + let targetPlanId = "PLAN_CLASSLESS_ROLLOVER_TARGET" + let sourceSecurityId = "SEC_CLASSLESS_ROLLOVER_SOURCE" + let sourcePlan = (defaultStockPlan sourcePlanId firstStockClassId) with initial_shares_reserved = 100.0 + let targetPlan = (defaultStockPlan targetPlanId firstStockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_ROLLOVER_SOURCE" + sourceSecurityId + stakeholderId + firstStockClassId + sourcePlanId + 100.0) with stock_class_id = None + let targetIssuance = stockPlanEquityCompensationIssuance + "TX_CLASSLESS_ROLLOVER_TARGET" + "SEC_CLASSLESS_ROLLOVER_TARGET" + stakeholderId + firstStockClassId + targetPlanId + 200.0 + rolledOver <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan sourcePlan + , CT.OcfCreateStockPlan targetPlan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + , CT.OcfCreateEquityCompensationCancellation + (equityCompensationCancellation "TX_CLASSLESS_ROLLOVER_CANCEL" sourceSecurityId 100.0) + , CT.OcfCreateStockPlanReturnToPool + (stockPlanReturnToPool "TX_CLASSLESS_ROLLOVER_POOL" sourceSecurityId targetPlanId 100.0) + , CT.OcfCreateEquityCompensationIssuance targetIssuance + ] edits = [] deletes = [] - let disambiguatedSourceIssuance = sourceIssuance with stock_class_id = Some stockClassId - disambiguated <- submit issuer do - exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + let editedSourcePlan = sourcePlan with stock_class_ids = [firstStockClassId, secondStockClassId] + planEdited <- submit issuer do + exerciseCmd rolledOver.updatedCapTableCid CT.UpdateCapTable with creates = [] - edits = [CT.OcfEditEquityCompensationIssuance disambiguatedSourceIssuance] + edits = [CT.OcfEditStockPlan editedSourcePlan] deletes = [] _ <- submit issuer do - exerciseCmd disambiguated.updatedCapTableCid CT.UpdateCapTable with - creates = - [ CT.OcfCreateEquityCompensationCancellation - cancellation - , CT.OcfCreateStockPlanReturnToPool - returnToPool - , CT.OcfCreateEquityCompensationIssuance targetIssuance - ] + exerciseCmd planEdited.updatedCapTableCid CT.UpdateCapTable with + creates = [] edits = [] deletes = [] pure () @@ -2691,3 +2893,100 @@ testStockPlanReservedShares_StockSuccessorPlanMustMatchInheritedPlan = script do edits = [] deletes = [] pure () + +testStockPlanReservedShares_ClasslessTransferCannotOverrideInheritedPlanForReturn = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourcePlanId = "PLAN_CLASSLESS_TRANSFER_SOURCE" + let targetPlanId = "PLAN_CLASSLESS_TRANSFER_TARGET" + let sourceSecurityId = "SEC_CLASSLESS_TRANSFER_SOURCE" + let successorSecurityId = "SEC_CLASSLESS_TRANSFER_SUCCESSOR" + let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 100.0 + let targetPlan = (defaultStockPlan targetPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_TRANSFER_SOURCE" + sourceSecurityId + stakeholderId + stockClassId + sourcePlanId + 100.0) with stock_class_id = None + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan sourcePlan + , CT.OcfCreateStockPlan targetPlan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let transferDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let returnDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let successorIssuance = (stockPlanEquityCompensationIssuance + "TX_CLASSLESS_TRANSFER_SUCCESSOR" + successorSecurityId + stakeholderId + stockClassId + targetPlanId + 100.0) with + date = transferDate + stock_class_id = None + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationTransfer + ((planEquityCompensationTransfer "TX_CLASSLESS_TRANSFER" sourceSecurityId successorSecurityId 100.0) with date = transferDate) + , CT.OcfCreateEquityCompensationIssuance successorIssuance + , CT.OcfCreateEquityCompensationCancellation + ((equityCompensationCancellation "TX_CLASSLESS_TRANSFER_CANCEL" successorSecurityId 100.0) with date = returnDate) + , CT.OcfCreateStockPlanReturnToPool + ((stockPlanReturnToPool "TX_CLASSLESS_TRANSFER_RETURN" successorSecurityId targetPlanId 100.0) with date = returnDate) + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_ConsolidationSourceCannotOverrideInheritedPlan = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourcePlanId = "PLAN_CONSOLIDATION_LINEAGE_SOURCE" + let targetPlanId = "PLAN_CONSOLIDATION_LINEAGE_TARGET" + let sourceSecurityId = "SEC_CONSOLIDATION_LINEAGE_SOURCE" + let successorSecurityId = "SEC_CONSOLIDATION_LINEAGE_SUCCESSOR" + let resultSecurityId = "SEC_CONSOLIDATION_LINEAGE_RESULT" + let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 100.0 + let targetPlan = (defaultStockPlan targetPlanId stockClassId) with initial_shares_reserved = 100.0 + let sourceIssuance = (defaultStockIssuance "TX_CONSOLIDATION_LINEAGE_SOURCE" sourceSecurityId stakeholderId stockClassId) with + stock_plan_id = Some sourcePlanId + quantity = 100.0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan sourcePlan + , CT.OcfCreateStockPlan targetPlan + , CT.OcfCreateStockIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let transferDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let consolidationDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let successorIssuance = (defaultStockIssuance "TX_CONSOLIDATION_LINEAGE_SUCCESSOR" successorSecurityId stakeholderId stockClassId) with + date = transferDate + stock_plan_id = Some targetPlanId + quantity = 100.0 + let resultIssuance = (defaultStockIssuance "TX_CONSOLIDATION_LINEAGE_RESULT" resultSecurityId stakeholderId stockClassId) with + date = consolidationDate + stock_plan_id = Some targetPlanId + quantity = 100.0 + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer + ((planStockTransfer "TX_CONSOLIDATION_LINEAGE_TRANSFER" sourceSecurityId successorSecurityId 100.0) with date = transferDate) + , CT.OcfCreateStockIssuance successorIssuance + , CT.OcfCreateStockConsolidation + (planStockConsolidation "TX_CONSOLIDATION_LINEAGE" [successorSecurityId] resultSecurityId consolidationDate) + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 2c6fb78d..68dc6538 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a9080210d0812062c029b54c625fc80624eb310770b2625ae5e4445f59d8264 -size 2942894 +oid sha256:0bff92de697063dee24325e804315f003b9fe399d28cd61ddd76d773886d7d87 +size 2967712 diff --git a/dars/dars.lock b/dars/dars.lock index a8d415ce..5fa3477d 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "3a9080210d0812062c029b54c625fc80624eb310770b2625ae5e4445f59d8264", - "size": 2942894, + "sha256": "0bff92de697063dee24325e804315f003b9fe399d28cd61ddd76d773886d7d87", + "size": 2967712, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-11T00:41:54.852Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index b42462f9..e51365d4 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -89,6 +89,12 @@ data CapTableMaps = CapTableMaps with warrant_issuances_by_security_id: Map Text (ContractId WarrantIssuance) deriving (Eq, Show) +data StockPlanValidationSnapshot = StockPlanValidationSnapshot with + classless_equity_successors: Map Text (Text, Text, Optional Text) + stock_plan_classes: Map Text [Text] + stock_plan_return_to_pool_cids: Map Text (ContractId StockPlanReturnToPool) + deriving (Eq, Show) + -- | Extract maps from CapTable for processing toMaps : CapTable -> CapTableMaps toMaps ct = CapTableMaps with @@ -1880,12 +1886,160 @@ equityCompensationSuccessorStockClassIds maps issuanceData = Some stockPlanId -> case Map.lookup stockPlanId maps.stock_plans of Some stockPlanCid -> do stockPlan <- fetch stockPlanCid - pure $ case stockPlan.plan_data.stock_class_ids of - [stockClassId] -> [None, Some stockClassId] - _ -> [None] + pure (None :: map Some stockPlan.plan_data.stock_class_ids) None -> pure [None] None -> pure [None] +addClasslessEquitySuccessorHistory : + CapTableMaps -> + Map Text (Text, Text, Optional Text) -> + EquityCompensationIssuanceOcfData -> + [Text] -> + Update (Map Text (Text, Text, Optional Text)) +addClasslessEquitySuccessorHistory maps history sourceIssuanceData successorSecurityIds = + case (sourceIssuanceData.stock_class_id, sourceIssuanceData.stock_plan_id) of + (None, Some stockPlanId) -> + foldlA + (\updatedHistory successorSecurityId -> + case Map.lookup successorSecurityId maps.equity_compensation_issuances_by_security_id of + Some successorCid -> do + successor <- fetch successorCid + pure (Map.insert + successorSecurityId + (sourceIssuanceData.security_id, stockPlanId, successor.issuance_data.stock_class_id) + updatedHistory) + None -> pure updatedHistory) + history + successorSecurityIds + _ -> pure history + +classlessEquitySuccessorHistoryIndex : CapTableMaps -> Update (Map Text (Text, Text, Optional Text)) +classlessEquitySuccessorHistoryIndex maps = do + transferHistory <- foldlA + (\history transferCid -> do + transfer <- fetch transferCid + case Map.lookup transfer.transfer_data.security_id maps.equity_compensation_issuances_by_security_id of + Some sourceCid -> do + source <- fetch sourceCid + addClasslessEquitySuccessorHistory + maps + history + source.issuance_data + (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids) + None -> pure history) + Map.empty + (Map.values maps.equity_compensation_transfers) + foldlA + (\history cancellationCid -> do + cancellation <- fetch cancellationCid + case (cancellation.cancellation_data.balance_security_id, Map.lookup cancellation.cancellation_data.security_id maps.equity_compensation_issuances_by_security_id) of + (Some balanceSecurityId, Some sourceCid) -> do + source <- fetch sourceCid + addClasslessEquitySuccessorHistory maps history source.issuance_data [balanceSecurityId] + _ -> pure history) + transferHistory + (Map.values maps.equity_compensation_cancellations) + +stockPlanClassIndex : CapTableMaps -> Update (Map Text [Text]) +stockPlanClassIndex maps = + foldlA + (\index stockPlanCid -> do + stockPlan <- fetch stockPlanCid + pure (Map.insert stockPlan.plan_data.id stockPlan.plan_data.stock_class_ids index)) + Map.empty + (Map.values maps.stock_plans) + +stockPlanValidationSnapshot : CapTableMaps -> Update StockPlanValidationSnapshot +stockPlanValidationSnapshot maps = do + classlessEquitySuccessors <- classlessEquitySuccessorHistoryIndex maps + stockPlanClasses <- stockPlanClassIndex maps + pure StockPlanValidationSnapshot with + classless_equity_successors = classlessEquitySuccessors + stock_plan_classes = stockPlanClasses + stock_plan_return_to_pool_cids = maps.stock_plan_return_to_pools + +validateNewClasslessEquitySuccessorStockClass : + StockPlanValidationSnapshot -> + CapTableMaps -> + Text -> + Text -> + Text -> + Update () +validateNewClasslessEquitySuccessorStockClass snapshot maps label stockPlanId stockClassId = + case Map.lookup stockPlanId maps.stock_plans of + Some stockPlanCid -> do + stockPlan <- fetch stockPlanCid + let isCurrentSingleton = stockPlan.plan_data.stock_class_ids == [stockClassId] + let isPriorSingleton = + Map.lookup stockPlanId snapshot.stock_plan_classes == Some [stockClassId] + && stockClassId `elem` stockPlan.plan_data.stock_class_ids + assertMsg + (label <> " cannot assign stock class " <> stockClassId <> " without an unambiguous current or pre-update plan class") + (isCurrentSingleton || isPriorSingleton) + None -> assertMsg (label <> " references missing stock plan " <> stockPlanId) False + +validateClasslessEquitySuccessorStockClass : + StockPlanValidationSnapshot -> + CapTableMaps -> + Text -> + EquityCompensationIssuanceOcfData -> + Text -> + Update () +validateClasslessEquitySuccessorStockClass snapshot maps label sourceIssuanceData successorSecurityId = + case (sourceIssuanceData.stock_class_id, sourceIssuanceData.stock_plan_id) of + (None, Some stockPlanId) -> + case Map.lookup successorSecurityId maps.equity_compensation_issuances_by_security_id of + Some successorCid -> do + successor <- fetch successorCid + case successor.issuance_data.stock_class_id of + Some stockClassId -> + case Map.lookup successorSecurityId snapshot.classless_equity_successors of + Some (priorSourceSecurityId, priorStockPlanId, Some priorStockClassId) -> + if priorSourceSecurityId == sourceIssuanceData.security_id + && priorStockPlanId == stockPlanId + && priorStockClassId == stockClassId + then pure () + else validateNewClasslessEquitySuccessorStockClass snapshot maps label stockPlanId stockClassId + _ -> validateNewClasslessEquitySuccessorStockClass snapshot maps label stockPlanId stockClassId + None -> pure () + None -> pure () + _ -> pure () + +validateClasslessEquitySuccessorStockClassHistory : + StockPlanValidationSnapshot -> + CapTableMaps -> + Update () +validateClasslessEquitySuccessorStockClassHistory snapshot maps = do + mapA_ + (\transferCid -> do + transfer <- fetch transferCid + case Map.lookup transfer.transfer_data.security_id maps.equity_compensation_issuances_by_security_id of + Some sourceCid -> do + source <- fetch sourceCid + mapA_ + (validateClasslessEquitySuccessorStockClass + snapshot + maps + ("Equity compensation transfer " <> transfer.transfer_data.id) + source.issuance_data) + (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids) + None -> pure ()) + (Map.values maps.equity_compensation_transfers) + mapA_ + (\cancellationCid -> do + cancellation <- fetch cancellationCid + case (cancellation.cancellation_data.balance_security_id, Map.lookup cancellation.cancellation_data.security_id maps.equity_compensation_issuances_by_security_id) of + (Some balanceSecurityId, Some sourceCid) -> do + source <- fetch sourceCid + validateClasslessEquitySuccessorStockClass + snapshot + maps + ("Equity compensation cancellation " <> cancellation.cancellation_data.id) + source.issuance_data + balanceSecurityId + _ -> pure ()) + (Map.values maps.equity_compensation_cancellations) + equityCompensationIssuanceQuantityTotal : CapTableMaps -> Text -> @@ -3111,26 +3265,43 @@ equityCompensationPlanAllocationIndex successorIndex maps = Map.empty (Map.values maps.equity_compensation_issuances) -stockPlanReturnToPoolStockClassId : CapTableMaps -> Text -> Update (Optional Text) -stockPlanReturnToPoolStockClassId maps securityId = - case (Map.lookup securityId maps.stock_issuances_by_security_id, Map.lookup securityId maps.equity_compensation_issuances_by_security_id) of +stockPlanClassIds : CapTableMaps -> Text -> Update [Text] +stockPlanClassIds maps stockPlanId = + case Map.lookup stockPlanId maps.stock_plans of + Some stockPlanCid -> do + stockPlan <- fetch stockPlanCid + pure stockPlan.plan_data.stock_class_ids + None -> pure [] + +stockPlanReturnToPoolStockClassId : CapTableMaps -> StockPlanReturnToPoolOcfData -> Update (Optional Text) +stockPlanReturnToPoolStockClassId maps returnData = + case (Map.lookup returnData.security_id maps.stock_issuances_by_security_id, Map.lookup returnData.security_id maps.equity_compensation_issuances_by_security_id) of (Some stockCid, None) -> do issuance <- fetch stockCid pure (Some issuance.issuance_data.stock_class_id) (None, Some equityCompensationCid) -> do issuance <- fetch equityCompensationCid - stockClassIds <- equityCompensationPlanStockClassIds maps issuance.issuance_data - case stockClassIds of - [stockClassId] -> pure (Some stockClassId) - _ -> pure None + case issuance.issuance_data.stock_class_id of + Some stockClassId -> pure (Some stockClassId) + None -> case issuance.issuance_data.stock_plan_id of + Some sourcePlanId -> do + sourceClassIds <- stockPlanClassIds maps sourcePlanId + case sourceClassIds of + [stockClassId] -> pure (Some stockClassId) + _ -> do + targetClassIds <- stockPlanClassIds maps returnData.stock_plan_id + case filter (`elem` targetClassIds) sourceClassIds of + [stockClassId] -> pure (Some stockClassId) + _ -> pure None + None -> pure None (Some _, Some _) -> do - assertMsg ("Stock plan return-to-pool security id is ambiguous: " <> securityId) False + assertMsg ("Stock plan return-to-pool security id is ambiguous: " <> returnData.security_id) False pure None (None, None) -> pure None stockPlanReturnToPoolEvents : CapTableMaps -> StockPlanReturnToPoolOcfData -> Update [StockClassShareEvent] stockPlanReturnToPoolEvents maps returnData = do - stockClassIdOpt <- stockPlanReturnToPoolStockClassId maps returnData.security_id + stockClassIdOpt <- stockPlanReturnToPoolStockClassId maps returnData splitEvents <- case stockClassIdOpt of Some stockClassId -> foldlA @@ -3720,6 +3891,28 @@ stockPlanSecurityRecordedStockClassId maps securityId = pure None (None, None) -> pure None +classlessStockPlanSourceClassId : + StockPlanValidationSnapshot -> + CapTableMaps -> + Text -> + Update (Optional Text) +classlessStockPlanSourceClassId snapshot maps sourcePlanId = do + currentClassIds <- stockPlanClassIds maps sourcePlanId + pure $ case currentClassIds of + [stockClassId] -> Some stockClassId + _ -> case Map.lookup sourcePlanId snapshot.stock_plan_classes of + Some [stockClassId] -> + if stockClassId `elem` currentClassIds then Some stockClassId else None + _ -> None + +stockPlanReturnToPoolWasUnchanged : + StockPlanValidationSnapshot -> + StockPlanReturnToPoolOcfData -> + ContractId StockPlanReturnToPool -> + Bool +stockPlanReturnToPoolWasUnchanged snapshot returnData returnCid = + Map.lookup returnData.id snapshot.stock_plan_return_to_pool_cids == Some returnCid + validateStockPlanReturnToPoolQuantities : CapTableMaps -> Update () validateStockPlanReturnToPoolQuantities maps = do successorIndex <- stockPlanSuccessorIndex maps @@ -3733,15 +3926,15 @@ validateStockPlanReturnToPoolQuantities maps = do (returnedQuantity <= securityAllocation)) (Map.values maps.stock_plan_return_to_pools) -validateStockPlanReturnToPoolClasses : CapTableMaps -> Update () -validateStockPlanReturnToPoolClasses maps = do +validateStockPlanReturnToPoolClasses : StockPlanValidationSnapshot -> CapTableMaps -> Update () +validateStockPlanReturnToPoolClasses snapshot maps = do successorIndex <- stockPlanSuccessorIndex maps mapA_ (\returnCid -> do returnToPool <- fetch returnCid - stockClassIdOpt <- stockPlanSecurityRecordedStockClassId maps returnToPool.return_data.security_id + recordedStockClassIdOpt <- stockPlanSecurityRecordedStockClassId maps returnToPool.return_data.security_id sourcePlanIdOpt <- stockPlanSecurityEffectivePlanId successorIndex maps returnToPool.return_data.security_id - case stockClassIdOpt of + case recordedStockClassIdOpt of Some stockClassId -> assertStockPlanIncludesClass ("Stock plan return-to-pool " <> returnToPool.return_data.id) @@ -3750,9 +3943,24 @@ validateStockPlanReturnToPoolClasses maps = do stockClassId None -> case sourcePlanIdOpt of Some sourcePlanId -> - assertMsg - ("Stock plan return-to-pool " <> returnToPool.return_data.id <> " cannot roll a security without a recorded stock class into a different stock plan") - (returnToPool.return_data.stock_plan_id == sourcePlanId) + if returnToPool.return_data.stock_plan_id == sourcePlanId + then pure () + else do + inferredStockClassIdOpt <- + if stockPlanReturnToPoolWasUnchanged snapshot returnToPool.return_data returnCid + then stockPlanReturnToPoolStockClassId maps returnToPool.return_data + else classlessStockPlanSourceClassId snapshot maps sourcePlanId + case inferredStockClassIdOpt of + Some stockClassId -> + assertStockPlanIncludesClass + ("Stock plan return-to-pool " <> returnToPool.return_data.id) + maps + returnToPool.return_data.stock_plan_id + stockClassId + None -> + assertMsg + ("Stock plan return-to-pool " <> returnToPool.return_data.id <> " cannot roll a classless security into a different plan without an unambiguous historical class") + False None -> assertMsg ("Stock plan return-to-pool " <> returnToPool.return_data.id <> " cannot determine the source stock plan") @@ -4181,15 +4389,16 @@ validateStockPlanReservedSharesAtDates eventIndex maps adjustmentHistory = do (fromOptional [] (Map.lookup stockPlan.plan_data.id validationDates))) (Map.values maps.stock_plans) -validateStockPlanPoolCeilings : StockClassShareEventIndex -> CapTableMaps -> Update () -validateStockPlanPoolCeilings eventIndex maps = do +validateStockPlanPoolCeilings : StockPlanValidationSnapshot -> StockClassShareEventIndex -> CapTableMaps -> Update () +validateStockPlanPoolCeilings snapshot eventIndex maps = do validateStockPlanPoolAdjustmentReferences maps adjustmentIndex <- stockPlanPoolAdjustmentIndex maps adjustmentHistory <- stockPlanPoolAdjustmentHistoryIndex maps + validateClasslessEquitySuccessorStockClassHistory snapshot maps validateStockPlanAllocationClasses maps validateStockPlanAllocationEffectiveDates maps validateStockPlanEquityCompensationSourceQuantities maps - validateStockPlanReturnToPoolClasses maps + validateStockPlanReturnToPoolClasses snapshot maps validateStockPlanReturnToPoolQuantities maps validateStockPlanReservedShares eventIndex maps adjustmentIndex validateStockPlanReservedSharesAtDates eventIndex maps adjustmentHistory @@ -4579,11 +4788,12 @@ validateSuccessorSecurityIdContinuity maps = do (Map.values maps.equity_compensation_releases) validateAuthorizedShareCeilings : + StockPlanValidationSnapshot -> CapTableMaps -> Map Text StockClassAuthorizedSharesAdjustmentOcfData -> ContractId Issuer -> Update () -validateAuthorizedShareCeilings maps adjustmentIndex issuerCid = do +validateAuthorizedShareCeilings stockPlanSnapshot maps adjustmentIndex issuerCid = do eventIndex <- stockClassShareEventIndex maps validateFinalSecurityIdUniqueness maps validateSuccessorSecurityIdContinuity maps @@ -4609,7 +4819,7 @@ validateAuthorizedShareCeilings maps adjustmentIndex issuerCid = do validateStockClassAuthorizedSharesAtDates eventIndex stockClasses lotsByStockClass stockClassAdjustmentHistory stockClassValidationDates validateIssuerAuthorizedShares stockClasses maps adjustmentIndex issuerCid validateIssuerAuthorizedSharesAtDates stockClasses maps stockClassAdjustmentHistory issuerAdjustmentHistory issuerCid - validateStockPlanPoolCeilings eventIndex maps + validateStockPlanPoolCeilings stockPlanSnapshot eventIndex maps -- | Get the processing tier for a create operation getCreateTier : OcfCreateData -> Int @@ -4690,6 +4900,7 @@ template CapTable do -- Start with current maps let initialMaps = toMaps this + initialStockPlanValidation <- stockPlanValidationSnapshot initialMaps -- Process deletes first to free unique constraints (e.g., security_id) -- This allows object replacements (delete old + create new with same security_id) @@ -4746,6 +4957,7 @@ template CapTable authorizedShareAdjustmentIndex <- stockClassAuthorizedAdjustmentIndex finalMaps validateAuthorizedShareCeilings + initialStockPlanValidation finalMaps authorizedShareAdjustmentIndex newIssuerCid From 832cd989a69f234f4586f6ae654f884238795036 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 21:00:59 -0400 Subject: [PATCH 55/62] Revert "fix: reconcile stock plan validation history" This reverts commit 4d039eaec0a2e1e2723474f8b887ca88985045be. --- .../OpenCapTable/TestLifecycleInvariants.daml | 80 ---- .../TestStockPlanReservedShares.daml | 353 ++---------------- .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 260 ++----------- 5 files changed, 55 insertions(+), 646 deletions(-) diff --git a/Test/daml/OpenCapTable/TestLifecycleInvariants.daml b/Test/daml/OpenCapTable/TestLifecycleInvariants.daml index 2f706e49..4e55399e 100644 --- a/Test/daml/OpenCapTable/TestLifecycleInvariants.daml +++ b/Test/daml/OpenCapTable/TestLifecycleInvariants.daml @@ -18,7 +18,6 @@ import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup import OpenCapTable.TestHelpers -import qualified DA.Map as Map import Daml.Script lifecycleWarrantTrigger : Text -> Text -> OcfConversionTrigger @@ -286,85 +285,6 @@ testRepeatedEquityTerminalEventsAtSameTimestampFail = script do edits = [] deletes = [] -testSecurityIssuanceDuplicateAcrossTypesFails = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_GLOBAL_SECURITY_ID" 1_000.0) - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_GLOBAL_SECURITY_ID" "Global Security ID Holder" - let sharedSecurityId = "SEC_GLOBAL_SECURITY_ID" - issued <- submit issuer do - exerciseCmd capTableReady CT.UpdateCapTable with - creates = [CT.OcfCreateStockIssuance (defaultStockIssuance "TX_GLOBAL_SECURITY_ID_STOCK" sharedSecurityId stakeholderId stockClassId)] - edits = [] - deletes = [] - - submitMustFail issuer do - exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_GLOBAL_SECURITY_ID_WARRANT" sharedSecurityId stakeholderId stockClassId)] - edits = [] - deletes = [] - -testSecurityIssuanceDuplicateAcrossTypesInSameBatchFails = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_BATCH_SECURITY_ID" 1_000.0) - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_BATCH_SECURITY_ID" "Batch Security ID Holder" - let sharedSecurityId = "SEC_BATCH_SECURITY_ID" - - submitMustFail issuer do - exerciseCmd capTableReady CT.UpdateCapTable with - creates = - [ CT.OcfCreateStockIssuance (defaultStockIssuance "TX_BATCH_SECURITY_ID_STOCK" sharedSecurityId stakeholderId stockClassId) - , CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_BATCH_SECURITY_ID_WARRANT" sharedSecurityId stakeholderId stockClassId) - ] - edits = [] - deletes = [] - -testSecurityIssuanceEditCannotReuseAnotherTypeSecurityId = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_EDIT_SECURITY_ID" 1_000.0) - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_EDIT_SECURITY_ID" "Edit Security ID Holder" - let stockSecurityId = "SEC_EDIT_SECURITY_ID_STOCK" - let warrantSecurityId = "SEC_EDIT_SECURITY_ID_WARRANT" - issued <- submit issuer do - exerciseCmd capTableReady CT.UpdateCapTable with - creates = - [ CT.OcfCreateStockIssuance (defaultStockIssuance "TX_EDIT_SECURITY_ID_STOCK" stockSecurityId stakeholderId stockClassId) - , CT.OcfCreateWarrantIssuance (lifecycleWarrantIssuance "TX_EDIT_SECURITY_ID_WARRANT" warrantSecurityId stakeholderId stockClassId) - ] - edits = [] - deletes = [] - - submitMustFail issuer do - exerciseCmd issued.updatedCapTableCid CT.UpdateCapTable with - creates = [] - edits = [CT.OcfEditStockIssuance (defaultStockIssuance "TX_EDIT_SECURITY_ID_STOCK" warrantSecurityId stakeholderId stockClassId)] - deletes = [] - -testFinalSecurityIdUniquenessRejectsStaleLegacyIndex = script do - TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_LEGACY_SECURITY_ID" 1_000.0) - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_LEGACY_SECURITY_ID" "Legacy Security ID Holder" - let sharedSecurityId = "SEC_LEGACY_SECURITY_ID" - issued <- submit issuer do - exerciseCmd capTableReady CT.UpdateCapTable with - creates = [CT.OcfCreateStockIssuance (defaultStockIssuance "TX_LEGACY_SECURITY_ID_STOCK" sharedSecurityId stakeholderId stockClassId)] - edits = [] - deletes = [] - capTableDataOpt <- queryContractId issuer issued.updatedCapTableCid - let Some capTableData = capTableDataOpt - - duplicateWarrantCid <- submit (actAs issuer <> actAs system_operator) $ createCmd $ WarrantIssuance with - context = ctx - issuance_data = lifecycleWarrantIssuance "TX_LEGACY_SECURITY_ID_WARRANT" sharedSecurityId stakeholderId stockClassId - let corruptedCapTable = capTableData with - warrant_issuances = Map.insert "TX_LEGACY_SECURITY_ID_WARRANT" duplicateWarrantCid capTableData.warrant_issuances - corruptedCapTableCid <- submit (actAs issuer <> actAs system_operator) $ createCmd corruptedCapTable - - submitMustFail issuer do - exerciseCmd corruptedCapTableCid CT.UpdateCapTable with - creates = [] - edits = [] - deletes = [] - testBalanceSecurityCannotAlsoBeResultingSecurity = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_BALANCE_RESULT_ALIAS" 1_000.0) diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index 30a3f18f..8e6b5b0c 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -2186,7 +2186,7 @@ testStockPlanReservedShares_CumulativeReturnsAreSplitAdjusted = script do edits = [] deletes = [] -testStockPlanReservedShares_ClasslessSuccessorRemainsValidWhenPlanAddsClass = script do +testStockPlanReservedShares_PlanAddClassRequiresSourceDisambiguation = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_CLASSLESS_SUCCESSOR_PLAN_EDIT_FIRST" (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_CLASSLESS_SUCCESSOR_PLAN_EDIT_SECOND" @@ -2229,186 +2229,20 @@ testStockPlanReservedShares_ClasslessSuccessorRemainsValidWhenPlanAddsClass = sc deletes = [] let editedPlan = plan with stock_class_ids = [firstStockClassId, secondStockClassId] - planEdited <- submit issuer do - exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with - creates = [] - edits = [CT.OcfEditStockPlan editedPlan] - deletes = [] - - let reclassifiedResult = resultIssuance with stock_class_id = Some secondStockClassId submitMustFail issuer do - exerciseCmd planEdited.updatedCapTableCid CT.UpdateCapTable with + exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with creates = [] - edits = [CT.OcfEditEquityCompensationIssuance reclassifiedResult] - deletes = [] - pure () - -testStockPlanReservedShares_ClasslessSuccessorCannotUseNewlyAddedPlanClass = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_CLASSLESS_NEW_CLASS_FIRST" - (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_CLASSLESS_NEW_CLASS_SECOND" - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_CLASSLESS_NEW_CLASS" "Classless New Class Holder" - let stockPlanId = "PLAN_CLASSLESS_NEW_CLASS" - let sourceSecurityId = "SEC_CLASSLESS_NEW_CLASS_SOURCE" - let newClassResultSecurityId = "SEC_CLASSLESS_NEW_CLASS_RESULT" - let priorClassResultSecurityId = "SEC_CLASSLESS_PRIOR_CLASS_RESULT" - let plan = (defaultStockPlan stockPlanId firstStockClassId) with initial_shares_reserved = 100.0 - let sourceIssuance = (stockPlanEquityCompensationIssuance - "TX_CLASSLESS_NEW_CLASS_SOURCE" - sourceSecurityId - stakeholderId - firstStockClassId - stockPlanId - 100.0) with stock_class_id = None - source <- submit issuer do - exerciseCmd capTableReady CT.UpdateCapTable with - creates = - [ CT.OcfCreateStockPlan plan - , CT.OcfCreateEquityCompensationIssuance sourceIssuance - ] - edits = [] - deletes = [] - - let editedPlan = plan with stock_class_ids = [firstStockClassId, secondStockClassId] - let newClassResultIssuance = (stockPlanEquityCompensationIssuance - "TX_CLASSLESS_NEW_CLASS_RESULT" - newClassResultSecurityId - stakeholderId - secondStockClassId - stockPlanId - 100.0) with stock_plan_id = None - submitMustFail issuer do - exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with - creates = - [ CT.OcfCreateEquityCompensationTransfer - (planEquityCompensationTransfer "TX_CLASSLESS_NEW_CLASS_TRANSFER" sourceSecurityId newClassResultSecurityId 100.0) - , CT.OcfCreateEquityCompensationIssuance newClassResultIssuance - ] - edits = [CT.OcfEditStockPlan editedPlan] - deletes = [] - - let priorClassResultIssuance = (stockPlanEquityCompensationIssuance - "TX_CLASSLESS_PRIOR_CLASS_RESULT" - priorClassResultSecurityId - stakeholderId - firstStockClassId - stockPlanId - 100.0) with stock_plan_id = None - _ <- submit issuer do - exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with - creates = - [ CT.OcfCreateEquityCompensationTransfer - (planEquityCompensationTransfer "TX_CLASSLESS_PRIOR_CLASS_TRANSFER" sourceSecurityId priorClassResultSecurityId 100.0) - , CT.OcfCreateEquityCompensationIssuance priorClassResultIssuance - ] - edits = [CT.OcfEditStockPlan editedPlan] - deletes = [] - pure () - -testStockPlanReservedShares_ClasslessTransferCannotAdoptExistingNewClassSecurity = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_CLASSLESS_ADOPT_FIRST" - (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_CLASSLESS_ADOPT_SECOND" - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_CLASSLESS_ADOPT" "Classless Adopt Holder" - let stockPlanId = "PLAN_CLASSLESS_ADOPT" - let sourceSecurityId = "SEC_CLASSLESS_ADOPT_SOURCE" - let existingSecurityId = "SEC_CLASSLESS_ADOPT_EXISTING" - let plan = (defaultStockPlan stockPlanId firstStockClassId) with initial_shares_reserved = 100.0 - let sourceIssuance = (stockPlanEquityCompensationIssuance - "TX_CLASSLESS_ADOPT_SOURCE" - sourceSecurityId - stakeholderId - firstStockClassId - stockPlanId - 100.0) with stock_class_id = None - let existingIssuance = (stockPlanEquityCompensationIssuance - "TX_CLASSLESS_ADOPT_EXISTING" - existingSecurityId - stakeholderId - secondStockClassId - stockPlanId - 100.0) with stock_plan_id = None - source <- submit issuer do - exerciseCmd capTableReady CT.UpdateCapTable with - creates = - [ CT.OcfCreateStockPlan plan - , CT.OcfCreateEquityCompensationIssuance sourceIssuance - , CT.OcfCreateEquityCompensationIssuance existingIssuance - ] - edits = [] - deletes = [] - - let editedPlan = plan with stock_class_ids = [firstStockClassId, secondStockClassId] - submitMustFail issuer do - exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with - creates = - [ CT.OcfCreateEquityCompensationTransfer - (planEquityCompensationTransfer "TX_CLASSLESS_ADOPT_TRANSFER" sourceSecurityId existingSecurityId 100.0) - ] - edits = [CT.OcfEditStockPlan editedPlan] - deletes = [] - -testStockPlanReservedShares_ClasslessCancellationCannotUseNewlyAddedPlanClass = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_CLASSLESS_CANCEL_NEW_CLASS_FIRST" - (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_CLASSLESS_CANCEL_NEW_CLASS_SECOND" - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_CLASSLESS_CANCEL_NEW_CLASS" "Classless Cancellation New Class Holder" - let stockPlanId = "PLAN_CLASSLESS_CANCEL_NEW_CLASS" - let sourceSecurityId = "SEC_CLASSLESS_CANCEL_NEW_CLASS_SOURCE" - let newClassBalanceSecurityId = "SEC_CLASSLESS_CANCEL_NEW_CLASS_BALANCE" - let priorClassBalanceSecurityId = "SEC_CLASSLESS_CANCEL_PRIOR_CLASS_BALANCE" - let plan = (defaultStockPlan stockPlanId firstStockClassId) with initial_shares_reserved = 100.0 - let sourceIssuance = (stockPlanEquityCompensationIssuance - "TX_CLASSLESS_CANCEL_NEW_CLASS_SOURCE" - sourceSecurityId - stakeholderId - firstStockClassId - stockPlanId - 100.0) with stock_class_id = None - source <- submit issuer do - exerciseCmd capTableReady CT.UpdateCapTable with - creates = - [ CT.OcfCreateStockPlan plan - , CT.OcfCreateEquityCompensationIssuance sourceIssuance - ] - edits = [] - deletes = [] - - let editedPlan = plan with stock_class_ids = [firstStockClassId, secondStockClassId] - let newClassCancellation = (equityCompensationCancellation "TX_CLASSLESS_CANCEL_NEW_CLASS" sourceSecurityId 40.0) with - balance_security_id = Some newClassBalanceSecurityId - let newClassBalanceIssuance = (stockPlanEquityCompensationIssuance - "TX_CLASSLESS_CANCEL_NEW_CLASS_BALANCE" - newClassBalanceSecurityId - stakeholderId - secondStockClassId - stockPlanId - 60.0) with stock_plan_id = None - submitMustFail issuer do - exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with - creates = - [ CT.OcfCreateEquityCompensationCancellation newClassCancellation - , CT.OcfCreateEquityCompensationIssuance newClassBalanceIssuance - ] edits = [CT.OcfEditStockPlan editedPlan] deletes = [] - let priorClassCancellation = (equityCompensationCancellation "TX_CLASSLESS_CANCEL_PRIOR_CLASS" sourceSecurityId 40.0) with - balance_security_id = Some priorClassBalanceSecurityId - let priorClassBalanceIssuance = (stockPlanEquityCompensationIssuance - "TX_CLASSLESS_CANCEL_PRIOR_CLASS_BALANCE" - priorClassBalanceSecurityId - stakeholderId - firstStockClassId - stockPlanId - 60.0) with stock_plan_id = None + let disambiguatedSourceIssuance = sourceIssuance with stock_class_id = Some firstStockClassId _ <- submit issuer do - exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with - creates = - [ CT.OcfCreateEquityCompensationCancellation priorClassCancellation - , CT.OcfCreateEquityCompensationIssuance priorClassBalanceIssuance + exerciseCmd transferred.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = + [ CT.OcfEditStockPlan editedPlan + , CT.OcfEditEquityCompensationIssuance disambiguatedSourceIssuance ] - edits = [CT.OcfEditStockPlan editedPlan] deletes = [] pure () @@ -2551,30 +2385,28 @@ testStockPlanReservedShares_AmbiguousClasslessReturnStaysInSourcePlan = script d deletes = [] pure () -testStockPlanReservedShares_ClasslessRolloverCannotBackdateNewPlanClass = script do +testStockPlanReservedShares_ClasslessCrossPlanReturnRequiresRecordedClass = script do TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_CLASSLESS_ROLLOVER_BACKDATE_FIRST" - (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_CLASSLESS_ROLLOVER_BACKDATE_SECOND" - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_CLASSLESS_ROLLOVER_BACKDATE" "Classless Rollover Backdate Holder" + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table let sourcePlanId = "PLAN_CLASSLESS_ROLLOVER_SOURCE" let targetPlanId = "PLAN_CLASSLESS_ROLLOVER_TARGET" let sourceSecurityId = "SEC_CLASSLESS_ROLLOVER_SOURCE" - let sourcePlan = (defaultStockPlan sourcePlanId firstStockClassId) with initial_shares_reserved = 100.0 - let targetPlan = (defaultStockPlan targetPlanId secondStockClassId) with initial_shares_reserved = 100.0 + let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 100.0 + let targetPlan = (defaultStockPlan targetPlanId stockClassId) with initial_shares_reserved = 100.0 let sourceIssuance = (stockPlanEquityCompensationIssuance "TX_CLASSLESS_ROLLOVER_SOURCE" sourceSecurityId stakeholderId - firstStockClassId + stockClassId sourcePlanId 100.0) with stock_class_id = None let targetIssuance = stockPlanEquityCompensationIssuance "TX_CLASSLESS_ROLLOVER_TARGET" "SEC_CLASSLESS_ROLLOVER_TARGET" stakeholderId - secondStockClassId + stockClassId targetPlanId - 100.0 + 200.0 source <- submit issuer do exerciseCmd capTableReady CT.UpdateCapTable with creates = @@ -2587,7 +2419,6 @@ testStockPlanReservedShares_ClasslessRolloverCannotBackdateNewPlanClass = script let cancellation = equityCompensationCancellation "TX_CLASSLESS_ROLLOVER_CANCEL" sourceSecurityId 100.0 let returnToPool = stockPlanReturnToPool "TX_CLASSLESS_ROLLOVER_POOL" sourceSecurityId targetPlanId 100.0 - let editedSourcePlan = sourcePlan with stock_class_ids = [firstStockClassId, secondStockClassId] submitMustFail issuer do exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with creates = @@ -2595,58 +2426,25 @@ testStockPlanReservedShares_ClasslessRolloverCannotBackdateNewPlanClass = script , CT.OcfCreateStockPlanReturnToPool returnToPool , CT.OcfCreateEquityCompensationIssuance targetIssuance ] - edits = [CT.OcfEditStockPlan editedSourcePlan] - deletes = [] - -testStockPlanReservedShares_ClasslessRolloverRemainsValidWhenSourcePlanAddsClass = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithFirstClass, firstStockClassId) <- setupStockClassWithId issuer cap_table "SC_CLASSLESS_ROLLOVER_FIRST" - (capTableWithClasses, secondStockClassId) <- setupStockClassWithId issuer capTableWithFirstClass "SC_CLASSLESS_ROLLOVER_SECOND" - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH_CLASSLESS_ROLLOVER_PLAN_EDIT" "Classless Rollover Plan Edit Holder" - let sourcePlanId = "PLAN_CLASSLESS_ROLLOVER_SOURCE" - let targetPlanId = "PLAN_CLASSLESS_ROLLOVER_TARGET" - let sourceSecurityId = "SEC_CLASSLESS_ROLLOVER_SOURCE" - let sourcePlan = (defaultStockPlan sourcePlanId firstStockClassId) with initial_shares_reserved = 100.0 - let targetPlan = (defaultStockPlan targetPlanId firstStockClassId) with initial_shares_reserved = 100.0 - let sourceIssuance = (stockPlanEquityCompensationIssuance - "TX_CLASSLESS_ROLLOVER_SOURCE" - sourceSecurityId - stakeholderId - firstStockClassId - sourcePlanId - 100.0) with stock_class_id = None - let targetIssuance = stockPlanEquityCompensationIssuance - "TX_CLASSLESS_ROLLOVER_TARGET" - "SEC_CLASSLESS_ROLLOVER_TARGET" - stakeholderId - firstStockClassId - targetPlanId - 200.0 - rolledOver <- submit issuer do - exerciseCmd capTableReady CT.UpdateCapTable with - creates = - [ CT.OcfCreateStockPlan sourcePlan - , CT.OcfCreateStockPlan targetPlan - , CT.OcfCreateEquityCompensationIssuance sourceIssuance - , CT.OcfCreateEquityCompensationCancellation - (equityCompensationCancellation "TX_CLASSLESS_ROLLOVER_CANCEL" sourceSecurityId 100.0) - , CT.OcfCreateStockPlanReturnToPool - (stockPlanReturnToPool "TX_CLASSLESS_ROLLOVER_POOL" sourceSecurityId targetPlanId 100.0) - , CT.OcfCreateEquityCompensationIssuance targetIssuance - ] edits = [] deletes = [] - let editedSourcePlan = sourcePlan with stock_class_ids = [firstStockClassId, secondStockClassId] - planEdited <- submit issuer do - exerciseCmd rolledOver.updatedCapTableCid CT.UpdateCapTable with + let disambiguatedSourceIssuance = sourceIssuance with stock_class_id = Some stockClassId + disambiguated <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with creates = [] - edits = [CT.OcfEditStockPlan editedSourcePlan] + edits = [CT.OcfEditEquityCompensationIssuance disambiguatedSourceIssuance] deletes = [] _ <- submit issuer do - exerciseCmd planEdited.updatedCapTableCid CT.UpdateCapTable with - creates = [] + exerciseCmd disambiguated.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation + cancellation + , CT.OcfCreateStockPlanReturnToPool + returnToPool + , CT.OcfCreateEquityCompensationIssuance targetIssuance + ] edits = [] deletes = [] pure () @@ -2893,100 +2691,3 @@ testStockPlanReservedShares_StockSuccessorPlanMustMatchInheritedPlan = script do edits = [] deletes = [] pure () - -testStockPlanReservedShares_ClasslessTransferCannotOverrideInheritedPlanForReturn = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table - let sourcePlanId = "PLAN_CLASSLESS_TRANSFER_SOURCE" - let targetPlanId = "PLAN_CLASSLESS_TRANSFER_TARGET" - let sourceSecurityId = "SEC_CLASSLESS_TRANSFER_SOURCE" - let successorSecurityId = "SEC_CLASSLESS_TRANSFER_SUCCESSOR" - let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 100.0 - let targetPlan = (defaultStockPlan targetPlanId stockClassId) with initial_shares_reserved = 100.0 - let sourceIssuance = (stockPlanEquityCompensationIssuance - "TX_CLASSLESS_TRANSFER_SOURCE" - sourceSecurityId - stakeholderId - stockClassId - sourcePlanId - 100.0) with stock_class_id = None - source <- submit issuer do - exerciseCmd capTableReady CT.UpdateCapTable with - creates = - [ CT.OcfCreateStockPlan sourcePlan - , CT.OcfCreateStockPlan targetPlan - , CT.OcfCreateEquityCompensationIssuance sourceIssuance - ] - edits = [] - deletes = [] - - let transferDate = DT.time (DA.date 2024 Jan 02) 0 0 0 - let returnDate = DT.time (DA.date 2024 Jan 03) 0 0 0 - let successorIssuance = (stockPlanEquityCompensationIssuance - "TX_CLASSLESS_TRANSFER_SUCCESSOR" - successorSecurityId - stakeholderId - stockClassId - targetPlanId - 100.0) with - date = transferDate - stock_class_id = None - submitMustFail issuer do - exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with - creates = - [ CT.OcfCreateEquityCompensationTransfer - ((planEquityCompensationTransfer "TX_CLASSLESS_TRANSFER" sourceSecurityId successorSecurityId 100.0) with date = transferDate) - , CT.OcfCreateEquityCompensationIssuance successorIssuance - , CT.OcfCreateEquityCompensationCancellation - ((equityCompensationCancellation "TX_CLASSLESS_TRANSFER_CANCEL" successorSecurityId 100.0) with date = returnDate) - , CT.OcfCreateStockPlanReturnToPool - ((stockPlanReturnToPool "TX_CLASSLESS_TRANSFER_RETURN" successorSecurityId targetPlanId 100.0) with date = returnDate) - ] - edits = [] - deletes = [] - -testStockPlanReservedShares_ConsolidationSourceCannotOverrideInheritedPlan = script do - TestOcp{issuer, cap_table} <- setupTestOcp - (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table - let sourcePlanId = "PLAN_CONSOLIDATION_LINEAGE_SOURCE" - let targetPlanId = "PLAN_CONSOLIDATION_LINEAGE_TARGET" - let sourceSecurityId = "SEC_CONSOLIDATION_LINEAGE_SOURCE" - let successorSecurityId = "SEC_CONSOLIDATION_LINEAGE_SUCCESSOR" - let resultSecurityId = "SEC_CONSOLIDATION_LINEAGE_RESULT" - let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 100.0 - let targetPlan = (defaultStockPlan targetPlanId stockClassId) with initial_shares_reserved = 100.0 - let sourceIssuance = (defaultStockIssuance "TX_CONSOLIDATION_LINEAGE_SOURCE" sourceSecurityId stakeholderId stockClassId) with - stock_plan_id = Some sourcePlanId - quantity = 100.0 - source <- submit issuer do - exerciseCmd capTableReady CT.UpdateCapTable with - creates = - [ CT.OcfCreateStockPlan sourcePlan - , CT.OcfCreateStockPlan targetPlan - , CT.OcfCreateStockIssuance sourceIssuance - ] - edits = [] - deletes = [] - - let transferDate = DT.time (DA.date 2024 Jan 02) 0 0 0 - let consolidationDate = DT.time (DA.date 2024 Jan 03) 0 0 0 - let successorIssuance = (defaultStockIssuance "TX_CONSOLIDATION_LINEAGE_SUCCESSOR" successorSecurityId stakeholderId stockClassId) with - date = transferDate - stock_plan_id = Some targetPlanId - quantity = 100.0 - let resultIssuance = (defaultStockIssuance "TX_CONSOLIDATION_LINEAGE_RESULT" resultSecurityId stakeholderId stockClassId) with - date = consolidationDate - stock_plan_id = Some targetPlanId - quantity = 100.0 - submitMustFail issuer do - exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with - creates = - [ CT.OcfCreateStockTransfer - ((planStockTransfer "TX_CONSOLIDATION_LINEAGE_TRANSFER" sourceSecurityId successorSecurityId 100.0) with date = transferDate) - , CT.OcfCreateStockIssuance successorIssuance - , CT.OcfCreateStockConsolidation - (planStockConsolidation "TX_CONSOLIDATION_LINEAGE" [successorSecurityId] resultSecurityId consolidationDate) - , CT.OcfCreateStockIssuance resultIssuance - ] - edits = [] - deletes = [] diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 68dc6538..2c6fb78d 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0bff92de697063dee24325e804315f003b9fe399d28cd61ddd76d773886d7d87 -size 2967712 +oid sha256:3a9080210d0812062c029b54c625fc80624eb310770b2625ae5e4445f59d8264 +size 2942894 diff --git a/dars/dars.lock b/dars/dars.lock index 5fa3477d..a8d415ce 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,8 +33,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "0bff92de697063dee24325e804315f003b9fe399d28cd61ddd76d773886d7d87", - "size": 2967712, + "sha256": "3a9080210d0812062c029b54c625fc80624eb310770b2625ae5e4445f59d8264", + "size": 2942894, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-11T00:41:54.852Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index e51365d4..b42462f9 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -89,12 +89,6 @@ data CapTableMaps = CapTableMaps with warrant_issuances_by_security_id: Map Text (ContractId WarrantIssuance) deriving (Eq, Show) -data StockPlanValidationSnapshot = StockPlanValidationSnapshot with - classless_equity_successors: Map Text (Text, Text, Optional Text) - stock_plan_classes: Map Text [Text] - stock_plan_return_to_pool_cids: Map Text (ContractId StockPlanReturnToPool) - deriving (Eq, Show) - -- | Extract maps from CapTable for processing toMaps : CapTable -> CapTableMaps toMaps ct = CapTableMaps with @@ -1886,160 +1880,12 @@ equityCompensationSuccessorStockClassIds maps issuanceData = Some stockPlanId -> case Map.lookup stockPlanId maps.stock_plans of Some stockPlanCid -> do stockPlan <- fetch stockPlanCid - pure (None :: map Some stockPlan.plan_data.stock_class_ids) + pure $ case stockPlan.plan_data.stock_class_ids of + [stockClassId] -> [None, Some stockClassId] + _ -> [None] None -> pure [None] None -> pure [None] -addClasslessEquitySuccessorHistory : - CapTableMaps -> - Map Text (Text, Text, Optional Text) -> - EquityCompensationIssuanceOcfData -> - [Text] -> - Update (Map Text (Text, Text, Optional Text)) -addClasslessEquitySuccessorHistory maps history sourceIssuanceData successorSecurityIds = - case (sourceIssuanceData.stock_class_id, sourceIssuanceData.stock_plan_id) of - (None, Some stockPlanId) -> - foldlA - (\updatedHistory successorSecurityId -> - case Map.lookup successorSecurityId maps.equity_compensation_issuances_by_security_id of - Some successorCid -> do - successor <- fetch successorCid - pure (Map.insert - successorSecurityId - (sourceIssuanceData.security_id, stockPlanId, successor.issuance_data.stock_class_id) - updatedHistory) - None -> pure updatedHistory) - history - successorSecurityIds - _ -> pure history - -classlessEquitySuccessorHistoryIndex : CapTableMaps -> Update (Map Text (Text, Text, Optional Text)) -classlessEquitySuccessorHistoryIndex maps = do - transferHistory <- foldlA - (\history transferCid -> do - transfer <- fetch transferCid - case Map.lookup transfer.transfer_data.security_id maps.equity_compensation_issuances_by_security_id of - Some sourceCid -> do - source <- fetch sourceCid - addClasslessEquitySuccessorHistory - maps - history - source.issuance_data - (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids) - None -> pure history) - Map.empty - (Map.values maps.equity_compensation_transfers) - foldlA - (\history cancellationCid -> do - cancellation <- fetch cancellationCid - case (cancellation.cancellation_data.balance_security_id, Map.lookup cancellation.cancellation_data.security_id maps.equity_compensation_issuances_by_security_id) of - (Some balanceSecurityId, Some sourceCid) -> do - source <- fetch sourceCid - addClasslessEquitySuccessorHistory maps history source.issuance_data [balanceSecurityId] - _ -> pure history) - transferHistory - (Map.values maps.equity_compensation_cancellations) - -stockPlanClassIndex : CapTableMaps -> Update (Map Text [Text]) -stockPlanClassIndex maps = - foldlA - (\index stockPlanCid -> do - stockPlan <- fetch stockPlanCid - pure (Map.insert stockPlan.plan_data.id stockPlan.plan_data.stock_class_ids index)) - Map.empty - (Map.values maps.stock_plans) - -stockPlanValidationSnapshot : CapTableMaps -> Update StockPlanValidationSnapshot -stockPlanValidationSnapshot maps = do - classlessEquitySuccessors <- classlessEquitySuccessorHistoryIndex maps - stockPlanClasses <- stockPlanClassIndex maps - pure StockPlanValidationSnapshot with - classless_equity_successors = classlessEquitySuccessors - stock_plan_classes = stockPlanClasses - stock_plan_return_to_pool_cids = maps.stock_plan_return_to_pools - -validateNewClasslessEquitySuccessorStockClass : - StockPlanValidationSnapshot -> - CapTableMaps -> - Text -> - Text -> - Text -> - Update () -validateNewClasslessEquitySuccessorStockClass snapshot maps label stockPlanId stockClassId = - case Map.lookup stockPlanId maps.stock_plans of - Some stockPlanCid -> do - stockPlan <- fetch stockPlanCid - let isCurrentSingleton = stockPlan.plan_data.stock_class_ids == [stockClassId] - let isPriorSingleton = - Map.lookup stockPlanId snapshot.stock_plan_classes == Some [stockClassId] - && stockClassId `elem` stockPlan.plan_data.stock_class_ids - assertMsg - (label <> " cannot assign stock class " <> stockClassId <> " without an unambiguous current or pre-update plan class") - (isCurrentSingleton || isPriorSingleton) - None -> assertMsg (label <> " references missing stock plan " <> stockPlanId) False - -validateClasslessEquitySuccessorStockClass : - StockPlanValidationSnapshot -> - CapTableMaps -> - Text -> - EquityCompensationIssuanceOcfData -> - Text -> - Update () -validateClasslessEquitySuccessorStockClass snapshot maps label sourceIssuanceData successorSecurityId = - case (sourceIssuanceData.stock_class_id, sourceIssuanceData.stock_plan_id) of - (None, Some stockPlanId) -> - case Map.lookup successorSecurityId maps.equity_compensation_issuances_by_security_id of - Some successorCid -> do - successor <- fetch successorCid - case successor.issuance_data.stock_class_id of - Some stockClassId -> - case Map.lookup successorSecurityId snapshot.classless_equity_successors of - Some (priorSourceSecurityId, priorStockPlanId, Some priorStockClassId) -> - if priorSourceSecurityId == sourceIssuanceData.security_id - && priorStockPlanId == stockPlanId - && priorStockClassId == stockClassId - then pure () - else validateNewClasslessEquitySuccessorStockClass snapshot maps label stockPlanId stockClassId - _ -> validateNewClasslessEquitySuccessorStockClass snapshot maps label stockPlanId stockClassId - None -> pure () - None -> pure () - _ -> pure () - -validateClasslessEquitySuccessorStockClassHistory : - StockPlanValidationSnapshot -> - CapTableMaps -> - Update () -validateClasslessEquitySuccessorStockClassHistory snapshot maps = do - mapA_ - (\transferCid -> do - transfer <- fetch transferCid - case Map.lookup transfer.transfer_data.security_id maps.equity_compensation_issuances_by_security_id of - Some sourceCid -> do - source <- fetch sourceCid - mapA_ - (validateClasslessEquitySuccessorStockClass - snapshot - maps - ("Equity compensation transfer " <> transfer.transfer_data.id) - source.issuance_data) - (successorSecurityIdsFor transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids) - None -> pure ()) - (Map.values maps.equity_compensation_transfers) - mapA_ - (\cancellationCid -> do - cancellation <- fetch cancellationCid - case (cancellation.cancellation_data.balance_security_id, Map.lookup cancellation.cancellation_data.security_id maps.equity_compensation_issuances_by_security_id) of - (Some balanceSecurityId, Some sourceCid) -> do - source <- fetch sourceCid - validateClasslessEquitySuccessorStockClass - snapshot - maps - ("Equity compensation cancellation " <> cancellation.cancellation_data.id) - source.issuance_data - balanceSecurityId - _ -> pure ()) - (Map.values maps.equity_compensation_cancellations) - equityCompensationIssuanceQuantityTotal : CapTableMaps -> Text -> @@ -3265,43 +3111,26 @@ equityCompensationPlanAllocationIndex successorIndex maps = Map.empty (Map.values maps.equity_compensation_issuances) -stockPlanClassIds : CapTableMaps -> Text -> Update [Text] -stockPlanClassIds maps stockPlanId = - case Map.lookup stockPlanId maps.stock_plans of - Some stockPlanCid -> do - stockPlan <- fetch stockPlanCid - pure stockPlan.plan_data.stock_class_ids - None -> pure [] - -stockPlanReturnToPoolStockClassId : CapTableMaps -> StockPlanReturnToPoolOcfData -> Update (Optional Text) -stockPlanReturnToPoolStockClassId maps returnData = - case (Map.lookup returnData.security_id maps.stock_issuances_by_security_id, Map.lookup returnData.security_id maps.equity_compensation_issuances_by_security_id) of +stockPlanReturnToPoolStockClassId : CapTableMaps -> Text -> Update (Optional Text) +stockPlanReturnToPoolStockClassId maps securityId = + case (Map.lookup securityId maps.stock_issuances_by_security_id, Map.lookup securityId maps.equity_compensation_issuances_by_security_id) of (Some stockCid, None) -> do issuance <- fetch stockCid pure (Some issuance.issuance_data.stock_class_id) (None, Some equityCompensationCid) -> do issuance <- fetch equityCompensationCid - case issuance.issuance_data.stock_class_id of - Some stockClassId -> pure (Some stockClassId) - None -> case issuance.issuance_data.stock_plan_id of - Some sourcePlanId -> do - sourceClassIds <- stockPlanClassIds maps sourcePlanId - case sourceClassIds of - [stockClassId] -> pure (Some stockClassId) - _ -> do - targetClassIds <- stockPlanClassIds maps returnData.stock_plan_id - case filter (`elem` targetClassIds) sourceClassIds of - [stockClassId] -> pure (Some stockClassId) - _ -> pure None - None -> pure None + stockClassIds <- equityCompensationPlanStockClassIds maps issuance.issuance_data + case stockClassIds of + [stockClassId] -> pure (Some stockClassId) + _ -> pure None (Some _, Some _) -> do - assertMsg ("Stock plan return-to-pool security id is ambiguous: " <> returnData.security_id) False + assertMsg ("Stock plan return-to-pool security id is ambiguous: " <> securityId) False pure None (None, None) -> pure None stockPlanReturnToPoolEvents : CapTableMaps -> StockPlanReturnToPoolOcfData -> Update [StockClassShareEvent] stockPlanReturnToPoolEvents maps returnData = do - stockClassIdOpt <- stockPlanReturnToPoolStockClassId maps returnData + stockClassIdOpt <- stockPlanReturnToPoolStockClassId maps returnData.security_id splitEvents <- case stockClassIdOpt of Some stockClassId -> foldlA @@ -3891,28 +3720,6 @@ stockPlanSecurityRecordedStockClassId maps securityId = pure None (None, None) -> pure None -classlessStockPlanSourceClassId : - StockPlanValidationSnapshot -> - CapTableMaps -> - Text -> - Update (Optional Text) -classlessStockPlanSourceClassId snapshot maps sourcePlanId = do - currentClassIds <- stockPlanClassIds maps sourcePlanId - pure $ case currentClassIds of - [stockClassId] -> Some stockClassId - _ -> case Map.lookup sourcePlanId snapshot.stock_plan_classes of - Some [stockClassId] -> - if stockClassId `elem` currentClassIds then Some stockClassId else None - _ -> None - -stockPlanReturnToPoolWasUnchanged : - StockPlanValidationSnapshot -> - StockPlanReturnToPoolOcfData -> - ContractId StockPlanReturnToPool -> - Bool -stockPlanReturnToPoolWasUnchanged snapshot returnData returnCid = - Map.lookup returnData.id snapshot.stock_plan_return_to_pool_cids == Some returnCid - validateStockPlanReturnToPoolQuantities : CapTableMaps -> Update () validateStockPlanReturnToPoolQuantities maps = do successorIndex <- stockPlanSuccessorIndex maps @@ -3926,15 +3733,15 @@ validateStockPlanReturnToPoolQuantities maps = do (returnedQuantity <= securityAllocation)) (Map.values maps.stock_plan_return_to_pools) -validateStockPlanReturnToPoolClasses : StockPlanValidationSnapshot -> CapTableMaps -> Update () -validateStockPlanReturnToPoolClasses snapshot maps = do +validateStockPlanReturnToPoolClasses : CapTableMaps -> Update () +validateStockPlanReturnToPoolClasses maps = do successorIndex <- stockPlanSuccessorIndex maps mapA_ (\returnCid -> do returnToPool <- fetch returnCid - recordedStockClassIdOpt <- stockPlanSecurityRecordedStockClassId maps returnToPool.return_data.security_id + stockClassIdOpt <- stockPlanSecurityRecordedStockClassId maps returnToPool.return_data.security_id sourcePlanIdOpt <- stockPlanSecurityEffectivePlanId successorIndex maps returnToPool.return_data.security_id - case recordedStockClassIdOpt of + case stockClassIdOpt of Some stockClassId -> assertStockPlanIncludesClass ("Stock plan return-to-pool " <> returnToPool.return_data.id) @@ -3943,24 +3750,9 @@ validateStockPlanReturnToPoolClasses snapshot maps = do stockClassId None -> case sourcePlanIdOpt of Some sourcePlanId -> - if returnToPool.return_data.stock_plan_id == sourcePlanId - then pure () - else do - inferredStockClassIdOpt <- - if stockPlanReturnToPoolWasUnchanged snapshot returnToPool.return_data returnCid - then stockPlanReturnToPoolStockClassId maps returnToPool.return_data - else classlessStockPlanSourceClassId snapshot maps sourcePlanId - case inferredStockClassIdOpt of - Some stockClassId -> - assertStockPlanIncludesClass - ("Stock plan return-to-pool " <> returnToPool.return_data.id) - maps - returnToPool.return_data.stock_plan_id - stockClassId - None -> - assertMsg - ("Stock plan return-to-pool " <> returnToPool.return_data.id <> " cannot roll a classless security into a different plan without an unambiguous historical class") - False + assertMsg + ("Stock plan return-to-pool " <> returnToPool.return_data.id <> " cannot roll a security without a recorded stock class into a different stock plan") + (returnToPool.return_data.stock_plan_id == sourcePlanId) None -> assertMsg ("Stock plan return-to-pool " <> returnToPool.return_data.id <> " cannot determine the source stock plan") @@ -4389,16 +4181,15 @@ validateStockPlanReservedSharesAtDates eventIndex maps adjustmentHistory = do (fromOptional [] (Map.lookup stockPlan.plan_data.id validationDates))) (Map.values maps.stock_plans) -validateStockPlanPoolCeilings : StockPlanValidationSnapshot -> StockClassShareEventIndex -> CapTableMaps -> Update () -validateStockPlanPoolCeilings snapshot eventIndex maps = do +validateStockPlanPoolCeilings : StockClassShareEventIndex -> CapTableMaps -> Update () +validateStockPlanPoolCeilings eventIndex maps = do validateStockPlanPoolAdjustmentReferences maps adjustmentIndex <- stockPlanPoolAdjustmentIndex maps adjustmentHistory <- stockPlanPoolAdjustmentHistoryIndex maps - validateClasslessEquitySuccessorStockClassHistory snapshot maps validateStockPlanAllocationClasses maps validateStockPlanAllocationEffectiveDates maps validateStockPlanEquityCompensationSourceQuantities maps - validateStockPlanReturnToPoolClasses snapshot maps + validateStockPlanReturnToPoolClasses maps validateStockPlanReturnToPoolQuantities maps validateStockPlanReservedShares eventIndex maps adjustmentIndex validateStockPlanReservedSharesAtDates eventIndex maps adjustmentHistory @@ -4788,12 +4579,11 @@ validateSuccessorSecurityIdContinuity maps = do (Map.values maps.equity_compensation_releases) validateAuthorizedShareCeilings : - StockPlanValidationSnapshot -> CapTableMaps -> Map Text StockClassAuthorizedSharesAdjustmentOcfData -> ContractId Issuer -> Update () -validateAuthorizedShareCeilings stockPlanSnapshot maps adjustmentIndex issuerCid = do +validateAuthorizedShareCeilings maps adjustmentIndex issuerCid = do eventIndex <- stockClassShareEventIndex maps validateFinalSecurityIdUniqueness maps validateSuccessorSecurityIdContinuity maps @@ -4819,7 +4609,7 @@ validateAuthorizedShareCeilings stockPlanSnapshot maps adjustmentIndex issuerCid validateStockClassAuthorizedSharesAtDates eventIndex stockClasses lotsByStockClass stockClassAdjustmentHistory stockClassValidationDates validateIssuerAuthorizedShares stockClasses maps adjustmentIndex issuerCid validateIssuerAuthorizedSharesAtDates stockClasses maps stockClassAdjustmentHistory issuerAdjustmentHistory issuerCid - validateStockPlanPoolCeilings stockPlanSnapshot eventIndex maps + validateStockPlanPoolCeilings eventIndex maps -- | Get the processing tier for a create operation getCreateTier : OcfCreateData -> Int @@ -4900,7 +4690,6 @@ template CapTable do -- Start with current maps let initialMaps = toMaps this - initialStockPlanValidation <- stockPlanValidationSnapshot initialMaps -- Process deletes first to free unique constraints (e.g., security_id) -- This allows object replacements (delete old + create new with same security_id) @@ -4957,7 +4746,6 @@ template CapTable authorizedShareAdjustmentIndex <- stockClassAuthorizedAdjustmentIndex finalMaps validateAuthorizedShareCeilings - initialStockPlanValidation finalMaps authorizedShareAdjustmentIndex newIssuerCid From 4a37f43cbd3705b96f5b697dfd6b7907b52177b0 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 21:33:32 -0400 Subject: [PATCH 56/62] test: preserve grouped security identity coverage --- .../TestSecurityIdValidation.daml | 168 +++++++++++++++++- 1 file changed, 167 insertions(+), 1 deletion(-) diff --git a/Test/daml/OpenCapTable/TestSecurityIdValidation.daml b/Test/daml/OpenCapTable/TestSecurityIdValidation.daml index f43dcb17..cec9dbd8 100644 --- a/Test/daml/OpenCapTable/TestSecurityIdValidation.daml +++ b/Test/daml/OpenCapTable/TestSecurityIdValidation.daml @@ -11,17 +11,40 @@ module OpenCapTable.TestSecurityIdValidation where import qualified Fairmint.OpenCapTable.OCF.StockTransfer as ST import qualified Fairmint.OpenCapTable.OCF.WarrantTransfer as WT +import qualified Fairmint.OpenCapTable.OCF.WarrantIssuance as WI import qualified Fairmint.OpenCapTable.OCF.ConvertibleTransfer as CVT import qualified Fairmint.OpenCapTable.OCF.EquityCompensationTransfer as ECT import qualified Fairmint.OpenCapTable.CapTable as CT import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addPrerequisites, addDefaultStockIssuance, addDefaultWarrantIssuance, addDefaultConvertibleIssuance, addDefaultEquityCompensationIssuance) +import OpenCapTable.TestHelpers (addPrerequisites, addDefaultStockIssuance, addDefaultWarrantIssuance, addDefaultConvertibleIssuance, addDefaultEquityCompensationIssuance, addStockIssuance, defaultStockIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) +import qualified DA.Map as Map import qualified DA.Time as DT import Daml.Script +warrantIssuanceData : Text -> Text -> Text -> WI.WarrantIssuanceOcfData +warrantIssuanceData issuanceId securityId stakeholderId = WI.WarrantIssuanceOcfData with + id = issuanceId + custom_id = "WR-" <> issuanceId + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + purchase_price = OcfMonetary with amount = 1.0, currency = "USD" + security_id = securityId + stakeholder_id = stakeholderId + comments = [] + exercise_triggers = [] + security_law_exemptions = [] + vestings = [] + board_approval_date = None + consideration_text = None + exercise_price = None + quantity = Some 100.0 + quantity_source = None + stockholder_approval_date = None + vesting_terms_id = None + warrant_expiration_date = None + -- ============================================================================= -- Test: Non-existent security_id fails with clear error @@ -204,6 +227,149 @@ testEquityCompensationTransfer_WrongSecurityTypeFails = script do deletes = [] pure () +-- ============================================================================= +-- Test: security_id values are globally unique across security issuance types +-- ============================================================================= + +testSecurityIssuance_DuplicateSecurityIdAcrossTypesFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + cap_table <- addPrerequisites issuer cap_table + cap_table <- addDefaultStockIssuance issuer cap_table + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance (warrantIssuanceData "WR-DUP-STOCK-SECURITY" "SEC-STOCK-1" "SH-1")] + edits = [] + deletes = [] + pure () + +testSecurityIssuance_DuplicateSecurityIdAcrossTypesFailsInSameBatch = script do + TestOcp{issuer, cap_table} <- setupTestOcp + cap_table <- addPrerequisites issuer cap_table + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance (defaultStockIssuance "TX-BATCH-STOCK" "SEC-BATCH-DUP" "SH-1" "SC_COMMON") + , CT.OcfCreateWarrantIssuance (warrantIssuanceData "TX-BATCH-WARRANT" "SEC-BATCH-DUP" "SH-1") + ] + edits = [] + deletes = [] + pure () + +testSecurityIssuance_EditCannotReuseSecurityIdFromAnotherType = script do + TestOcp{issuer, cap_table} <- setupTestOcp + cap_table <- addPrerequisites issuer cap_table + cap_table <- addDefaultWarrantIssuance issuer cap_table + cap_table <- addStockIssuance issuer cap_table "STOCK-EDIT-SRC" "SEC-STOCK-EDIT" "SH-1" "SC_COMMON" + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockIssuance (defaultStockIssuance "STOCK-EDIT-SRC" "SEC-WR-1" "SH-1" "SC_COMMON")] + deletes = [] + pure () + +-- Validate issuance contracts rather than trusting potentially stale indexes +-- on cap tables upgraded from an earlier package. +testSecurityIssuance_FinalStateDuplicateSecurityIdsFail = script do + TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp + cap_table <- addPrerequisites issuer cap_table + cap_table <- addDefaultStockIssuance issuer cap_table + capTableDataOpt <- queryContractId issuer cap_table + let Some capTableData = capTableDataOpt + + duplicateWarrantCid <- submit (actAs issuer <> actAs system_operator) $ createCmd $ WI.WarrantIssuance with + context = ctx + issuance_data = warrantIssuanceData "WR-UPGRADED-DUPLICATE" "SEC-STOCK-1" "SH-1" + + let corruptedCapTable = capTableData with + warrant_issuances = Map.insert "WR-UPGRADED-DUPLICATE" duplicateWarrantCid capTableData.warrant_issuances + corruptedCapTableCid <- submit (actAs issuer <> actAs system_operator) $ createCmd corruptedCapTable + + submitMustFail issuer do + exerciseCmd corruptedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [] + pure () + +testSuccessorSecurityId_CannotReuseSourceSecurityId = script do + TestOcp{issuer, cap_table} <- setupTestOcp + cap_table <- addPrerequisites issuer cap_table + cap_table <- addStockIssuance issuer cap_table "STOCK-SOURCE-REUSE" "SEC-SOURCE-REUSE" "SH-1" "SC_COMMON" + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateStockTransfer ST.StockTransferOcfData with + id = "TX-SOURCE-REUSE" + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + security_id = "SEC-SOURCE-REUSE" + quantity = 100.0 + comments = [] + resulting_security_ids = ["SEC-SOURCE-REUSE"] + balance_security_id = None + consideration_text = None] + edits = [] + deletes = [] + pure () + +testSuccessorSecurityId_BalanceCannotAlsoBeResult = script do + TestOcp{issuer, cap_table} <- setupTestOcp + cap_table <- addPrerequisites issuer cap_table + cap_table <- addStockIssuance issuer cap_table "STOCK-BALANCE-RESULT-SOURCE" "SEC-BALANCE-RESULT-SOURCE" "SH-1" "SC_COMMON" + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance + ((defaultStockIssuance "STOCK-BALANCE-RESULT" "SEC-BALANCE-RESULT" "SH-1" "SC_COMMON") with quantity = 50.0) + , CT.OcfCreateStockTransfer ST.StockTransferOcfData with + id = "TX-BALANCE-RESULT" + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + security_id = "SEC-BALANCE-RESULT-SOURCE" + quantity = 50.0 + comments = [] + resulting_security_ids = ["SEC-BALANCE-RESULT"] + balance_security_id = Some "SEC-BALANCE-RESULT" + consideration_text = None + ] + edits = [] + deletes = [] + pure () + +testSuccessorSecurityId_CannotBeProducedByMultipleTransactionTypes = script do + TestOcp{issuer, cap_table} <- setupTestOcp + cap_table <- addPrerequisites issuer cap_table + cap_table <- addStockIssuance issuer cap_table "STOCK-SHARED-RESULT-A" "SEC-SHARED-RESULT-A" "SH-1" "SC_COMMON" + cap_table <- addDefaultWarrantIssuance issuer cap_table + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockTransfer ST.StockTransferOcfData with + id = "TX-SHARED-RESULT-A" + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + security_id = "SEC-SHARED-RESULT-A" + quantity = 100.0 + comments = [] + resulting_security_ids = ["SEC-SHARED-RESULT"] + balance_security_id = None + consideration_text = None + , CT.OcfCreateWarrantTransfer WT.WarrantTransferOcfData with + id = "TX-SHARED-RESULT-B" + date = DT.time (DA.date 2024 Jan 01) 0 0 0 + security_id = "SEC-WR-1" + quantity = 100.0 + comments = [] + resulting_security_ids = ["SEC-SHARED-RESULT"] + balance_security_id = None + consideration_text = None + ] + edits = [] + deletes = [] + pure () + -- ============================================================================= -- Test: Valid security_id succeeds From 0a55da2e59d5f4e762bc69f4b7d989ea55366041 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 22:02:40 -0400 Subject: [PATCH 57/62] fix: validate plan returns and convertible outputs --- .../TestConvertibleConversion.daml | 115 +++-- .../TestStockClassAuthorizedShares.daml | 411 ++++++++++++++++++ .../TestStockPlanReservedShares.daml | 72 ++- .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- .../codegen/templates/CapTable.daml.template | 210 ++++++++- 6 files changed, 762 insertions(+), 56 deletions(-) diff --git a/Test/daml/OpenCapTable/TestConvertibleConversion.daml b/Test/daml/OpenCapTable/TestConvertibleConversion.daml index 83452d47..d92638d2 100644 --- a/Test/daml/OpenCapTable/TestConvertibleConversion.daml +++ b/Test/daml/OpenCapTable/TestConvertibleConversion.daml @@ -6,7 +6,7 @@ import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Conversion (OcfConversionTriggerType(..), OcfConvertibleConversionRight(..), OcfConvertibleConversionMechanism(..), OcfSAFEConversionMechanism(..), OcfConvertibleType(..), OcfCapitalizationDefinition(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addDefaultStakeholder) +import OpenCapTable.TestHelpers (defaultStockIssuance, setupForStockIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -59,22 +59,33 @@ createConvertibleIssuance issuer capTableCid securityId amount = -- Based on prod: SAFE conversion to Series Seed-2 Preferred testConvertibleConversion_SAFEConversion = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp - capTableCid <- addDefaultStakeholder issuer cap_table + (capTableCid, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table let securityId = "7b8c9d0e-1f2a-3b4c-5d6e-7f8a9b0c1d2e" + let resultSecurityId = "5f4a6b7c-9d0e-2f3a-6b7c-8d9e0f1a2b3c" + let conversionDate = DT.time (DA.date 2025 Nov 10) 0 0 0 result <- createConvertibleIssuance issuer capTableCid securityId 100000.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with - id = "25d377f8-a8de-4d8d-853c-2d75927e7927" - date = DT.time (DA.date 2025 Nov 10) 0 0 0 - security_id = securityId - trigger_id = "equity-financing-trigger" - reason_text = "Automatic conversion upon Series Seed-2 equity financing" - resulting_security_ids = ["5f4a6b7c-9d0e-2f3a-6b7c-8d9e0f1a2b3c"] - comments = ["SAFE conversion to Series Seed-2 Preferred for Investor A"] - balance_security_id = None - capitalization_definition = None - quantity_converted = None] + creates = + [ CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with + id = "25d377f8-a8de-4d8d-853c-2d75927e7927" + date = conversionDate + security_id = securityId + trigger_id = "equity-financing-trigger" + reason_text = "Automatic conversion upon Series Seed-2 equity financing" + resulting_security_ids = [resultSecurityId] + comments = ["SAFE conversion to Series Seed-2 Preferred for Investor A"] + balance_security_id = None + capitalization_definition = None + quantity_converted = None + , CT.OcfCreateStockIssuance ((defaultStockIssuance + "TX_CONV_RESULT_1" + resultSecurityId + stakeholderId + stockClassId) with + date = conversionDate + quantity = 100.0) + ] edits = [] deletes = [] pure () @@ -82,26 +93,37 @@ testConvertibleConversion_SAFEConversion = script do -- Test 2: Convertible conversion with capitalization definition testConvertibleConversion_WithCapDefinition = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp - capTableCid <- addDefaultStakeholder issuer cap_table + (capTableCid, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table let securityId = "CONV-SEC-002" + let resultSecurityId = "SSEC-PREFERRED-001" + let conversionDate = DT.time (DA.date 2025 Dec 01) 0 0 0 result <- createConvertibleIssuance issuer capTableCid securityId 250000.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with - id = "TX_CONV_CONVERSION_2" - date = DT.time (DA.date 2025 Dec 01) 0 0 0 - security_id = securityId - trigger_id = "equity-financing-trigger" - reason_text = "Conversion based on fully diluted capitalization at $16M valuation cap" - resulting_security_ids = ["SSEC-PREFERRED-001"] - comments = ["Conversion of $250,000 SAFE at $16M post-money valuation cap"] - balance_security_id = None - capitalization_definition = Some (OcfCapitalizationDefinition with - exclude_security_ids = [] - include_security_ids = [] - include_stock_class_ids = ["COMMON", "PREFERRED"] - include_stock_plans_ids = ["ESOP-2024"]) - quantity_converted = Some 15625.0] -- $250K / $16 per share + creates = + [ CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with + id = "TX_CONV_CONVERSION_2" + date = conversionDate + security_id = securityId + trigger_id = "equity-financing-trigger" + reason_text = "Conversion based on fully diluted capitalization at $16M valuation cap" + resulting_security_ids = [resultSecurityId] + comments = ["Conversion of $250,000 SAFE at $16M post-money valuation cap"] + balance_security_id = None + capitalization_definition = Some (OcfCapitalizationDefinition with + exclude_security_ids = [] + include_security_ids = [] + include_stock_class_ids = ["COMMON", "PREFERRED"] + include_stock_plans_ids = ["ESOP-2024"]) + quantity_converted = Some 15625.0 + , CT.OcfCreateStockIssuance ((defaultStockIssuance + "TX_CONV_RESULT_2" + resultSecurityId + stakeholderId + stockClassId) with + date = conversionDate + quantity = 15625.0) + ] edits = [] deletes = [] pure () @@ -109,22 +131,33 @@ testConvertibleConversion_WithCapDefinition = script do -- Test 3: Partial conversion with balance security testConvertibleConversion_PartialConversion = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp - capTableCid <- addDefaultStakeholder issuer cap_table + (capTableCid, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table let securityId = "CONV-SEC-003" + let resultSecurityId = "SSEC-PREFERRED-002" + let conversionDate = DT.time (DA.date 2025 Nov 15) 0 0 0 result <- createConvertibleIssuance issuer capTableCid securityId 500000.0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with - id = "TX_CONV_CONVERSION_3" - date = DT.time (DA.date 2025 Nov 15) 0 0 0 - security_id = securityId - trigger_id = "equity-financing-trigger" - reason_text = "Partial conversion - investor electing to convert $300,000 of $500,000 SAFE" - resulting_security_ids = ["SSEC-PREFERRED-002"] - comments = [] - balance_security_id = Some "CONV-SEC-003-REMAINDER" - capitalization_definition = None - quantity_converted = Some 300000.0] + creates = + [ CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with + id = "TX_CONV_CONVERSION_3" + date = conversionDate + security_id = securityId + trigger_id = "equity-financing-trigger" + reason_text = "Partial conversion - investor electing to convert $300,000 of $500,000 SAFE" + resulting_security_ids = [resultSecurityId] + comments = [] + balance_security_id = Some "CONV-SEC-003-REMAINDER" + capitalization_definition = None + quantity_converted = Some 300000.0 + , CT.OcfCreateStockIssuance ((defaultStockIssuance + "TX_CONV_RESULT_3" + resultSecurityId + stakeholderId + stockClassId) with + date = conversionDate + quantity = 300000.0) + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 6ec6a1af..1deaca84 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -1,5 +1,7 @@ module OpenCapTable.TestStockClassAuthorizedShares where +import Fairmint.OpenCapTable.OCF.ConvertibleConversion +import Fairmint.OpenCapTable.OCF.ConvertibleIssuance import Fairmint.OpenCapTable.OCF.StockConversion import Fairmint.OpenCapTable.OCF.StockConsolidation import Fairmint.OpenCapTable.OCF.EquityCompensationExercise @@ -187,6 +189,56 @@ warrantExercise exerciseId securityId quantity = consideration_text = None quantity = Some quantity +convertibleConversionTriggerWith : Text -> OcfConvertibleConversionMechanism -> Optional Text -> OcfConvertibleConversionTrigger +convertibleConversionTriggerWith triggerId conversionMechanism stockClassId = + OcfConvertibleConversionTrigger with + conversion_right = OcfConvertibleConversionRight with + conversion_mechanism = conversionMechanism + converts_to_future_round = None + converts_to_stock_class_id = stockClassId + type_ = "CONVERTIBLE_CONVERSION_RIGHT" + end_date = None + nickname = None + start_date = None + trigger_condition = None + trigger_date = None + trigger_description = None + trigger_id = triggerId + type_ = OcfTriggerTypeTypeElectiveAtWill + +convertibleIssuanceWith : Text -> Text -> Text -> Optional Text -> OcfConvertibleConversionMechanism -> Decimal -> ConvertibleIssuanceOcfData +convertibleIssuanceWith issuanceId securityId stakeholderId stockClassId conversionMechanism units = + ConvertibleIssuanceOcfData with + id = issuanceId + convertible_type = OcfConvertibleSafe + custom_id = "CV-" <> issuanceId + date = defaultTestDate + investment_amount = OcfMonetary with amount = units, currency = "USD" + security_id = securityId + seniority = 1 + stakeholder_id = stakeholderId + comments = [] + conversion_triggers = [convertibleConversionTriggerWith "TRIGGER_CONVERTIBLE" conversionMechanism stockClassId] + security_law_exemptions = [] + board_approval_date = None + consideration_text = None + pro_rata = None + stockholder_approval_date = None + +convertibleConversionWith : Text -> Text -> Optional Decimal -> [Text] -> ConvertibleConversionOcfData +convertibleConversionWith conversionId securityId quantityConverted resultingSecurityIds = + ConvertibleConversionOcfData with + id = conversionId + date = defaultTestDate + security_id = securityId + trigger_id = "TRIGGER_CONVERTIBLE" + reason_text = "Convertible converted into stock" + resulting_security_ids = resultingSecurityIds + comments = [] + balance_security_id = None + capitalization_definition = None + quantity_converted = quantityConverted + equityCompensationIssuance : Text -> Text -> Text -> Text -> Decimal -> EquityCompensationIssuanceOcfData equityCompensationIssuance issuanceId securityId stakeholderId stockClassId quantity = EquityCompensationIssuanceOcfData with @@ -1202,6 +1254,365 @@ testStockConversionSkipsNonComputableRightWhenRatioRightMatchesResult = script d deletes = [] pure () +testFixedConvertibleConversionEnforcesCeilingWithoutMaterializedResult = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERTIBLE_FIXED" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERTIBLE_FIXED" "Convertible Fixed Holder" + let excessiveIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_FIXED_EXCESS_SOURCE" + "CONV-CONVERTIBLE-FIXED-EXCESS" + stakeholderId + (Some stockClassId) + (OcfConvMechFixedAmount (OcfFixedAmountConversionMechanism with converts_to_quantity = 101.0)) + 1_000.0 + let excessiveConversion = convertibleConversionWith + "TX_CONVERTIBLE_FIXED_EXCESS" + excessiveIssuance.security_id + None + ["SEC-CONVERTIBLE-FIXED-EXCESS-RESULT"] + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance excessiveIssuance + , CT.OcfCreateConvertibleConversion excessiveConversion + ] + edits = [] + deletes = [] + + let boundaryIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_FIXED_BOUNDARY_SOURCE" + "CONV-CONVERTIBLE-FIXED-BOUNDARY" + stakeholderId + (Some stockClassId) + (OcfConvMechFixedAmount (OcfFixedAmountConversionMechanism with converts_to_quantity = 100.0)) + 1_000.0 + let boundaryConversion = convertibleConversionWith + "TX_CONVERTIBLE_FIXED_BOUNDARY" + boundaryIssuance.security_id + None + ["SEC-CONVERTIBLE-FIXED-BOUNDARY-RESULT"] + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance boundaryIssuance + , CT.OcfCreateConvertibleConversion boundaryConversion + ] + edits = [] + deletes = [] + pure () + +testPartialFixedConvertibleConversionScalesOutput = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERTIBLE_PARTIAL_FIXED" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERTIBLE_PARTIAL_FIXED" "Convertible Partial Fixed Holder" + let excessiveIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_PARTIAL_FIXED_EXCESS_SOURCE" + "CONV-CONVERTIBLE-PARTIAL-FIXED-EXCESS" + stakeholderId + (Some stockClassId) + (OcfConvMechFixedAmount (OcfFixedAmountConversionMechanism with converts_to_quantity = 202.0)) + 200.0 + let excessiveConversion = (convertibleConversionWith + "TX_CONVERTIBLE_PARTIAL_FIXED_EXCESS" + excessiveIssuance.security_id + (Some 100.0) + ["SEC-CONVERTIBLE-PARTIAL-FIXED-EXCESS-RESULT"]) with + balance_security_id = Some "CONV-CONVERTIBLE-PARTIAL-FIXED-EXCESS-BALANCE" + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance excessiveIssuance + , CT.OcfCreateConvertibleConversion excessiveConversion + ] + edits = [] + deletes = [] + + let boundaryIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_PARTIAL_FIXED_BOUNDARY_SOURCE" + "CONV-CONVERTIBLE-PARTIAL-FIXED-BOUNDARY" + stakeholderId + (Some stockClassId) + (OcfConvMechFixedAmount (OcfFixedAmountConversionMechanism with converts_to_quantity = 200.0)) + 200.0 + let boundaryConversion = (convertibleConversionWith + "TX_CONVERTIBLE_PARTIAL_FIXED_BOUNDARY" + boundaryIssuance.security_id + (Some 100.0) + ["SEC-CONVERTIBLE-PARTIAL-FIXED-BOUNDARY-RESULT"]) with + balance_security_id = Some "CONV-CONVERTIBLE-PARTIAL-FIXED-BOUNDARY-BALANCE" + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance boundaryIssuance + , CT.OcfCreateConvertibleConversion boundaryConversion + ] + edits = [] + deletes = [] + pure () + +testConvertibleConversionValidatesConvertedUnitBounds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERTIBLE_UNIT_BOUNDS" 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERTIBLE_UNIT_BOUNDS" "Convertible Unit Bounds Holder" + let conversionMechanism = OcfConvMechFixedAmount (OcfFixedAmountConversionMechanism with converts_to_quantity = 100.0) + let excessiveIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_UNIT_EXCESS_SOURCE" + "CONV-CONVERTIBLE-UNIT-EXCESS" + stakeholderId + (Some stockClassId) + conversionMechanism + 100.0 + let excessiveConversion = convertibleConversionWith + "TX_CONVERTIBLE_UNIT_EXCESS" + excessiveIssuance.security_id + (Some 101.0) + ["SEC-CONVERTIBLE-UNIT-EXCESS-RESULT"] + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance excessiveIssuance + , CT.OcfCreateConvertibleConversion excessiveConversion + ] + edits = [] + deletes = [] + + let unknownPartialIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_UNIT_UNKNOWN_SOURCE" + "CONV-CONVERTIBLE-UNIT-UNKNOWN" + stakeholderId + (Some stockClassId) + conversionMechanism + 100.0 + let unknownPartialConversion = (convertibleConversionWith + "TX_CONVERTIBLE_UNIT_UNKNOWN" + unknownPartialIssuance.security_id + None + ["SEC-CONVERTIBLE-UNIT-UNKNOWN-RESULT"]) with + balance_security_id = Some "CONV-CONVERTIBLE-UNIT-UNKNOWN-BALANCE" + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance unknownPartialIssuance + , CT.OcfCreateConvertibleConversion unknownPartialConversion + ] + edits = [] + deletes = [] + +testRatioConvertibleConversionEnforcesCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERTIBLE_RATIO" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERTIBLE_RATIO" "Convertible Ratio Holder" + let ratio = OcfRatio with numerator = 2.0, denominator = 1.0 + let excessiveIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_RATIO_EXCESS_SOURCE" + "CONV-CONVERTIBLE-RATIO-EXCESS" + stakeholderId + (Some stockClassId) + (OcfConvMechRatio ratio) + 100.0 + let excessiveConversion = convertibleConversionWith + "TX_CONVERTIBLE_RATIO_EXCESS" + excessiveIssuance.security_id + (Some 51.0) + ["SEC-CONVERTIBLE-RATIO-EXCESS-RESULT"] + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance excessiveIssuance + , CT.OcfCreateConvertibleConversion excessiveConversion + ] + edits = [] + deletes = [] + + let boundaryIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_RATIO_BOUNDARY_SOURCE" + "CONV-CONVERTIBLE-RATIO-BOUNDARY" + stakeholderId + (Some stockClassId) + (OcfConvMechRatio ratio) + 100.0 + let boundaryConversion = convertibleConversionWith + "TX_CONVERTIBLE_RATIO_BOUNDARY" + boundaryIssuance.security_id + (Some 50.0) + ["SEC-CONVERTIBLE-RATIO-BOUNDARY-RESULT"] + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance boundaryIssuance + , CT.OcfCreateConvertibleConversion boundaryConversion + ] + edits = [] + deletes = [] + pure () + +testIndeterminateConvertibleConversionRequiresMaterializedResult = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERTIBLE_CUSTOM" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERTIBLE_CUSTOM" "Convertible Custom Holder" + let customMechanism = OcfConvMechCustom (OcfCustomConversionMechanism with custom_conversion_description = "Custom negotiated conversion") + let unmaterializedIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_CUSTOM_MISSING_SOURCE" + "CONV-CONVERTIBLE-CUSTOM-MISSING" + stakeholderId + (Some stockClassId) + customMechanism + 100.0 + let unmaterializedConversion = convertibleConversionWith + "TX_CONVERTIBLE_CUSTOM_MISSING" + unmaterializedIssuance.security_id + None + ["SEC-CONVERTIBLE-CUSTOM-MISSING-RESULT"] + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance unmaterializedIssuance + , CT.OcfCreateConvertibleConversion unmaterializedConversion + ] + edits = [] + deletes = [] + + let materializedIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_CUSTOM_RECORDED_SOURCE" + "CONV-CONVERTIBLE-CUSTOM-RECORDED" + stakeholderId + (Some stockClassId) + customMechanism + 100.0 + let resultSecurityId = "SEC-CONVERTIBLE-CUSTOM-RECORDED-RESULT" + let materializedConversion = convertibleConversionWith + "TX_CONVERTIBLE_CUSTOM_RECORDED" + materializedIssuance.security_id + None + [resultSecurityId] + let resultIssuance = (defaultStockIssuance + "TX_CONVERTIBLE_CUSTOM_RECORDED_RESULT" + resultSecurityId + stakeholderId + stockClassId) with quantity = 100.0 + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance materializedIssuance + , CT.OcfCreateConvertibleConversion materializedConversion + , CT.OcfCreateStockIssuance resultIssuance + ] + edits = [] + deletes = [] + pure () + +testMaterializedConvertibleResultMustMatchDerivableTerms = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERTIBLE_RECORDED" 200.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERTIBLE_RECORDED" "Convertible Recorded Holder" + let conversionMechanism = OcfConvMechFixedAmount (OcfFixedAmountConversionMechanism with converts_to_quantity = 100.0) + let sourceIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_RECORDED_SOURCE" + "CONV-CONVERTIBLE-RECORDED" + stakeholderId + (Some stockClassId) + conversionMechanism + 1_000.0 + let resultSecurityId = "SEC-CONVERTIBLE-RECORDED-RESULT" + let conversion = convertibleConversionWith + "TX_CONVERTIBLE_RECORDED" + sourceIssuance.security_id + None + [resultSecurityId] + let mismatchedResult = (defaultStockIssuance + "TX_CONVERTIBLE_RECORDED-MISMATCH" + resultSecurityId + stakeholderId + stockClassId) with quantity = 99.0 + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance sourceIssuance + , CT.OcfCreateConvertibleConversion conversion + , CT.OcfCreateStockIssuance mismatchedResult + ] + edits = [] + deletes = [] + + let matchingResult = mismatchedResult with + id = "TX_CONVERTIBLE_RECORDED-MATCH" + quantity = 100.0 + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance sourceIssuance + , CT.OcfCreateConvertibleConversion conversion + , CT.OcfCreateStockIssuance matchingResult + ] + edits = [] + deletes = [] + pure () + +testUnmaterializedConvertibleResultRequiresTargetClass = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERTIBLE_NO_TARGET" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERTIBLE_NO_TARGET" "Convertible No Target Holder" + let sourceIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_NO_TARGET_SOURCE" + "CONV-CONVERTIBLE-NO-TARGET" + stakeholderId + None + (OcfConvMechFixedAmount (OcfFixedAmountConversionMechanism with converts_to_quantity = 100.0)) + 1_000.0 + let conversion = convertibleConversionWith + "TX_CONVERTIBLE_NO_TARGET" + sourceIssuance.security_id + None + ["SEC-CONVERTIBLE-NO-TARGET-RESULT"] + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance sourceIssuance + , CT.OcfCreateConvertibleConversion conversion + ] + edits = [] + deletes = [] + +testConvertibleConversionRejectsPartialResultMaterialization = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERTIBLE_PARTIAL_RESULT" 200.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CONVERTIBLE_PARTIAL_RESULT" "Convertible Partial Result Holder" + let sourceIssuance = convertibleIssuanceWith + "TX_CONVERTIBLE_PARTIAL_RESULT_SOURCE" + "CONV-CONVERTIBLE-PARTIAL-RESULT" + stakeholderId + (Some stockClassId) + (OcfConvMechFixedAmount (OcfFixedAmountConversionMechanism with converts_to_quantity = 100.0)) + 1_000.0 + let firstResultSecurityId = "SEC-CONVERTIBLE-PARTIAL-RESULT-A" + let conversion = convertibleConversionWith + "TX_CONVERTIBLE_PARTIAL_RESULT" + sourceIssuance.security_id + None + [firstResultSecurityId, "SEC-CONVERTIBLE-PARTIAL-RESULT-B"] + let firstResult = (defaultStockIssuance + "TX_CONVERTIBLE_PARTIAL-RESULT-A" + firstResultSecurityId + stakeholderId + stockClassId) with quantity = 50.0 + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance sourceIssuance + , CT.OcfCreateConvertibleConversion conversion + , CT.OcfCreateStockIssuance firstResult + ] + edits = [] + deletes = [] + testWarrantExerciseCannotExceedStockClassAuthorizedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index 8e6b5b0c..e4f77872 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -542,6 +542,45 @@ testStockPlanReservedShares_StockIssuanceBeforePlanEffectiveFails = script do edits = [] deletes = [] +testStockPlanReservedShares_ReturnBeforeTargetPlanEffectiveFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourcePlanId = "PLAN_RETURN_EFFECTIVE_SOURCE" + let targetPlanId = "PLAN_RETURN_EFFECTIVE_TARGET" + let sourceSecurityId = "SEC-RETURN-EFFECTIVE-SOURCE" + let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 50.0 + let targetPlan = (defaultStockPlan targetPlanId stockClassId) with + initial_shares_reserved = 50.0 + board_approval_date = Some (DT.time (DA.date 2024 Jan 03) 0 0 0) + let sourceIssuance = (stockPlanEquityCompensationIssuance + "TX_RETURN_EFFECTIVE_SOURCE" + sourceSecurityId + stakeholderId + stockClassId + sourcePlanId + 50.0) with date = DT.time (DA.date 2024 Jan 01) 0 0 0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan sourcePlan + , CT.OcfCreateStockPlan targetPlan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + edits = [] + deletes = [] + + let returnDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let cancellation = (equityCompensationCancellation "TX_RETURN_EFFECTIVE_CANCEL" sourceSecurityId 50.0) with date = returnDate + let returnToPool = (stockPlanReturnToPool "TX_RETURN_EFFECTIVE_POOL" sourceSecurityId targetPlanId 50.0) with date = returnDate + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation cancellation + , CT.OcfCreateStockPlanReturnToPool returnToPool + ] + edits = [] + deletes = [] + testStockPlanReservedShares_AdjustmentAllowsAdditionalIssuanceSameBatch = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table @@ -957,7 +996,7 @@ testStockPlanReservedShares_ReturnToPoolRejectsTargetPlanOutsideSecurityClass = edits = [] deletes = [] -testStockPlanReservedShares_ReturnCannotCreateNegativeHistoricalAllocation = script do +testStockPlanReservedShares_CrossPlanReturnCanPrecedeTargetAllocation = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table let sourcePlanId = "PLAN_RETURN_HISTORY_SOURCE" @@ -974,19 +1013,44 @@ testStockPlanReservedShares_ReturnCannotCreateNegativeHistoricalAllocation = scr let targetIssuance = (stockPlanEquityCompensationIssuance "TX_RETURN_HISTORY_TARGET" "SEC-RETURN-HISTORY-TARGET" stakeholderId stockClassId targetPlanId 100.0) with date = DT.time (DA.date 2024 Jan 03) 0 0 0 - submitMustFail issuer do + source <- submit issuer do exerciseCmd capTableReady CT.UpdateCapTable with creates = [ CT.OcfCreateStockPlan sourcePlan , CT.OcfCreateStockPlan targetPlan , CT.OcfCreateEquityCompensationIssuance sourceIssuance - , CT.OcfCreateEquityCompensationCancellation cancellation + ] + edits = [] + deletes = [] + + returned <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateEquityCompensationCancellation cancellation , CT.OcfCreateStockPlanReturnToPool returnToPool - , CT.OcfCreateEquityCompensationIssuance targetIssuance ] edits = [] deletes = [] + allocated <- submit issuer do + exerciseCmd returned.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance targetIssuance] + edits = [] + deletes = [] + + let excessIssuance = (stockPlanEquityCompensationIssuance + "TX_RETURN_HISTORY_EXCESS" + "SEC-RETURN-HISTORY-EXCESS" + stakeholderId + stockClassId + targetPlanId + 1.0) with date = DT.time (DA.date 2024 Jan 04) 0 0 0 + submitMustFail issuer do + exerciseCmd allocated.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance excessIssuance] + edits = [] + deletes = [] + testStockPlanReservedShares_AdjustmentCannotDropBelowAllocatedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithIssuance, _, _) <- createPlanAndStockIssuance issuer cap_table "PLAN_ADJ_DROP" 200.0 150.0 diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 2c6fb78d..0fb66f97 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a9080210d0812062c029b54c625fc80624eb310770b2625ae5e4445f59d8264 -size 2942894 +oid sha256:4d386637f0500f74555e9ecdf864c95e48db1e91c92609676c2d1feaa18cde92 +size 2961945 diff --git a/dars/dars.lock b/dars/dars.lock index a8d415ce..245ddb33 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,10 +33,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "3a9080210d0812062c029b54c625fc80624eb310770b2625ae5e4445f59d8264", - "size": 2942894, + "sha256": "4d386637f0500f74555e9ecdf864c95e48db1e91c92609676c2d1feaa18cde92", + "size": 2961945, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T00:41:54.852Z", + "uploadedAt": "2026-07-11T01:56:50.000Z", "networks": [] } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index b42462f9..cec8309e 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -18,9 +18,10 @@ import DA.Foldable (mapA_) import DA.Optional (fromOptional, isNone, isSome) import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfConversionMechanism(..), OcfConversionTrigger(..), OcfConversionTriggerType(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) +import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfConversionMechanism(..), OcfConversionTrigger(..), OcfConversionTriggerType(..), OcfConvertibleConversionMechanism(..), OcfConvertibleConversionRight(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..)) import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) +import qualified Fairmint.OpenCapTable.OCF.ConvertibleIssuance as ConvertibleIssuance (OcfConvertibleConversionTrigger(..)) import Fairmint.OpenCapTable.OCF.Issuer (Issuer(..), IssuerOcfData) -- OCF Types @@ -513,6 +514,93 @@ warrantExerciseTrigger issuanceData triggerId = None issuanceData.exercise_triggers +convertibleConversionTrigger : ConvertibleIssuanceOcfData -> Text -> Optional ConvertibleIssuance.OcfConvertibleConversionTrigger +convertibleConversionTrigger issuanceData triggerId = + foldl + (\matched trigger -> + case matched of + Some _ -> matched + None -> if trigger.trigger_id == triggerId then Some trigger else None) + None + issuanceData.conversion_triggers + +convertibleConversionOutputQuantity : + ConvertibleIssuanceOcfData -> + ConvertibleConversionOcfData -> + OcfConvertibleConversionRight -> + Update (Optional Decimal) +convertibleConversionOutputQuantity issuanceData conversionData conversionRight = do + let issuedUnits = issuanceData.investment_amount.amount + assertMsg + ("Convertible conversion source has no positive unit base: " <> issuanceData.security_id) + (issuedUnits > 0.0) + case (conversionData.balance_security_id, conversionData.quantity_converted) of + (Some _, None) -> + assertMsg + ("Partial convertible conversion requires quantity_converted: " <> conversionData.id) + False + _ -> pure () + let convertedUnits = fromOptional issuedUnits conversionData.quantity_converted + assertMsg + ("Convertible conversion quantity exceeds source units for " <> conversionData.id <> ": converted " <> show convertedUnits <> " exceeds " <> show issuedUnits) + (convertedUnits > 0.0 && convertedUnits <= issuedUnits) + let outputQuantityOpt = case conversionRight.conversion_mechanism of + OcfConvMechFixedAmount fixedAmount -> + Some (fixedAmount.converts_to_quantity * convertedUnits / issuedUnits) + OcfConvMechRatio ratio -> + Some (convertedUnits * ratio.numerator / ratio.denominator) + _ -> None + case outputQuantityOpt of + Some outputQuantity -> do + assertNonNegativeQuantity + ("Convertible conversion output quantity for " <> conversionData.id) + outputQuantity + pure (Some outputQuantity) + None -> pure None + +convertibleConversionExpectedResult : + ConvertibleIssuanceOcfData -> + ConvertibleConversionOcfData -> + Update (Optional Text, Optional Decimal) +convertibleConversionExpectedResult issuanceData conversionData = + case convertibleConversionTrigger issuanceData conversionData.trigger_id of + Some trigger -> do + quantityOpt <- convertibleConversionOutputQuantity issuanceData conversionData trigger.conversion_right + pure (trigger.conversion_right.converts_to_stock_class_id, quantityOpt) + None -> do + assertMsg + ("Convertible conversion trigger not found: " <> conversionData.trigger_id) + False + pure (None, None) + +convertibleConversionOutputShareLot : + Map Text StockClassOcfData -> + ConvertibleIssuanceOcfData -> + ConvertibleConversionOcfData -> + Update (Optional StockClassShareLot) +convertibleConversionOutputShareLot stockClassIndex issuanceData conversionData = do + (stockClassIdOpt, quantityOpt) <- convertibleConversionExpectedResult issuanceData conversionData + case (stockClassIdOpt, quantityOpt) of + (Some stockClassId, Some quantity) -> do + assertMsg + ("Convertible conversion output references missing stock class: " <> stockClassId) + (isSome (Map.lookup stockClassId stockClassIndex)) + pure (Some (StockClassShareLot with + stock_class_id = stockClassId + security_id = None + event_date = conversionData.date + quantity = quantity)) + (None, _) -> do + assertMsg + ("Convertible conversion output cannot be reconstructed because trigger has no destination stock class: " <> conversionData.trigger_id) + False + pure None + (_, None) -> do + assertMsg + ("Convertible conversion output cannot be reconstructed for its conversion mechanism; resulting stock issuances are required: " <> conversionData.id) + False + pure None + securityQuantityReductionEvent : Time -> Decimal -> Optional Text -> SecurityQuantityEvent securityQuantityReductionEvent eventDate quantity balanceSecurityId = case balanceSecurityId of @@ -917,6 +1005,17 @@ validateWarrantExerciseResultIssuances maps issuanceData exerciseData = _ -> assertMsg ("Warrant exercise trigger is not a warrant conversion right: " <> exerciseData.trigger_id) False None -> assertMsg ("Warrant exercise trigger not found: " <> exerciseData.trigger_id) False +validateConvertibleConversionResultIssuances : CapTableMaps -> ConvertibleIssuanceOcfData -> ConvertibleConversionOcfData -> Update () +validateConvertibleConversionResultIssuances maps issuanceData conversionData = do + (expectedStockClassIdOpt, expectedQuantityOpt) <- convertibleConversionExpectedResult issuanceData conversionData + validateStockResultIssuances + maps + ("Convertible conversion " <> conversionData.id) + conversionData.date + expectedStockClassIdOpt + expectedQuantityOpt + conversionData.resulting_security_ids + validateEquityCompensationExerciseResultIssuances : CapTableMaps -> EquityCompensationIssuanceOcfData -> EquityCompensationExerciseOcfData -> Update () validateEquityCompensationExerciseResultIssuances maps issuanceData exerciseData = do let label = "Equity compensation exercise " <> exerciseData.id @@ -1018,6 +1117,27 @@ stockClassShareLotsByStockClass maps stockClassIndex = do stockTransferLots (Map.values maps.stock_conversions) + convertibleConversionLots <- foldlA + (\index conversionCid -> do + conversion <- fetch conversionCid + let label = "Convertible conversion " <> conversion.conversion_data.id + assertNoPartialResultingStockIssuanceCoverage label maps conversion.conversion_data.resulting_security_ids + case Map.lookup conversion.conversion_data.security_id maps.convertible_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + if allResultingStockIssuancesExist maps conversion.conversion_data.resulting_security_ids + then do + validateConvertibleConversionResultIssuances maps sourceIssuance.issuance_data conversion.conversion_data + pure index + else do + lotOpt <- convertibleConversionOutputShareLot stockClassIndex sourceIssuance.issuance_data conversion.conversion_data + pure (addOptionalStockClassShareLot lotOpt index) + None -> do + assertMsg (label <> " references missing source convertible issuance: " <> conversion.conversion_data.security_id) False + pure index) + stockConversionLots + (Map.values maps.convertible_conversions) + warrantExerciseLots <- foldlA (\index exerciseCid -> do exercise <- fetch exerciseCid @@ -1039,7 +1159,7 @@ stockClassShareLotsByStockClass maps stockClassIndex = do None -> do assertMsg ("Warrant exercise references missing warrant issuance: " <> exercise.exercise_data.security_id) False pure index) - stockConversionLots + convertibleConversionLots (Map.values maps.warrant_exercises) equityExerciseLots <- foldlA @@ -3706,6 +3826,65 @@ stockPlanSecurityEffectivePlanId successorIndex maps securityId = pure None (None, None) -> pure None +stockPlanCrossPlanReturnedQuantityIndex : CapTableMaps -> Update (Map Text Decimal) +stockPlanCrossPlanReturnedQuantityIndex maps = do + successorIndex <- stockPlanSuccessorIndex maps + foldlA + (\totals returnCid -> do + returnToPool <- fetch returnCid + sourcePlanIdOpt <- stockPlanSecurityEffectivePlanId successorIndex maps returnToPool.return_data.security_id + case sourcePlanIdOpt of + Some sourcePlanId -> + if sourcePlanId /= returnToPool.return_data.stock_plan_id + then do + returnedQuantityOpt <- stockPlanReturnToPoolQuantity maps returnToPool.return_data + case returnedQuantityOpt of + Some returnedQuantity -> do + assertNonNegativeQuantity + ("Cross-plan return-to-pool quantity for " <> returnToPool.return_data.id) + returnedQuantity + pure (addQuantityById returnToPool.return_data.stock_plan_id returnedQuantity totals) + None -> do + assertMsg + ("Cross-plan return-to-pool has an invalid quantity history for " <> returnToPool.return_data.id) + False + pure totals + else pure totals + None -> pure totals) + Map.empty + (Map.values maps.stock_plan_return_to_pools) + +stockPlanCrossPlanReturnedQuantityIndexAt : CapTableMaps -> Time -> Update (Map Text Decimal) +stockPlanCrossPlanReturnedQuantityIndexAt maps asOf = do + successorIndex <- stockPlanSuccessorIndex maps + foldlA + (\totals returnCid -> do + returnToPool <- fetch returnCid + if returnToPool.return_data.date <= asOf + then do + sourcePlanIdOpt <- stockPlanSecurityEffectivePlanId successorIndex maps returnToPool.return_data.security_id + case sourcePlanIdOpt of + Some sourcePlanId -> + if sourcePlanId /= returnToPool.return_data.stock_plan_id + then do + returnedQuantityOpt <- stockPlanReturnToPoolQuantityAt maps returnToPool.return_data asOf + case returnedQuantityOpt of + Some returnedQuantity -> do + assertNonNegativeQuantity + ("Cross-plan return-to-pool quantity for " <> returnToPool.return_data.id <> " on " <> show asOf) + returnedQuantity + pure (addQuantityById returnToPool.return_data.stock_plan_id returnedQuantity totals) + None -> do + assertMsg + ("Cross-plan return-to-pool has an invalid quantity history for " <> returnToPool.return_data.id <> " on " <> show asOf) + False + pure totals + else pure totals + None -> pure totals + else pure totals) + Map.empty + (Map.values maps.stock_plan_return_to_pools) + stockPlanSecurityRecordedStockClassId : CapTableMaps -> Text -> Update (Optional Text) stockPlanSecurityRecordedStockClassId maps securityId = case (Map.lookup securityId maps.stock_issuances_by_security_id, Map.lookup securityId maps.equity_compensation_issuances_by_security_id) of @@ -3965,6 +4144,18 @@ validateStockPlanAllocationEffectiveDates maps = do None -> pure ()) (Map.values maps.equity_compensation_issuances) +validateStockPlanReturnToPoolEffectiveDates : CapTableMaps -> Update () +validateStockPlanReturnToPoolEffectiveDates maps = + mapA_ + (\returnCid -> do + returnToPool <- fetch returnCid + assertStockPlanEffectiveForAllocation + ("Stock plan return-to-pool " <> returnToPool.return_data.id) + maps + returnToPool.return_data.stock_plan_id + returnToPool.return_data.date) + (Map.values maps.stock_plan_return_to_pools) + validateStockPlanReservedShares : StockClassShareEventIndex -> CapTableMaps -> @@ -3972,13 +4163,17 @@ validateStockPlanReservedShares : Update () validateStockPlanReservedShares eventIndex maps adjustmentIndex = do netAllocatedQuantityIndex <- stockPlanNetAllocatedQuantityIndex eventIndex maps + crossPlanReturnedQuantityIndex <- stockPlanCrossPlanReturnedQuantityIndex maps validateStockPlanAllocationReferences maps netAllocatedQuantityIndex mapA_ (\stockPlanCid -> do stockPlan <- fetch stockPlanCid let reservedShares = currentStockPlanReservedShares adjustmentIndex stockPlan.plan_data let allocatedQuantity = fromOptional 0.0 (Map.lookup stockPlan.plan_data.id netAllocatedQuantityIndex) - assertNonNegativeQuantity ("Stock plan net allocated quantity for " <> stockPlan.plan_data.id) allocatedQuantity + let crossPlanReturnedQuantity = fromOptional 0.0 (Map.lookup stockPlan.plan_data.id crossPlanReturnedQuantityIndex) + assertMsg + ("Stock plan net allocated quantity for " <> stockPlan.plan_data.id <> " is below its validated cross-plan return credit") + (allocatedQuantity >= 0.0 - crossPlanReturnedQuantity) assertMsg ("Stock plan reserved shares exceeded for " <> stockPlan.plan_data.id <> ": allocated " <> show allocatedQuantity <> " exceeds reserved " <> show reservedShares) (allocatedQuantity <= reservedShares)) @@ -4166,11 +4361,13 @@ validateStockPlanReservedSharesAtDates eventIndex maps adjustmentHistory = do mapA_ (\asOf -> do netAllocatedQuantityIndex <- stockPlanNetAllocatedQuantityIndexAt eventIndex maps asOf + crossPlanReturnedQuantityIndex <- stockPlanCrossPlanReturnedQuantityIndexAt maps asOf validateStockPlanAllocationReferences maps netAllocatedQuantityIndex let allocatedQuantity = fromOptional 0.0 (Map.lookup stockPlan.plan_data.id netAllocatedQuantityIndex) - assertNonNegativeQuantity - ("Stock plan allocated quantity for " <> stockPlan.plan_data.id <> " on " <> show asOf) - allocatedQuantity + let crossPlanReturnedQuantity = fromOptional 0.0 (Map.lookup stockPlan.plan_data.id crossPlanReturnedQuantityIndex) + assertMsg + ("Stock plan allocated quantity for " <> stockPlan.plan_data.id <> " on " <> show asOf <> " is below its validated cross-plan return credit") + (allocatedQuantity >= 0.0 - crossPlanReturnedQuantity) if stockPlanEffectiveOnOrBefore asOf stockPlan.plan_data then do let reservedShares = stockPlanReservedSharesAt adjustmentHistory stockPlan.plan_data asOf @@ -4188,6 +4385,7 @@ validateStockPlanPoolCeilings eventIndex maps = do adjustmentHistory <- stockPlanPoolAdjustmentHistoryIndex maps validateStockPlanAllocationClasses maps validateStockPlanAllocationEffectiveDates maps + validateStockPlanReturnToPoolEffectiveDates maps validateStockPlanEquityCompensationSourceQuantities maps validateStockPlanReturnToPoolClasses maps validateStockPlanReturnToPoolQuantities maps From 91021de897afb7124059537902557ecaa5cccf5f Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 22:03:17 -0400 Subject: [PATCH 58/62] fix: close authorized ceiling review gaps --- .../TestConvertibleConversion.daml | 100 +++++---- .../TestStockClassAuthorizedShares.daml | 195 ++++++++++++++++++ .../TestStockPlanReservedShares.daml | 39 +++- .../0.0.5/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- .../codegen/templates/CapTable.daml.template | 90 +++++++- 6 files changed, 380 insertions(+), 54 deletions(-) diff --git a/Test/daml/OpenCapTable/TestConvertibleConversion.daml b/Test/daml/OpenCapTable/TestConvertibleConversion.daml index 83452d47..3093433d 100644 --- a/Test/daml/OpenCapTable/TestConvertibleConversion.daml +++ b/Test/daml/OpenCapTable/TestConvertibleConversion.daml @@ -6,7 +6,7 @@ import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Conversion (OcfConversionTriggerType(..), OcfConvertibleConversionRight(..), OcfConvertibleConversionMechanism(..), OcfSAFEConversionMechanism(..), OcfConvertibleType(..), OcfCapitalizationDefinition(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup -import OpenCapTable.TestHelpers (addDefaultStakeholder) +import OpenCapTable.TestHelpers (addPrerequisites, defaultStockIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -59,22 +59,28 @@ createConvertibleIssuance issuer capTableCid securityId amount = -- Based on prod: SAFE conversion to Series Seed-2 Preferred testConvertibleConversion_SAFEConversion = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp - capTableCid <- addDefaultStakeholder issuer cap_table + capTableCid <- addPrerequisites issuer cap_table let securityId = "7b8c9d0e-1f2a-3b4c-5d6e-7f8a9b0c1d2e" result <- createConvertibleIssuance issuer capTableCid securityId 100000.0 + let conversionDate = DT.time (DA.date 2025 Nov 10) 0 0 0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with - id = "25d377f8-a8de-4d8d-853c-2d75927e7927" - date = DT.time (DA.date 2025 Nov 10) 0 0 0 - security_id = securityId - trigger_id = "equity-financing-trigger" - reason_text = "Automatic conversion upon Series Seed-2 equity financing" - resulting_security_ids = ["5f4a6b7c-9d0e-2f3a-6b7c-8d9e0f1a2b3c"] - comments = ["SAFE conversion to Series Seed-2 Preferred for Investor A"] - balance_security_id = None - capitalization_definition = None - quantity_converted = None] + creates = + [ CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with + id = "25d377f8-a8de-4d8d-853c-2d75927e7927" + date = conversionDate + security_id = securityId + trigger_id = "equity-financing-trigger" + reason_text = "Automatic conversion upon Series Seed-2 equity financing" + resulting_security_ids = ["5f4a6b7c-9d0e-2f3a-6b7c-8d9e0f1a2b3c"] + comments = ["SAFE conversion to Series Seed-2 Preferred for Investor A"] + balance_security_id = None + capitalization_definition = None + quantity_converted = None + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX-CONV-SAFE-RESULT" "5f4a6b7c-9d0e-2f3a-6b7c-8d9e0f1a2b3c" "SH-1" "SC_COMMON") with + date = conversionDate + quantity = 6250.0) + ] edits = [] deletes = [] pure () @@ -82,26 +88,32 @@ testConvertibleConversion_SAFEConversion = script do -- Test 2: Convertible conversion with capitalization definition testConvertibleConversion_WithCapDefinition = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp - capTableCid <- addDefaultStakeholder issuer cap_table + capTableCid <- addPrerequisites issuer cap_table let securityId = "CONV-SEC-002" result <- createConvertibleIssuance issuer capTableCid securityId 250000.0 + let conversionDate = DT.time (DA.date 2025 Dec 01) 0 0 0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with - id = "TX_CONV_CONVERSION_2" - date = DT.time (DA.date 2025 Dec 01) 0 0 0 - security_id = securityId - trigger_id = "equity-financing-trigger" - reason_text = "Conversion based on fully diluted capitalization at $16M valuation cap" - resulting_security_ids = ["SSEC-PREFERRED-001"] - comments = ["Conversion of $250,000 SAFE at $16M post-money valuation cap"] - balance_security_id = None - capitalization_definition = Some (OcfCapitalizationDefinition with - exclude_security_ids = [] - include_security_ids = [] - include_stock_class_ids = ["COMMON", "PREFERRED"] - include_stock_plans_ids = ["ESOP-2024"]) - quantity_converted = Some 15625.0] -- $250K / $16 per share + creates = + [ CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with + id = "TX_CONV_CONVERSION_2" + date = conversionDate + security_id = securityId + trigger_id = "equity-financing-trigger" + reason_text = "Conversion based on fully diluted capitalization at $16M valuation cap" + resulting_security_ids = ["SSEC-PREFERRED-001"] + comments = ["Conversion of $250,000 SAFE at $16M post-money valuation cap"] + balance_security_id = None + capitalization_definition = Some (OcfCapitalizationDefinition with + exclude_security_ids = [] + include_security_ids = [] + include_stock_class_ids = ["COMMON", "PREFERRED"] + include_stock_plans_ids = ["ESOP-2024"]) + quantity_converted = Some 15625.0 -- $250K / $16 per share + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX-CONV-CAP-RESULT" "SSEC-PREFERRED-001" "SH-1" "SC_COMMON") with + date = conversionDate + quantity = 15625.0) + ] edits = [] deletes = [] pure () @@ -109,22 +121,28 @@ testConvertibleConversion_WithCapDefinition = script do -- Test 3: Partial conversion with balance security testConvertibleConversion_PartialConversion = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp - capTableCid <- addDefaultStakeholder issuer cap_table + capTableCid <- addPrerequisites issuer cap_table let securityId = "CONV-SEC-003" result <- createConvertibleIssuance issuer capTableCid securityId 500000.0 + let conversionDate = DT.time (DA.date 2025 Nov 15) 0 0 0 _ <- submit issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with - id = "TX_CONV_CONVERSION_3" - date = DT.time (DA.date 2025 Nov 15) 0 0 0 - security_id = securityId - trigger_id = "equity-financing-trigger" - reason_text = "Partial conversion - investor electing to convert $300,000 of $500,000 SAFE" - resulting_security_ids = ["SSEC-PREFERRED-002"] - comments = [] - balance_security_id = Some "CONV-SEC-003-REMAINDER" - capitalization_definition = None - quantity_converted = Some 300000.0] + creates = + [ CT.OcfCreateConvertibleConversion ConvertibleConversionOcfData with + id = "TX_CONV_CONVERSION_3" + date = conversionDate + security_id = securityId + trigger_id = "equity-financing-trigger" + reason_text = "Partial conversion - investor electing to convert $300,000 of $500,000 SAFE" + resulting_security_ids = ["SSEC-PREFERRED-002"] + comments = [] + balance_security_id = Some "CONV-SEC-003-REMAINDER" + capitalization_definition = None + quantity_converted = Some 300000.0 + , CT.OcfCreateStockIssuance ((defaultStockIssuance "TX-CONV-PARTIAL-RESULT" "SSEC-PREFERRED-002" "SH-1" "SC_COMMON") with + date = conversionDate + quantity = 18750.0) + ] edits = [] deletes = [] pure () diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 6ec6a1af..aae93ab5 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -1,6 +1,8 @@ module OpenCapTable.TestStockClassAuthorizedShares where import Fairmint.OpenCapTable.OCF.StockConversion +import Fairmint.OpenCapTable.OCF.ConvertibleConversion +import Fairmint.OpenCapTable.OCF.ConvertibleIssuance import Fairmint.OpenCapTable.OCF.StockConsolidation import Fairmint.OpenCapTable.OCF.EquityCompensationExercise import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance @@ -48,6 +50,199 @@ stockConversion conversionId securityId quantity = balance_security_id = None comments = [] +convertibleConversion : Text -> Text -> Text -> Time -> ConvertibleConversionOcfData +convertibleConversion conversionId securityId resultingSecurityId conversionDate = + ConvertibleConversionOcfData with + id = conversionId + date = conversionDate + security_id = securityId + trigger_id = "TRIGGER-1" + reason_text = "Converted into stock" + resulting_security_ids = [resultingSecurityId] + balance_security_id = None + capitalization_definition = None + quantity_converted = None + comments = [] + +fixedConvertibleIssuance : Text -> Text -> Text -> Text -> Decimal -> ConvertibleIssuanceOcfData +fixedConvertibleIssuance issuanceId securityId stakeholderId stockClassId outputQuantity = + ConvertibleIssuanceOcfData with + id = issuanceId + convertible_type = OcfConvertibleSafe + custom_id = "CV-" <> issuanceId + date = defaultTestDate + investment_amount = OcfMonetary with amount = 10_000.0, currency = "USD" + security_id = securityId + seniority = 1 + stakeholder_id = stakeholderId + comments = [] + conversion_triggers = [OcfConvertibleConversionTrigger with + conversion_right = OcfConvertibleConversionRight with + conversion_mechanism = OcfConvMechFixedAmount (OcfFixedAmountConversionMechanism with + converts_to_quantity = outputQuantity) + converts_to_future_round = None + converts_to_stock_class_id = Some stockClassId + type_ = "CONVERTIBLE_CONVERSION_RIGHT" + end_date = None + nickname = None + start_date = None + trigger_condition = None + trigger_date = None + trigger_description = None + trigger_id = "TRIGGER-1" + type_ = OcfTriggerTypeTypeUnspecified] + security_law_exemptions = [] + board_approval_date = None + consideration_text = None + pro_rata = None + stockholder_approval_date = None + +testConvertibleConversionRequiresResultIssuanceForCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, _, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourceSecurityId = "SEC-CONVERTIBLE-MISSING-RESULT-SOURCE" + let resultSecurityId = "SEC-CONVERTIBLE-MISSING-RESULT" + let conversionDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + capTableWithSource <- addConvertibleIssuance + issuer + capTableReady + "TX-CONVERTIBLE-MISSING-RESULT-SOURCE" + sourceSecurityId + stakeholderId + + submitMustFail issuer do + exerciseCmd capTableWithSource CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleConversion + (convertibleConversion + "TX-CONVERTIBLE-MISSING-RESULT" + sourceSecurityId + resultSecurityId + conversionDate) + ] + edits = [] + deletes = [] + +testConvertibleConversionResultCountsTowardClassCeiling = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockClassId = "SC-CONVERTIBLE-RESULT-CEILING" + (capTableWithClass, _) <- setupStockClassWith issuer cap_table (limitedStockClass stockClassId 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH-CONVERTIBLE-RESULT-CEILING" "Convertible Result Holder" + let sourceSecurityId = "SEC-CONVERTIBLE-RESULT-CEILING-SOURCE" + let resultSecurityId = "SEC-CONVERTIBLE-RESULT-CEILING" + let conversionDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + capTableWithSource <- addConvertibleIssuance + issuer + capTableReady + "TX-CONVERTIBLE-RESULT-CEILING-SOURCE" + sourceSecurityId + stakeholderId + let conversion = convertibleConversion + "TX-CONVERTIBLE-RESULT-CEILING" + sourceSecurityId + resultSecurityId + conversionDate + let resultIssuance quantity = (defaultStockIssuance + "TX-CONVERTIBLE-RESULT-CEILING-ISSUANCE" + resultSecurityId + stakeholderId + stockClassId) with + date = conversionDate + quantity = quantity + + submitMustFail issuer do + exerciseCmd capTableWithSource CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleConversion conversion + , CT.OcfCreateStockIssuance (resultIssuance 101.0) + ] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd capTableWithSource CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleConversion conversion + , CT.OcfCreateStockIssuance (resultIssuance 100.0) + ] + edits = [] + deletes = [] + pure () + +testConvertibleConversionResultMustMatchFixedConversionTerms = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let expectedClassId = "SC-CONVERTIBLE-FIXED-EXPECTED" + let wrongClassId = "SC-CONVERTIBLE-FIXED-WRONG" + (capTableWithExpectedClass, _) <- setupStockClassWith issuer cap_table (limitedStockClass expectedClassId 1_000.0) + (capTableWithClasses, _) <- setupStockClassWith issuer capTableWithExpectedClass (limitedStockClass wrongClassId 1_000.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClasses "SH-CONVERTIBLE-FIXED" "Fixed Convertible Holder" + let sourceSecurityId = "SEC-CONVERTIBLE-FIXED-SOURCE" + let resultSecurityId = "SEC-CONVERTIBLE-FIXED-RESULT" + let conversionDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + source <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleIssuance + (fixedConvertibleIssuance + "TX-CONVERTIBLE-FIXED-SOURCE" + sourceSecurityId + stakeholderId + expectedClassId + 100.0) + ] + edits = [] + deletes = [] + let conversion = convertibleConversion + "TX-CONVERTIBLE-FIXED-CONVERSION" + sourceSecurityId + resultSecurityId + conversionDate + let resultIssuance stockClassId date quantity = (defaultStockIssuance + "TX-CONVERTIBLE-FIXED-RESULT" + resultSecurityId + stakeholderId + stockClassId) with + date = date + quantity = quantity + + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleConversion conversion + , CT.OcfCreateStockIssuance (resultIssuance expectedClassId conversionDate 99.0) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleConversion conversion + , CT.OcfCreateStockIssuance (resultIssuance wrongClassId conversionDate 100.0) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleConversion conversion + , CT.OcfCreateStockIssuance + (resultIssuance expectedClassId (DT.time (DA.date 2024 Jan 03) 0 0 0) 100.0) + ] + edits = [] + deletes = [] + + _ <- submit issuer do + exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateConvertibleConversion (conversion with quantity_converted = Some 5_000.0) + , CT.OcfCreateStockIssuance (resultIssuance expectedClassId conversionDate 50.0) + ] + edits = [] + deletes = [] + pure () + stockRetraction : Text -> Text -> StockRetractionOcfData stockRetraction retractionId securityId = StockRetractionOcfData with diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index 8e6b5b0c..026e0468 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -542,6 +542,40 @@ testStockPlanReservedShares_StockIssuanceBeforePlanEffectiveFails = script do edits = [] deletes = [] +testStockPlanReservedShares_ReturnBeforeTargetPlanEffectiveFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let sourcePlanId = "PLAN_RETURN_EFFECTIVE_SOURCE" + let targetPlanId = "PLAN_RETURN_EFFECTIVE_TARGET" + let sourceSecurityId = "SEC-RETURN-EFFECTIVE-SOURCE" + let returnDate = DT.time (DA.date 2024 Jan 02) 0 0 0 + let targetEffectiveDate = DT.time (DA.date 2024 Jan 03) 0 0 0 + let sourcePlan = (defaultStockPlan sourcePlanId stockClassId) with initial_shares_reserved = 100.0 + let targetPlan = (defaultStockPlan targetPlanId stockClassId) with + initial_shares_reserved = 100.0 + board_approval_date = Some targetEffectiveDate + let sourceIssuance = (stockPlanEquityCompensationIssuance + "TX_RETURN_EFFECTIVE_SOURCE" + sourceSecurityId + stakeholderId + stockClassId + sourcePlanId + 100.0) with date = DT.time (DA.date 2024 Jan 01) 0 0 0 + let cancellation = (equityCompensationCancellation "TX_RETURN_EFFECTIVE_CANCEL" sourceSecurityId 100.0) with date = returnDate + let returnToPool = (stockPlanReturnToPool "TX_RETURN_EFFECTIVE_POOL" sourceSecurityId targetPlanId 100.0) with date = returnDate + + submitMustFail issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan sourcePlan + , CT.OcfCreateStockPlan targetPlan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + , CT.OcfCreateEquityCompensationCancellation cancellation + , CT.OcfCreateStockPlanReturnToPool returnToPool + ] + edits = [] + deletes = [] + testStockPlanReservedShares_AdjustmentAllowsAdditionalIssuanceSameBatch = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table @@ -957,7 +991,7 @@ testStockPlanReservedShares_ReturnToPoolRejectsTargetPlanOutsideSecurityClass = edits = [] deletes = [] -testStockPlanReservedShares_ReturnCannotCreateNegativeHistoricalAllocation = script do +testStockPlanReservedShares_CrossPlanReturnCanPrecedeTargetGrant = script do TestOcp{issuer, cap_table} <- setupTestOcp (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table let sourcePlanId = "PLAN_RETURN_HISTORY_SOURCE" @@ -974,7 +1008,7 @@ testStockPlanReservedShares_ReturnCannotCreateNegativeHistoricalAllocation = scr let targetIssuance = (stockPlanEquityCompensationIssuance "TX_RETURN_HISTORY_TARGET" "SEC-RETURN-HISTORY-TARGET" stakeholderId stockClassId targetPlanId 100.0) with date = DT.time (DA.date 2024 Jan 03) 0 0 0 - submitMustFail issuer do + _ <- submit issuer do exerciseCmd capTableReady CT.UpdateCapTable with creates = [ CT.OcfCreateStockPlan sourcePlan @@ -986,6 +1020,7 @@ testStockPlanReservedShares_ReturnCannotCreateNegativeHistoricalAllocation = scr ] edits = [] deletes = [] + pure () testStockPlanReservedShares_AdjustmentCannotDropBelowAllocatedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 2c6fb78d..3dfc9680 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a9080210d0812062c029b54c625fc80624eb310770b2625ae5e4445f59d8264 -size 2942894 +oid sha256:177eb61787833a6e5b31b7640c73bfd0ecec34f8fc5d8542db65b7a9acbd1e8c +size 2948142 diff --git a/dars/dars.lock b/dars/dars.lock index a8d415ce..99ecdd89 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,10 +33,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "3a9080210d0812062c029b54c625fc80624eb310770b2625ae5e4445f59d8264", - "size": 2942894, + "sha256": "177eb61787833a6e5b31b7640c73bfd0ecec34f8fc5d8542db65b7a9acbd1e8c", + "size": 2948142, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T00:41:54.852Z", + "uploadedAt": "2026-07-11T02:00:45Z", "networks": [] } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index b42462f9..8e16f2a1 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -18,7 +18,7 @@ import DA.Foldable (mapA_) import DA.Optional (fromOptional, isNone, isSome) import Fairmint.OpenCapTable.Types.Core (Context) -import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfConversionMechanism(..), OcfConversionTrigger(..), OcfConversionTriggerType(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) +import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfConversionMechanism(..), OcfConversionTrigger(..), OcfConversionTriggerType(..), OcfConvertibleConversionMechanism(..), OcfConvertibleConversionRight(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..)) import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..)) import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) import Fairmint.OpenCapTable.OCF.Issuer (Issuer(..), IssuerOcfData) @@ -503,6 +503,42 @@ stockConversionOutputShareLot maps stockClassIndex sourceIssuanceData conversion False pure None +convertibleConversionRight : + ConvertibleIssuanceOcfData -> + Text -> + Optional OcfConvertibleConversionRight +convertibleConversionRight issuanceData triggerId = + foldl + (\matched trigger -> + case matched of + Some _ -> matched + None -> + if trigger.trigger_id == triggerId + then Some trigger.conversion_right + else None) + None + issuanceData.conversion_triggers + +convertibleConversionOutputQuantity : + ConvertibleIssuanceOcfData -> + ConvertibleConversionOcfData -> + OcfConvertibleConversionRight -> + Optional Decimal +convertibleConversionOutputQuantity issuanceData conversionData conversionRight = + case conversionRight.conversion_mechanism of + OcfConvMechFixedAmount mechanism -> + case conversionData.quantity_converted of + Some quantity -> + if issuanceData.investment_amount.amount > 0.0 + then Some (mechanism.converts_to_quantity * quantity / issuanceData.investment_amount.amount) + else None + None -> Some mechanism.converts_to_quantity + OcfConvMechRatio ratio -> + case conversionData.quantity_converted of + Some quantity -> Some (quantity * ratio.numerator / ratio.denominator) + None -> None + _ -> None + warrantExerciseTrigger : WarrantIssuanceOcfData -> Text -> Optional OcfConversionTrigger warrantExerciseTrigger issuanceData triggerId = foldl @@ -901,6 +937,26 @@ validateStockConversionResultIssuances maps conversionData expectedTerms = do None -> assertMsg ("Stock conversion has no resulting security: " <> conversionData.id) False +validateConvertibleConversionResultIssuances : + CapTableMaps -> + ConvertibleIssuanceOcfData -> + ConvertibleConversionOcfData -> + Update () +validateConvertibleConversionResultIssuances maps issuanceData conversionData = + case convertibleConversionRight issuanceData conversionData.trigger_id of + Some conversionRight -> + validateStockResultIssuances + maps + ("Convertible conversion " <> conversionData.id) + conversionData.date + conversionRight.converts_to_stock_class_id + (convertibleConversionOutputQuantity issuanceData conversionData conversionRight) + conversionData.resulting_security_ids + None -> + assertMsg + ("Convertible conversion trigger not found: " <> conversionData.trigger_id) + False + validateWarrantExerciseResultIssuances : CapTableMaps -> WarrantIssuanceOcfData -> WarrantExerciseOcfData -> Update () validateWarrantExerciseResultIssuances maps issuanceData exerciseData = case warrantExerciseTrigger issuanceData exerciseData.trigger_id of @@ -1018,6 +1074,22 @@ stockClassShareLotsByStockClass maps stockClassIndex = do stockTransferLots (Map.values maps.stock_conversions) + convertibleConversionLots <- foldlA + (\index conversionCid -> do + conversion <- fetch conversionCid + let label = "Convertible conversion " <> conversion.conversion_data.id + assertAllResultingStockIssuancesExist label maps conversion.conversion_data.resulting_security_ids + case Map.lookup conversion.conversion_data.security_id maps.convertible_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + validateConvertibleConversionResultIssuances maps issuance.issuance_data conversion.conversion_data + pure index + None -> do + assertMsg (label <> " references missing convertible issuance: " <> conversion.conversion_data.security_id) False + pure index) + stockConversionLots + (Map.values maps.convertible_conversions) + warrantExerciseLots <- foldlA (\index exerciseCid -> do exercise <- fetch exerciseCid @@ -1039,7 +1111,7 @@ stockClassShareLotsByStockClass maps stockClassIndex = do None -> do assertMsg ("Warrant exercise references missing warrant issuance: " <> exercise.exercise_data.security_id) False pure index) - stockConversionLots + convertibleConversionLots (Map.values maps.warrant_exercises) equityExerciseLots <- foldlA @@ -3965,6 +4037,16 @@ validateStockPlanAllocationEffectiveDates maps = do None -> pure ()) (Map.values maps.equity_compensation_issuances) + mapA_ + (\returnCid -> do + returnToPool <- fetch returnCid + assertStockPlanEffectiveForAllocation + ("Stock plan return-to-pool " <> returnToPool.return_data.id) + maps + returnToPool.return_data.stock_plan_id + returnToPool.return_data.date) + (Map.values maps.stock_plan_return_to_pools) + validateStockPlanReservedShares : StockClassShareEventIndex -> CapTableMaps -> @@ -3978,7 +4060,6 @@ validateStockPlanReservedShares eventIndex maps adjustmentIndex = do stockPlan <- fetch stockPlanCid let reservedShares = currentStockPlanReservedShares adjustmentIndex stockPlan.plan_data let allocatedQuantity = fromOptional 0.0 (Map.lookup stockPlan.plan_data.id netAllocatedQuantityIndex) - assertNonNegativeQuantity ("Stock plan net allocated quantity for " <> stockPlan.plan_data.id) allocatedQuantity assertMsg ("Stock plan reserved shares exceeded for " <> stockPlan.plan_data.id <> ": allocated " <> show allocatedQuantity <> " exceeds reserved " <> show reservedShares) (allocatedQuantity <= reservedShares)) @@ -4168,9 +4249,6 @@ validateStockPlanReservedSharesAtDates eventIndex maps adjustmentHistory = do netAllocatedQuantityIndex <- stockPlanNetAllocatedQuantityIndexAt eventIndex maps asOf validateStockPlanAllocationReferences maps netAllocatedQuantityIndex let allocatedQuantity = fromOptional 0.0 (Map.lookup stockPlan.plan_data.id netAllocatedQuantityIndex) - assertNonNegativeQuantity - ("Stock plan allocated quantity for " <> stockPlan.plan_data.id <> " on " <> show asOf) - allocatedQuantity if stockPlanEffectiveOnOrBefore asOf stockPlan.plan_data then do let reservedShares = stockPlanReservedSharesAt adjustmentHistory stockPlan.plan_data asOf From 15aab77373e15ec2e125006a7d36a8a0c8d964f0 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Fri, 10 Jul 2026 22:49:41 -0400 Subject: [PATCH 59/62] fix: reject negative equity exercises --- .../TestStockClassAuthorizedShares.daml | 22 +++++++++++++++++++ .../0.0.5/OpenCapTable-v34.dar | 4 ++-- dars/dars.lock | 6 ++--- .../codegen/templates/CapTable.daml.template | 14 +++++++----- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 1deaca84..dee539f2 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -2026,6 +2026,28 @@ testClasslessCashSettledSarExerciseAllowsNoStockOutput = script do deletes = [] pure () +testNegativeCashSettledSarExerciseCannotIncreaseGrantBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CASH_SETTLED_SAR_NEGATIVE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_CASH_SETTLED_SAR_NEGATIVE" "Cash Settled SAR Negative Holder" + let issuance = (equityCompensationIssuance "TX_CASH_SETTLED_SAR_NEGATIVE_GRANTED" "ECSEC_CASH_SETTLED_SAR_NEGATIVE" stakeholderId stockClassId 50.0) with + compensation_type = OcfCompensationTypeCSAR + base_price = Some (OcfMonetary with amount = 1.0, currency = "USD") + stock_class_id = None + stock_plan_id = None + result <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance issuance] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise ((equityCompensationExercise "TX_CASH_SETTLED_SAR_NEGATIVE_EXERCISE" "ECSEC_CASH_SETTLED_SAR_NEGATIVE" (-1.0)) with resulting_security_ids = [])] + edits = [] + deletes = [] + testClasslessEquityCompensationReleaseUsesSinglePlanClassForCeiling = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar index 0fb66f97..f3398cbf 100644 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d386637f0500f74555e9ecdf864c95e48db1e91c92609676c2d1feaa18cde92 -size 2961945 +oid sha256:b99c41fe3a2eb3c3d4ce21febfc7568c90c7e9a509d06cb66db924570435c50f +size 2962199 diff --git a/dars/dars.lock b/dars/dars.lock index 245ddb33..b1346629 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -33,10 +33,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "4d386637f0500f74555e9ecdf864c95e48db1e91c92609676c2d1feaa18cde92", - "size": 2961945, + "sha256": "b99c41fe3a2eb3c3d4ce21febfc7568c90c7e9a509d06cb66db924570435c50f", + "size": 2962199, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T01:56:50.000Z", + "uploadedAt": "2026-07-11T02:46:51.000Z", "networks": [] } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index cec8309e..e6784c71 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -3431,11 +3431,15 @@ equityCompensationSecurityQuantityEvents maps issuanceData = do (\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) + then do + assertNonNegativeQuantity + ("Equity compensation exercise quantity for " <> exercise.exercise_data.id) + exercise.exercise_data.quantity + 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) From 87942997988353f62ae297fc757b2e1ec54da3f3 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Mon, 13 Jul 2026 15:05:44 -0400 Subject: [PATCH 60/62] Enforce global OCF object ID uniqueness --- .../OpenCapTable/TestObjectIdUniqueness.daml | 85 +++++++++++++++++++ .../TestStockPlanReservedShares.daml | 6 +- .../codegen/templates/CapTable.daml.template | 24 ++++++ 3 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 Test/daml/OpenCapTable/TestObjectIdUniqueness.daml diff --git a/Test/daml/OpenCapTable/TestObjectIdUniqueness.daml b/Test/daml/OpenCapTable/TestObjectIdUniqueness.daml new file mode 100644 index 00000000..ac93079c --- /dev/null +++ b/Test/daml/OpenCapTable/TestObjectIdUniqueness.daml @@ -0,0 +1,85 @@ +module OpenCapTable.TestObjectIdUniqueness where + +import qualified Fairmint.OpenCapTable.CapTable as CT +import qualified Fairmint.OpenCapTable.OCF.Document as Document +import OpenCapTable.Setup +import OpenCapTable.TestHelpers (defaultStakeholder, setupStakeholderWithId) +import qualified DA.Map as Map +import Daml.Script + +documentData : Text -> Document.DocumentOcfData +documentData objectId = Document.DocumentOcfData with + id = objectId + md5 = "0123456789abcdef0123456789abcdef" + comments = [] + related_objects = [] + path = Some ("documents/" <> objectId <> ".pdf") + uri = None + +testObjectIdUniqueness_SameBatchCrossTypeDuplicateFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStakeholder (defaultStakeholder "DUPLICATE-OBJECT-ID" "Duplicate Holder") + , CT.OcfCreateDocument (documentData "DUPLICATE-OBJECT-ID") + ] + edits = [] + deletes = [] + pure () + +testObjectIdUniqueness_PriorBatchCrossTypeDuplicateFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithStakeholder, _) <- setupStakeholderWithId issuer cap_table "PRIOR-OBJECT-ID" "Prior Holder" + + submitMustFail issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = [CT.OcfCreateDocument (documentData "PRIOR-OBJECT-ID")] + edits = [] + deletes = [] + pure () + +testObjectIdUniqueness_IssuerIdCannotBeReused = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateDocument (documentData "ISSUER_1")] + edits = [] + deletes = [] + pure () + +-- Validate fetched object data rather than trusting map keys on cap tables +-- upgraded from a package that did not enforce global object-id uniqueness. +testObjectIdUniqueness_CorruptedFinalStateFails = script do + TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp + (capTableWithStakeholder, _) <- setupStakeholderWithId issuer cap_table "UPGRADED-DUPLICATE-ID" "Existing Holder" + capTableDataOpt <- queryContractId issuer capTableWithStakeholder + let Some capTableData = capTableDataOpt + + duplicateDocumentCid <- submit (actAs issuer <> actAs system_operator) $ createCmd $ Document.Document with + context = ctx + document_data = documentData "UPGRADED-DUPLICATE-ID" + + let corruptedCapTable = capTableData with + documents = Map.insert "CORRUPTED-DOCUMENT-MAP-KEY" duplicateDocumentCid capTableData.documents + corruptedCapTableCid <- submit (actAs issuer <> actAs system_operator) $ createCmd corruptedCapTable + + submitMustFail issuer do + exerciseCmd corruptedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [] + pure () + +testObjectIdUniqueness_DeleteThenReuseAcrossTypesSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithStakeholder, _) <- setupStakeholderWithId issuer cap_table "REUSED-OBJECT-ID" "Replaced Holder" + + _ <- submit issuer do + exerciseCmd capTableWithStakeholder CT.UpdateCapTable with + creates = [CT.OcfCreateDocument (documentData "REUSED-OBJECT-ID")] + edits = [] + deletes = [CT.OcfDeleteStakeholder "REUSED-OBJECT-ID"] + pure () diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index e4f77872..59d27a90 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -1467,7 +1467,7 @@ testStockPlanReservedShares_ResultRetractionReleasesConvertedAllocation = script _ <- submit issuer do exerciseCmd convertedCapTable CT.UpdateCapTable with creates = - [ CT.OcfCreateStockRetraction (planStockRetraction "TX_PLAN_CONVERSION_RESULT_RETRACTION" resultSecurityId retractionDate) + [ CT.OcfCreateStockRetraction (planStockRetraction "TX_PLAN_CONVERSION_RESULT_SECURITY_RETRACTION" resultSecurityId retractionDate) , CT.OcfCreateStockIssuance replacementIssuance ] edits = [] @@ -1996,7 +1996,7 @@ testStockPlanReservedShares_EquityTransferRejectsLaterBalance = script do date = transferDate balance_security_id = Some balanceSecurityId) let resultIssuance = (stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_LATER_BALANCE_RESULT" resultSecurityId stakeholderId stockClassId stockPlanId 40.0) with date = transferDate - let balanceIssuance = (stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_LATER_BALANCE" balanceSecurityId stakeholderId stockClassId stockPlanId 60.0) with date = balanceDate + let balanceIssuance = (stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_LATER_BALANCE_ISSUANCE" balanceSecurityId stakeholderId stockClassId stockPlanId 60.0) with date = balanceDate submitMustFail issuer do exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with creates = @@ -2028,7 +2028,7 @@ testStockPlanReservedShares_EquityTransferAcceptsExactBalance = script do let transfer = (planEquityCompensationTransfer "TX_EQUITY_TRANSFER_EXACT_BALANCE" sourceSecurityId resultSecurityId 40.0) with balance_security_id = Some balanceSecurityId let resultIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_EXACT_BALANCE_RESULT" resultSecurityId stakeholderId stockClassId stockPlanId 40.0 - let balanceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_EXACT_BALANCE" balanceSecurityId stakeholderId stockClassId stockPlanId 60.0 + let balanceIssuance = stockPlanEquityCompensationIssuance "TX_EQUITY_TRANSFER_EXACT_BALANCE_ISSUANCE" balanceSecurityId stakeholderId stockClassId stockPlanId 60.0 _ <- submit issuer do exerciseCmd source.updatedCapTableCid CT.UpdateCapTable with creates = diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index e6784c71..c21ba261 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -135,6 +135,29 @@ validateFinalSecurityIdUniqueness maps = do securityIds <- securityIdsInFinalMaps maps assertMsg "Security IDs must be globally unique" (unique securityIds) +ocfObjectIdsInFinalState : CapTableMaps -> ContractId Issuer -> Update [Text] +ocfObjectIdsInFinalState maps issuerCid = do + issuer <- fetch issuerCid +{{#each types_alpha}} + {{map_field}}ObjectIds <- foldlA + (\objectIds objectCid -> do + object <- fetch objectCid + pure (object.{{data_param}}.id :: objectIds)) + [] + (Map.values maps.{{map_field}}) +{{/each}} + pure + ([issuer.issuer_data.id] +{{#each types_alpha}} + ++ {{map_field}}ObjectIds +{{/each}} + ) + +validateFinalOcfObjectIdUniqueness : CapTableMaps -> ContractId Issuer -> Update () +validateFinalOcfObjectIdUniqueness maps issuerCid = do + objectIds <- ocfObjectIdsInFinalState maps issuerCid + assertMsg "OCF object IDs must be globally unique across the cap table" (unique objectIds) + data AuthorizedSharesCeiling = AuthorizedSharesFinite Decimal | AuthorizedSharesUnlimited @@ -4788,6 +4811,7 @@ validateAuthorizedShareCeilings : validateAuthorizedShareCeilings maps adjustmentIndex issuerCid = do eventIndex <- stockClassShareEventIndex maps validateFinalSecurityIdUniqueness maps + validateFinalOcfObjectIdUniqueness maps issuerCid validateSuccessorSecurityIdContinuity maps validateStockSecurityBalances eventIndex maps validateWarrantSecurityBalances maps From 8d7aff98f74e5187cb5876337f30bb6aabcbb78c Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Tue, 14 Jul 2026 08:17:09 -0400 Subject: [PATCH 61/62] Scope OCF object ID uniqueness by type --- .../OpenCapTable/TestObjectIdUniqueness.daml | 76 ++++++++++++++----- .../codegen/templates/CapTable.daml.template | 32 ++++---- 2 files changed, 73 insertions(+), 35 deletions(-) diff --git a/Test/daml/OpenCapTable/TestObjectIdUniqueness.daml b/Test/daml/OpenCapTable/TestObjectIdUniqueness.daml index ac93079c..1380703f 100644 --- a/Test/daml/OpenCapTable/TestObjectIdUniqueness.daml +++ b/Test/daml/OpenCapTable/TestObjectIdUniqueness.daml @@ -16,34 +16,62 @@ documentData objectId = Document.DocumentOcfData with path = Some ("documents/" <> objectId <> ".pdf") uri = None -testObjectIdUniqueness_SameBatchCrossTypeDuplicateFails = script do +testObjectIdUniqueness_SameBatchSameTypeDuplicateFails = script do TestOcp{issuer, cap_table} <- setupTestOcp submitMustFail issuer do exerciseCmd cap_table CT.UpdateCapTable with creates = - [ CT.OcfCreateStakeholder (defaultStakeholder "DUPLICATE-OBJECT-ID" "Duplicate Holder") - , CT.OcfCreateDocument (documentData "DUPLICATE-OBJECT-ID") + [ CT.OcfCreateDocument (documentData "DUPLICATE-DOCUMENT-ID") + , CT.OcfCreateDocument (documentData "DUPLICATE-DOCUMENT-ID") ] edits = [] deletes = [] pure () -testObjectIdUniqueness_PriorBatchCrossTypeDuplicateFails = script do +testObjectIdUniqueness_PriorBatchSameTypeDuplicateFails = script do TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithStakeholder, _) <- setupStakeholderWithId issuer cap_table "PRIOR-OBJECT-ID" "Prior Holder" + createResult <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateDocument (documentData "PRIOR-DOCUMENT-ID")] + edits = [] + deletes = [] submitMustFail issuer do + exerciseCmd createResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateDocument (documentData "PRIOR-DOCUMENT-ID")] + edits = [] + deletes = [] + pure () + +testObjectIdUniqueness_SameBatchCrossTypeDuplicateSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + _ <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStakeholder (defaultStakeholder "SHARED-CROSS-TYPE-ID" "Shared ID Holder") + , CT.OcfCreateDocument (documentData "SHARED-CROSS-TYPE-ID") + ] + edits = [] + deletes = [] + pure () + +testObjectIdUniqueness_PriorBatchCrossTypeDuplicateSucceeds = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithStakeholder, _) <- setupStakeholderWithId issuer cap_table "PRIOR-CROSS-TYPE-ID" "Prior Holder" + + _ <- submit issuer do exerciseCmd capTableWithStakeholder CT.UpdateCapTable with - creates = [CT.OcfCreateDocument (documentData "PRIOR-OBJECT-ID")] + creates = [CT.OcfCreateDocument (documentData "PRIOR-CROSS-TYPE-ID")] edits = [] deletes = [] pure () -testObjectIdUniqueness_IssuerIdCannotBeReused = script do +testObjectIdUniqueness_IssuerIdMayBeReusedByOtherType = script do TestOcp{issuer, cap_table} <- setupTestOcp - submitMustFail issuer do + _ <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with creates = [CT.OcfCreateDocument (documentData "ISSUER_1")] edits = [] @@ -51,19 +79,23 @@ testObjectIdUniqueness_IssuerIdCannotBeReused = script do pure () -- Validate fetched object data rather than trusting map keys on cap tables --- upgraded from a package that did not enforce global object-id uniqueness. -testObjectIdUniqueness_CorruptedFinalStateFails = script do +-- upgraded from a package that did not enforce per-type object-id uniqueness. +testObjectIdUniqueness_CorruptedSameTypeFinalStateFails = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp - (capTableWithStakeholder, _) <- setupStakeholderWithId issuer cap_table "UPGRADED-DUPLICATE-ID" "Existing Holder" - capTableDataOpt <- queryContractId issuer capTableWithStakeholder + capTableDataOpt <- queryContractId issuer cap_table let Some capTableData = capTableDataOpt - duplicateDocumentCid <- submit (actAs issuer <> actAs system_operator) $ createCmd $ Document.Document with + firstDocumentCid <- submit (actAs issuer <> actAs system_operator) $ createCmd $ Document.Document with + context = ctx + document_data = documentData "UPGRADED-DUPLICATE-DOCUMENT-ID" + secondDocumentCid <- submit (actAs issuer <> actAs system_operator) $ createCmd $ Document.Document with context = ctx - document_data = documentData "UPGRADED-DUPLICATE-ID" + document_data = documentData "UPGRADED-DUPLICATE-DOCUMENT-ID" let corruptedCapTable = capTableData with - documents = Map.insert "CORRUPTED-DOCUMENT-MAP-KEY" duplicateDocumentCid capTableData.documents + documents = + Map.insert "CORRUPTED-DOCUMENT-MAP-KEY-1" firstDocumentCid + (Map.insert "CORRUPTED-DOCUMENT-MAP-KEY-2" secondDocumentCid capTableData.documents) corruptedCapTableCid <- submit (actAs issuer <> actAs system_operator) $ createCmd corruptedCapTable submitMustFail issuer do @@ -73,13 +105,17 @@ testObjectIdUniqueness_CorruptedFinalStateFails = script do deletes = [] pure () -testObjectIdUniqueness_DeleteThenReuseAcrossTypesSucceeds = script do +testObjectIdUniqueness_DeleteThenReuseWithinTypeSucceeds = script do TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithStakeholder, _) <- setupStakeholderWithId issuer cap_table "REUSED-OBJECT-ID" "Replaced Holder" + createResult <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateDocument (documentData "REUSED-DOCUMENT-ID")] + edits = [] + deletes = [] _ <- submit issuer do - exerciseCmd capTableWithStakeholder CT.UpdateCapTable with - creates = [CT.OcfCreateDocument (documentData "REUSED-OBJECT-ID")] + exerciseCmd createResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateDocument (documentData "REUSED-DOCUMENT-ID")] edits = [] - deletes = [CT.OcfDeleteStakeholder "REUSED-OBJECT-ID"] + deletes = [CT.OcfDeleteDocument "REUSED-DOCUMENT-ID"] pure () diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 6978b9cd..c12dc515 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -153,28 +153,30 @@ validateFinalSecurityIdUniqueness maps = do securityIds <- securityIdsInFinalMaps maps assertMsg "Security IDs must be globally unique" (unique securityIds) -ocfObjectIdsInFinalState : CapTableMaps -> ContractId Issuer -> Update [Text] -ocfObjectIdsInFinalState maps issuerCid = do - issuer <- fetch issuerCid -{{#each types_alpha}} +validateFinalOcfObjectIdUniqueness : CapTableMaps -> Update () +validateFinalOcfObjectIdUniqueness maps = do +{{#each types_existing}} {{map_field}}ObjectIds <- foldlA (\objectIds objectCid -> do object <- fetch objectCid pure (object.{{data_param}}.id :: objectIds)) [] (Map.values maps.{{map_field}}) + assertMsg + "{{name}} OCF object IDs must be unique within their object type" + (unique {{map_field}}ObjectIds) {{/each}} - pure - ([issuer.issuer_data.id] -{{#each types_alpha}} - ++ {{map_field}}ObjectIds +{{#each types_appended}} + {{map_field}}ObjectIds <- foldlA + (\objectIds objectCid -> do + object <- fetch objectCid + pure (object.{{data_param}}.id :: objectIds)) + [] + (Map.values (fromOptional Map.empty maps.{{map_field}})) + assertMsg + "{{name}} OCF object IDs must be unique within their object type" + (unique {{map_field}}ObjectIds) {{/each}} - ) - -validateFinalOcfObjectIdUniqueness : CapTableMaps -> ContractId Issuer -> Update () -validateFinalOcfObjectIdUniqueness maps issuerCid = do - objectIds <- ocfObjectIdsInFinalState maps issuerCid - assertMsg "OCF object IDs must be globally unique across the cap table" (unique objectIds) data AuthorizedSharesCeiling = AuthorizedSharesFinite Decimal @@ -4829,7 +4831,7 @@ validateAuthorizedShareCeilings : validateAuthorizedShareCeilings maps adjustmentIndex issuerCid = do eventIndex <- stockClassShareEventIndex maps validateFinalSecurityIdUniqueness maps - validateFinalOcfObjectIdUniqueness maps issuerCid + validateFinalOcfObjectIdUniqueness maps validateSuccessorSecurityIdContinuity maps validateStockSecurityBalances eventIndex maps validateWarrantSecurityBalances maps From 48a4be210dae097ef31910e4a1195adc70af31c8 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Tue, 14 Jul 2026 08:17:31 -0400 Subject: [PATCH 62/62] Normalize OpenCapTable DAR candidate --- OpenCapTable-v34/daml.yaml | 2 +- Test/daml.yaml | 2 +- .../0.0.2/OpenCapTable-v34.dar | 4 +-- .../0.0.4/OpenCapTable-v34.dar | 3 --- .../0.0.5/OpenCapTable-v34.dar | 3 --- dars/dars.lock | 27 +++---------------- 6 files changed, 7 insertions(+), 34 deletions(-) delete mode 100644 dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar delete mode 100644 dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar diff --git a/OpenCapTable-v34/daml.yaml b/OpenCapTable-v34/daml.yaml index f16d912a..6d6d6797 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.2 build-options: - -Wno-crypto-text-is-alpha - --typecheck-upgrades=no diff --git a/Test/daml.yaml b/Test/daml.yaml index 64cd009c..cdf173c0 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.2.dar build-options: - -Wno-template-interface-depends-on-daml-script diff --git a/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar index 9821b4b1..736a92a6 100644 --- a/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:524b71370b42d579bd3b204c2ca05023da73c1bd4396efe9aa1aabf438576143 -size 2471277 +oid sha256:5f74b5cbcd0221d65802c2a76d87f90a533425f7f5d81005f76c90d89f17060f +size 3024844 diff --git a/dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar deleted file mode 100644 index f36ce95c..00000000 --- a/dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ced34adbf61393475b2e00e51d8f112f5c423f3bbf0bb544c10febd2058c80ad -size 2576174 diff --git a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar deleted file mode 100644 index f3398cbf..00000000 --- a/dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b99c41fe3a2eb3c3d4ce21febfc7568c90c7e9a509d06cb66db924570435c50f -size 2962199 diff --git a/dars/dars.lock b/dars/dars.lock index b4fc06f6..5424fa18 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -12,10 +12,10 @@ ] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { - "sha256": "524b71370b42d579bd3b204c2ca05023da73c1bd4396efe9aa1aabf438576143", - "size": 2471277, + "sha256": "5f74b5cbcd0221d65802c2a76d87f90a533425f7f5d81005f76c90d89f17060f", + "size": 3024844, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-14T01:30:05.159Z", + "uploadedAt": "2026-07-14T12:14:38.847Z", "networks": [] }, "OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar": { @@ -24,27 +24,6 @@ "sdkVersion": "3.4.10", "uploadedAt": "2026-07-13T15:33:09.214Z", "networks": [] - }, - "OpenCapTable-v34/0.0.3/OpenCapTable-v34.dar": { - "sha256": "97ff8995277f708334f31219aa06c7a742ecb30fe3aaa23fb046583561b053c5", - "size": 2554649, - "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T00:41:37.578Z", - "networks": [] - }, - "OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar": { - "sha256": "ced34adbf61393475b2e00e51d8f112f5c423f3bbf0bb544c10febd2058c80ad", - "size": 2576174, - "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-09T12:34:07.425Z", - "networks": [] - }, - "OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar": { - "sha256": "b99c41fe3a2eb3c3d4ce21febfc7568c90c7e9a509d06cb66db924570435c50f", - "size": 2962199, - "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T02:46:51.000Z", - "networks": [] } } }