From 4c46218d12e2676439e9391da7a14e72d11bde34 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 14:19:25 -0400 Subject: [PATCH 01/10] feat: enforce vesting quantity integrity --- OpenCapTable-v34/daml.yaml | 2 +- .../OCF/EquityCompensationIssuance.daml | 3 +- .../OpenCapTable/OCF/StockIssuance.daml | 3 +- .../OpenCapTable/OCF/WarrantIssuance.daml | 7 +- .../Fairmint/OpenCapTable/Types/Vesting.daml | 7 + Test/daml.yaml | 2 +- .../TestVestingQuantityIntegrity.daml | 372 ++++++++++++++++++ .../0.0.17/OpenCapTable-v34.dar | 3 + dars/dars.lock | 7 + .../codegen/templates/CapTable.daml.template | 104 ++++- 10 files changed, 503 insertions(+), 7 deletions(-) create mode 100644 Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml create mode 100644 dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar diff --git a/OpenCapTable-v34/daml.yaml b/OpenCapTable-v34/daml.yaml index 76f7c6c1..0e021f17 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.16 +version: 0.0.17 build-options: - -Wno-crypto-text-is-alpha - --typecheck-upgrades=no diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationIssuance.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationIssuance.daml index 5f39b12c..cc29ba6d 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationIssuance.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/EquityCompensationIssuance.daml @@ -6,7 +6,7 @@ import Fairmint.OpenCapTable.Types.Core (Context) import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary, validateOcfMonetary) import Fairmint.OpenCapTable.Types.Stock (OcfSecurityExemption, validateOcfSecurityExemption) -import Fairmint.OpenCapTable.Types.Vesting (OcfTerminationWindow, OcfCompensationType(..), OcfVesting, validateOcfTerminationWindow, validateOcfVesting) +import Fairmint.OpenCapTable.Types.Vesting (OcfTerminationWindow, OcfCompensationType(..), OcfVesting, validateOcfTerminationWindow, validateOcfVesting, validateOcfVestingsWithinQuantity) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -96,6 +96,7 @@ validateEquityCompensationIssuanceOcfData d = all validateOcfSecurityExemption d.security_law_exemptions && validateNonEmptyStringsArray d.comments && all validateOcfVesting d.vestings && + validateOcfVestingsWithinQuantity d.quantity d.vestings && all validateOcfTerminationWindow d.termination_exercise_windows && -- Optional Text values, if present, must be non-empty validateOptionalText d.consideration_text && diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockIssuance.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockIssuance.daml index c08016ad..7089aab2 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockIssuance.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/StockIssuance.daml @@ -5,7 +5,7 @@ import Fairmint.OpenCapTable.Types.Core (Context) import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary, validateOcfMonetary) import Fairmint.OpenCapTable.Types.Stock (OcfSecurityExemption, OcfShareNumberRange, validateOcfSecurityExemption, validateOcfShareNumberRangesForQuantity) -import Fairmint.OpenCapTable.Types.Vesting (OcfVesting, validateOcfVesting) +import Fairmint.OpenCapTable.Types.Vesting (OcfVesting, validateOcfVesting, validateOcfVestingsWithinQuantity) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -92,6 +92,7 @@ validateOcfStockIssuanceData d = all validateOcfSecurityExemption d.security_law_exemptions && validateOcfShareNumberRangesForQuantity d.quantity d.share_numbers_issued && all validateOcfVesting d.vestings && + validateOcfVestingsWithinQuantity d.quantity d.vestings && validateNonEmptyStringsArray d.comments && -- Arrays per schema: schema requires at least 1 legend, but we allow empty validateNonEmptyStringsArray d.stock_legend_ids && diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantIssuance.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantIssuance.daml index 74a22490..94162032 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantIssuance.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantIssuance.daml @@ -5,7 +5,7 @@ import Fairmint.OpenCapTable.Types.Core (Context) import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary, validateOcfMonetary) import Fairmint.OpenCapTable.Types.Stock (OcfQuantitySourceType, OcfSecurityExemption, validateOcfSecurityExemption) -import Fairmint.OpenCapTable.Types.Vesting (OcfVesting, validateOcfVesting) +import Fairmint.OpenCapTable.Types.Vesting (OcfVesting, validateOcfVesting, validateOcfVestingsWithinQuantity) import Fairmint.OpenCapTable.Types.Conversion (OcfConversionTrigger, validateOcfConversionTrigger) import Fairmint.Shared.TypeHelpers (validateOptionalText) import DA.List (unique) @@ -102,4 +102,7 @@ validateOcfWarrantIssuanceDataTx d = -- If vestings array present, enforce items are valid and minItems >= 1 (case d.vestings of [] -> True - xs -> all validateOcfVesting xs) + xs -> all validateOcfVesting xs) && + (case d.quantity of + Some quantity -> validateOcfVestingsWithinQuantity quantity d.vestings + None -> True) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Vesting.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Vesting.daml index 4d68ac96..35b3c71b 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Vesting.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Vesting.daml @@ -61,6 +61,13 @@ data OcfVesting = OcfVesting validateOcfVesting : OcfVesting -> Bool validateOcfVesting v = v.amount > (0.0) +ocfVestingAmountTotal : [OcfVesting] -> Decimal +ocfVestingAmountTotal = foldl (\total vesting -> total + vesting.amount) 0.0 + +validateOcfVestingsWithinQuantity : Decimal -> [OcfVesting] -> Bool +validateOcfVestingsWithinQuantity quantity vestings = + null vestings || ocfVestingAmountTotal vestings <= quantity + -- Enum - Compensation Type -- Equity compensation instrument type -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/enums/CompensationType.schema.json diff --git a/Test/daml.yaml b/Test/daml.yaml index 5693c135..36057ec4 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.16.dar + - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.17.dar build-options: - -Wno-template-interface-depends-on-daml-script diff --git a/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml b/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml new file mode 100644 index 00000000..1c0a5523 --- /dev/null +++ b/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml @@ -0,0 +1,372 @@ +module OpenCapTable.TestVestingQuantityIntegrity where + +import Fairmint.OpenCapTable.OCF.VestingAcceleration (VestingAccelerationOcfData(..)) +import Fairmint.OpenCapTable.Types.Vesting (OcfVesting(..)) +import qualified Fairmint.OpenCapTable.CapTable as CT +import OpenCapTable.Setup +import OpenCapTable.TestHelpers + ( addPrerequisites + , defaultEquityCompensationIssuance + , defaultStockIssuance + , defaultWarrantIssuance + , stockClassSplitOn + ) +import qualified DA.Date as DA +import DA.Date (Month(..)) +import qualified DA.Time as DT +import Daml.Script + +firstVestingDate : Time +firstVestingDate = DT.time (DA.date 2025 Jan 01) 0 0 0 + +secondVestingDate : Time +secondVestingDate = DT.time (DA.date 2026 Jan 01) 0 0 0 + +accelerationDate : Time +accelerationDate = DT.time (DA.date 2025 Jun 01) 0 0 0 + +splitDate : Time +splitDate = DT.time (DA.date 2026 Jan 01) 0 0 0 + +postSplitAccelerationDate : Time +postSplitAccelerationDate = DT.time (DA.date 2026 Jun 01) 0 0 0 + +vestingOn : Time -> Decimal -> OcfVesting +vestingOn date amount = OcfVesting with + date = date + amount = amount + +acceleration : Text -> Text -> Decimal -> VestingAccelerationOcfData +acceleration accelerationId securityId quantity = VestingAccelerationOcfData with + id = accelerationId + date = accelerationDate + security_id = securityId + quantity = quantity + reason_text = "Board-approved acceleration" + comments = [] + +applyUpdate : + Party -> + ContractId CT.CapTable -> + [CT.OcfCreateData] -> + [CT.OcfEditData] -> + [CT.OcfDeleteData] -> + Script CT.UpdateCapTableResult +applyUpdate issuer capTableCid creates edits deletes = + submit issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = creates + edits = edits + deletes = deletes + +assertAdditionalAccelerationRejected : + Party -> + ContractId CT.CapTable -> + Text -> + Decimal -> + Decimal -> + Script () +assertAdditionalAccelerationRejected issuer capTableCid securityId firstQuantity secondQuantity = do + firstResult <- applyUpdate + issuer + capTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-FIRST" securityId firstQuantity)] + [] + [] + submitMustFail issuer do + exerciseCmd firstResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration (acceleration "ACC-SECOND" securityId secondQuantity)] + edits = [] + deletes = [] + +testVestingQuantityIntegrity_InlinePartialExactAndUnknownWarrantAccepted = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let stockIssuance = + (defaultStockIssuance "TX-INLINE-STOCK" "SEC-INLINE-STOCK" "SH-1" "SC_COMMON") with + quantity = 100.0 + vestings = [vestingOn firstVestingDate 40.0, vestingOn secondVestingDate 60.0] + let equityCompensationIssuance = + (defaultEquityCompensationIssuance "TX-INLINE-EC" "SEC-INLINE-EC" "SH-1") with + quantity = 100.0 + vestings = [vestingOn firstVestingDate 25.0] + let knownWarrantIssuance = + (defaultWarrantIssuance "TX-INLINE-WARRANT" "SEC-INLINE-WARRANT" "SH-1") with + quantity = Some 100.0 + vestings = [vestingOn firstVestingDate 100.0] + let unknownWarrantIssuance = + (defaultWarrantIssuance "TX-INLINE-WARRANT-UNKNOWN" "SEC-INLINE-WARRANT-UNKNOWN" "SH-1") with + quantity = None + vestings = [vestingOn firstVestingDate 500.0] + _ <- applyUpdate + issuer + capTableCid + [ CT.OcfCreateStockIssuance stockIssuance + , CT.OcfCreateEquityCompensationIssuance equityCompensationIssuance + , CT.OcfCreateWarrantIssuance knownWarrantIssuance + , CT.OcfCreateWarrantIssuance unknownWarrantIssuance + ] + [] + [] + pure () + +testVestingQuantityIntegrity_StockInlineOversubscriptionRejected = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let issuance = + (defaultStockIssuance "TX-INLINE-STOCK-OVER" "SEC-INLINE-STOCK-OVER" "SH-1" "SC_COMMON") with + quantity = 100.0 + vestings = [vestingOn firstVestingDate 60.0, vestingOn secondVestingDate 50.0] + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateStockIssuance issuance] + edits = [] + deletes = [] + +testVestingQuantityIntegrity_EquityCompensationInlineOversubscriptionRejected = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let issuance = + (defaultEquityCompensationIssuance "TX-INLINE-EC-OVER" "SEC-INLINE-EC-OVER" "SH-1") with + quantity = 100.0 + vestings = [vestingOn firstVestingDate 101.0] + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationIssuance issuance] + edits = [] + deletes = [] + +testVestingQuantityIntegrity_KnownWarrantInlineOversubscriptionRejected = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let issuance = + (defaultWarrantIssuance "TX-INLINE-WARRANT-OVER" "SEC-INLINE-WARRANT-OVER" "SH-1") with + quantity = Some 100.0 + vestings = [vestingOn firstVestingDate 101.0] + submitMustFail issuer do + exerciseCmd capTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateWarrantIssuance issuance] + edits = [] + deletes = [] + +testVestingQuantityIntegrity_InlineOversubscriptionEditRejected = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let issuance = + (defaultStockIssuance "TX-INLINE-EDIT" "SEC-INLINE-EDIT" "SH-1" "SC_COMMON") with + quantity = 100.0 + vestings = [vestingOn firstVestingDate 60.0] + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateStockIssuance issuance] [] [] + let invalidEdit = issuance with + vestings = [vestingOn firstVestingDate 60.0, vestingOn secondVestingDate 50.0] + submitMustFail issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockIssuance invalidEdit] + deletes = [] + +testVestingQuantityIntegrity_AccelerationExactBoundariesAndUnknownWarrantAccepted = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let stockIssuance = + (defaultStockIssuance "TX-ACC-STOCK" "SEC-ACC-STOCK" "SH-1" "SC_COMMON") with + quantity = 100.0 + let equityCompensationIssuance = + (defaultEquityCompensationIssuance "TX-ACC-EC" "SEC-ACC-EC" "SH-1") with + quantity = 100.0 + let knownWarrantIssuance = + (defaultWarrantIssuance "TX-ACC-WARRANT" "SEC-ACC-WARRANT" "SH-1") with + quantity = Some 100.0 + let unknownWarrantIssuance = + (defaultWarrantIssuance "TX-ACC-WARRANT-UNKNOWN" "SEC-ACC-WARRANT-UNKNOWN" "SH-1") with + quantity = None + issuanceResult <- applyUpdate + issuer + capTableCid + [ CT.OcfCreateStockIssuance stockIssuance + , CT.OcfCreateEquityCompensationIssuance equityCompensationIssuance + , CT.OcfCreateWarrantIssuance knownWarrantIssuance + , CT.OcfCreateWarrantIssuance unknownWarrantIssuance + ] + [] + [] + _ <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [ CT.OcfCreateVestingAcceleration (acceleration "ACC-STOCK-1" "SEC-ACC-STOCK" 40.0) + , CT.OcfCreateVestingAcceleration (acceleration "ACC-STOCK-2" "SEC-ACC-STOCK" 60.0) + , CT.OcfCreateVestingAcceleration (acceleration "ACC-EC" "SEC-ACC-EC" 100.0) + , CT.OcfCreateVestingAcceleration (acceleration "ACC-WARRANT-1" "SEC-ACC-WARRANT" 70.0) + , CT.OcfCreateVestingAcceleration (acceleration "ACC-WARRANT-2" "SEC-ACC-WARRANT" 30.0) + , CT.OcfCreateVestingAcceleration (acceleration "ACC-WARRANT-UNKNOWN" "SEC-ACC-WARRANT-UNKNOWN" 1000.0) + ] + [] + [] + pure () + +testVestingQuantityIntegrity_StockAccelerationsNormalizeAcrossForwardSplit = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let issuance = + (defaultStockIssuance "TX-ACC-STOCK-SPLIT" "SEC-ACC-STOCK-SPLIT" "SH-1" "SC_COMMON") with + quantity = 100.0 + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateStockIssuance issuance] [] [] + firstAccelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-STOCK-PRE-SPLIT" issuance.security_id 75.0)] + [] + [] + splitResult <- applyUpdate + issuer + firstAccelerationResult.updatedCapTableCid + [CT.OcfCreateStockClassSplit (stockClassSplitOn "SPLIT-STOCK-ACC" "SC_COMMON" 2.0 1.0 splitDate)] + [] + [] + _ <- applyUpdate + issuer + splitResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration ((acceleration "ACC-STOCK-POST-SPLIT" issuance.security_id 50.0) with + date = postSplitAccelerationDate)] + [] + [] + pure () + +testVestingQuantityIntegrity_EquityCompensationAccelerationsNormalizeAcrossReverseSplit = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let issuance = + (defaultEquityCompensationIssuance "TX-ACC-EC-REVERSE-SPLIT" "SEC-ACC-EC-REVERSE-SPLIT" "SH-1") with + quantity = 100.0 + stock_class_id = Some "SC_COMMON" + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateEquityCompensationIssuance issuance] [] [] + firstAccelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-EC-PRE-REVERSE-SPLIT" issuance.security_id 75.0)] + [] + [] + splitResult <- applyUpdate + issuer + firstAccelerationResult.updatedCapTableCid + [CT.OcfCreateStockClassSplit (stockClassSplitOn "REVERSE-SPLIT-EC-ACC" "SC_COMMON" 1.0 2.0 splitDate)] + [] + [] + submitMustFail issuer do + exerciseCmd splitResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-EC-POST-REVERSE-SPLIT" issuance.security_id 20.0) with + date = postSplitAccelerationDate)] + edits = [] + deletes = [] + +testVestingQuantityIntegrity_StockCumulativeAccelerationOversubscriptionRejected = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let issuance = + (defaultStockIssuance "TX-ACC-STOCK-OVER" "SEC-ACC-STOCK-OVER" "SH-1" "SC_COMMON") with + quantity = 100.0 + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateStockIssuance issuance] [] [] + assertAdditionalAccelerationRejected issuer issuanceResult.updatedCapTableCid issuance.security_id 60.0 50.0 + +testVestingQuantityIntegrity_EquityCompensationCumulativeAccelerationOversubscriptionRejected = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let issuance = + (defaultEquityCompensationIssuance "TX-ACC-EC-OVER" "SEC-ACC-EC-OVER" "SH-1") with + quantity = 100.0 + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateEquityCompensationIssuance issuance] [] [] + assertAdditionalAccelerationRejected issuer issuanceResult.updatedCapTableCid issuance.security_id 60.0 50.0 + +testVestingQuantityIntegrity_KnownWarrantCumulativeAccelerationOversubscriptionRejected = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let issuance = + (defaultWarrantIssuance "TX-ACC-WARRANT-OVER" "SEC-ACC-WARRANT-OVER" "SH-1") with + quantity = Some 100.0 + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateWarrantIssuance issuance] [] [] + assertAdditionalAccelerationRejected issuer issuanceResult.updatedCapTableCid issuance.security_id 60.0 50.0 + +testVestingQuantityIntegrity_AccelerationOversubscriptionEditRejected = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let issuance = + (defaultEquityCompensationIssuance "TX-ACC-EDIT" "SEC-ACC-EDIT" "SH-1") with + quantity = 100.0 + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateEquityCompensationIssuance issuance] [] [] + accelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [ CT.OcfCreateVestingAcceleration (acceleration "ACC-EDIT-A" issuance.security_id 60.0) + , CT.OcfCreateVestingAcceleration (acceleration "ACC-EDIT-B" issuance.security_id 30.0) + ] + [] + [] + submitMustFail issuer do + exerciseCmd accelerationResult.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditVestingAcceleration (acceleration "ACC-EDIT-A" issuance.security_id 80.0)] + deletes = [] + +testVestingQuantityIntegrity_LoweringKnownQuantityBelowAccelerationsRejected = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let issuance = + (defaultWarrantIssuance "TX-ACC-LOWER" "SEC-ACC-LOWER" "SH-1") with + quantity = Some 100.0 + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateWarrantIssuance issuance] [] [] + accelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-LOWER" issuance.security_id 80.0)] + [] + [] + submitMustFail issuer do + exerciseCmd accelerationResult.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditWarrantIssuance (issuance with quantity = Some 70.0)] + deletes = [] + +testVestingQuantityIntegrity_UnknownWarrantBecomingKnownBelowAccelerationsRejected = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let issuance = + (defaultWarrantIssuance "TX-ACC-UNKNOWN-TO-KNOWN" "SEC-ACC-UNKNOWN-TO-KNOWN" "SH-1") with + quantity = None + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateWarrantIssuance issuance] [] [] + accelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-UNKNOWN-TO-KNOWN" issuance.security_id 1000.0)] + [] + [] + submitMustFail issuer do + exerciseCmd accelerationResult.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditWarrantIssuance (issuance with quantity = Some 100.0)] + deletes = [] + +testVestingQuantityIntegrity_DeletingAccelerationReleasesCapacity = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let issuance = + (defaultEquityCompensationIssuance "TX-ACC-DELETE" "SEC-ACC-DELETE" "SH-1") with + quantity = 100.0 + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateEquityCompensationIssuance issuance] [] [] + accelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-DELETE-OLD" issuance.security_id 80.0)] + [] + [] + submitMustFail issuer do + exerciseCmd accelerationResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration (acceleration "ACC-DELETE-OVER" issuance.security_id 30.0)] + edits = [] + deletes = [] + _ <- applyUpdate + issuer + accelerationResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-DELETE-REPLACEMENT" issuance.security_id 100.0)] + [] + [CT.OcfDeleteVestingAcceleration "ACC-DELETE-OLD"] + pure () diff --git a/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar new file mode 100644 index 00000000..920f14a2 --- /dev/null +++ b/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a4a7d2904e7941e42f541af12da81d8582c8ce0afb5f6b7bcbe432f602d8fc3 +size 3341411 diff --git a/dars/dars.lock b/dars/dars.lock index 6a5b4a37..04dd0c6d 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -60,6 +60,13 @@ "uploadedAt": "2026-07-11T17:31:18.000Z", "networks": [] }, + "OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar": { + "sha256": "0a4a7d2904e7941e42f541af12da81d8582c8ce0afb5f6b7bcbe432f602d8fc3", + "size": 3341411, + "sdkVersion": "3.4.10", + "uploadedAt": "2026-07-11T18:15:35.516Z", + "networks": [] + }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { "sha256": "2ce462b8afcb1bcdbe351c56b0a47de478477f3c655da00e41cb397f63e997d6", "size": 2446238, diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 474ea856..fe0d1dc3 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -22,7 +22,7 @@ import DA.Optional (fromOptional, isNone, isSome) import Fairmint.OpenCapTable.Types.Core (Context) import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfCapitalizationDefinition, OcfConversionMechanism(..), OcfConversionTrigger(..), OcfConversionTriggerType(..), OcfConvertibleConversionMechanism(..), OcfConvertibleConversionRight(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..), validateOcfInterestRateSchedule) import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..), OcfShareNumberRange(..), shareNumberRangeCoveredByRanges, shareNumberRangesOverlap) -import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..), OcfVesting(..)) +import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..), OcfVesting(..), validateOcfVestingsWithinQuantity) import qualified Fairmint.OpenCapTable.OCF.ConvertibleIssuance as ConvertibleIssuance (OcfConvertibleConversionTrigger(..)) import Fairmint.OpenCapTable.OCF.Issuer (Issuer(..), IssuerOcfData) import Fairmint.OpenCapTable.OCF.Document (Document(..), DocumentOcfData, OcfObjectReference(..), OcfObjectType(..)) @@ -4155,6 +4155,107 @@ validateVestingLifecycleDates maps = do validateVestingSourceTransactionDate maps ("Vesting acceleration " <> acceleration.acceleration_data.id) acceleration.acceleration_data.date acceleration.acceleration_data.security_id) (Map.values maps.vesting_accelerations) +validateInlineVestingQuantities : CapTableMaps -> Update () +validateInlineVestingQuantities maps = do + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + assertMsg + ("Stock issuance " <> issuance.issuance_data.id <> " inline vestings exceed security quantity") + (validateOcfVestingsWithinQuantity issuance.issuance_data.quantity issuance.issuance_data.vestings)) + (Map.values maps.stock_issuances) + + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + assertMsg + ("Equity compensation issuance " <> issuance.issuance_data.id <> " inline vestings exceed security quantity") + (validateOcfVestingsWithinQuantity issuance.issuance_data.quantity issuance.issuance_data.vestings)) + (Map.values maps.equity_compensation_issuances) + + mapA_ + (\issuanceCid -> do + issuance <- fetch issuanceCid + case issuance.issuance_data.quantity of + Some quantity -> + assertMsg + ("Warrant issuance " <> issuance.issuance_data.id <> " inline vestings exceed security quantity") + (validateOcfVestingsWithinQuantity quantity issuance.issuance_data.vestings) + None -> pure ()) + (Map.values maps.warrant_issuances) + +vestingAccelerationQuantityAtIssuance : + CapTableMaps -> + VestingAccelerationOcfData -> + Update Decimal +vestingAccelerationQuantityAtIssuance maps accelerationData = + case Map.lookup accelerationData.security_id maps.stock_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + stockClassQuantityAtSortKey + maps + issuance.issuance_data.stock_class_id + accelerationData.date + accelerationData.quantity + (issuance.issuance_data.date, 0, 0) + None -> case Map.lookup accelerationData.security_id maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + case issuance.issuance_data.stock_class_id of + Some stockClassId -> + stockClassQuantityAtSortKey + maps + stockClassId + accelerationData.date + accelerationData.quantity + (issuance.issuance_data.date, 0, 0) + None -> pure accelerationData.quantity + None -> pure accelerationData.quantity + +vestingAccelerationTotals : CapTableMaps -> Update (Map Text Decimal) +vestingAccelerationTotals maps = + foldlA + (\totals accelerationCid -> do + acceleration <- fetch accelerationCid + normalizedQuantity <- vestingAccelerationQuantityAtIssuance maps acceleration.acceleration_data + pure (addQuantityById acceleration.acceleration_data.security_id normalizedQuantity totals)) + Map.empty + (Map.values maps.vesting_accelerations) + +validateVestingAccelerationTotal : CapTableMaps -> (Text, Decimal) -> Update () +validateVestingAccelerationTotal maps (securityId, acceleratedQuantity) = + case Map.lookup securityId maps.stock_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertMsg + ("Vesting accelerations exceed stock security quantity for " <> securityId) + (acceleratedQuantity <= issuance.issuance_data.quantity) + None -> case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertMsg + ("Vesting accelerations exceed equity compensation security quantity for " <> securityId) + (acceleratedQuantity <= issuance.issuance_data.quantity) + None -> case Map.lookup securityId maps.warrant_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + case issuance.issuance_data.quantity of + Some quantity -> + assertMsg + ("Vesting accelerations exceed warrant security quantity for " <> securityId) + (acceleratedQuantity <= quantity) + None -> pure () + None -> + assertMsg + ("Vesting accelerations reference a missing or unsupported security: " <> securityId) + False + +validateVestingQuantityIntegrity : CapTableMaps -> Update () +validateVestingQuantityIntegrity maps = do + validateInlineVestingQuantities maps + accelerationTotals <- vestingAccelerationTotals maps + mapA_ (validateVestingAccelerationTotal maps) (Map.toList accelerationTotals) + validateTransactionLifecycleDates : CapTableMaps -> Update () validateTransactionLifecycleDates maps = do validateStockLifecycleDates maps @@ -7435,6 +7536,7 @@ template CapTable validateConfiguredReferences finalMaps validateFinalSecurityIdUniqueness finalMaps validateSuccessorSecurityIdContinuity finalMaps + validateVestingQuantityIntegrity finalMaps validateTransactionLifecycleDates finalMaps validateInstrumentTemporalEligibility finalMaps validateStockBalanceSuccessorQuantities stockEventIndex finalMaps From a829ae29e6441ced9bfe41721513ac9c82aba984 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 15:02:01 -0400 Subject: [PATCH 02/10] fix: validate accelerations against live balances --- .../OpenCapTable/OCF/WarrantIssuance.daml | 4 +- .../Fairmint/OpenCapTable/Types/Stock.daml | 7 + .../TestVestingQuantityIntegrity.daml | 191 +++++++++++++++- .../0.0.17/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- .../codegen/templates/CapTable.daml.template | 215 +++++++++++++----- 6 files changed, 363 insertions(+), 64 deletions(-) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantIssuance.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantIssuance.daml index 94162032..60327c0b 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantIssuance.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/OCF/WarrantIssuance.daml @@ -4,7 +4,7 @@ module Fairmint.OpenCapTable.OCF.WarrantIssuance where import Fairmint.OpenCapTable.Types.Core (Context) import Fairmint.OpenCapTable.Types.Validation (validateNonEmptyStringsArray) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary, validateOcfMonetary) -import Fairmint.OpenCapTable.Types.Stock (OcfQuantitySourceType, OcfSecurityExemption, validateOcfSecurityExemption) +import Fairmint.OpenCapTable.Types.Stock (OcfQuantitySourceType, OcfSecurityExemption, ocfQuantityUpperBound, validateOcfSecurityExemption) import Fairmint.OpenCapTable.Types.Vesting (OcfVesting, validateOcfVesting, validateOcfVestingsWithinQuantity) import Fairmint.OpenCapTable.Types.Conversion (OcfConversionTrigger, validateOcfConversionTrigger) import Fairmint.Shared.TypeHelpers (validateOptionalText) @@ -103,6 +103,6 @@ validateOcfWarrantIssuanceDataTx d = (case d.vestings of [] -> True xs -> all validateOcfVesting xs) && - (case d.quantity of + (case ocfQuantityUpperBound d.quantity d.quantity_source of Some quantity -> validateOcfVestingsWithinQuantity quantity d.vestings None -> True) diff --git a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Stock.daml b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Stock.daml index 98ee9e65..f3d36417 100644 --- a/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Stock.daml +++ b/OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Stock.daml @@ -152,6 +152,13 @@ data OcfQuantitySourceType = | OcfQuantityInstrumentMin deriving (Eq, Show) +ocfQuantityUpperBound : Optional Decimal -> Optional OcfQuantitySourceType -> Optional Decimal +ocfQuantityUpperBound quantity quantitySource = + case quantitySource of + Some OcfQuantityInstrumentFixed -> quantity + Some OcfQuantityInstrumentMax -> quantity + _ -> None + -- Enum - File Type -- File types within an OCF manifest -- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/enums/FileType.schema.json diff --git a/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml b/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml index 1c0a5523..b757e7d0 100644 --- a/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml +++ b/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml @@ -1,6 +1,8 @@ module OpenCapTable.TestVestingQuantityIntegrity where import Fairmint.OpenCapTable.OCF.VestingAcceleration (VestingAccelerationOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockReissuance (StockReissuanceOcfData(..)) +import Fairmint.OpenCapTable.Types.Stock (OcfQuantitySourceType(..)) import Fairmint.OpenCapTable.Types.Vesting (OcfVesting(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup @@ -8,8 +10,11 @@ import OpenCapTable.TestHelpers ( addPrerequisites , defaultEquityCompensationIssuance , defaultStockIssuance + , defaultStockPlan , defaultWarrantIssuance + , stockCancellationOn , stockClassSplitOn + , stockSuccessorIssuance ) import qualified DA.Date as DA import DA.Date (Month(..)) @@ -93,6 +98,7 @@ testVestingQuantityIntegrity_InlinePartialExactAndUnknownWarrantAccepted = scrip let knownWarrantIssuance = (defaultWarrantIssuance "TX-INLINE-WARRANT" "SEC-INLINE-WARRANT" "SH-1") with quantity = Some 100.0 + quantity_source = Some OcfQuantityInstrumentFixed vestings = [vestingOn firstVestingDate 100.0] let unknownWarrantIssuance = (defaultWarrantIssuance "TX-INLINE-WARRANT-UNKNOWN" "SEC-INLINE-WARRANT-UNKNOWN" "SH-1") with @@ -142,6 +148,7 @@ testVestingQuantityIntegrity_KnownWarrantInlineOversubscriptionRejected = script let issuance = (defaultWarrantIssuance "TX-INLINE-WARRANT-OVER" "SEC-INLINE-WARRANT-OVER" "SH-1") with quantity = Some 100.0 + quantity_source = Some OcfQuantityInstrumentMax vestings = [vestingOn firstVestingDate 101.0] submitMustFail issuer do exerciseCmd capTableCid CT.UpdateCapTable with @@ -149,6 +156,58 @@ testVestingQuantityIntegrity_KnownWarrantInlineOversubscriptionRejected = script edits = [] deletes = [] +testVestingQuantityIntegrity_NonCeilingWarrantSourcesDoNotCapVestingsOrAccelerations = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let minimumWarrant = + (defaultWarrantIssuance "TX-WARRANT-MINIMUM" "SEC-WARRANT-MINIMUM" "SH-1") with + quantity = Some 100.0 + quantity_source = Some OcfQuantityInstrumentMin + vestings = [vestingOn firstVestingDate 150.0] + let humanEstimateWarrant = + (defaultWarrantIssuance "TX-WARRANT-HUMAN-ESTIMATE" "SEC-WARRANT-HUMAN-ESTIMATE" "SH-1") with + quantity = Some 100.0 + quantity_source = Some OcfQuantityHumanEstimated + vestings = [vestingOn firstVestingDate 150.0] + let machineEstimateWarrant = + (defaultWarrantIssuance "TX-WARRANT-MACHINE-ESTIMATE" "SEC-WARRANT-MACHINE-ESTIMATE" "SH-1") with + quantity = Some 100.0 + quantity_source = Some OcfQuantityMachineEstimated + vestings = [vestingOn firstVestingDate 150.0] + let explicitUnspecifiedWarrant = + (defaultWarrantIssuance "TX-WARRANT-UNSPECIFIED" "SEC-WARRANT-UNSPECIFIED" "SH-1") with + quantity = Some 100.0 + quantity_source = Some OcfQuantityUnspecified + vestings = [vestingOn firstVestingDate 150.0] + let defaultUnspecifiedWarrant = + (defaultWarrantIssuance "TX-WARRANT-DEFAULT-UNSPECIFIED" "SEC-WARRANT-DEFAULT-UNSPECIFIED" "SH-1") with + quantity = Some 100.0 + quantity_source = None + vestings = [vestingOn firstVestingDate 150.0] + issuanceResult <- applyUpdate + issuer + capTableCid + [ CT.OcfCreateWarrantIssuance minimumWarrant + , CT.OcfCreateWarrantIssuance humanEstimateWarrant + , CT.OcfCreateWarrantIssuance machineEstimateWarrant + , CT.OcfCreateWarrantIssuance explicitUnspecifiedWarrant + , CT.OcfCreateWarrantIssuance defaultUnspecifiedWarrant + ] + [] + [] + _ <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [ CT.OcfCreateVestingAcceleration (acceleration "ACC-WARRANT-MINIMUM" minimumWarrant.security_id 200.0) + , CT.OcfCreateVestingAcceleration (acceleration "ACC-WARRANT-HUMAN-ESTIMATE" humanEstimateWarrant.security_id 200.0) + , CT.OcfCreateVestingAcceleration (acceleration "ACC-WARRANT-MACHINE-ESTIMATE" machineEstimateWarrant.security_id 200.0) + , CT.OcfCreateVestingAcceleration (acceleration "ACC-WARRANT-UNSPECIFIED" explicitUnspecifiedWarrant.security_id 200.0) + , CT.OcfCreateVestingAcceleration (acceleration "ACC-WARRANT-DEFAULT-UNSPECIFIED" defaultUnspecifiedWarrant.security_id 200.0) + ] + [] + [] + pure () + testVestingQuantityIntegrity_InlineOversubscriptionEditRejected = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableCid <- addPrerequisites issuer cap_table @@ -177,6 +236,7 @@ testVestingQuantityIntegrity_AccelerationExactBoundariesAndUnknownWarrantAccepte let knownWarrantIssuance = (defaultWarrantIssuance "TX-ACC-WARRANT" "SEC-ACC-WARRANT" "SH-1") with quantity = Some 100.0 + quantity_source = Some OcfQuantityInstrumentFixed let unknownWarrantIssuance = (defaultWarrantIssuance "TX-ACC-WARRANT-UNKNOWN" "SEC-ACC-WARRANT-UNKNOWN" "SH-1") with quantity = None @@ -232,14 +292,59 @@ testVestingQuantityIntegrity_StockAccelerationsNormalizeAcrossForwardSplit = scr [] pure () +testVestingQuantityIntegrity_PlanLinkedEquityCompensationAccelerationsNormalizeAcrossForwardSplit = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let stockPlan = defaultStockPlan "PLAN-ACC-EC-FORWARD-SPLIT" "SC_COMMON" + let issuance = + (defaultEquityCompensationIssuance "TX-ACC-EC-FORWARD-SPLIT" "SEC-ACC-EC-FORWARD-SPLIT" "SH-1") with + quantity = 100.0 + stock_plan_id = Some stockPlan.id + issuanceResult <- applyUpdate + issuer + capTableCid + [ CT.OcfCreateStockPlan stockPlan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + [] + [] + firstAccelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-EC-PLAN-PRE-SPLIT" issuance.security_id 75.0)] + [] + [] + splitResult <- applyUpdate + issuer + firstAccelerationResult.updatedCapTableCid + [CT.OcfCreateStockClassSplit (stockClassSplitOn "SPLIT-EC-PLAN-ACC" "SC_COMMON" 2.0 1.0 splitDate)] + [] + [] + _ <- applyUpdate + issuer + splitResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration ((acceleration "ACC-EC-PLAN-POST-SPLIT" issuance.security_id 50.0) with + date = postSplitAccelerationDate)] + [] + [] + pure () + testVestingQuantityIntegrity_EquityCompensationAccelerationsNormalizeAcrossReverseSplit = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableCid <- addPrerequisites issuer cap_table + let stockPlan = defaultStockPlan "PLAN-ACC-EC-REVERSE-SPLIT" "SC_COMMON" let issuance = (defaultEquityCompensationIssuance "TX-ACC-EC-REVERSE-SPLIT" "SEC-ACC-EC-REVERSE-SPLIT" "SH-1") with quantity = 100.0 - stock_class_id = Some "SC_COMMON" - issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateEquityCompensationIssuance issuance] [] [] + stock_plan_id = Some stockPlan.id + issuanceResult <- applyUpdate + issuer + capTableCid + [ CT.OcfCreateStockPlan stockPlan + , CT.OcfCreateEquityCompensationIssuance issuance + ] + [] + [] firstAccelerationResult <- applyUpdate issuer issuanceResult.updatedCapTableCid @@ -259,6 +364,82 @@ testVestingQuantityIntegrity_EquityCompensationAccelerationsNormalizeAcrossRever edits = [] deletes = [] +testVestingQuantityIntegrity_SameDaySplitSuccessorUsesPostSplitAccelerationUnits = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let sourceSecurityId = "SEC-ACC-SAME-DAY-SPLIT-SOURCE" + successorSecurityId = "SEC-ACC-SAME-DAY-SPLIT-SUCCESSOR" + splitId = "SPLIT-ACC-SAME-DAY" + reissuanceId = "REISSUE-ACC-SAME-DAY" + sourceIssuance = + (defaultStockIssuance "TX-ACC-SAME-DAY-SPLIT-SOURCE" sourceSecurityId "SH-1" "SC_COMMON") with + quantity = 25.0 + reissuance = StockReissuanceOcfData with + id = reissuanceId + date = splitDate + security_id = sourceSecurityId + resulting_security_ids = [successorSecurityId] + split_transaction_id = Some splitId + reason_text = None + comments = [] + successorResult <- applyUpdate + issuer + capTableCid + [ CT.OcfCreateStockIssuance sourceIssuance + , CT.OcfCreateStockClassSplit (stockClassSplitOn splitId "SC_COMMON" 2.0 1.0 splitDate) + , stockSuccessorIssuance "TX-ACC-SAME-DAY-SPLIT-SUCCESSOR" successorSecurityId "SH-1" "SC_COMMON" splitDate 50.0 + , CT.OcfCreateStockReissuance reissuance + ] + [] + [] + submitMustFail issuer do + exerciseCmd successorResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-SAME-DAY-SPLIT-SUCCESSOR" successorSecurityId 75.0) with + date = postSplitAccelerationDate)] + edits = [] + deletes = [] + +testVestingQuantityIntegrity_AccelerationCannotExceedLiveStockBalance = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let issuance = + (defaultStockIssuance "TX-ACC-LIVE-BALANCE" "SEC-ACC-LIVE-BALANCE" "SH-1" "SC_COMMON") with + quantity = 100.0 + balanceResult <- applyUpdate + issuer + capTableCid + [ CT.OcfCreateStockIssuance issuance + , CT.OcfCreateStockCancellation (stockCancellationOn "CANCEL-ACC-LIVE-BALANCE" issuance.security_id 80.0 firstVestingDate) + ] + [] + [] + submitMustFail issuer do + exerciseCmd balanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration (acceleration "ACC-LIVE-BALANCE" issuance.security_id 50.0)] + edits = [] + deletes = [] + +testVestingQuantityIntegrity_LaterBalanceReductionDoesNotRetroactivelyInvalidateAcceleration = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let issuance = + (defaultStockIssuance "TX-ACC-BEFORE-REDUCTION" "SEC-ACC-BEFORE-REDUCTION" "SH-1" "SC_COMMON") with + quantity = 100.0 + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateStockIssuance issuance] [] [] + accelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-BEFORE-REDUCTION" issuance.security_id 60.0)] + [] + [] + _ <- applyUpdate + issuer + accelerationResult.updatedCapTableCid + [CT.OcfCreateStockCancellation (stockCancellationOn "CANCEL-AFTER-ACCELERATION" issuance.security_id 50.0 secondVestingDate)] + [] + [] + pure () + testVestingQuantityIntegrity_StockCumulativeAccelerationOversubscriptionRejected = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableCid <- addPrerequisites issuer cap_table @@ -283,6 +464,7 @@ testVestingQuantityIntegrity_KnownWarrantCumulativeAccelerationOversubscriptionR let issuance = (defaultWarrantIssuance "TX-ACC-WARRANT-OVER" "SEC-ACC-WARRANT-OVER" "SH-1") with quantity = Some 100.0 + quantity_source = Some OcfQuantityInstrumentFixed issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateWarrantIssuance issuance] [] [] assertAdditionalAccelerationRejected issuer issuanceResult.updatedCapTableCid issuance.security_id 60.0 50.0 @@ -313,6 +495,7 @@ testVestingQuantityIntegrity_LoweringKnownQuantityBelowAccelerationsRejected = s let issuance = (defaultWarrantIssuance "TX-ACC-LOWER" "SEC-ACC-LOWER" "SH-1") with quantity = Some 100.0 + quantity_source = Some OcfQuantityInstrumentFixed issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateWarrantIssuance issuance] [] [] accelerationResult <- applyUpdate issuer @@ -342,7 +525,9 @@ testVestingQuantityIntegrity_UnknownWarrantBecomingKnownBelowAccelerationsReject submitMustFail issuer do exerciseCmd accelerationResult.updatedCapTableCid CT.UpdateCapTable with creates = [] - edits = [CT.OcfEditWarrantIssuance (issuance with quantity = Some 100.0)] + edits = [CT.OcfEditWarrantIssuance (issuance with + quantity = Some 100.0 + quantity_source = Some OcfQuantityInstrumentFixed)] deletes = [] testVestingQuantityIntegrity_DeletingAccelerationReleasesCapacity = script do diff --git a/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar index 920f14a2..6a5277e0 100644 --- a/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a4a7d2904e7941e42f541af12da81d8582c8ce0afb5f6b7bcbe432f602d8fc3 -size 3341411 +oid sha256:b9c089dcfece6697d48f6bb61e187688e7def3b37b495eea601b237af18bcd43 +size 3351437 diff --git a/dars/dars.lock b/dars/dars.lock index 04dd0c6d..fcd15bff 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -61,10 +61,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar": { - "sha256": "0a4a7d2904e7941e42f541af12da81d8582c8ce0afb5f6b7bcbe432f602d8fc3", - "size": 3341411, + "sha256": "b9c089dcfece6697d48f6bb61e187688e7def3b37b495eea601b237af18bcd43", + "size": 3351437, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T18:15:35.516Z", + "uploadedAt": "2026-07-11T18:59:11.000Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index fe0d1dc3..ec1e16fc 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -21,7 +21,7 @@ import DA.Optional (fromOptional, isNone, isSome) import Fairmint.OpenCapTable.Types.Core (Context) import Fairmint.OpenCapTable.Types.Conversion (OcfAnyConversionRight(..), OcfCapitalizationDefinition, OcfConversionMechanism(..), OcfConversionTrigger(..), OcfConversionTriggerType(..), OcfConvertibleConversionMechanism(..), OcfConvertibleConversionRight(..), OcfStockClassConversionRight(..), OcfWarrantConversionMechanism(..), OcfWarrantConversionRight(..), validateOcfInterestRateSchedule) -import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..), OcfShareNumberRange(..), shareNumberRangeCoveredByRanges, shareNumberRangesOverlap) +import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..), OcfRatio(..), OcfShareNumberRange(..), ocfQuantityUpperBound, shareNumberRangeCoveredByRanges, shareNumberRangesOverlap) import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..), OcfVesting(..), validateOcfVestingsWithinQuantity) import qualified Fairmint.OpenCapTable.OCF.ConvertibleIssuance as ConvertibleIssuance (OcfConvertibleConversionTrigger(..)) import Fairmint.OpenCapTable.OCF.Issuer (Issuer(..), IssuerOcfData) @@ -4176,7 +4176,7 @@ validateInlineVestingQuantities maps = do mapA_ (\issuanceCid -> do issuance <- fetch issuanceCid - case issuance.issuance_data.quantity of + case ocfQuantityUpperBound issuance.issuance_data.quantity issuance.issuance_data.quantity_source of Some quantity -> assertMsg ("Warrant issuance " <> issuance.issuance_data.id <> " inline vestings exceed security quantity") @@ -4184,77 +4184,184 @@ validateInlineVestingQuantities maps = do None -> pure ()) (Map.values maps.warrant_issuances) -vestingAccelerationQuantityAtIssuance : - CapTableMaps -> - VestingAccelerationOcfData -> - Update Decimal -vestingAccelerationQuantityAtIssuance maps accelerationData = - case Map.lookup accelerationData.security_id maps.stock_issuances_by_security_id of - Some issuanceCid -> do - issuance <- fetch issuanceCid - stockClassQuantityAtSortKey - maps - issuance.issuance_data.stock_class_id - accelerationData.date - accelerationData.quantity - (issuance.issuance_data.date, 0, 0) - None -> case Map.lookup accelerationData.security_id maps.equity_compensation_issuances_by_security_id of - Some issuanceCid -> do - issuance <- fetch issuanceCid - case issuance.issuance_data.stock_class_id of - Some stockClassId -> - stockClassQuantityAtSortKey - maps - stockClassId - accelerationData.date - accelerationData.quantity - (issuance.issuance_data.date, 0, 0) - None -> pure accelerationData.quantity - None -> pure accelerationData.quantity - -vestingAccelerationTotals : CapTableMaps -> Update (Map Text Decimal) -vestingAccelerationTotals maps = +vestingAccelerationsBySecurityId : CapTableMaps -> Update (Map Text [VestingAccelerationOcfData]) +vestingAccelerationsBySecurityId maps = foldlA - (\totals accelerationCid -> do + (\index accelerationCid -> do acceleration <- fetch accelerationCid - normalizedQuantity <- vestingAccelerationQuantityAtIssuance maps acceleration.acceleration_data - pure (addQuantityById acceleration.acceleration_data.security_id normalizedQuantity totals)) + let securityId = acceleration.acceleration_data.security_id + pure (Map.insert + securityId + (acceleration.acceleration_data :: fromOptional [] (Map.lookup securityId index)) + index)) Map.empty (Map.values maps.vesting_accelerations) -validateVestingAccelerationTotal : CapTableMaps -> (Text, Decimal) -> Update () -validateVestingAccelerationTotal maps (securityId, acceleratedQuantity) = +vestingAccelerationTotalAt : Time -> [VestingAccelerationOcfData] -> Decimal +vestingAccelerationTotalAt latest = + foldl + (\total accelerationData -> + if accelerationData.date <= latest + then total + accelerationData.quantity + else total) + 0.0 + +-- Accelerations are evaluated after every applicable same-day security quantity event. +vestingAccelerationEvaluationSortKey : Time -> (Time, Int, Int) +vestingAccelerationEvaluationSortKey date = (date, 4, 0) + +stockVestingAccelerationTotalAt : + CapTableMaps -> + Text -> + (Time, Int, Int) -> + [VestingAccelerationOcfData] -> + Update Decimal +stockVestingAccelerationTotalAt maps stockClassId targetSortKey accelerations = + let (targetDate, _, _) = targetSortKey + in foldlA + (\total accelerationData -> + if accelerationData.date <= targetDate + then do + adjustedQuantity <- stockClassQuantityAtSortKey + maps + stockClassId + accelerationData.date + accelerationData.quantity + targetSortKey + pure (total + adjustedQuantity) + else pure total) + 0.0 + accelerations + +validateStockVestingAccelerationQuantities : + StockClassShareEventIndex -> + CapTableMaps -> + StockIssuanceOcfData -> + [VestingAccelerationOcfData] -> + Update () +validateStockVestingAccelerationQuantities eventIndex maps issuanceData accelerations = + mapA_ + (\accelerationData -> do + let targetSortKey = vestingAccelerationEvaluationSortKey accelerationData.date + liveQuantity <- stockSecurityQuantityBeforeEvent eventIndex maps issuanceData targetSortKey + acceleratedQuantity <- stockVestingAccelerationTotalAt + maps + issuanceData.stock_class_id + targetSortKey + accelerations + assertMsg + ("Vesting accelerations exceed live stock security quantity for " <> issuanceData.security_id <> " at " <> show accelerationData.date) + (acceleratedQuantity <= liveQuantity)) + accelerations + +validateEquityCompensationVestingAccelerationQuantities : + StockClassShareEventIndex -> + NonStockSecurityBalanceEventIndex -> + CapTableMaps -> + EquityCompensationIssuanceOcfData -> + [VestingAccelerationOcfData] -> + Update () +validateEquityCompensationVestingAccelerationQuantities stockEventIndex balanceEventIndex maps issuanceData accelerations = do + stockClassIds <- equityCompensationPlanStockClassIds maps issuanceData + quantityEvents <- equityCompensationSecurityQuantityEvents stockEventIndex balanceEventIndex maps issuanceData + mapA_ + (\accelerationData -> do + let targetSortKey = vestingAccelerationEvaluationSortKey accelerationData.date + liveQuantityOpt <- securityQuantityBeforeEvent + issuanceData.security_id + (Some 0.0) + ((SecurityQuantityDelta with + event_date = issuanceData.date + event_priority = 0 + quantity_delta = issuanceData.quantity) + :: quantityEvents) + targetSortKey + liveQuantity <- case liveQuantityOpt of + Some quantity -> pure quantity + None -> do + assertMsg ("Equity compensation quantity unexpectedly became unknown: " <> issuanceData.security_id) False + pure 0.0 + acceleratedQuantity <- case stockClassIds of + [stockClassId] -> stockVestingAccelerationTotalAt maps stockClassId targetSortKey accelerations + _ -> pure (vestingAccelerationTotalAt accelerationData.date accelerations) + assertMsg + ("Vesting accelerations exceed live equity compensation security quantity for " <> issuanceData.security_id <> " at " <> show accelerationData.date) + (acceleratedQuantity <= liveQuantity)) + accelerations + +validateWarrantVestingAccelerationQuantities : + NonStockSecurityBalanceEventIndex -> + WarrantIssuanceOcfData -> + [VestingAccelerationOcfData] -> + Update () +validateWarrantVestingAccelerationQuantities eventIndex issuanceData accelerations = + case ocfQuantityUpperBound issuanceData.quantity issuanceData.quantity_source of + Some quantity -> do + let quantityEvents = + (SecurityQuantityDelta with + event_date = issuanceData.date + event_priority = 0 + quantity_delta = quantity) + :: warrantSecurityQuantityEvents eventIndex issuanceData + mapA_ + (\accelerationData -> do + liveQuantityOpt <- securityQuantityBeforeEvent + issuanceData.security_id + (Some 0.0) + quantityEvents + (vestingAccelerationEvaluationSortKey accelerationData.date) + liveQuantity <- case liveQuantityOpt of + Some currentQuantity -> pure currentQuantity + None -> do + assertMsg ("Warrant quantity unexpectedly became unknown: " <> issuanceData.security_id) False + pure 0.0 + let acceleratedQuantity = vestingAccelerationTotalAt accelerationData.date accelerations + assertMsg + ("Vesting accelerations exceed live warrant security quantity for " <> issuanceData.security_id <> " at " <> show accelerationData.date) + (acceleratedQuantity <= liveQuantity)) + accelerations + None -> pure () + +validateVestingAccelerationQuantities : + StockClassShareEventIndex -> + NonStockSecurityBalanceEventIndex -> + CapTableMaps -> + (Text, [VestingAccelerationOcfData]) -> + Update () +validateVestingAccelerationQuantities stockEventIndex balanceEventIndex maps (securityId, accelerations) = case Map.lookup securityId maps.stock_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid - assertMsg - ("Vesting accelerations exceed stock security quantity for " <> securityId) - (acceleratedQuantity <= issuance.issuance_data.quantity) + validateStockVestingAccelerationQuantities stockEventIndex maps issuance.issuance_data accelerations None -> case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid - assertMsg - ("Vesting accelerations exceed equity compensation security quantity for " <> securityId) - (acceleratedQuantity <= issuance.issuance_data.quantity) + validateEquityCompensationVestingAccelerationQuantities + stockEventIndex + balanceEventIndex + maps + issuance.issuance_data + accelerations None -> case Map.lookup securityId maps.warrant_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid - case issuance.issuance_data.quantity of - Some quantity -> - assertMsg - ("Vesting accelerations exceed warrant security quantity for " <> securityId) - (acceleratedQuantity <= quantity) - None -> pure () + validateWarrantVestingAccelerationQuantities balanceEventIndex issuance.issuance_data accelerations None -> assertMsg ("Vesting accelerations reference a missing or unsupported security: " <> securityId) False -validateVestingQuantityIntegrity : CapTableMaps -> Update () -validateVestingQuantityIntegrity maps = do +validateVestingQuantityIntegrity : + StockClassShareEventIndex -> + NonStockSecurityBalanceEventIndex -> + CapTableMaps -> + Update () +validateVestingQuantityIntegrity stockEventIndex balanceEventIndex maps = do validateInlineVestingQuantities maps - accelerationTotals <- vestingAccelerationTotals maps - mapA_ (validateVestingAccelerationTotal maps) (Map.toList accelerationTotals) + accelerationIndex <- vestingAccelerationsBySecurityId maps + mapA_ + (validateVestingAccelerationQuantities stockEventIndex balanceEventIndex maps) + (Map.toList accelerationIndex) validateTransactionLifecycleDates : CapTableMaps -> Update () validateTransactionLifecycleDates maps = do @@ -7536,7 +7643,7 @@ template CapTable validateConfiguredReferences finalMaps validateFinalSecurityIdUniqueness finalMaps validateSuccessorSecurityIdContinuity finalMaps - validateVestingQuantityIntegrity finalMaps + validateVestingQuantityIntegrity stockEventIndex nonStockBalanceEventIndex finalMaps validateTransactionLifecycleDates finalMaps validateInstrumentTemporalEligibility finalMaps validateStockBalanceSuccessorQuantities stockEventIndex finalMaps From 18cee719b6d4872bbed22ef877adf4a2652fd164 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 15:28:47 -0400 Subject: [PATCH 03/10] fix: preserve same-day acceleration units --- .../TestVestingQuantityIntegrity.daml | 41 ++++++++++++++++++- .../0.0.17/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 26 ++++++++---- 4 files changed, 61 insertions(+), 14 deletions(-) diff --git a/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml b/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml index b757e7d0..2e5376b1 100644 --- a/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml +++ b/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml @@ -392,9 +392,46 @@ testVestingQuantityIntegrity_SameDaySplitSuccessorUsesPostSplitAccelerationUnits ] [] [] + sameDayAccelerationResult <- applyUpdate + issuer + successorResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration ((acceleration "ACC-SAME-DAY-SPLIT-SUCCESSOR-FIRST" successorSecurityId 25.0) with + date = splitDate)] + [] + [] + exactResult <- applyUpdate + issuer + sameDayAccelerationResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration ((acceleration "ACC-SAME-DAY-SPLIT-SUCCESSOR-SECOND" successorSecurityId 25.0) with + date = postSplitAccelerationDate)] + [] + [] + submitMustFail issuer do + exerciseCmd exactResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-SAME-DAY-SPLIT-SUCCESSOR-OVER" successorSecurityId 1.0) with + date = postSplitAccelerationDate)] + edits = [] + deletes = [] + +testVestingQuantityIntegrity_SameDayReverseSplitAccelerationCannotBeUndercounted = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let issuance = + (defaultStockIssuance "TX-ACC-SAME-DAY-REVERSE-SPLIT" "SEC-ACC-SAME-DAY-REVERSE-SPLIT" "SH-1" "SC_COMMON") with + quantity = 100.0 + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateStockIssuance issuance] [] [] + sameDayAccelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [ CT.OcfCreateStockClassSplit (stockClassSplitOn "REVERSE-SPLIT-ACC-SAME-DAY" "SC_COMMON" 1.0 2.0 splitDate) + , CT.OcfCreateVestingAcceleration ((acceleration "ACC-SAME-DAY-REVERSE-SPLIT-FIRST" issuance.security_id 30.0) with + date = splitDate) + ] + [] + [] submitMustFail issuer do - exerciseCmd successorResult.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-SAME-DAY-SPLIT-SUCCESSOR" successorSecurityId 75.0) with + exerciseCmd sameDayAccelerationResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-SAME-DAY-REVERSE-SPLIT-SECOND" issuance.security_id 30.0) with date = postSplitAccelerationDate)] edits = [] deletes = [] diff --git a/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar index 6a5277e0..264ea909 100644 --- a/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9c089dcfece6697d48f6bb61e187688e7def3b37b495eea601b237af18bcd43 -size 3351437 +oid sha256:96f92cf7fdddf017068b5a84c11e0f014b8abe9c39a5a2f810eb3e410d7586cb +size 3352503 diff --git a/dars/dars.lock b/dars/dars.lock index fcd15bff..2d3008b5 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -61,8 +61,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar": { - "sha256": "b9c089dcfece6697d48f6bb61e187688e7def3b37b495eea601b237af18bcd43", - "size": 3351437, + "sha256": "96f92cf7fdddf017068b5a84c11e0f014b8abe9c39a5a2f810eb3e410d7586cb", + "size": 3352503, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-11T18:59:11.000Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index ec1e16fc..ea4707e5 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -1823,17 +1823,15 @@ stockSecurityQuantityBeforeEvent eventIndex _maps issuanceData targetSortKey = (\event -> stockClassShareEventSortKey event < targetSortKey) (sortOn stockClassShareEventSortKey (stockSecurityShareEvents eventIndex issuanceData))) -stockClassQuantityAtSortKey : +stockClassQuantityBetweenSortKeys : CapTableMaps -> Text -> - Time -> + (Time, Int, Int) -> 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 +stockClassQuantityBetweenSortKeys maps stockClassId quantitySortKey quantity targetSortKey = + foldlA (\adjustedQuantity splitCid -> do split <- fetch splitCid let splitSortKey = (split.split_data.date, 2, 2) @@ -1848,6 +1846,18 @@ stockClassQuantityAtSortKey maps stockClassId quantityDate quantity targetSortKe quantity (Map.values maps.stock_class_splits) +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 stockClassQuantityBetweenSortKeys maps stockClassId quantitySortKey quantity targetSortKey + validateStockBalanceSuccessorQuantity : CapTableMaps -> Text -> @@ -4222,10 +4232,10 @@ stockVestingAccelerationTotalAt maps stockClassId targetSortKey accelerations = (\total accelerationData -> if accelerationData.date <= targetDate then do - adjustedQuantity <- stockClassQuantityAtSortKey + adjustedQuantity <- stockClassQuantityBetweenSortKeys maps stockClassId - accelerationData.date + (vestingAccelerationEvaluationSortKey accelerationData.date) accelerationData.quantity targetSortKey pure (total + adjustedQuantity) From e9e02633af789e188fbad7c8966fc0d938d63838 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 15:54:54 -0400 Subject: [PATCH 04/10] perf: fold vesting accelerations once --- .../0.0.17/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 116 ++++++++++-------- 3 files changed, 71 insertions(+), 53 deletions(-) diff --git a/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar index 264ea909..d172a513 100644 --- a/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96f92cf7fdddf017068b5a84c11e0f014b8abe9c39a5a2f810eb3e410d7586cb -size 3352503 +oid sha256:fbf4d706ec54630679ab3c2daf9b3d0060f4b4edab1c5f436b9eb6dce4479171 +size 3359296 diff --git a/dars/dars.lock b/dars/dars.lock index 2d3008b5..aaf4fa81 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -61,8 +61,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar": { - "sha256": "96f92cf7fdddf017068b5a84c11e0f014b8abe9c39a5a2f810eb3e410d7586cb", - "size": 3352503, + "sha256": "fbf4d706ec54630679ab3c2daf9b3d0060f4b4edab1c5f436b9eb6dce4479171", + "size": 3359296, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-11T18:59:11.000Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index ea4707e5..6c9af4d7 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -210,6 +210,15 @@ data SecurityQuantityEvent event_priority: Int deriving (Eq, Show) +data VestingAccelerationTimelineEvent + = VestingAccelerationQuantity with + event_date: Time + quantity: Decimal + | VestingAccelerationSplit with + event_date: Time + ratio: OcfRatio + deriving (Eq, Show) + data NonStockSecurityBalanceEventIndex = NonStockSecurityBalanceEventIndex with warrant_events_by_security_id: Map Text [SecurityQuantityEvent] equity_compensation_events_by_security_id: Map Text [SecurityQuantityEvent] @@ -4207,41 +4216,56 @@ vestingAccelerationsBySecurityId maps = Map.empty (Map.values maps.vesting_accelerations) -vestingAccelerationTotalAt : Time -> [VestingAccelerationOcfData] -> Decimal -vestingAccelerationTotalAt latest = - foldl - (\total accelerationData -> - if accelerationData.date <= latest - then total + accelerationData.quantity - else total) - 0.0 +vestingAccelerationBuckets : [VestingAccelerationOcfData] -> [(Time, Decimal)] +vestingAccelerationBuckets accelerations = + sortOn + fst + (Map.toList (Map.fromListWithR (+) (map (\accelerationData -> (accelerationData.date, accelerationData.quantity)) accelerations))) + +vestingAccelerationTotals : [VestingAccelerationOcfData] -> [(Time, Decimal)] +vestingAccelerationTotals accelerations = + let (_, reversedTotals) = foldl + (\(total, totals) (date, quantity) -> + let nextTotal = total + quantity + in (nextTotal, (date, nextTotal) :: totals)) + (0.0, []) + (vestingAccelerationBuckets accelerations) + in reverse reversedTotals -- Accelerations are evaluated after every applicable same-day security quantity event. vestingAccelerationEvaluationSortKey : Time -> (Time, Int, Int) vestingAccelerationEvaluationSortKey date = (date, 4, 0) -stockVestingAccelerationTotalAt : - CapTableMaps -> +vestingAccelerationTimelineEventSortKey : VestingAccelerationTimelineEvent -> (Time, Int) +vestingAccelerationTimelineEventSortKey event = case event of + VestingAccelerationSplit with event_date -> (event_date, 0) + VestingAccelerationQuantity with event_date -> (event_date, 1) + +stockVestingAccelerationTotals : + StockClassShareEventIndex -> Text -> - (Time, Int, Int) -> [VestingAccelerationOcfData] -> - Update Decimal -stockVestingAccelerationTotalAt maps stockClassId targetSortKey accelerations = - let (targetDate, _, _) = targetSortKey - in foldlA - (\total accelerationData -> - if accelerationData.date <= targetDate - then do - adjustedQuantity <- stockClassQuantityBetweenSortKeys - maps - stockClassId - (vestingAccelerationEvaluationSortKey accelerationData.date) - accelerationData.quantity - targetSortKey - pure (total + adjustedQuantity) - else pure total) - 0.0 - accelerations + [(Time, Decimal)] +stockVestingAccelerationTotals eventIndex stockClassId accelerations = + let accelerationEvents = map + (\(date, quantity) -> VestingAccelerationQuantity with event_date = date; quantity) + (vestingAccelerationBuckets accelerations) + splitEvents = concatMap + (\event -> case event of + StockClassShareMultiplier with event_date; ratio -> + [VestingAccelerationSplit with event_date; ratio] + _ -> []) + (fromOptional [] (Map.lookup stockClassId eventIndex.stock_class_split_events_by_stock_class_id)) + (_, reversedTotals) = foldl + (\(total, totals) event -> case event of + VestingAccelerationSplit with ratio -> + (total * ratio.numerator / ratio.denominator, totals) + VestingAccelerationQuantity with event_date; quantity -> + let nextTotal = total + quantity + in (nextTotal, (event_date, nextTotal) :: totals)) + (0.0, []) + (sortOn vestingAccelerationTimelineEventSortKey (splitEvents ++ accelerationEvents)) + in reverse reversedTotals validateStockVestingAccelerationQuantities : StockClassShareEventIndex -> @@ -4251,18 +4275,13 @@ validateStockVestingAccelerationQuantities : Update () validateStockVestingAccelerationQuantities eventIndex maps issuanceData accelerations = mapA_ - (\accelerationData -> do - let targetSortKey = vestingAccelerationEvaluationSortKey accelerationData.date + (\(accelerationDate, acceleratedQuantity) -> do + let targetSortKey = vestingAccelerationEvaluationSortKey accelerationDate liveQuantity <- stockSecurityQuantityBeforeEvent eventIndex maps issuanceData targetSortKey - acceleratedQuantity <- stockVestingAccelerationTotalAt - maps - issuanceData.stock_class_id - targetSortKey - accelerations assertMsg - ("Vesting accelerations exceed live stock security quantity for " <> issuanceData.security_id <> " at " <> show accelerationData.date) + ("Vesting accelerations exceed live stock security quantity for " <> issuanceData.security_id <> " at " <> show accelerationDate) (acceleratedQuantity <= liveQuantity)) - accelerations + (stockVestingAccelerationTotals eventIndex issuanceData.stock_class_id accelerations) validateEquityCompensationVestingAccelerationQuantities : StockClassShareEventIndex -> @@ -4274,9 +4293,12 @@ validateEquityCompensationVestingAccelerationQuantities : validateEquityCompensationVestingAccelerationQuantities stockEventIndex balanceEventIndex maps issuanceData accelerations = do stockClassIds <- equityCompensationPlanStockClassIds maps issuanceData quantityEvents <- equityCompensationSecurityQuantityEvents stockEventIndex balanceEventIndex maps issuanceData + let accelerationTotals = case stockClassIds of + [stockClassId] -> stockVestingAccelerationTotals stockEventIndex stockClassId accelerations + _ -> vestingAccelerationTotals accelerations mapA_ - (\accelerationData -> do - let targetSortKey = vestingAccelerationEvaluationSortKey accelerationData.date + (\(accelerationDate, acceleratedQuantity) -> do + let targetSortKey = vestingAccelerationEvaluationSortKey accelerationDate liveQuantityOpt <- securityQuantityBeforeEvent issuanceData.security_id (Some 0.0) @@ -4291,13 +4313,10 @@ validateEquityCompensationVestingAccelerationQuantities stockEventIndex balanceE None -> do assertMsg ("Equity compensation quantity unexpectedly became unknown: " <> issuanceData.security_id) False pure 0.0 - acceleratedQuantity <- case stockClassIds of - [stockClassId] -> stockVestingAccelerationTotalAt maps stockClassId targetSortKey accelerations - _ -> pure (vestingAccelerationTotalAt accelerationData.date accelerations) assertMsg - ("Vesting accelerations exceed live equity compensation security quantity for " <> issuanceData.security_id <> " at " <> show accelerationData.date) + ("Vesting accelerations exceed live equity compensation security quantity for " <> issuanceData.security_id <> " at " <> show accelerationDate) (acceleratedQuantity <= liveQuantity)) - accelerations + accelerationTotals validateWarrantVestingAccelerationQuantities : NonStockSecurityBalanceEventIndex -> @@ -4314,22 +4333,21 @@ validateWarrantVestingAccelerationQuantities eventIndex issuanceData acceleratio quantity_delta = quantity) :: warrantSecurityQuantityEvents eventIndex issuanceData mapA_ - (\accelerationData -> do + (\(accelerationDate, acceleratedQuantity) -> do liveQuantityOpt <- securityQuantityBeforeEvent issuanceData.security_id (Some 0.0) quantityEvents - (vestingAccelerationEvaluationSortKey accelerationData.date) + (vestingAccelerationEvaluationSortKey accelerationDate) liveQuantity <- case liveQuantityOpt of Some currentQuantity -> pure currentQuantity None -> do assertMsg ("Warrant quantity unexpectedly became unknown: " <> issuanceData.security_id) False pure 0.0 - let acceleratedQuantity = vestingAccelerationTotalAt accelerationData.date accelerations assertMsg - ("Vesting accelerations exceed live warrant security quantity for " <> issuanceData.security_id <> " at " <> show accelerationData.date) + ("Vesting accelerations exceed live warrant security quantity for " <> issuanceData.security_id <> " at " <> show accelerationDate) (acceleratedQuantity <= liveQuantity)) - accelerations + (vestingAccelerationTotals accelerations) None -> pure () validateVestingAccelerationQuantities : From 58c57190bdf8cf0a1c38f85390685477387b2921 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 16:38:06 -0400 Subject: [PATCH 05/10] fix: carry vesting accelerations across balances --- .../TestVestingQuantityIntegrity.daml | 180 +++++++ .../0.0.17/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 443 ++++++++++++++++-- 4 files changed, 582 insertions(+), 49 deletions(-) diff --git a/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml b/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml index 2e5376b1..79fc73bc 100644 --- a/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml +++ b/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml @@ -1,7 +1,9 @@ module OpenCapTable.TestVestingQuantityIntegrity where import Fairmint.OpenCapTable.OCF.VestingAcceleration (VestingAccelerationOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationCancellation (EquityCompensationCancellationOcfData(..)) import Fairmint.OpenCapTable.OCF.StockReissuance (StockReissuanceOcfData(..)) +import Fairmint.OpenCapTable.OCF.WarrantCancellation (WarrantCancellationOcfData(..)) import Fairmint.OpenCapTable.Types.Stock (OcfQuantitySourceType(..)) import Fairmint.OpenCapTable.Types.Vesting (OcfVesting(..)) import qualified Fairmint.OpenCapTable.CapTable as CT @@ -12,6 +14,7 @@ import OpenCapTable.TestHelpers , defaultStockIssuance , defaultStockPlan , defaultWarrantIssuance + , equityCompensationSuccessorIssuance , stockCancellationOn , stockClassSplitOn , stockSuccessorIssuance @@ -36,6 +39,15 @@ splitDate = DT.time (DA.date 2026 Jan 01) 0 0 0 postSplitAccelerationDate : Time postSplitAccelerationDate = DT.time (DA.date 2026 Jun 01) 0 0 0 +successorAccelerationDate : Time +successorAccelerationDate = DT.time (DA.date 2027 Jan 01) 0 0 0 + +chainedBalanceDate : Time +chainedBalanceDate = DT.time (DA.date 2027 Jun 01) 0 0 0 + +finalSuccessorAccelerationDate : Time +finalSuccessorAccelerationDate = DT.time (DA.date 2028 Jan 01) 0 0 0 + vestingOn : Time -> Decimal -> OcfVesting vestingOn date amount = OcfVesting with date = date @@ -436,6 +448,174 @@ testVestingQuantityIntegrity_SameDayReverseSplitAccelerationCannotBeUndercounted edits = [] deletes = [] +testVestingQuantityIntegrity_StockBalanceSuccessorCarriesSplitAdjustedAccelerations = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let sourceSecurityId = "SEC-ACC-STOCK-LINEAGE-SOURCE" + successorSecurityId = "SEC-ACC-STOCK-LINEAGE-BALANCE" + sourceIssuance = + (defaultStockIssuance "TX-ACC-STOCK-LINEAGE-SOURCE" sourceSecurityId "SH-1" "SC_COMMON") with + quantity = 100.0 + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateStockIssuance sourceIssuance] [] [] + accelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-STOCK-LINEAGE-SOURCE" sourceSecurityId 50.0)] + [] + [] + balanceResult <- applyUpdate + issuer + accelerationResult.updatedCapTableCid + [ CT.OcfCreateStockClassSplit (stockClassSplitOn "SPLIT-ACC-STOCK-LINEAGE" "SC_COMMON" 2.0 1.0 splitDate) + , stockSuccessorIssuance + "TX-ACC-STOCK-LINEAGE-BALANCE" + successorSecurityId + "SH-1" + "SC_COMMON" + splitDate + 60.0 + , CT.OcfCreateStockCancellation + ((stockCancellationOn "CANCEL-ACC-STOCK-LINEAGE" sourceSecurityId 40.0 splitDate) with + balance_security_id = Some successorSecurityId) + ] + [] + [] + exactResult <- applyUpdate + issuer + balanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration ((acceleration "ACC-STOCK-LINEAGE-BALANCE-EXACT" successorSecurityId 60.0) with + date = postSplitAccelerationDate)] + [] + [] + submitMustFail issuer do + exerciseCmd exactResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-STOCK-LINEAGE-BALANCE-OVER" successorSecurityId 1.0) with + date = postSplitAccelerationDate)] + edits = [] + deletes = [] + +testVestingQuantityIntegrity_EquityCompensationBalanceSuccessorCarriesAccelerations = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let sourceSecurityId = "SEC-ACC-EC-LINEAGE-SOURCE" + successorSecurityId = "SEC-ACC-EC-LINEAGE-BALANCE" + chainedSuccessorSecurityId = "SEC-ACC-EC-LINEAGE-BALANCE-2" + sourceIssuance = + (defaultEquityCompensationIssuance "TX-ACC-EC-LINEAGE-SOURCE" sourceSecurityId "SH-1") with + quantity = 100.0 + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateEquityCompensationIssuance sourceIssuance] [] [] + accelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-EC-LINEAGE-SOURCE" sourceSecurityId 50.0)] + [] + [] + balanceResult <- applyUpdate + issuer + accelerationResult.updatedCapTableCid + [ equityCompensationSuccessorIssuance + "TX-ACC-EC-LINEAGE-BALANCE" + successorSecurityId + "SH-1" + postSplitAccelerationDate + 60.0 + None + , CT.OcfCreateEquityCompensationCancellation EquityCompensationCancellationOcfData with + id = "CANCEL-ACC-EC-LINEAGE" + date = postSplitAccelerationDate + security_id = sourceSecurityId + quantity = 40.0 + reason_text = "Partial cancellation" + balance_security_id = Some successorSecurityId + comments = [] + ] + [] + [] + exactResult <- applyUpdate + issuer + balanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration ((acceleration "ACC-EC-LINEAGE-BALANCE-EXACT" successorSecurityId 30.0) with + date = successorAccelerationDate)] + [] + [] + chainedResult <- applyUpdate + issuer + exactResult.updatedCapTableCid + [ equityCompensationSuccessorIssuance + "TX-ACC-EC-LINEAGE-BALANCE-2" + chainedSuccessorSecurityId + "SH-1" + chainedBalanceDate + 30.0 + None + , CT.OcfCreateEquityCompensationCancellation EquityCompensationCancellationOcfData with + id = "CANCEL-ACC-EC-LINEAGE-2" + date = chainedBalanceDate + security_id = successorSecurityId + quantity = 30.0 + reason_text = "Second partial cancellation" + balance_security_id = Some chainedSuccessorSecurityId + comments = [] + ] + [] + [] + submitMustFail issuer do + exerciseCmd chainedResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-EC-LINEAGE-BALANCE-OVER" chainedSuccessorSecurityId 1.0) with + date = finalSuccessorAccelerationDate)] + edits = [] + deletes = [] + +testVestingQuantityIntegrity_KnownWarrantBalanceSuccessorCarriesAccelerations = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let sourceSecurityId = "SEC-ACC-WARRANT-LINEAGE-SOURCE" + successorSecurityId = "SEC-ACC-WARRANT-LINEAGE-BALANCE" + sourceIssuance = + (defaultWarrantIssuance "TX-ACC-WARRANT-LINEAGE-SOURCE" sourceSecurityId "SH-1") with + quantity = Some 100.0 + quantity_source = Some OcfQuantityInstrumentFixed + successorIssuance = + (defaultWarrantIssuance "TX-ACC-WARRANT-LINEAGE-BALANCE" successorSecurityId "SH-1") with + date = postSplitAccelerationDate + quantity = Some 60.0 + quantity_source = Some OcfQuantityInstrumentFixed + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateWarrantIssuance sourceIssuance] [] [] + accelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-WARRANT-LINEAGE-SOURCE" sourceSecurityId 50.0)] + [] + [] + balanceResult <- applyUpdate + issuer + accelerationResult.updatedCapTableCid + [ CT.OcfCreateWarrantIssuance successorIssuance + , CT.OcfCreateWarrantCancellation WarrantCancellationOcfData with + id = "CANCEL-ACC-WARRANT-LINEAGE" + date = postSplitAccelerationDate + security_id = sourceSecurityId + quantity = 40.0 + reason_text = "Partial cancellation" + balance_security_id = Some successorSecurityId + comments = [] + ] + [] + [] + exactResult <- applyUpdate + issuer + balanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration ((acceleration "ACC-WARRANT-LINEAGE-BALANCE-EXACT" successorSecurityId 30.0) with + date = successorAccelerationDate)] + [] + [] + submitMustFail issuer do + exerciseCmd exactResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-WARRANT-LINEAGE-BALANCE-OVER" successorSecurityId 1.0) with + date = successorAccelerationDate)] + edits = [] + deletes = [] + testVestingQuantityIntegrity_AccelerationCannotExceedLiveStockBalance = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableCid <- addPrerequisites issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar index d172a513..eabb846a 100644 --- a/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbf4d706ec54630679ab3c2daf9b3d0060f4b4edab1c5f436b9eb6dce4479171 -size 3359296 +oid sha256:3c71ee3c535aeae75b9ce2e1e46da5638796453825d090edd25062c25b7575e6 +size 3395375 diff --git a/dars/dars.lock b/dars/dars.lock index aaf4fa81..0e80a8fa 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -61,8 +61,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar": { - "sha256": "fbf4d706ec54630679ab3c2daf9b3d0060f4b4edab1c5f436b9eb6dce4479171", - "size": 3359296, + "sha256": "3c71ee3c535aeae75b9ce2e1e46da5638796453825d090edd25062c25b7575e6", + "size": 3395375, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-11T18:59:11.000Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 6c9af4d7..36e240a8 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -212,13 +212,21 @@ data SecurityQuantityEvent data VestingAccelerationTimelineEvent = VestingAccelerationQuantity with - event_date: Time + event_sort_key: (Time, Int, Int) quantity: Decimal + report_total: Bool | VestingAccelerationSplit with - event_date: Time + event_sort_key: (Time, Int, Int) ratio: OcfRatio deriving (Eq, Show) +data VestingAccelerationPredecessor = VestingAccelerationPredecessor with + source_security_id: Text + source_event_sort_key: (Time, Int, Int) + successor_seed_sort_key: (Time, Int, Int) + successor_fraction: Decimal + deriving (Eq, Show) + data NonStockSecurityBalanceEventIndex = NonStockSecurityBalanceEventIndex with warrant_events_by_security_id: Map Text [SecurityQuantityEvent] equity_compensation_events_by_security_id: Map Text [SecurityQuantityEvent] @@ -4216,64 +4224,401 @@ vestingAccelerationsBySecurityId maps = Map.empty (Map.values maps.vesting_accelerations) +insertVestingAccelerationPredecessor : + Text -> + Text -> + VestingAccelerationPredecessor -> + Map Text VestingAccelerationPredecessor -> + Update (Map Text VestingAccelerationPredecessor) +insertVestingAccelerationPredecessor label successorSecurityId predecessor index = do + assertMsg + (label <> " gives vesting acceleration successor " <> successorSecurityId <> " multiple predecessors") + (isNone (Map.lookup successorSecurityId index)) + pure (Map.insert successorSecurityId predecessor index) + +addStockBalanceVestingAccelerationPredecessor : + StockClassShareEventIndex -> + CapTableMaps -> + Text -> + Text -> + Time -> + Optional Text -> + Map Text VestingAccelerationPredecessor -> + Update (Map Text VestingAccelerationPredecessor) +addStockBalanceVestingAccelerationPredecessor eventIndex maps label sourceSecurityId eventDate balanceSecurityId index = + case balanceSecurityId of + Some successorSecurityId -> case + ( Map.lookup sourceSecurityId maps.stock_issuances_by_security_id + , Map.lookup successorSecurityId maps.stock_issuances_by_security_id + ) of + (Some sourceIssuanceCid, Some successorIssuanceCid) -> do + sourceIssuance <- fetch sourceIssuanceCid + successorIssuance <- fetch successorIssuanceCid + let eventSortKey = (eventDate, 2, 1) + sourceQuantity <- stockSecurityQuantityBeforeEvent eventIndex maps sourceIssuance.issuance_data eventSortKey + successorQuantity <- stockIssuanceQuantityAtSortKey maps successorIssuance.issuance_data eventSortKey + assertMsg (label <> " has a non-positive source quantity") (sourceQuantity > 0.0) + assertMsg + (label <> " has an invalid balance successor quantity for vesting acceleration continuity") + (successorQuantity > 0.0 && successorQuantity <= sourceQuantity) + insertVestingAccelerationPredecessor + label + successorSecurityId + (VestingAccelerationPredecessor with + source_security_id = sourceSecurityId + source_event_sort_key = eventSortKey + successor_seed_sort_key = eventSortKey + successor_fraction = successorQuantity / sourceQuantity) + index + _ -> pure index + None -> pure index + +addEquityCompensationBalanceVestingAccelerationPredecessor : + StockClassShareEventIndex -> + NonStockSecurityBalanceEventIndex -> + CapTableMaps -> + Text -> + Text -> + Time -> + Optional Text -> + Map Text VestingAccelerationPredecessor -> + Update (Map Text VestingAccelerationPredecessor) +addEquityCompensationBalanceVestingAccelerationPredecessor stockEventIndex balanceEventIndex maps label sourceSecurityId eventDate balanceSecurityId index = + case balanceSecurityId of + Some successorSecurityId -> case + ( Map.lookup sourceSecurityId maps.equity_compensation_issuances_by_security_id + , Map.lookup successorSecurityId maps.equity_compensation_issuances_by_security_id + ) of + (Some sourceIssuanceCid, Some successorIssuanceCid) -> do + sourceIssuance <- fetch sourceIssuanceCid + successorIssuance <- fetch successorIssuanceCid + let eventSortKey = (eventDate, 2, 1) + sourceQuantity <- equityCompensationSecurityQuantityBeforeTerminal + stockEventIndex balanceEventIndex maps sourceIssuance.issuance_data eventDate + successorQuantity <- equityCompensationIssuanceQuantityAtSortKey maps successorIssuance.issuance_data eventSortKey + assertMsg (label <> " has a non-positive source quantity") (sourceQuantity > 0.0) + assertMsg + (label <> " has an invalid balance successor quantity for vesting acceleration continuity") + (successorQuantity > 0.0 && successorQuantity <= sourceQuantity) + insertVestingAccelerationPredecessor + label + successorSecurityId + (VestingAccelerationPredecessor with + source_security_id = sourceSecurityId + source_event_sort_key = eventSortKey + successor_seed_sort_key = eventSortKey + successor_fraction = successorQuantity / sourceQuantity) + index + _ -> pure index + None -> pure index + +addWarrantBalanceVestingAccelerationPredecessor : + NonStockSecurityBalanceEventIndex -> + CapTableMaps -> + Text -> + Text -> + Time -> + Optional Text -> + Map Text VestingAccelerationPredecessor -> + Update (Map Text VestingAccelerationPredecessor) +addWarrantBalanceVestingAccelerationPredecessor eventIndex maps label sourceSecurityId eventDate balanceSecurityId index = + case balanceSecurityId of + Some successorSecurityId -> case + ( Map.lookup sourceSecurityId maps.warrant_issuances_by_security_id + , Map.lookup successorSecurityId maps.warrant_issuances_by_security_id + ) of + (Some sourceIssuanceCid, Some successorIssuanceCid) -> do + sourceIssuance <- fetch sourceIssuanceCid + successorIssuance <- fetch successorIssuanceCid + sourceQuantityOpt <- warrantSecurityQuantityBeforeTerminal eventIndex sourceIssuance.issuance_data eventDate + let sourceCeilingOpt = ocfQuantityUpperBound sourceQuantityOpt sourceIssuance.issuance_data.quantity_source + successorCeilingOpt = ocfQuantityUpperBound successorIssuance.issuance_data.quantity successorIssuance.issuance_data.quantity_source + eventSortKey = (eventDate, 2, 1) + case (sourceCeilingOpt, successorCeilingOpt) of + (Some sourceQuantity, Some successorQuantity) -> do + assertMsg (label <> " has a non-positive source quantity") (sourceQuantity > 0.0) + assertMsg + (label <> " has an invalid balance successor quantity for vesting acceleration continuity") + (successorQuantity > 0.0 && successorQuantity <= sourceQuantity) + insertVestingAccelerationPredecessor + label + successorSecurityId + (VestingAccelerationPredecessor with + source_security_id = sourceSecurityId + source_event_sort_key = eventSortKey + successor_seed_sort_key = eventSortKey + successor_fraction = successorQuantity / sourceQuantity) + index + _ -> pure index + _ -> pure index + None -> pure index + +vestingAccelerationPredecessorIndex : + StockClassShareEventIndex -> + NonStockSecurityBalanceEventIndex -> + CapTableMaps -> + Update (Map Text VestingAccelerationPredecessor) +vestingAccelerationPredecessorIndex stockEventIndex balanceEventIndex maps = do + stockCancellationIndex <- foldlA + (\index cancellationCid -> do + cancellation <- fetch cancellationCid + addStockBalanceVestingAccelerationPredecessor + stockEventIndex maps ("Stock cancellation " <> cancellation.cancellation_data.id) + cancellation.cancellation_data.security_id cancellation.cancellation_data.date + cancellation.cancellation_data.balance_security_id index) + Map.empty + (Map.values maps.stock_cancellations) + stockRepurchaseIndex <- foldlA + (\index repurchaseCid -> do + repurchase <- fetch repurchaseCid + addStockBalanceVestingAccelerationPredecessor + stockEventIndex maps ("Stock repurchase " <> repurchase.repurchase_data.id) + repurchase.repurchase_data.security_id repurchase.repurchase_data.date + repurchase.repurchase_data.balance_security_id index) + stockCancellationIndex + (Map.values maps.stock_repurchases) + stockTransferIndex <- foldlA + (\index transferCid -> do + transfer <- fetch transferCid + addStockBalanceVestingAccelerationPredecessor + stockEventIndex maps ("Stock transfer " <> transfer.transfer_data.id) + transfer.transfer_data.security_id transfer.transfer_data.date + transfer.transfer_data.balance_security_id index) + stockRepurchaseIndex + (Map.values maps.stock_transfers) + stockConversionIndex <- foldlA + (\index conversionCid -> do + conversion <- fetch conversionCid + addStockBalanceVestingAccelerationPredecessor + stockEventIndex maps ("Stock conversion " <> conversion.conversion_data.id) + conversion.conversion_data.security_id conversion.conversion_data.date + conversion.conversion_data.balance_security_id index) + stockTransferIndex + (Map.values maps.stock_conversions) + equityCancellationIndex <- foldlA + (\index cancellationCid -> do + cancellation <- fetch cancellationCid + addEquityCompensationBalanceVestingAccelerationPredecessor + stockEventIndex balanceEventIndex maps ("Equity compensation cancellation " <> cancellation.cancellation_data.id) + cancellation.cancellation_data.security_id cancellation.cancellation_data.date + cancellation.cancellation_data.balance_security_id index) + stockConversionIndex + (Map.values maps.equity_compensation_cancellations) + equityTransferIndex <- foldlA + (\index transferCid -> do + transfer <- fetch transferCid + addEquityCompensationBalanceVestingAccelerationPredecessor + stockEventIndex balanceEventIndex maps ("Equity compensation transfer " <> transfer.transfer_data.id) + transfer.transfer_data.security_id transfer.transfer_data.date + transfer.transfer_data.balance_security_id index) + equityCancellationIndex + (Map.values maps.equity_compensation_transfers) + warrantCancellationIndex <- foldlA + (\index cancellationCid -> do + cancellation <- fetch cancellationCid + addWarrantBalanceVestingAccelerationPredecessor + balanceEventIndex maps ("Warrant cancellation " <> cancellation.cancellation_data.id) + cancellation.cancellation_data.security_id cancellation.cancellation_data.date + cancellation.cancellation_data.balance_security_id index) + equityTransferIndex + (Map.values maps.warrant_cancellations) + foldlA + (\index transferCid -> do + transfer <- fetch transferCid + addWarrantBalanceVestingAccelerationPredecessor + balanceEventIndex maps ("Warrant transfer " <> transfer.transfer_data.id) + transfer.transfer_data.security_id transfer.transfer_data.date + transfer.transfer_data.balance_security_id index) + warrantCancellationIndex + (Map.values maps.warrant_transfers) + vestingAccelerationBuckets : [VestingAccelerationOcfData] -> [(Time, Decimal)] vestingAccelerationBuckets accelerations = sortOn fst (Map.toList (Map.fromListWithR (+) (map (\accelerationData -> (accelerationData.date, accelerationData.quantity)) accelerations))) -vestingAccelerationTotals : [VestingAccelerationOcfData] -> [(Time, Decimal)] -vestingAccelerationTotals accelerations = - let (_, reversedTotals) = foldl - (\(total, totals) (date, quantity) -> - let nextTotal = total + quantity - in (nextTotal, (date, nextTotal) :: totals)) - (0.0, []) - (vestingAccelerationBuckets accelerations) - in reverse reversedTotals - -- Accelerations are evaluated after every applicable same-day security quantity event. vestingAccelerationEvaluationSortKey : Time -> (Time, Int, Int) vestingAccelerationEvaluationSortKey date = (date, 4, 0) -vestingAccelerationTimelineEventSortKey : VestingAccelerationTimelineEvent -> (Time, Int) +vestingAccelerationTimelineEvents : + Optional ((Time, Int, Int), Decimal) -> + [VestingAccelerationOcfData] -> + [VestingAccelerationTimelineEvent] +vestingAccelerationTimelineEvents inheritedSeed accelerations = + let directEvents = map + (\(date, quantity) -> VestingAccelerationQuantity with + event_sort_key = vestingAccelerationEvaluationSortKey date + quantity + report_total = True) + (vestingAccelerationBuckets accelerations) + in case inheritedSeed of + Some (seedSortKey, seedQuantity) -> + (VestingAccelerationQuantity with + event_sort_key = seedSortKey + quantity = seedQuantity + report_total = False) :: directEvents + None -> directEvents + +vestingAccelerationTimelineEventSortKey : VestingAccelerationTimelineEvent -> (Time, Int, Int) vestingAccelerationTimelineEventSortKey event = case event of - VestingAccelerationSplit with event_date -> (event_date, 0) - VestingAccelerationQuantity with event_date -> (event_date, 1) + VestingAccelerationSplit with event_sort_key -> event_sort_key + VestingAccelerationQuantity with event_sort_key -> event_sort_key + +applyVestingAccelerationTimelineEvent : Decimal -> VestingAccelerationTimelineEvent -> Decimal +applyVestingAccelerationTimelineEvent total event = case event of + VestingAccelerationSplit with ratio -> + total * ratio.numerator / ratio.denominator + VestingAccelerationQuantity with quantity -> + total + quantity + +vestingAccelerationTimelineTotals : [VestingAccelerationTimelineEvent] -> [(Time, Decimal)] +vestingAccelerationTimelineTotals events = + let (_, reversedTotals) = foldl + (\(total, totals) event -> + let nextTotal = applyVestingAccelerationTimelineEvent total event + in case event of + VestingAccelerationQuantity with event_sort_key; report_total -> + if report_total + then let (eventDate, _, _) = event_sort_key + in (nextTotal, (eventDate, nextTotal) :: totals) + else (nextTotal, totals) + VestingAccelerationSplit _ _ -> (nextTotal, totals)) + (0.0, []) + (sortOn vestingAccelerationTimelineEventSortKey events) + in reverse reversedTotals -stockVestingAccelerationTotals : +vestingAccelerationTimelineTotalBefore : + (Time, Int, Int) -> + [VestingAccelerationTimelineEvent] -> + Decimal +vestingAccelerationTimelineTotalBefore targetSortKey events = + foldl + applyVestingAccelerationTimelineEvent + 0.0 + (filter + (\event -> + vestingAccelerationTimelineEventSortKey event < targetSortKey || + (vestingAccelerationTimelineEventSortKey event == targetSortKey && case event of + VestingAccelerationQuantity with report_total -> not report_total + VestingAccelerationSplit _ _ -> False)) + (sortOn vestingAccelerationTimelineEventSortKey events)) + +vestingAccelerationTotals : + Optional ((Time, Int, Int), Decimal) -> + [VestingAccelerationOcfData] -> + [(Time, Decimal)] +vestingAccelerationTotals inheritedSeed accelerations = + vestingAccelerationTimelineTotals (vestingAccelerationTimelineEvents inheritedSeed accelerations) + +stockVestingAccelerationTimelineEvents : StockClassShareEventIndex -> Text -> + Optional ((Time, Int, Int), Decimal) -> [VestingAccelerationOcfData] -> - [(Time, Decimal)] -stockVestingAccelerationTotals eventIndex stockClassId accelerations = - let accelerationEvents = map - (\(date, quantity) -> VestingAccelerationQuantity with event_date = date; quantity) - (vestingAccelerationBuckets accelerations) - splitEvents = concatMap + [VestingAccelerationTimelineEvent] +stockVestingAccelerationTimelineEvents eventIndex stockClassId inheritedSeed accelerations = + let splitEvents = concatMap (\event -> case event of - StockClassShareMultiplier with event_date; ratio -> - [VestingAccelerationSplit with event_date; ratio] + StockClassShareMultiplier with event_date; event_priority; ratio -> + [VestingAccelerationSplit with + event_sort_key = (event_date, event_priority, 2) + ratio] _ -> []) (fromOptional [] (Map.lookup stockClassId eventIndex.stock_class_split_events_by_stock_class_id)) - (_, reversedTotals) = foldl - (\(total, totals) event -> case event of - VestingAccelerationSplit with ratio -> - (total * ratio.numerator / ratio.denominator, totals) - VestingAccelerationQuantity with event_date; quantity -> - let nextTotal = total + quantity - in (nextTotal, (event_date, nextTotal) :: totals)) - (0.0, []) - (sortOn vestingAccelerationTimelineEventSortKey (splitEvents ++ accelerationEvents)) - in reverse reversedTotals + in splitEvents ++ vestingAccelerationTimelineEvents inheritedSeed accelerations + +stockVestingAccelerationTotals : + StockClassShareEventIndex -> + Text -> + Optional ((Time, Int, Int), Decimal) -> + [VestingAccelerationOcfData] -> + [(Time, Decimal)] +stockVestingAccelerationTotals eventIndex stockClassId inheritedSeed accelerations = + vestingAccelerationTimelineTotals + (stockVestingAccelerationTimelineEvents eventIndex stockClassId inheritedSeed accelerations) + +vestingAccelerationInheritedSeed : + StockClassShareEventIndex -> + NonStockSecurityBalanceEventIndex -> + CapTableMaps -> + Map Text [VestingAccelerationOcfData] -> + Map Text VestingAccelerationPredecessor -> + Set Text -> + Text -> + Update (Optional ((Time, Int, Int), Decimal)) +vestingAccelerationInheritedSeed stockEventIndex balanceEventIndex maps accelerationIndex predecessorIndex visited securityId = + if Set.member securityId visited + then do + assertMsg ("Vesting acceleration successor lineage contains a cycle at " <> securityId) False + pure None + else case Map.lookup securityId predecessorIndex of + Some predecessor -> do + sourceTotal <- vestingAccelerationQuantityBefore + stockEventIndex + balanceEventIndex + maps + accelerationIndex + predecessorIndex + (Set.insert securityId visited) + predecessor.source_security_id + predecessor.source_event_sort_key + pure (Some + ( predecessor.successor_seed_sort_key + , sourceTotal * predecessor.successor_fraction + )) + None -> pure None + +vestingAccelerationQuantityBefore : + StockClassShareEventIndex -> + NonStockSecurityBalanceEventIndex -> + CapTableMaps -> + Map Text [VestingAccelerationOcfData] -> + Map Text VestingAccelerationPredecessor -> + Set Text -> + Text -> + (Time, Int, Int) -> + Update Decimal +vestingAccelerationQuantityBefore stockEventIndex balanceEventIndex maps accelerationIndex predecessorIndex visited securityId targetSortKey = do + inheritedSeed <- vestingAccelerationInheritedSeed + stockEventIndex balanceEventIndex maps accelerationIndex predecessorIndex visited securityId + let accelerations = fromOptional [] (Map.lookup securityId accelerationIndex) + case Map.lookup securityId maps.stock_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + pure (vestingAccelerationTimelineTotalBefore + targetSortKey + (stockVestingAccelerationTimelineEvents + stockEventIndex + issuance.issuance_data.stock_class_id + inheritedSeed + accelerations)) + None -> case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + stockClassIds <- equityCompensationPlanStockClassIds maps issuance.issuance_data + let events = case stockClassIds of + [stockClassId] -> stockVestingAccelerationTimelineEvents stockEventIndex stockClassId inheritedSeed accelerations + _ -> vestingAccelerationTimelineEvents inheritedSeed accelerations + pure (vestingAccelerationTimelineTotalBefore targetSortKey events) + None -> case Map.lookup securityId maps.warrant_issuances_by_security_id of + Some _ -> + pure (vestingAccelerationTimelineTotalBefore + targetSortKey + (vestingAccelerationTimelineEvents inheritedSeed accelerations)) + None -> do + assertMsg ("Vesting acceleration predecessor security not found: " <> securityId) False + pure 0.0 validateStockVestingAccelerationQuantities : StockClassShareEventIndex -> CapTableMaps -> StockIssuanceOcfData -> + Optional ((Time, Int, Int), Decimal) -> [VestingAccelerationOcfData] -> Update () -validateStockVestingAccelerationQuantities eventIndex maps issuanceData accelerations = +validateStockVestingAccelerationQuantities eventIndex maps issuanceData inheritedSeed accelerations = mapA_ (\(accelerationDate, acceleratedQuantity) -> do let targetSortKey = vestingAccelerationEvaluationSortKey accelerationDate @@ -4281,21 +4626,22 @@ validateStockVestingAccelerationQuantities eventIndex maps issuanceData accelera assertMsg ("Vesting accelerations exceed live stock security quantity for " <> issuanceData.security_id <> " at " <> show accelerationDate) (acceleratedQuantity <= liveQuantity)) - (stockVestingAccelerationTotals eventIndex issuanceData.stock_class_id accelerations) + (stockVestingAccelerationTotals eventIndex issuanceData.stock_class_id inheritedSeed accelerations) validateEquityCompensationVestingAccelerationQuantities : StockClassShareEventIndex -> NonStockSecurityBalanceEventIndex -> CapTableMaps -> EquityCompensationIssuanceOcfData -> + Optional ((Time, Int, Int), Decimal) -> [VestingAccelerationOcfData] -> Update () -validateEquityCompensationVestingAccelerationQuantities stockEventIndex balanceEventIndex maps issuanceData accelerations = do +validateEquityCompensationVestingAccelerationQuantities stockEventIndex balanceEventIndex maps issuanceData inheritedSeed accelerations = do stockClassIds <- equityCompensationPlanStockClassIds maps issuanceData quantityEvents <- equityCompensationSecurityQuantityEvents stockEventIndex balanceEventIndex maps issuanceData let accelerationTotals = case stockClassIds of - [stockClassId] -> stockVestingAccelerationTotals stockEventIndex stockClassId accelerations - _ -> vestingAccelerationTotals accelerations + [stockClassId] -> stockVestingAccelerationTotals stockEventIndex stockClassId inheritedSeed accelerations + _ -> vestingAccelerationTotals inheritedSeed accelerations mapA_ (\(accelerationDate, acceleratedQuantity) -> do let targetSortKey = vestingAccelerationEvaluationSortKey accelerationDate @@ -4321,9 +4667,10 @@ validateEquityCompensationVestingAccelerationQuantities stockEventIndex balanceE validateWarrantVestingAccelerationQuantities : NonStockSecurityBalanceEventIndex -> WarrantIssuanceOcfData -> + Optional ((Time, Int, Int), Decimal) -> [VestingAccelerationOcfData] -> Update () -validateWarrantVestingAccelerationQuantities eventIndex issuanceData accelerations = +validateWarrantVestingAccelerationQuantities eventIndex issuanceData inheritedSeed accelerations = case ocfQuantityUpperBound issuanceData.quantity issuanceData.quantity_source of Some quantity -> do let quantityEvents = @@ -4347,20 +4694,24 @@ validateWarrantVestingAccelerationQuantities eventIndex issuanceData acceleratio assertMsg ("Vesting accelerations exceed live warrant security quantity for " <> issuanceData.security_id <> " at " <> show accelerationDate) (acceleratedQuantity <= liveQuantity)) - (vestingAccelerationTotals accelerations) + (vestingAccelerationTotals inheritedSeed accelerations) None -> pure () validateVestingAccelerationQuantities : StockClassShareEventIndex -> NonStockSecurityBalanceEventIndex -> CapTableMaps -> + Map Text [VestingAccelerationOcfData] -> + Map Text VestingAccelerationPredecessor -> (Text, [VestingAccelerationOcfData]) -> Update () -validateVestingAccelerationQuantities stockEventIndex balanceEventIndex maps (securityId, accelerations) = +validateVestingAccelerationQuantities stockEventIndex balanceEventIndex maps accelerationIndex predecessorIndex (securityId, accelerations) = do + inheritedSeed <- vestingAccelerationInheritedSeed + stockEventIndex balanceEventIndex maps accelerationIndex predecessorIndex Set.empty securityId case Map.lookup securityId maps.stock_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid - validateStockVestingAccelerationQuantities stockEventIndex maps issuance.issuance_data accelerations + validateStockVestingAccelerationQuantities stockEventIndex maps issuance.issuance_data inheritedSeed accelerations None -> case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid @@ -4369,11 +4720,12 @@ validateVestingAccelerationQuantities stockEventIndex balanceEventIndex maps (se balanceEventIndex maps issuance.issuance_data + inheritedSeed accelerations None -> case Map.lookup securityId maps.warrant_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid - validateWarrantVestingAccelerationQuantities balanceEventIndex issuance.issuance_data accelerations + validateWarrantVestingAccelerationQuantities balanceEventIndex issuance.issuance_data inheritedSeed accelerations None -> assertMsg ("Vesting accelerations reference a missing or unsupported security: " <> securityId) @@ -4387,8 +4739,9 @@ validateVestingQuantityIntegrity : validateVestingQuantityIntegrity stockEventIndex balanceEventIndex maps = do validateInlineVestingQuantities maps accelerationIndex <- vestingAccelerationsBySecurityId maps + predecessorIndex <- vestingAccelerationPredecessorIndex stockEventIndex balanceEventIndex maps mapA_ - (validateVestingAccelerationQuantities stockEventIndex balanceEventIndex maps) + (validateVestingAccelerationQuantities stockEventIndex balanceEventIndex maps accelerationIndex predecessorIndex) (Map.toList accelerationIndex) validateTransactionLifecycleDates : CapTableMaps -> Update () From 4699866d78b0358088404032072d4b280f9f4a85 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 16:53:37 -0400 Subject: [PATCH 06/10] fix: account for reductions in acceleration timelines --- .../TestVestingQuantityIntegrity.daml | 119 ++++++++++++++++++ .../0.0.17/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 116 ++++++++++++----- 4 files changed, 205 insertions(+), 38 deletions(-) diff --git a/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml b/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml index 79fc73bc..758cf455 100644 --- a/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml +++ b/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml @@ -477,6 +477,8 @@ testVestingQuantityIntegrity_StockBalanceSuccessorCarriesSplitAdjustedAccelerati , CT.OcfCreateStockCancellation ((stockCancellationOn "CANCEL-ACC-STOCK-LINEAGE" sourceSecurityId 40.0 splitDate) with balance_security_id = Some successorSecurityId) + , CT.OcfCreateStockCancellation + (stockCancellationOn "CANCEL-ACC-STOCK-LINEAGE-SUCCESSOR" successorSecurityId 10.0 splitDate) ] [] [] @@ -616,6 +618,123 @@ testVestingQuantityIntegrity_KnownWarrantBalanceSuccessorCarriesAccelerations = edits = [] deletes = [] +testVestingQuantityIntegrity_StockReductionConsumesPriorAccelerations = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let securityId = "SEC-ACC-STOCK-REDUCTION" + issuance = + (defaultStockIssuance "TX-ACC-STOCK-REDUCTION" securityId "SH-1" "SC_COMMON") with + quantity = 100.0 + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateStockIssuance issuance] [] [] + accelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-STOCK-REDUCTION-BEFORE" securityId 60.0)] + [] + [] + reductionResult <- applyUpdate + issuer + accelerationResult.updatedCapTableCid + [CT.OcfCreateStockCancellation (stockCancellationOn "CANCEL-ACC-STOCK-REDUCTION" securityId 40.0 postSplitAccelerationDate)] + [] + [] + exactResult <- applyUpdate + issuer + reductionResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration ((acceleration "ACC-STOCK-REDUCTION-EXACT" securityId 40.0) with + date = successorAccelerationDate)] + [] + [] + submitMustFail issuer do + exerciseCmd exactResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-STOCK-REDUCTION-OVER" securityId 1.0) with + date = successorAccelerationDate)] + edits = [] + deletes = [] + +testVestingQuantityIntegrity_EquityCompensationReductionConsumesPriorAccelerations = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let securityId = "SEC-ACC-EC-REDUCTION" + issuance = + (defaultEquityCompensationIssuance "TX-ACC-EC-REDUCTION" securityId "SH-1") with + quantity = 100.0 + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateEquityCompensationIssuance issuance] [] [] + accelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-EC-REDUCTION-BEFORE" securityId 60.0)] + [] + [] + reductionResult <- applyUpdate + issuer + accelerationResult.updatedCapTableCid + [CT.OcfCreateEquityCompensationCancellation EquityCompensationCancellationOcfData with + id = "CANCEL-ACC-EC-REDUCTION" + date = postSplitAccelerationDate + security_id = securityId + quantity = 40.0 + reason_text = "Partial cancellation" + balance_security_id = None + comments = []] + [] + [] + exactResult <- applyUpdate + issuer + reductionResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration ((acceleration "ACC-EC-REDUCTION-EXACT" securityId 40.0) with + date = successorAccelerationDate)] + [] + [] + submitMustFail issuer do + exerciseCmd exactResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-EC-REDUCTION-OVER" securityId 1.0) with + date = successorAccelerationDate)] + edits = [] + deletes = [] + +testVestingQuantityIntegrity_KnownWarrantReductionConsumesPriorAccelerations = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let securityId = "SEC-ACC-WARRANT-REDUCTION" + issuance = + (defaultWarrantIssuance "TX-ACC-WARRANT-REDUCTION" securityId "SH-1") with + quantity = Some 100.0 + quantity_source = Some OcfQuantityInstrumentFixed + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateWarrantIssuance issuance] [] [] + accelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-WARRANT-REDUCTION-BEFORE" securityId 60.0)] + [] + [] + reductionResult <- applyUpdate + issuer + accelerationResult.updatedCapTableCid + [CT.OcfCreateWarrantCancellation WarrantCancellationOcfData with + id = "CANCEL-ACC-WARRANT-REDUCTION" + date = postSplitAccelerationDate + security_id = securityId + quantity = 40.0 + reason_text = "Partial cancellation" + balance_security_id = None + comments = []] + [] + [] + exactResult <- applyUpdate + issuer + reductionResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration ((acceleration "ACC-WARRANT-REDUCTION-EXACT" securityId 40.0) with + date = successorAccelerationDate)] + [] + [] + submitMustFail issuer do + exerciseCmd exactResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-WARRANT-REDUCTION-OVER" securityId 1.0) with + date = successorAccelerationDate)] + edits = [] + deletes = [] + testVestingQuantityIntegrity_AccelerationCannotExceedLiveStockBalance = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableCid <- addPrerequisites issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar index eabb846a..eed307ee 100644 --- a/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c71ee3c535aeae75b9ce2e1e46da5638796453825d090edd25062c25b7575e6 -size 3395375 +oid sha256:00abc8ac918d4bce66df81762a28fb863bc91b2236b24782c7d81d083979635c +size 3400371 diff --git a/dars/dars.lock b/dars/dars.lock index 0e80a8fa..6417e60b 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -61,8 +61,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar": { - "sha256": "3c71ee3c535aeae75b9ce2e1e46da5638796453825d090edd25062c25b7575e6", - "size": 3395375, + "sha256": "00abc8ac918d4bce66df81762a28fb863bc91b2236b24782c7d81d083979635c", + "size": 3400371, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-11T18:59:11.000Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 36e240a8..c5614541 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -218,6 +218,11 @@ data VestingAccelerationTimelineEvent | VestingAccelerationSplit with event_sort_key: (Time, Int, Int) ratio: OcfRatio + | VestingAccelerationReduction with + event_sort_key: (Time, Int, Int) + quantity_delta: Decimal + | VestingAccelerationRetirement with + event_sort_key: (Time, Int, Int) deriving (Eq, Show) data VestingAccelerationPredecessor = VestingAccelerationPredecessor with @@ -4267,7 +4272,7 @@ addStockBalanceVestingAccelerationPredecessor eventIndex maps label sourceSecuri (VestingAccelerationPredecessor with source_security_id = sourceSecurityId source_event_sort_key = eventSortKey - successor_seed_sort_key = eventSortKey + successor_seed_sort_key = (eventDate, 0, 0) successor_fraction = successorQuantity / sourceQuantity) index _ -> pure index @@ -4306,7 +4311,7 @@ addEquityCompensationBalanceVestingAccelerationPredecessor stockEventIndex balan (VestingAccelerationPredecessor with source_security_id = sourceSecurityId source_event_sort_key = eventSortKey - successor_seed_sort_key = eventSortKey + successor_seed_sort_key = (eventDate, 0, 0) successor_fraction = successorQuantity / sourceQuantity) index _ -> pure index @@ -4346,7 +4351,7 @@ addWarrantBalanceVestingAccelerationPredecessor eventIndex maps label sourceSecu (VestingAccelerationPredecessor with source_security_id = sourceSecurityId source_event_sort_key = eventSortKey - successor_seed_sort_key = eventSortKey + successor_seed_sort_key = (eventDate, 0, 0) successor_fraction = successorQuantity / sourceQuantity) index _ -> pure index @@ -4465,6 +4470,8 @@ vestingAccelerationTimelineEventSortKey : VestingAccelerationTimelineEvent -> (T vestingAccelerationTimelineEventSortKey event = case event of VestingAccelerationSplit with event_sort_key -> event_sort_key VestingAccelerationQuantity with event_sort_key -> event_sort_key + VestingAccelerationReduction with event_sort_key -> event_sort_key + VestingAccelerationRetirement with event_sort_key -> event_sort_key applyVestingAccelerationTimelineEvent : Decimal -> VestingAccelerationTimelineEvent -> Decimal applyVestingAccelerationTimelineEvent total event = case event of @@ -4472,6 +4479,10 @@ applyVestingAccelerationTimelineEvent total event = case event of total * ratio.numerator / ratio.denominator VestingAccelerationQuantity with quantity -> total + quantity + VestingAccelerationReduction with quantity_delta -> + let nextTotal = total + quantity_delta + in if nextTotal < 0.0 then 0.0 else nextTotal + VestingAccelerationRetirement _ -> 0.0 vestingAccelerationTimelineTotals : [VestingAccelerationTimelineEvent] -> [(Time, Decimal)] vestingAccelerationTimelineTotals events = @@ -4484,7 +4495,7 @@ vestingAccelerationTimelineTotals events = then let (eventDate, _, _) = event_sort_key in (nextTotal, (eventDate, nextTotal) :: totals) else (nextTotal, totals) - VestingAccelerationSplit _ _ -> (nextTotal, totals)) + _ -> (nextTotal, totals)) (0.0, []) (sortOn vestingAccelerationTimelineEventSortKey events) in reverse reversedTotals @@ -4502,42 +4513,73 @@ vestingAccelerationTimelineTotalBefore targetSortKey events = vestingAccelerationTimelineEventSortKey event < targetSortKey || (vestingAccelerationTimelineEventSortKey event == targetSortKey && case event of VestingAccelerationQuantity with report_total -> not report_total - VestingAccelerationSplit _ _ -> False)) + _ -> False)) (sortOn vestingAccelerationTimelineEventSortKey events)) -vestingAccelerationTotals : - Optional ((Time, Int, Int), Decimal) -> - [VestingAccelerationOcfData] -> - [(Time, Decimal)] -vestingAccelerationTotals inheritedSeed accelerations = - vestingAccelerationTimelineTotals (vestingAccelerationTimelineEvents inheritedSeed accelerations) - stockVestingAccelerationTimelineEvents : StockClassShareEventIndex -> - Text -> + StockIssuanceOcfData -> Optional ((Time, Int, Int), Decimal) -> [VestingAccelerationOcfData] -> [VestingAccelerationTimelineEvent] -stockVestingAccelerationTimelineEvents eventIndex stockClassId inheritedSeed accelerations = - let splitEvents = concatMap +stockVestingAccelerationTimelineEvents eventIndex issuanceData inheritedSeed accelerations = + let securityEvents = concatMap (\event -> case event of + StockClassShareDelta with event_date; event_priority; quantity_delta -> + if quantity_delta < 0.0 + then [VestingAccelerationReduction with + event_sort_key = (event_date, event_priority, 0) + quantity_delta] + else [] + StockClassShareTerminalDelta with event_date; event_priority -> + [VestingAccelerationRetirement with + event_sort_key = (event_date, event_priority, 1)] StockClassShareMultiplier with event_date; event_priority; ratio -> [VestingAccelerationSplit with event_sort_key = (event_date, event_priority, 2) ratio] - _ -> []) - (fromOptional [] (Map.lookup stockClassId eventIndex.stock_class_split_events_by_stock_class_id)) - in splitEvents ++ vestingAccelerationTimelineEvents inheritedSeed accelerations + StockClassShareRetraction with event_date; event_priority -> + [VestingAccelerationRetirement with + event_sort_key = (event_date, event_priority, 3)]) + (stockSecurityShareEvents eventIndex issuanceData) + in securityEvents ++ vestingAccelerationTimelineEvents inheritedSeed accelerations stockVestingAccelerationTotals : StockClassShareEventIndex -> - Text -> + StockIssuanceOcfData -> Optional ((Time, Int, Int), Decimal) -> [VestingAccelerationOcfData] -> [(Time, Decimal)] -stockVestingAccelerationTotals eventIndex stockClassId inheritedSeed accelerations = +stockVestingAccelerationTotals eventIndex issuanceData inheritedSeed accelerations = vestingAccelerationTimelineTotals - (stockVestingAccelerationTimelineEvents eventIndex stockClassId inheritedSeed accelerations) + (stockVestingAccelerationTimelineEvents eventIndex issuanceData inheritedSeed accelerations) + +securityVestingAccelerationTimelineEvents : + [SecurityQuantityEvent] -> + Optional ((Time, Int, Int), Decimal) -> + [VestingAccelerationOcfData] -> + [VestingAccelerationTimelineEvent] +securityVestingAccelerationTimelineEvents quantityEvents inheritedSeed accelerations = + let securityEvents = concatMap + (\event -> case event of + SecurityQuantityDelta with event_date; event_priority; quantity_delta -> + if quantity_delta < 0.0 + then [VestingAccelerationReduction with + event_sort_key = (event_date, event_priority, 0) + quantity_delta] + else [] + SecurityQuantityTerminalDelta with event_date; event_priority -> + [VestingAccelerationRetirement with + event_sort_key = (event_date, event_priority, 1)] + SecurityQuantityMultiplier with event_date; event_priority; ratio -> + [VestingAccelerationSplit with + event_sort_key = (event_date, event_priority, 2) + ratio] + SecurityQuantityRetraction with event_date; event_priority -> + [VestingAccelerationRetirement with + event_sort_key = (event_date, event_priority, 3)]) + quantityEvents + in securityEvents ++ vestingAccelerationTimelineEvents inheritedSeed accelerations vestingAccelerationInheritedSeed : StockClassShareEventIndex -> @@ -4591,22 +4633,25 @@ vestingAccelerationQuantityBefore stockEventIndex balanceEventIndex maps acceler targetSortKey (stockVestingAccelerationTimelineEvents stockEventIndex - issuance.issuance_data.stock_class_id + issuance.issuance_data inheritedSeed accelerations)) None -> case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid - stockClassIds <- equityCompensationPlanStockClassIds maps issuance.issuance_data - let events = case stockClassIds of - [stockClassId] -> stockVestingAccelerationTimelineEvents stockEventIndex stockClassId inheritedSeed accelerations - _ -> vestingAccelerationTimelineEvents inheritedSeed accelerations + quantityEvents <- equityCompensationSecurityQuantityEvents + stockEventIndex balanceEventIndex maps issuance.issuance_data + let events = securityVestingAccelerationTimelineEvents quantityEvents inheritedSeed accelerations pure (vestingAccelerationTimelineTotalBefore targetSortKey events) None -> case Map.lookup securityId maps.warrant_issuances_by_security_id of - Some _ -> + Some issuanceCid -> do + issuance <- fetch issuanceCid pure (vestingAccelerationTimelineTotalBefore targetSortKey - (vestingAccelerationTimelineEvents inheritedSeed accelerations)) + (securityVestingAccelerationTimelineEvents + (warrantSecurityQuantityEvents balanceEventIndex issuance.issuance_data) + inheritedSeed + accelerations)) None -> do assertMsg ("Vesting acceleration predecessor security not found: " <> securityId) False pure 0.0 @@ -4626,7 +4671,7 @@ validateStockVestingAccelerationQuantities eventIndex maps issuanceData inherite assertMsg ("Vesting accelerations exceed live stock security quantity for " <> issuanceData.security_id <> " at " <> show accelerationDate) (acceleratedQuantity <= liveQuantity)) - (stockVestingAccelerationTotals eventIndex issuanceData.stock_class_id inheritedSeed accelerations) + (stockVestingAccelerationTotals eventIndex issuanceData inheritedSeed accelerations) validateEquityCompensationVestingAccelerationQuantities : StockClassShareEventIndex -> @@ -4637,11 +4682,9 @@ validateEquityCompensationVestingAccelerationQuantities : [VestingAccelerationOcfData] -> Update () validateEquityCompensationVestingAccelerationQuantities stockEventIndex balanceEventIndex maps issuanceData inheritedSeed accelerations = do - stockClassIds <- equityCompensationPlanStockClassIds maps issuanceData quantityEvents <- equityCompensationSecurityQuantityEvents stockEventIndex balanceEventIndex maps issuanceData - let accelerationTotals = case stockClassIds of - [stockClassId] -> stockVestingAccelerationTotals stockEventIndex stockClassId inheritedSeed accelerations - _ -> vestingAccelerationTotals inheritedSeed accelerations + let accelerationTotals = vestingAccelerationTimelineTotals + (securityVestingAccelerationTimelineEvents quantityEvents inheritedSeed accelerations) mapA_ (\(accelerationDate, acceleratedQuantity) -> do let targetSortKey = vestingAccelerationEvaluationSortKey accelerationDate @@ -4679,6 +4722,11 @@ validateWarrantVestingAccelerationQuantities eventIndex issuanceData inheritedSe event_priority = 0 quantity_delta = quantity) :: warrantSecurityQuantityEvents eventIndex issuanceData + accelerationTotals = vestingAccelerationTimelineTotals + (securityVestingAccelerationTimelineEvents + (warrantSecurityQuantityEvents eventIndex issuanceData) + inheritedSeed + accelerations) mapA_ (\(accelerationDate, acceleratedQuantity) -> do liveQuantityOpt <- securityQuantityBeforeEvent @@ -4694,7 +4742,7 @@ validateWarrantVestingAccelerationQuantities eventIndex issuanceData inheritedSe assertMsg ("Vesting accelerations exceed live warrant security quantity for " <> issuanceData.security_id <> " at " <> show accelerationDate) (acceleratedQuantity <= liveQuantity)) - (vestingAccelerationTotals inheritedSeed accelerations) + accelerationTotals None -> pure () validateVestingAccelerationQuantities : From 51a175d60efbcdf7d24f93b7a8e21b909fdb2f97 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 17:50:04 -0400 Subject: [PATCH 07/10] fix: carry accelerations across all successors --- .../TestVestingQuantityIntegrity.daml | 308 +++++++++++++ .../0.0.17/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- .../codegen/templates/CapTable.daml.template | 407 ++++++++++++------ 4 files changed, 586 insertions(+), 139 deletions(-) diff --git a/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml b/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml index 758cf455..d4396f63 100644 --- a/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml +++ b/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml @@ -2,8 +2,13 @@ module OpenCapTable.TestVestingQuantityIntegrity where import Fairmint.OpenCapTable.OCF.VestingAcceleration (VestingAccelerationOcfData(..)) import Fairmint.OpenCapTable.OCF.EquityCompensationCancellation (EquityCompensationCancellationOcfData(..)) +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.StockTransfer (StockTransferOcfData(..)) import Fairmint.OpenCapTable.OCF.WarrantCancellation (WarrantCancellationOcfData(..)) +import Fairmint.OpenCapTable.OCF.WarrantTransfer (WarrantTransferOcfData(..)) import Fairmint.OpenCapTable.Types.Stock (OcfQuantitySourceType(..)) import Fairmint.OpenCapTable.Types.Vesting (OcfVesting(..)) import qualified Fairmint.OpenCapTable.CapTable as CT @@ -19,6 +24,7 @@ import OpenCapTable.TestHelpers , stockClassSplitOn , stockSuccessorIssuance ) +import OpenCapTable.TestStockConversion (addPreferredStockClass, createStockIssuance) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT @@ -618,6 +624,308 @@ testVestingQuantityIntegrity_KnownWarrantBalanceSuccessorCarriesAccelerations = edits = [] deletes = [] +testVestingQuantityIntegrity_StockTransferResultCarriesAccelerations = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let sourceSecurityId = "SEC-ACC-STOCK-TRANSFER-SOURCE" + resultSecurityId = "SEC-ACC-STOCK-TRANSFER-RESULT" + sourceIssuance = + (defaultStockIssuance "TX-ACC-STOCK-TRANSFER-SOURCE" sourceSecurityId "SH-1" "SC_COMMON") with + quantity = 100.0 + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateStockIssuance sourceIssuance] [] [] + accelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-STOCK-TRANSFER-SOURCE" sourceSecurityId 60.0)] + [] + [] + transferResult <- applyUpdate + issuer + accelerationResult.updatedCapTableCid + [ stockSuccessorIssuance + "TX-ACC-STOCK-TRANSFER-RESULT" + resultSecurityId + "SH-1" + "SC_COMMON" + postSplitAccelerationDate + 100.0 + , CT.OcfCreateStockTransfer StockTransferOcfData with + id = "TRANSFER-ACC-STOCK-RESULT" + date = postSplitAccelerationDate + security_id = sourceSecurityId + quantity = 100.0 + resulting_security_ids = [resultSecurityId] + balance_security_id = None + consideration_text = None + comments = [] + ] + [] + [] + exactResult <- applyUpdate + issuer + transferResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration ((acceleration "ACC-STOCK-TRANSFER-RESULT-EXACT" resultSecurityId 40.0) with + date = successorAccelerationDate)] + [] + [] + submitMustFail issuer do + exerciseCmd exactResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-STOCK-TRANSFER-RESULT-OVER" resultSecurityId 1.0) with + date = successorAccelerationDate)] + edits = [] + deletes = [] + +testVestingQuantityIntegrity_StockReissuanceResultCarriesAccelerations = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let sourceSecurityId = "SEC-ACC-STOCK-REISSUE-SOURCE" + resultSecurityId = "SEC-ACC-STOCK-REISSUE-RESULT" + sourceIssuance = + (defaultStockIssuance "TX-ACC-STOCK-REISSUE-SOURCE" sourceSecurityId "SH-1" "SC_COMMON") with + quantity = 100.0 + reissuance = StockReissuanceOcfData with + id = "REISSUE-ACC-STOCK-RESULT" + date = postSplitAccelerationDate + security_id = sourceSecurityId + resulting_security_ids = [resultSecurityId] + split_transaction_id = None + reason_text = Some "Replace certificate" + comments = [] + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateStockIssuance sourceIssuance] [] [] + accelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-STOCK-REISSUE-SOURCE" sourceSecurityId 60.0)] + [] + [] + reissuanceResult <- applyUpdate + issuer + accelerationResult.updatedCapTableCid + [ stockSuccessorIssuance + "TX-ACC-STOCK-REISSUE-RESULT" + resultSecurityId + "SH-1" + "SC_COMMON" + postSplitAccelerationDate + 100.0 + , CT.OcfCreateStockReissuance reissuance + ] + [] + [] + exactResult <- applyUpdate + issuer + reissuanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration ((acceleration "ACC-STOCK-REISSUE-RESULT-EXACT" resultSecurityId 40.0) with + date = successorAccelerationDate)] + [] + [] + submitMustFail issuer do + exerciseCmd exactResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-STOCK-REISSUE-RESULT-OVER" resultSecurityId 1.0) with + date = successorAccelerationDate)] + edits = [] + deletes = [] + +testVestingQuantityIntegrity_StockConsolidationAggregatesSourceAccelerations = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let firstSourceSecurityId = "SEC-ACC-STOCK-CONSOLIDATE-SOURCE-A" + secondSourceSecurityId = "SEC-ACC-STOCK-CONSOLIDATE-SOURCE-B" + resultSecurityId = "SEC-ACC-STOCK-CONSOLIDATE-RESULT" + firstSourceIssuance = + (defaultStockIssuance "TX-ACC-STOCK-CONSOLIDATE-SOURCE-A" firstSourceSecurityId "SH-1" "SC_COMMON") with + quantity = 40.0 + secondSourceIssuance = + (defaultStockIssuance "TX-ACC-STOCK-CONSOLIDATE-SOURCE-B" secondSourceSecurityId "SH-1" "SC_COMMON") with + quantity = 60.0 + issuanceResult <- applyUpdate + issuer + capTableCid + [ CT.OcfCreateStockIssuance firstSourceIssuance + , CT.OcfCreateStockIssuance secondSourceIssuance + ] + [] + [] + accelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [ CT.OcfCreateVestingAcceleration (acceleration "ACC-STOCK-CONSOLIDATE-SOURCE-A" firstSourceSecurityId 20.0) + , CT.OcfCreateVestingAcceleration (acceleration "ACC-STOCK-CONSOLIDATE-SOURCE-B" secondSourceSecurityId 30.0) + ] + [] + [] + consolidationResult <- applyUpdate + issuer + accelerationResult.updatedCapTableCid + [ stockSuccessorIssuance + "TX-ACC-STOCK-CONSOLIDATE-RESULT" + resultSecurityId + "SH-1" + "SC_COMMON" + postSplitAccelerationDate + 100.0 + , CT.OcfCreateStockConsolidation StockConsolidationOcfData with + id = "CONSOLIDATE-ACC-STOCK-RESULT" + date = postSplitAccelerationDate + security_ids = [firstSourceSecurityId, secondSourceSecurityId] + resulting_security_id = resultSecurityId + reason_text = Some "Combine certificates" + comments = [] + ] + [] + [] + exactResult <- applyUpdate + issuer + consolidationResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration ((acceleration "ACC-STOCK-CONSOLIDATE-RESULT-EXACT" resultSecurityId 50.0) with + date = successorAccelerationDate)] + [] + [] + submitMustFail issuer do + exerciseCmd exactResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-STOCK-CONSOLIDATE-RESULT-OVER" resultSecurityId 1.0) with + date = successorAccelerationDate)] + edits = [] + deletes = [] + +testVestingQuantityIntegrity_StockConversionResultCarriesAccelerations = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + capTableCid <- addPreferredStockClass issuer capTableCid + let sourceSecurityId = "SEC-ACC-STOCK-CONVERSION-SOURCE" + resultSecurityId = "SEC-ACC-STOCK-CONVERSION-RESULT" + issuanceResult <- createStockIssuance issuer capTableCid sourceSecurityId 100.0 firstVestingDate + accelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-STOCK-CONVERSION-SOURCE" sourceSecurityId 60.0)] + [] + [] + conversionResult <- applyUpdate + issuer + accelerationResult.updatedCapTableCid + [ stockSuccessorIssuance + "TX-ACC-STOCK-CONVERSION-RESULT" + resultSecurityId + "SH-1" + "SC_COMMON" + postSplitAccelerationDate + 100.0 + , CT.OcfCreateStockConversion StockConversionOcfData with + id = "CONVERT-ACC-STOCK-RESULT" + date = postSplitAccelerationDate + security_id = sourceSecurityId + quantity_converted = 100.0 + resulting_security_ids = [resultSecurityId] + balance_security_id = None + comments = [] + ] + [] + [] + exactResult <- applyUpdate + issuer + conversionResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration ((acceleration "ACC-STOCK-CONVERSION-RESULT-EXACT" resultSecurityId 40.0) with + date = successorAccelerationDate)] + [] + [] + submitMustFail issuer do + exerciseCmd exactResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-STOCK-CONVERSION-RESULT-OVER" resultSecurityId 1.0) with + date = successorAccelerationDate)] + edits = [] + deletes = [] + +testVestingQuantityIntegrity_NonStockTransferResultsCarryAccelerations = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let equitySourceSecurityId = "SEC-ACC-EC-TRANSFER-SOURCE" + equityResultSecurityId = "SEC-ACC-EC-TRANSFER-RESULT" + warrantSourceSecurityId = "SEC-ACC-WARRANT-TRANSFER-SOURCE" + warrantResultSecurityId = "SEC-ACC-WARRANT-TRANSFER-RESULT" + equitySourceIssuance = + (defaultEquityCompensationIssuance "TX-ACC-EC-TRANSFER-SOURCE" equitySourceSecurityId "SH-1") with + quantity = 100.0 + warrantSourceIssuance = + (defaultWarrantIssuance "TX-ACC-WARRANT-TRANSFER-SOURCE" warrantSourceSecurityId "SH-1") with + quantity = Some 100.0 + quantity_source = Some OcfQuantityInstrumentFixed + warrantResultIssuance = + (defaultWarrantIssuance "TX-ACC-WARRANT-TRANSFER-RESULT" warrantResultSecurityId "SH-1") with + date = postSplitAccelerationDate + quantity = Some 100.0 + quantity_source = Some OcfQuantityInstrumentFixed + issuanceResult <- applyUpdate + issuer + capTableCid + [ CT.OcfCreateEquityCompensationIssuance equitySourceIssuance + , CT.OcfCreateWarrantIssuance warrantSourceIssuance + ] + [] + [] + accelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [ CT.OcfCreateVestingAcceleration (acceleration "ACC-EC-TRANSFER-SOURCE" equitySourceSecurityId 50.0) + , CT.OcfCreateVestingAcceleration (acceleration "ACC-WARRANT-TRANSFER-SOURCE" warrantSourceSecurityId 50.0) + ] + [] + [] + transferResult <- applyUpdate + issuer + accelerationResult.updatedCapTableCid + [ equityCompensationSuccessorIssuance + "TX-ACC-EC-TRANSFER-RESULT" + equityResultSecurityId + "SH-1" + postSplitAccelerationDate + 100.0 + None + , CT.OcfCreateWarrantIssuance warrantResultIssuance + , CT.OcfCreateEquityCompensationTransfer EquityCompensationTransferOcfData with + id = "TRANSFER-ACC-EC-RESULT" + date = postSplitAccelerationDate + security_id = equitySourceSecurityId + quantity = 100.0 + resulting_security_ids = [equityResultSecurityId] + balance_security_id = None + consideration_text = None + comments = [] + , CT.OcfCreateWarrantTransfer WarrantTransferOcfData with + id = "TRANSFER-ACC-WARRANT-RESULT" + date = postSplitAccelerationDate + security_id = warrantSourceSecurityId + quantity = 100.0 + resulting_security_ids = [warrantResultSecurityId] + balance_security_id = None + consideration_text = None + comments = [] + ] + [] + [] + exactResult <- applyUpdate + issuer + transferResult.updatedCapTableCid + [ CT.OcfCreateVestingAcceleration ((acceleration "ACC-EC-TRANSFER-RESULT-EXACT" equityResultSecurityId 50.0) with + date = successorAccelerationDate) + , CT.OcfCreateVestingAcceleration ((acceleration "ACC-WARRANT-TRANSFER-RESULT-EXACT" warrantResultSecurityId 50.0) with + date = successorAccelerationDate) + ] + [] + [] + submitMustFail issuer do + exerciseCmd exactResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-EC-TRANSFER-RESULT-OVER" equityResultSecurityId 1.0) with + date = successorAccelerationDate)] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd exactResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-WARRANT-TRANSFER-RESULT-OVER" warrantResultSecurityId 1.0) with + date = successorAccelerationDate)] + edits = [] + deletes = [] + testVestingQuantityIntegrity_StockReductionConsumesPriorAccelerations = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableCid <- addPrerequisites issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar index eed307ee..dfa69d4e 100644 --- a/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00abc8ac918d4bce66df81762a28fb863bc91b2236b24782c7d81d083979635c -size 3400371 +oid sha256:b27d4b3003e6e051504c4cb2d5a69ae6117d06be8dca975cb013ab71a03fb9f9 +size 3410787 diff --git a/dars/dars.lock b/dars/dars.lock index 6417e60b..c0cfbd10 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -61,10 +61,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar": { - "sha256": "00abc8ac918d4bce66df81762a28fb863bc91b2236b24782c7d81d083979635c", - "size": 3400371, + "sha256": "b27d4b3003e6e051504c4cb2d5a69ae6117d06be8dca975cb013ab71a03fb9f9", + "size": 3410787, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T18:59:11.000Z", + "uploadedAt": "2026-07-11T21:46:52.000Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index c5614541..0a807068 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -232,6 +232,8 @@ data VestingAccelerationPredecessor = VestingAccelerationPredecessor with successor_fraction: Decimal deriving (Eq, Show) +type VestingAccelerationPredecessorIndex = Map Text [VestingAccelerationPredecessor] + data NonStockSecurityBalanceEventIndex = NonStockSecurityBalanceEventIndex with warrant_events_by_security_id: Map Text [SecurityQuantityEvent] equity_compensation_events_by_security_id: Map Text [SecurityQuantityEvent] @@ -2574,8 +2576,15 @@ warrantSecurityQuantityBeforeTerminal : Time -> Update (Optional Decimal) warrantSecurityQuantityBeforeTerminal eventIndex issuanceData eventDate = + warrantSecurityQuantityBeforeSortKey eventIndex issuanceData (eventDate, 2, 1) + +warrantSecurityQuantityBeforeSortKey : + NonStockSecurityBalanceEventIndex -> + WarrantIssuanceOcfData -> + (Time, Int, Int) -> + Update (Optional Decimal) +warrantSecurityQuantityBeforeSortKey eventIndex issuanceData targetSortKey = let events = warrantSecurityQuantityEvents eventIndex issuanceData - targetSortKey = (eventDate, 2, 1) in case issuanceData.quantity of Some quantity -> securityQuantityBeforeEvent @@ -2655,7 +2664,18 @@ equityCompensationSecurityQuantityBeforeTerminal : EquityCompensationIssuanceOcfData -> Time -> Update Decimal -equityCompensationSecurityQuantityBeforeTerminal stockEventIndex balanceEventIndex maps issuanceData eventDate = do +equityCompensationSecurityQuantityBeforeTerminal stockEventIndex balanceEventIndex maps issuanceData eventDate = + equityCompensationSecurityQuantityBeforeSortKey + stockEventIndex balanceEventIndex maps issuanceData (eventDate, 2, 1) + +equityCompensationSecurityQuantityBeforeSortKey : + StockClassShareEventIndex -> + NonStockSecurityBalanceEventIndex -> + CapTableMaps -> + EquityCompensationIssuanceOcfData -> + (Time, Int, Int) -> + Update Decimal +equityCompensationSecurityQuantityBeforeSortKey stockEventIndex balanceEventIndex maps issuanceData targetSortKey = do events <- equityCompensationSecurityQuantityEvents stockEventIndex balanceEventIndex maps issuanceData quantityOpt <- securityQuantityBeforeEvent issuanceData.security_id @@ -2665,7 +2685,7 @@ equityCompensationSecurityQuantityBeforeTerminal stockEventIndex balanceEventInd event_priority = 0 quantity_delta = issuanceData.quantity) :: events) - (eventDate, 2, 1) + targetSortKey case quantityOpt of Some quantity -> pure quantity None -> do @@ -4233,52 +4253,66 @@ insertVestingAccelerationPredecessor : Text -> Text -> VestingAccelerationPredecessor -> - Map Text VestingAccelerationPredecessor -> - Update (Map Text VestingAccelerationPredecessor) + VestingAccelerationPredecessorIndex -> + Update VestingAccelerationPredecessorIndex insertVestingAccelerationPredecessor label successorSecurityId predecessor index = do + let existingPredecessors = fromOptional [] (Map.lookup successorSecurityId index) assertMsg - (label <> " gives vesting acceleration successor " <> successorSecurityId <> " multiple predecessors") - (isNone (Map.lookup successorSecurityId index)) - pure (Map.insert successorSecurityId predecessor index) + (label <> " gives vesting acceleration successor " <> successorSecurityId <> " predecessors from different lifecycle events") + (all + (\existing -> existing.successor_seed_sort_key == predecessor.successor_seed_sort_key) + existingPredecessors) + assertMsg + (label <> " gives vesting acceleration successor " <> successorSecurityId <> " duplicate predecessor " <> predecessor.source_security_id) + (all + (\existing -> existing.source_security_id /= predecessor.source_security_id) + existingPredecessors) + pure (Map.insert successorSecurityId (predecessor :: existingPredecessors) index) -addStockBalanceVestingAccelerationPredecessor : +addStockVestingAccelerationPredecessorsAt : StockClassShareEventIndex -> CapTableMaps -> Text -> Text -> - Time -> - Optional Text -> - Map Text VestingAccelerationPredecessor -> - Update (Map Text VestingAccelerationPredecessor) -addStockBalanceVestingAccelerationPredecessor eventIndex maps label sourceSecurityId eventDate balanceSecurityId index = - case balanceSecurityId of - Some successorSecurityId -> case - ( Map.lookup sourceSecurityId maps.stock_issuances_by_security_id - , Map.lookup successorSecurityId maps.stock_issuances_by_security_id - ) of - (Some sourceIssuanceCid, Some successorIssuanceCid) -> do + (Time, Int, Int) -> + (Time, Int, Int) -> + [Text] -> + VestingAccelerationPredecessorIndex -> + Update VestingAccelerationPredecessorIndex +addStockVestingAccelerationPredecessorsAt eventIndex maps label sourceSecurityId sourceEventSortKey successorSeedSortKey successorSecurityIds index = + case successorSecurityIds of + [] -> pure index + _ -> case Map.lookup sourceSecurityId maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do sourceIssuance <- fetch sourceIssuanceCid - successorIssuance <- fetch successorIssuanceCid - let eventSortKey = (eventDate, 2, 1) - sourceQuantity <- stockSecurityQuantityBeforeEvent eventIndex maps sourceIssuance.issuance_data eventSortKey - successorQuantity <- stockIssuanceQuantityAtSortKey maps successorIssuance.issuance_data eventSortKey + sourceQuantity <- stockSecurityQuantityBeforeEvent + eventIndex maps sourceIssuance.issuance_data sourceEventSortKey assertMsg (label <> " has a non-positive source quantity") (sourceQuantity > 0.0) - assertMsg - (label <> " has an invalid balance successor quantity for vesting acceleration continuity") - (successorQuantity > 0.0 && successorQuantity <= sourceQuantity) - insertVestingAccelerationPredecessor - label - successorSecurityId - (VestingAccelerationPredecessor with - source_security_id = sourceSecurityId - source_event_sort_key = eventSortKey - successor_seed_sort_key = (eventDate, 0, 0) - successor_fraction = successorQuantity / sourceQuantity) + foldlA + (\currentIndex successorSecurityId -> + case Map.lookup successorSecurityId maps.stock_issuances_by_security_id of + Some successorIssuanceCid -> do + successorIssuance <- fetch successorIssuanceCid + successorQuantity <- stockIssuanceQuantityAtSortKey + maps successorIssuance.issuance_data sourceEventSortKey + assertMsg + (label <> " has a non-positive successor quantity for vesting acceleration continuity") + (successorQuantity > 0.0) + insertVestingAccelerationPredecessor + label + successorSecurityId + (VestingAccelerationPredecessor with + source_security_id = sourceSecurityId + source_event_sort_key = sourceEventSortKey + successor_seed_sort_key = successorSeedSortKey + successor_fraction = successorQuantity / sourceQuantity) + currentIndex + None -> pure currentIndex) index - _ -> pure index - None -> pure index + successorSecurityIds + None -> pure index -addEquityCompensationBalanceVestingAccelerationPredecessor : +addEquityCompensationVestingAccelerationPredecessors : StockClassShareEventIndex -> NonStockSecurityBalanceEventIndex -> CapTableMaps -> @@ -4286,154 +4320,251 @@ addEquityCompensationBalanceVestingAccelerationPredecessor : Text -> Time -> Optional Text -> - Map Text VestingAccelerationPredecessor -> - Update (Map Text VestingAccelerationPredecessor) -addEquityCompensationBalanceVestingAccelerationPredecessor stockEventIndex balanceEventIndex maps label sourceSecurityId eventDate balanceSecurityId index = - case balanceSecurityId of - Some successorSecurityId -> case - ( Map.lookup sourceSecurityId maps.equity_compensation_issuances_by_security_id - , Map.lookup successorSecurityId maps.equity_compensation_issuances_by_security_id - ) of - (Some sourceIssuanceCid, Some successorIssuanceCid) -> do + [Text] -> + VestingAccelerationPredecessorIndex -> + Update VestingAccelerationPredecessorIndex +addEquityCompensationVestingAccelerationPredecessors stockEventIndex balanceEventIndex maps label sourceSecurityId eventDate balanceSecurityId resultingSecurityIds index = + let successorSecurityIds = successorSecurityIdsFor balanceSecurityId resultingSecurityIds + sourceEventSortKey = stockReductionSortKey eventDate balanceSecurityId + in case successorSecurityIds of + [] -> pure index + _ -> case Map.lookup sourceSecurityId maps.equity_compensation_issuances_by_security_id of + Some sourceIssuanceCid -> do sourceIssuance <- fetch sourceIssuanceCid - successorIssuance <- fetch successorIssuanceCid - let eventSortKey = (eventDate, 2, 1) - sourceQuantity <- equityCompensationSecurityQuantityBeforeTerminal - stockEventIndex balanceEventIndex maps sourceIssuance.issuance_data eventDate - successorQuantity <- equityCompensationIssuanceQuantityAtSortKey maps successorIssuance.issuance_data eventSortKey + sourceQuantity <- equityCompensationSecurityQuantityBeforeSortKey + stockEventIndex balanceEventIndex maps sourceIssuance.issuance_data sourceEventSortKey assertMsg (label <> " has a non-positive source quantity") (sourceQuantity > 0.0) - assertMsg - (label <> " has an invalid balance successor quantity for vesting acceleration continuity") - (successorQuantity > 0.0 && successorQuantity <= sourceQuantity) - insertVestingAccelerationPredecessor - label - successorSecurityId - (VestingAccelerationPredecessor with - source_security_id = sourceSecurityId - source_event_sort_key = eventSortKey - successor_seed_sort_key = (eventDate, 0, 0) - successor_fraction = successorQuantity / sourceQuantity) + foldlA + (\currentIndex successorSecurityId -> + case Map.lookup successorSecurityId maps.equity_compensation_issuances_by_security_id of + Some successorIssuanceCid -> do + successorIssuance <- fetch successorIssuanceCid + successorQuantity <- equityCompensationIssuanceQuantityAtSortKey + maps successorIssuance.issuance_data sourceEventSortKey + assertMsg + (label <> " has an invalid successor quantity for vesting acceleration continuity") + (successorQuantity > 0.0 && successorQuantity <= sourceQuantity) + insertVestingAccelerationPredecessor + label + successorSecurityId + (VestingAccelerationPredecessor with + source_security_id = sourceSecurityId + source_event_sort_key = sourceEventSortKey + successor_seed_sort_key = (eventDate, 0, 0) + successor_fraction = successorQuantity / sourceQuantity) + currentIndex + None -> pure currentIndex) index - _ -> pure index - None -> pure index + successorSecurityIds + None -> pure index -addWarrantBalanceVestingAccelerationPredecessor : +addWarrantVestingAccelerationPredecessors : NonStockSecurityBalanceEventIndex -> CapTableMaps -> Text -> Text -> Time -> Optional Text -> - Map Text VestingAccelerationPredecessor -> - Update (Map Text VestingAccelerationPredecessor) -addWarrantBalanceVestingAccelerationPredecessor eventIndex maps label sourceSecurityId eventDate balanceSecurityId index = - case balanceSecurityId of - Some successorSecurityId -> case - ( Map.lookup sourceSecurityId maps.warrant_issuances_by_security_id - , Map.lookup successorSecurityId maps.warrant_issuances_by_security_id - ) of - (Some sourceIssuanceCid, Some successorIssuanceCid) -> do + [Text] -> + VestingAccelerationPredecessorIndex -> + Update VestingAccelerationPredecessorIndex +addWarrantVestingAccelerationPredecessors eventIndex maps label sourceSecurityId eventDate balanceSecurityId resultingSecurityIds index = + let successorSecurityIds = successorSecurityIdsFor balanceSecurityId resultingSecurityIds + sourceEventSortKey = stockReductionSortKey eventDate balanceSecurityId + in case successorSecurityIds of + [] -> pure index + _ -> case Map.lookup sourceSecurityId maps.warrant_issuances_by_security_id of + Some sourceIssuanceCid -> do sourceIssuance <- fetch sourceIssuanceCid - successorIssuance <- fetch successorIssuanceCid - sourceQuantityOpt <- warrantSecurityQuantityBeforeTerminal eventIndex sourceIssuance.issuance_data eventDate + sourceQuantityOpt <- warrantSecurityQuantityBeforeSortKey + eventIndex sourceIssuance.issuance_data sourceEventSortKey let sourceCeilingOpt = ocfQuantityUpperBound sourceQuantityOpt sourceIssuance.issuance_data.quantity_source - successorCeilingOpt = ocfQuantityUpperBound successorIssuance.issuance_data.quantity successorIssuance.issuance_data.quantity_source - eventSortKey = (eventDate, 2, 1) - case (sourceCeilingOpt, successorCeilingOpt) of - (Some sourceQuantity, Some successorQuantity) -> do + case sourceCeilingOpt of + Some sourceQuantity -> do assertMsg (label <> " has a non-positive source quantity") (sourceQuantity > 0.0) - assertMsg - (label <> " has an invalid balance successor quantity for vesting acceleration continuity") - (successorQuantity > 0.0 && successorQuantity <= sourceQuantity) - insertVestingAccelerationPredecessor - label - successorSecurityId - (VestingAccelerationPredecessor with - source_security_id = sourceSecurityId - source_event_sort_key = eventSortKey - successor_seed_sort_key = (eventDate, 0, 0) - successor_fraction = successorQuantity / sourceQuantity) + foldlA + (\currentIndex successorSecurityId -> + case Map.lookup successorSecurityId maps.warrant_issuances_by_security_id of + Some successorIssuanceCid -> do + successorIssuance <- fetch successorIssuanceCid + let successorCeilingOpt = ocfQuantityUpperBound + successorIssuance.issuance_data.quantity + successorIssuance.issuance_data.quantity_source + case successorCeilingOpt of + Some successorQuantity -> do + assertMsg + (label <> " has an invalid successor quantity for vesting acceleration continuity") + (successorQuantity > 0.0 && successorQuantity <= sourceQuantity) + insertVestingAccelerationPredecessor + label + successorSecurityId + (VestingAccelerationPredecessor with + source_security_id = sourceSecurityId + source_event_sort_key = sourceEventSortKey + successor_seed_sort_key = (eventDate, 0, 0) + successor_fraction = successorQuantity / sourceQuantity) + currentIndex + None -> pure currentIndex + None -> pure currentIndex) index - _ -> pure index - _ -> pure index + successorSecurityIds + None -> pure index + None -> pure index + +addStockConsolidationVestingAccelerationPredecessors : + StockClassShareEventIndex -> + CapTableMaps -> + Text -> + [Text] -> + Text -> + Time -> + VestingAccelerationPredecessorIndex -> + Update VestingAccelerationPredecessorIndex +addStockConsolidationVestingAccelerationPredecessors eventIndex maps label sourceSecurityIds successorSecurityId eventDate index = + case Map.lookup successorSecurityId maps.stock_issuances_by_security_id of + Some _ -> + let sourceEventSortKey = (eventDate, 3, 3) + in foldlA + (\currentIndex sourceSecurityId -> + case Map.lookup sourceSecurityId maps.stock_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + sourceQuantity <- stockSecurityQuantityBeforeEvent + eventIndex maps sourceIssuance.issuance_data sourceEventSortKey + assertMsg (label <> " has a non-positive source quantity") (sourceQuantity > 0.0) + insertVestingAccelerationPredecessor + label + successorSecurityId + (VestingAccelerationPredecessor with + source_security_id = sourceSecurityId + source_event_sort_key = sourceEventSortKey + successor_seed_sort_key = (eventDate, 0, 0) + successor_fraction = 1.0) + currentIndex + None -> pure currentIndex) + index + sourceSecurityIds None -> pure index vestingAccelerationPredecessorIndex : StockClassShareEventIndex -> NonStockSecurityBalanceEventIndex -> CapTableMaps -> - Update (Map Text VestingAccelerationPredecessor) + Update VestingAccelerationPredecessorIndex vestingAccelerationPredecessorIndex stockEventIndex balanceEventIndex maps = do stockCancellationIndex <- foldlA (\index cancellationCid -> do cancellation <- fetch cancellationCid - addStockBalanceVestingAccelerationPredecessor + addStockVestingAccelerationPredecessorsAt stockEventIndex maps ("Stock cancellation " <> cancellation.cancellation_data.id) - cancellation.cancellation_data.security_id cancellation.cancellation_data.date - cancellation.cancellation_data.balance_security_id index) + cancellation.cancellation_data.security_id + (cancellation.cancellation_data.date, 2, 1) + (cancellation.cancellation_data.date, 0, 0) + (successorSecurityIdsFor cancellation.cancellation_data.balance_security_id []) + index) Map.empty (Map.values maps.stock_cancellations) stockRepurchaseIndex <- foldlA (\index repurchaseCid -> do repurchase <- fetch repurchaseCid - addStockBalanceVestingAccelerationPredecessor + addStockVestingAccelerationPredecessorsAt stockEventIndex maps ("Stock repurchase " <> repurchase.repurchase_data.id) - repurchase.repurchase_data.security_id repurchase.repurchase_data.date - repurchase.repurchase_data.balance_security_id index) + repurchase.repurchase_data.security_id + (repurchase.repurchase_data.date, 2, 1) + (repurchase.repurchase_data.date, 0, 0) + (successorSecurityIdsFor repurchase.repurchase_data.balance_security_id []) + index) stockCancellationIndex (Map.values maps.stock_repurchases) stockTransferIndex <- foldlA (\index transferCid -> do transfer <- fetch transferCid - addStockBalanceVestingAccelerationPredecessor + let sourceEventSortKey = stockReductionSortKey + transfer.transfer_data.date transfer.transfer_data.balance_security_id + addStockVestingAccelerationPredecessorsAt stockEventIndex maps ("Stock transfer " <> transfer.transfer_data.id) - transfer.transfer_data.security_id transfer.transfer_data.date - transfer.transfer_data.balance_security_id index) + transfer.transfer_data.security_id + sourceEventSortKey + (transfer.transfer_data.date, 0, 0) + (successorSecurityIdsFor + transfer.transfer_data.balance_security_id + transfer.transfer_data.resulting_security_ids) + index) stockRepurchaseIndex (Map.values maps.stock_transfers) stockConversionIndex <- foldlA (\index conversionCid -> do conversion <- fetch conversionCid - addStockBalanceVestingAccelerationPredecessor + let sourceEventSortKey = stockReductionSortKey + conversion.conversion_data.date conversion.conversion_data.balance_security_id + addStockVestingAccelerationPredecessorsAt stockEventIndex maps ("Stock conversion " <> conversion.conversion_data.id) - conversion.conversion_data.security_id conversion.conversion_data.date - conversion.conversion_data.balance_security_id index) + conversion.conversion_data.security_id + sourceEventSortKey + (conversion.conversion_data.date, 0, 0) + (successorSecurityIdsFor + conversion.conversion_data.balance_security_id + conversion.conversion_data.resulting_security_ids) + index) stockTransferIndex (Map.values maps.stock_conversions) + stockReissuanceIndex <- foldlA + (\index reissuanceCid -> do + reissuance <- fetch reissuanceCid + addStockVestingAccelerationPredecessorsAt + stockEventIndex maps ("Stock reissuance " <> reissuance.reissuance_data.id) + reissuance.reissuance_data.security_id + (reissuance.reissuance_data.date, 3, 3) + (reissuance.reissuance_data.date, 0, 0) + reissuance.reissuance_data.resulting_security_ids + index) + stockConversionIndex + (Map.values maps.stock_reissuances) + stockConsolidationIndex <- foldlA + (\index consolidationCid -> do + consolidation <- fetch consolidationCid + addStockConsolidationVestingAccelerationPredecessors + stockEventIndex maps ("Stock consolidation " <> consolidation.consolidation_data.id) + consolidation.consolidation_data.security_ids + consolidation.consolidation_data.resulting_security_id + consolidation.consolidation_data.date + index) + stockReissuanceIndex + (Map.values maps.stock_consolidations) equityCancellationIndex <- foldlA (\index cancellationCid -> do cancellation <- fetch cancellationCid - addEquityCompensationBalanceVestingAccelerationPredecessor + addEquityCompensationVestingAccelerationPredecessors stockEventIndex balanceEventIndex maps ("Equity compensation cancellation " <> cancellation.cancellation_data.id) cancellation.cancellation_data.security_id cancellation.cancellation_data.date - cancellation.cancellation_data.balance_security_id index) - stockConversionIndex + cancellation.cancellation_data.balance_security_id [] index) + stockConsolidationIndex (Map.values maps.equity_compensation_cancellations) equityTransferIndex <- foldlA (\index transferCid -> do transfer <- fetch transferCid - addEquityCompensationBalanceVestingAccelerationPredecessor + addEquityCompensationVestingAccelerationPredecessors stockEventIndex balanceEventIndex maps ("Equity compensation transfer " <> transfer.transfer_data.id) transfer.transfer_data.security_id transfer.transfer_data.date - transfer.transfer_data.balance_security_id index) + transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids index) equityCancellationIndex (Map.values maps.equity_compensation_transfers) warrantCancellationIndex <- foldlA (\index cancellationCid -> do cancellation <- fetch cancellationCid - addWarrantBalanceVestingAccelerationPredecessor + addWarrantVestingAccelerationPredecessors balanceEventIndex maps ("Warrant cancellation " <> cancellation.cancellation_data.id) cancellation.cancellation_data.security_id cancellation.cancellation_data.date - cancellation.cancellation_data.balance_security_id index) + cancellation.cancellation_data.balance_security_id [] index) equityTransferIndex (Map.values maps.warrant_cancellations) foldlA (\index transferCid -> do transfer <- fetch transferCid - addWarrantBalanceVestingAccelerationPredecessor + addWarrantVestingAccelerationPredecessors balanceEventIndex maps ("Warrant transfer " <> transfer.transfer_data.id) transfer.transfer_data.security_id transfer.transfer_data.date - transfer.transfer_data.balance_security_id index) + transfer.transfer_data.balance_security_id transfer.transfer_data.resulting_security_ids index) warrantCancellationIndex (Map.values maps.warrant_transfers) @@ -4586,7 +4717,7 @@ vestingAccelerationInheritedSeed : NonStockSecurityBalanceEventIndex -> CapTableMaps -> Map Text [VestingAccelerationOcfData] -> - Map Text VestingAccelerationPredecessor -> + VestingAccelerationPredecessorIndex -> Set Text -> Text -> Update (Optional ((Time, Int, Int), Decimal)) @@ -4596,28 +4727,36 @@ vestingAccelerationInheritedSeed stockEventIndex balanceEventIndex maps accelera assertMsg ("Vesting acceleration successor lineage contains a cycle at " <> securityId) False pure None else case Map.lookup securityId predecessorIndex of - Some predecessor -> do - sourceTotal <- vestingAccelerationQuantityBefore - stockEventIndex - balanceEventIndex - maps - accelerationIndex - predecessorIndex - (Set.insert securityId visited) - predecessor.source_security_id - predecessor.source_event_sort_key - pure (Some - ( predecessor.successor_seed_sort_key - , sourceTotal * predecessor.successor_fraction - )) - None -> pure None + Some (firstPredecessor :: remainingPredecessors) -> do + let predecessors = firstPredecessor :: remainingPredecessors + assertMsg + ("Vesting acceleration successor lineage has inconsistent seed dates for " <> securityId) + (all + (\predecessor -> predecessor.successor_seed_sort_key == firstPredecessor.successor_seed_sort_key) + remainingPredecessors) + inheritedQuantity <- foldlA + (\total predecessor -> do + sourceTotal <- vestingAccelerationQuantityBefore + stockEventIndex + balanceEventIndex + maps + accelerationIndex + predecessorIndex + (Set.insert securityId visited) + predecessor.source_security_id + predecessor.source_event_sort_key + pure (total + sourceTotal * predecessor.successor_fraction)) + 0.0 + predecessors + pure (Some (firstPredecessor.successor_seed_sort_key, inheritedQuantity)) + _ -> pure None vestingAccelerationQuantityBefore : StockClassShareEventIndex -> NonStockSecurityBalanceEventIndex -> CapTableMaps -> Map Text [VestingAccelerationOcfData] -> - Map Text VestingAccelerationPredecessor -> + VestingAccelerationPredecessorIndex -> Set Text -> Text -> (Time, Int, Int) -> @@ -4750,7 +4889,7 @@ validateVestingAccelerationQuantities : NonStockSecurityBalanceEventIndex -> CapTableMaps -> Map Text [VestingAccelerationOcfData] -> - Map Text VestingAccelerationPredecessor -> + VestingAccelerationPredecessorIndex -> (Text, [VestingAccelerationOcfData]) -> Update () validateVestingAccelerationQuantities stockEventIndex balanceEventIndex maps accelerationIndex predecessorIndex (securityId, accelerations) = do From 3c8f12d65487f6615448df63d29e7648d4d6204d Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 18:40:17 -0400 Subject: [PATCH 08/10] fix: enforce vesting schedule eligibility --- .../OpenCapTable/TestVestingAcceleration.daml | 6 +- .../TestVestingQuantityIntegrity.daml | 382 +++++++++++++++++- .../0.0.17/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +- .../codegen/templates/CapTable.daml.template | 94 ++++- 5 files changed, 474 insertions(+), 18 deletions(-) diff --git a/Test/daml/OpenCapTable/TestVestingAcceleration.daml b/Test/daml/OpenCapTable/TestVestingAcceleration.daml index 99aa5f77..915620d9 100644 --- a/Test/daml/OpenCapTable/TestVestingAcceleration.daml +++ b/Test/daml/OpenCapTable/TestVestingAcceleration.daml @@ -3,7 +3,7 @@ module OpenCapTable.TestVestingAcceleration where import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance import Fairmint.OpenCapTable.OCF.VestingAcceleration import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) -import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..)) +import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..), OcfVesting(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup import OpenCapTable.TestHelpers (addDefaultStakeholder) @@ -35,7 +35,9 @@ createEquityCompensationIssuance issuer capTableCid securityId quantity = exercise_price = Some (OcfMonetary with amount = 1.25, currency = "USD") expiration_date = Some (DT.time (DA.date 2034 Jan 01) 0 0 0) termination_exercise_windows = [] - vestings = [] + vestings = [OcfVesting with + date = DT.time (DA.date 2030 Jan 01) 0 0 0 + amount = quantity] vesting_terms_id = None comments = []] edits = [] diff --git a/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml b/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml index d4396f63..5e1d69c7 100644 --- a/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml +++ b/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml @@ -2,34 +2,121 @@ module OpenCapTable.TestVestingQuantityIntegrity where import Fairmint.OpenCapTable.OCF.VestingAcceleration (VestingAccelerationOcfData(..)) import Fairmint.OpenCapTable.OCF.EquityCompensationCancellation (EquityCompensationCancellationOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationExercise (EquityCompensationExerciseOcfData(..)) +import Fairmint.OpenCapTable.OCF.EquityCompensationIssuance (EquityCompensationIssuanceOcfData) +import Fairmint.OpenCapTable.OCF.EquityCompensationRelease (EquityCompensationReleaseOcfData(..)) import Fairmint.OpenCapTable.OCF.EquityCompensationTransfer (EquityCompensationTransferOcfData(..)) import Fairmint.OpenCapTable.OCF.StockConsolidation (StockConsolidationOcfData(..)) import Fairmint.OpenCapTable.OCF.StockConversion (StockConversionOcfData(..)) +import Fairmint.OpenCapTable.OCF.StockIssuance (StockIssuanceOcfData) import Fairmint.OpenCapTable.OCF.StockReissuance (StockReissuanceOcfData(..)) import Fairmint.OpenCapTable.OCF.StockTransfer (StockTransferOcfData(..)) +import Fairmint.OpenCapTable.OCF.VestingTerms + ( OcfAllocationType(..) + , OcfVestingCondition(..) + , OcfVestingConditionPortion(..) + , OcfVestingTrigger(..) + , VestingTermsOcfData(..) + ) import Fairmint.OpenCapTable.OCF.WarrantCancellation (WarrantCancellationOcfData(..)) +import Fairmint.OpenCapTable.OCF.WarrantExercise (WarrantExerciseOcfData(..)) +import Fairmint.OpenCapTable.OCF.WarrantIssuance (WarrantIssuanceOcfData) import Fairmint.OpenCapTable.OCF.WarrantTransfer (WarrantTransferOcfData(..)) +import Fairmint.OpenCapTable.Types.Conversion + ( OcfAnyConversionRight(..) + , OcfConversionTrigger(..) + , OcfConversionTriggerType(..) + , OcfWarrantConversionMechanism(..) + , OcfWarrantConversionRight(..) + ) +import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Stock (OcfQuantitySourceType(..)) -import Fairmint.OpenCapTable.Types.Vesting (OcfVesting(..)) +import Fairmint.OpenCapTable.Types.Vesting (OcfCompensationType(..), OcfVesting(..)) import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup import OpenCapTable.TestHelpers - ( addPrerequisites - , defaultEquityCompensationIssuance - , defaultStockIssuance - , defaultStockPlan - , defaultWarrantIssuance - , equityCompensationSuccessorIssuance + ( defaultStockPlan , stockCancellationOn , stockClassSplitOn - , stockSuccessorIssuance ) -import OpenCapTable.TestStockConversion (addPreferredStockClass, createStockIssuance) +import qualified OpenCapTable.TestHelpers as TestHelpers +import OpenCapTable.TestStockConversion (addPreferredStockClass) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT import Daml.Script +accelerationVestingTermsId : Text +accelerationVestingTermsId = "VESTING-TERMS-ACCELERATION" + +accelerationVestingTerms : VestingTermsOcfData +accelerationVestingTerms = VestingTermsOcfData with + id = accelerationVestingTermsId + allocation_type = OcfAllocationFractional + description = "All shares follow one vesting condition" + name = "Acceleration test vesting terms" + comments = [] + vesting_conditions = [OcfVestingCondition with + id = "ACCELERATION-TEST-START" + trigger = OcfVestingStartTrigger + next_condition_ids = [] + description = None + portion = Some (OcfVestingConditionPortion with + numerator = 1.0 + denominator = 1.0 + remainder = False) + quantity = None] + +defaultStockIssuance : Text -> Text -> Text -> Text -> StockIssuanceOcfData +defaultStockIssuance issuanceId securityId stakeholderId stockClassId = + (TestHelpers.defaultStockIssuance issuanceId securityId stakeholderId stockClassId) with + vesting_terms_id = Some accelerationVestingTermsId + +defaultEquityCompensationIssuance : Text -> Text -> Text -> EquityCompensationIssuanceOcfData +defaultEquityCompensationIssuance issuanceId securityId stakeholderId = + (TestHelpers.defaultEquityCompensationIssuance issuanceId securityId stakeholderId) with + vesting_terms_id = Some accelerationVestingTermsId + +defaultWarrantIssuance : Text -> Text -> Text -> WarrantIssuanceOcfData +defaultWarrantIssuance issuanceId securityId stakeholderId = + (TestHelpers.defaultWarrantIssuance issuanceId securityId stakeholderId) with + vesting_terms_id = Some accelerationVestingTermsId + +stockSuccessorIssuance : Text -> Text -> Text -> Text -> Time -> Decimal -> CT.OcfCreateData +stockSuccessorIssuance issuanceId securityId stakeholderId stockClassId date quantity = + CT.OcfCreateStockIssuance ((defaultStockIssuance issuanceId securityId stakeholderId stockClassId) with + date = date + quantity = quantity) + +fullyVestedStockSuccessorIssuance : Text -> Text -> Text -> Text -> Time -> Decimal -> CT.OcfCreateData +fullyVestedStockSuccessorIssuance issuanceId securityId stakeholderId stockClassId date quantity = + CT.OcfCreateStockIssuance ((TestHelpers.defaultStockIssuance issuanceId securityId stakeholderId stockClassId) with + date = date + quantity = quantity) + +equityCompensationSuccessorIssuance : Text -> Text -> Text -> Time -> Decimal -> Optional Time -> CT.OcfCreateData +equityCompensationSuccessorIssuance issuanceId securityId stakeholderId date quantity expirationDate = + CT.OcfCreateEquityCompensationIssuance ((defaultEquityCompensationIssuance issuanceId securityId stakeholderId) with + date = date + quantity = quantity + expiration_date = expirationDate) + +warrantExerciseTrigger : Decimal -> OcfConversionTrigger +warrantExerciseTrigger quantity = OcfConversionTrigger with + conversion_right = OcfRightWarrant (OcfWarrantConversionRight with + conversion_mechanism = OcfWarrantMechanismFixedAmount with converts_to_quantity = quantity + 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 + firstVestingDate : Time firstVestingDate = DT.time (DA.date 2025 Jan 01) 0 0 0 @@ -82,6 +169,17 @@ applyUpdate issuer capTableCid creates edits deletes = edits = edits deletes = deletes +addPrerequisites : Party -> ContractId CT.CapTable -> Script (ContractId CT.CapTable) +addPrerequisites issuer capTableCid = do + capTableCid <- TestHelpers.addPrerequisites issuer capTableCid + result <- applyUpdate + issuer + capTableCid + [CT.OcfCreateVestingTerms accelerationVestingTerms] + [] + [] + pure result.updatedCapTableCid + assertAdditionalAccelerationRejected : Party -> ContractId CT.CapTable -> @@ -794,7 +892,11 @@ testVestingQuantityIntegrity_StockConversionResultCarriesAccelerations = script capTableCid <- addPreferredStockClass issuer capTableCid let sourceSecurityId = "SEC-ACC-STOCK-CONVERSION-SOURCE" resultSecurityId = "SEC-ACC-STOCK-CONVERSION-RESULT" - issuanceResult <- createStockIssuance issuer capTableCid sourceSecurityId 100.0 firstVestingDate + sourceIssuance = + (defaultStockIssuance "TX-ACC-STOCK-CONVERSION-SOURCE" sourceSecurityId "SH-1" "SC_PREF") with + quantity = 100.0 + date = firstVestingDate + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateStockIssuance sourceIssuance] [] [] accelerationResult <- applyUpdate issuer issuanceResult.updatedCapTableCid @@ -1199,3 +1301,263 @@ testVestingQuantityIntegrity_DeletingAccelerationReleasesCapacity = script do [] [CT.OcfDeleteVestingAcceleration "ACC-DELETE-OLD"] pure () + +testVestingQuantityIntegrity_FullyVestedSecuritiesCannotBeAccelerated = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let stockIssuance = + TestHelpers.defaultStockIssuance "TX-ACC-FULLY-VESTED-STOCK" "SEC-ACC-FULLY-VESTED-STOCK" "SH-1" "SC_COMMON" + equityCompensationIssuance = + TestHelpers.defaultEquityCompensationIssuance "TX-ACC-FULLY-VESTED-EC" "SEC-ACC-FULLY-VESTED-EC" "SH-1" + warrantIssuance = + (TestHelpers.defaultWarrantIssuance "TX-ACC-FULLY-VESTED-WARRANT" "SEC-ACC-FULLY-VESTED-WARRANT" "SH-1") with + quantity_source = Some OcfQuantityInstrumentFixed + issuanceResult <- applyUpdate + issuer + capTableCid + [ CT.OcfCreateStockIssuance stockIssuance + , CT.OcfCreateEquityCompensationIssuance equityCompensationIssuance + , CT.OcfCreateWarrantIssuance warrantIssuance + ] + [] + [] + submitMustFail issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration (acceleration "ACC-FULLY-VESTED-STOCK" stockIssuance.security_id 1.0)] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration (acceleration "ACC-FULLY-VESTED-EC" equityCompensationIssuance.security_id 1.0)] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration (acceleration "ACC-FULLY-VESTED-WARRANT" warrantIssuance.security_id 1.0)] + edits = [] + deletes = [] + +testVestingQuantityIntegrity_RemovingScheduleRequiresRemovingAccelerations = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let issuance = defaultStockIssuance "TX-ACC-SCHEDULE-REMOVAL" "SEC-ACC-SCHEDULE-REMOVAL" "SH-1" "SC_COMMON" + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateStockIssuance issuance] [] [] + accelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-SCHEDULE-REMOVAL" issuance.security_id 50.0)] + [] + [] + let fullyVestedIssuance = issuance with + vesting_terms_id = None + vestings = [] + submitMustFail issuer do + exerciseCmd accelerationResult.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockIssuance fullyVestedIssuance] + deletes = [] + _ <- applyUpdate + issuer + accelerationResult.updatedCapTableCid + [] + [CT.OcfEditStockIssuance fullyVestedIssuance] + [CT.OcfDeleteVestingAcceleration "ACC-SCHEDULE-REMOVAL"] + pure () + +testVestingQuantityIntegrity_EarlyExerciseRetainsScheduleAndAccelerationHistory = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let sourceSecurityId = "SEC-ACC-EARLY-EXERCISE-SOURCE" + invalidResultSecurityId = "SEC-ACC-EARLY-EXERCISE-FULLY-VESTED" + resultSecurityId = "SEC-ACC-EARLY-EXERCISE-RESULT" + sourceIssuance = + (defaultEquityCompensationIssuance "TX-ACC-EARLY-EXERCISE-SOURCE" sourceSecurityId "SH-1") with + early_exercisable = Some True + stock_class_id = Some "SC_COMMON" + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateEquityCompensationIssuance sourceIssuance] [] [] + accelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-EARLY-EXERCISE-SOURCE" sourceSecurityId 60.0)] + [] + [] + submitMustFail issuer do + exerciseCmd accelerationResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateEquityCompensationExercise EquityCompensationExerciseOcfData with + id = "TX-ACC-EARLY-EXERCISE-UNMATERIALIZED" + date = postSplitAccelerationDate + security_id = sourceSecurityId + quantity = 40.0 + consideration_text = None + resulting_security_ids = [] + comments = []] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd accelerationResult.updatedCapTableCid CT.UpdateCapTable with + creates = + [ fullyVestedStockSuccessorIssuance + "TX-ACC-EARLY-EXERCISE-FULLY-VESTED" + invalidResultSecurityId + "SH-1" + "SC_COMMON" + postSplitAccelerationDate + 40.0 + , CT.OcfCreateEquityCompensationExercise EquityCompensationExerciseOcfData with + id = "TX-ACC-EARLY-EXERCISE-INVALID" + date = postSplitAccelerationDate + security_id = sourceSecurityId + quantity = 40.0 + consideration_text = None + resulting_security_ids = [invalidResultSecurityId] + comments = [] + ] + edits = [] + deletes = [] + exerciseResult <- applyUpdate + issuer + accelerationResult.updatedCapTableCid + [ stockSuccessorIssuance + "TX-ACC-EARLY-EXERCISE-RESULT" + resultSecurityId + "SH-1" + "SC_COMMON" + postSplitAccelerationDate + 40.0 + , CT.OcfCreateEquityCompensationExercise EquityCompensationExerciseOcfData with + id = "TX-ACC-EARLY-EXERCISE" + date = postSplitAccelerationDate + security_id = sourceSecurityId + quantity = 40.0 + consideration_text = None + resulting_security_ids = [resultSecurityId] + comments = [] + ] + [] + [] + exactResult <- applyUpdate + issuer + exerciseResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration ((acceleration "ACC-EARLY-EXERCISE-RESULT-EXACT" resultSecurityId 16.0) with + date = successorAccelerationDate)] + [] + [] + submitMustFail issuer do + exerciseCmd exactResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-EARLY-EXERCISE-RESULT-OVER" resultSecurityId 1.0) with + date = finalSuccessorAccelerationDate)] + edits = [] + deletes = [] + +testVestingQuantityIntegrity_FullyVestedExerciseReleaseAndWarrantResultsCannotBeAccelerated = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let exerciseSourceSecurityId = "SEC-ACC-EXERCISE-SOURCE" + releaseSourceSecurityId = "SEC-ACC-RELEASE-SOURCE" + warrantSourceSecurityId = "SEC-ACC-WARRANT-EXERCISE-SOURCE" + exerciseResultSecurityId = "SEC-ACC-EXERCISE-RESULT" + releaseResultSecurityId = "SEC-ACC-RELEASE-RESULT" + warrantResultSecurityId = "SEC-ACC-WARRANT-EXERCISE-RESULT" + exerciseSource = + (defaultEquityCompensationIssuance "TX-ACC-EXERCISE-SOURCE" exerciseSourceSecurityId "SH-1") with + early_exercisable = Some False + stock_class_id = Some "SC_COMMON" + releaseSource = + (defaultEquityCompensationIssuance "TX-ACC-RELEASE-SOURCE" releaseSourceSecurityId "SH-1") with + compensation_type = OcfCompensationTypeRSU + exercise_price = None + stock_class_id = Some "SC_COMMON" + warrantSource = + (defaultWarrantIssuance "TX-ACC-WARRANT-EXERCISE-SOURCE" warrantSourceSecurityId "SH-1") with + exercise_triggers = [warrantExerciseTrigger 100.0] + quantity_source = Some OcfQuantityInstrumentFixed + issuanceResult <- applyUpdate + issuer + capTableCid + [ CT.OcfCreateEquityCompensationIssuance exerciseSource + , CT.OcfCreateEquityCompensationIssuance releaseSource + , CT.OcfCreateWarrantIssuance warrantSource + ] + [] + [] + accelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [ CT.OcfCreateVestingAcceleration (acceleration "ACC-EXERCISE-SOURCE" exerciseSourceSecurityId 60.0) + , CT.OcfCreateVestingAcceleration (acceleration "ACC-RELEASE-SOURCE" releaseSourceSecurityId 60.0) + , CT.OcfCreateVestingAcceleration (acceleration "ACC-WARRANT-EXERCISE-SOURCE" warrantSourceSecurityId 60.0) + ] + [] + [] + result <- applyUpdate + issuer + accelerationResult.updatedCapTableCid + [ fullyVestedStockSuccessorIssuance + "TX-ACC-EXERCISE-RESULT" + exerciseResultSecurityId + "SH-1" + "SC_COMMON" + postSplitAccelerationDate + 100.0 + , fullyVestedStockSuccessorIssuance + "TX-ACC-RELEASE-RESULT" + releaseResultSecurityId + "SH-1" + "SC_COMMON" + successorAccelerationDate + 100.0 + , fullyVestedStockSuccessorIssuance + "TX-ACC-WARRANT-EXERCISE-RESULT" + warrantResultSecurityId + "SH-1" + "SC_COMMON" + postSplitAccelerationDate + 100.0 + , CT.OcfCreateEquityCompensationExercise EquityCompensationExerciseOcfData with + id = "TX-ACC-EXERCISE" + date = postSplitAccelerationDate + security_id = exerciseSourceSecurityId + quantity = 100.0 + consideration_text = None + resulting_security_ids = [exerciseResultSecurityId] + comments = [] + , CT.OcfCreateEquityCompensationRelease EquityCompensationReleaseOcfData with + id = "TX-ACC-RELEASE" + date = postSplitAccelerationDate + security_id = releaseSourceSecurityId + settlement_date = successorAccelerationDate + release_price = OcfMonetary with amount = 1.0; currency = "USD" + quantity = 100.0 + resulting_security_ids = [releaseResultSecurityId] + consideration_text = None + comments = [] + , CT.OcfCreateWarrantExercise WarrantExerciseOcfData with + id = "TX-ACC-WARRANT-EXERCISE" + date = postSplitAccelerationDate + security_id = warrantSourceSecurityId + trigger_id = "ELECTIVE_AT_WILL" + quantity = Some 100.0 + resulting_security_ids = [warrantResultSecurityId] + comments = [] + consideration_text = None + ] + [] + [] + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-EXERCISE-RESULT" exerciseResultSecurityId 1.0) with + date = finalSuccessorAccelerationDate)] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-RELEASE-RESULT" releaseResultSecurityId 1.0) with + date = finalSuccessorAccelerationDate)] + edits = [] + deletes = [] + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-WARRANT-EXERCISE-RESULT" warrantResultSecurityId 1.0) with + date = finalSuccessorAccelerationDate)] + edits = [] + deletes = [] diff --git a/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar index dfa69d4e..372fae7c 100644 --- a/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b27d4b3003e6e051504c4cb2d5a69ae6117d06be8dca975cb013ab71a03fb9f9 -size 3410787 +oid sha256:790047340fb41ca577aa9de9bf7f486850d49a8ae178c81ff66393b9f5db5869 +size 3418954 diff --git a/dars/dars.lock b/dars/dars.lock index c0cfbd10..ff952fbe 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -61,10 +61,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar": { - "sha256": "b27d4b3003e6e051504c4cb2d5a69ae6117d06be8dca975cb013ab71a03fb9f9", - "size": 3410787, + "sha256": "790047340fb41ca577aa9de9bf7f486850d49a8ae178c81ff66393b9f5db5869", + "size": 3418954, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T21:46:52.000Z", + "uploadedAt": "2026-07-11T22:36:35.000Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 0a807068..2ab5920b 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -846,6 +846,10 @@ equityCompensationExerciseOutputShareLot : EquityCompensationExerciseOcfData -> Update (Optional StockClassShareLot) equityCompensationExerciseOutputShareLot maps issuanceData exerciseData = do + when (issuanceData.early_exercisable == Some True) do + assertMsg + ("Early equity compensation exercise must materialize resulting stock so its vesting schedule remains in effect: " <> exerciseData.id) + False stockClassIdOpt <- equityCompensationOutputStockClassId maps ("Equity compensation exercise " <> exerciseData.id) @@ -923,6 +927,30 @@ validateStockResultIssuances maps label expectedDate expectedStockClassIdOpt exp (actualQuantity == expectedQuantity) None -> pure () +hasDeclaredVestingSchedule : Optional Text -> [OcfVesting] -> Bool +hasDeclaredVestingSchedule vestingTermsId vestings = + isSome vestingTermsId || not (null vestings) + +validateEarlyExerciseResultVestingSchedules : + CapTableMaps -> + Text -> + EquityCompensationIssuanceOcfData -> + [Text] -> + Update () +validateEarlyExerciseResultVestingSchedules maps label issuanceData securityIds = + when (issuanceData.early_exercisable == Some True) do + mapA_ + (\securityId -> case Map.lookup securityId maps.stock_issuances_by_security_id of + Some stockIssuanceCid -> do + stockIssuance <- fetch stockIssuanceCid + assertMsg + (label <> " result must retain a vesting schedule for early-exercisable grant: " <> securityId) + (hasDeclaredVestingSchedule + stockIssuance.issuance_data.vesting_terms_id + stockIssuance.issuance_data.vestings) + None -> pure ()) + securityIds + validateStockConversionResultIssuances : CapTableMaps -> StockConversionOcfData -> @@ -1004,6 +1032,11 @@ validateEquityCompensationExerciseResultIssuances maps issuanceData exerciseData expectedStockClassId (Some exerciseData.quantity) exerciseData.resulting_security_ids + validateEarlyExerciseResultVestingSchedules + maps + label + issuanceData + exerciseData.resulting_security_ids validateEquityCompensationReleaseResultIssuances : CapTableMaps -> EquityCompensationIssuanceOcfData -> EquityCompensationReleaseOcfData -> Update () validateEquityCompensationReleaseResultIssuances maps issuanceData releaseData = do @@ -4411,6 +4444,49 @@ addWarrantVestingAccelerationPredecessors eventIndex maps label sourceSecurityId None -> pure index None -> pure index +addEarlyExerciseStockVestingAccelerationPredecessors : + StockClassShareEventIndex -> + NonStockSecurityBalanceEventIndex -> + CapTableMaps -> + EquityCompensationExerciseOcfData -> + VestingAccelerationPredecessorIndex -> + Update VestingAccelerationPredecessorIndex +addEarlyExerciseStockVestingAccelerationPredecessors stockEventIndex balanceEventIndex maps exerciseData index = + case Map.lookup exerciseData.security_id maps.equity_compensation_issuances_by_security_id of + Some sourceIssuanceCid -> do + sourceIssuance <- fetch sourceIssuanceCid + case sourceIssuance.issuance_data.early_exercisable of + Some True -> do + let label = "Equity compensation exercise " <> exerciseData.id + sourceEventSortKey = (exerciseData.date, 1, 0) + sourceQuantity <- equityCompensationSecurityQuantityBeforeSortKey + stockEventIndex balanceEventIndex maps sourceIssuance.issuance_data sourceEventSortKey + assertMsg (label <> " has a non-positive source quantity") (sourceQuantity > 0.0) + foldlA + (\currentIndex successorSecurityId -> + case Map.lookup successorSecurityId maps.stock_issuances_by_security_id of + Some successorIssuanceCid -> do + successorIssuance <- fetch successorIssuanceCid + successorQuantity <- stockIssuanceQuantityAtSortKey + maps successorIssuance.issuance_data sourceEventSortKey + assertMsg + (label <> " has an invalid successor quantity for vesting acceleration continuity") + (successorQuantity > 0.0 && successorQuantity <= sourceQuantity) + insertVestingAccelerationPredecessor + label + successorSecurityId + (VestingAccelerationPredecessor with + source_security_id = exerciseData.security_id + source_event_sort_key = sourceEventSortKey + successor_seed_sort_key = (exerciseData.date, 0, 0) + successor_fraction = successorQuantity / sourceQuantity) + currentIndex + None -> pure currentIndex) + index + exerciseData.resulting_security_ids + _ -> pure index + None -> pure index + addStockConsolidationVestingAccelerationPredecessors : StockClassShareEventIndex -> CapTableMaps -> @@ -4531,6 +4607,13 @@ vestingAccelerationPredecessorIndex stockEventIndex balanceEventIndex maps = do index) stockReissuanceIndex (Map.values maps.stock_consolidations) + earlyExerciseIndex <- foldlA + (\index exerciseCid -> do + exercise <- fetch exerciseCid + addEarlyExerciseStockVestingAccelerationPredecessors + stockEventIndex balanceEventIndex maps exercise.exercise_data index) + stockConsolidationIndex + (Map.values maps.equity_compensation_exercises) equityCancellationIndex <- foldlA (\index cancellationCid -> do cancellation <- fetch cancellationCid @@ -4538,7 +4621,7 @@ vestingAccelerationPredecessorIndex stockEventIndex balanceEventIndex maps = do stockEventIndex balanceEventIndex maps ("Equity compensation cancellation " <> cancellation.cancellation_data.id) cancellation.cancellation_data.security_id cancellation.cancellation_data.date cancellation.cancellation_data.balance_security_id [] index) - stockConsolidationIndex + earlyExerciseIndex (Map.values maps.equity_compensation_cancellations) equityTransferIndex <- foldlA (\index transferCid -> do @@ -4898,10 +4981,16 @@ validateVestingAccelerationQuantities stockEventIndex balanceEventIndex maps acc case Map.lookup securityId maps.stock_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid + assertMsg + ("Vesting acceleration requires stock security to declare vesting terms or inline vestings: " <> securityId) + (hasDeclaredVestingSchedule issuance.issuance_data.vesting_terms_id issuance.issuance_data.vestings) validateStockVestingAccelerationQuantities stockEventIndex maps issuance.issuance_data inheritedSeed accelerations None -> case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid + assertMsg + ("Vesting acceleration requires equity compensation security to declare vesting terms or inline vestings: " <> securityId) + (hasDeclaredVestingSchedule issuance.issuance_data.vesting_terms_id issuance.issuance_data.vestings) validateEquityCompensationVestingAccelerationQuantities stockEventIndex balanceEventIndex @@ -4912,6 +5001,9 @@ validateVestingAccelerationQuantities stockEventIndex balanceEventIndex maps acc None -> case Map.lookup securityId maps.warrant_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid + assertMsg + ("Vesting acceleration requires warrant security to declare vesting terms or inline vestings: " <> securityId) + (hasDeclaredVestingSchedule issuance.issuance_data.vesting_terms_id issuance.issuance_data.vestings) validateWarrantVestingAccelerationQuantities balanceEventIndex issuance.issuance_data inheritedSeed accelerations None -> assertMsg From 3dfef07b7488b3487e4e99a03a62b678f683652b Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 19:26:01 -0400 Subject: [PATCH 09/10] fix: preserve vesting continuity on exercise --- .../TestVestingQuantityIntegrity.daml | 340 +++++++++++++++++- .../0.0.17/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 4 +- .../codegen/templates/CapTable.daml.template | 291 ++++++++++++--- 4 files changed, 582 insertions(+), 57 deletions(-) diff --git a/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml b/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml index 5e1d69c7..41936bce 100644 --- a/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml +++ b/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml @@ -44,6 +44,7 @@ import OpenCapTable.TestStockConversion (addPreferredStockClass) import qualified DA.Date as DA import DA.Date (Month(..)) import qualified DA.Time as DT +import DA.Foldable (mapA_) import Daml.Script accelerationVestingTermsId : Text @@ -67,6 +68,14 @@ accelerationVestingTerms = VestingTermsOcfData with remainder = False) quantity = None] +alternateAccelerationVestingTermsId : Text +alternateAccelerationVestingTermsId = "VESTING-TERMS-ACCELERATION-ALTERNATE" + +alternateAccelerationVestingTerms : VestingTermsOcfData +alternateAccelerationVestingTerms = accelerationVestingTerms with + id = alternateAccelerationVestingTermsId + name = "Alternate acceleration test vesting terms" + defaultStockIssuance : Text -> Text -> Text -> Text -> StockIssuanceOcfData defaultStockIssuance issuanceId securityId stakeholderId stockClassId = (TestHelpers.defaultStockIssuance issuanceId securityId stakeholderId stockClassId) with @@ -126,6 +135,9 @@ secondVestingDate = DT.time (DA.date 2026 Jan 01) 0 0 0 accelerationDate : Time accelerationDate = DT.time (DA.date 2025 Jun 01) 0 0 0 +earlyExerciseDate : Time +earlyExerciseDate = DT.time (DA.date 2025 Sep 01) 0 0 0 + splitDate : Time splitDate = DT.time (DA.date 2026 Jan 01) 0 0 0 @@ -1369,12 +1381,20 @@ testVestingQuantityIntegrity_EarlyExerciseRetainsScheduleAndAccelerationHistory capTableCid <- addPrerequisites issuer cap_table let sourceSecurityId = "SEC-ACC-EARLY-EXERCISE-SOURCE" invalidResultSecurityId = "SEC-ACC-EARLY-EXERCISE-FULLY-VESTED" + mismatchedResultSecurityId = "SEC-ACC-EARLY-EXERCISE-MISMATCHED-SCHEDULE" resultSecurityId = "SEC-ACC-EARLY-EXERCISE-RESULT" sourceIssuance = (defaultEquityCompensationIssuance "TX-ACC-EARLY-EXERCISE-SOURCE" sourceSecurityId "SH-1") with early_exercisable = Some True stock_class_id = Some "SC_COMMON" - issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateEquityCompensationIssuance sourceIssuance] [] [] + issuanceResult <- applyUpdate + issuer + capTableCid + [ CT.OcfCreateVestingTerms alternateAccelerationVestingTerms + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + [] + [] accelerationResult <- applyUpdate issuer issuanceResult.updatedCapTableCid @@ -1414,6 +1434,28 @@ testVestingQuantityIntegrity_EarlyExerciseRetainsScheduleAndAccelerationHistory ] edits = [] deletes = [] + submitMustFail issuer do + exerciseCmd accelerationResult.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance ((defaultStockIssuance + "TX-ACC-EARLY-EXERCISE-MISMATCHED-SCHEDULE" + mismatchedResultSecurityId + "SH-1" + "SC_COMMON") with + date = postSplitAccelerationDate + quantity = 40.0 + vesting_terms_id = Some alternateAccelerationVestingTermsId) + , CT.OcfCreateEquityCompensationExercise EquityCompensationExerciseOcfData with + id = "TX-ACC-EARLY-EXERCISE-MISMATCHED-SCHEDULE" + date = postSplitAccelerationDate + security_id = sourceSecurityId + quantity = 40.0 + consideration_text = None + resulting_security_ids = [mismatchedResultSecurityId] + comments = [] + ] + edits = [] + deletes = [] exerciseResult <- applyUpdate issuer accelerationResult.updatedCapTableCid @@ -1435,9 +1477,22 @@ testVestingQuantityIntegrity_EarlyExerciseRetainsScheduleAndAccelerationHistory ] [] [] - exactResult <- applyUpdate + submitMustFail issuer do + exerciseCmd exerciseResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-EARLY-EXERCISE-SOURCE-OVER" sourceSecurityId 25.0) with + date = successorAccelerationDate)] + edits = [] + deletes = [] + sourceExactResult <- applyUpdate issuer exerciseResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration ((acceleration "ACC-EARLY-EXERCISE-SOURCE-EXACT" sourceSecurityId 24.0) with + date = successorAccelerationDate)] + [] + [] + exactResult <- applyUpdate + issuer + sourceExactResult.updatedCapTableCid [CT.OcfCreateVestingAcceleration ((acceleration "ACC-EARLY-EXERCISE-RESULT-EXACT" resultSecurityId 16.0) with date = successorAccelerationDate)] [] @@ -1449,6 +1504,177 @@ testVestingQuantityIntegrity_EarlyExerciseRetainsScheduleAndAccelerationHistory edits = [] deletes = [] +testVestingQuantityIntegrity_EarlyExerciseRetainsInlineScheduleProRata = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let sourceSecurityId = "SEC-ACC-EARLY-INLINE-SOURCE" + invalidResultSecurityId = "SEC-ACC-EARLY-INLINE-INVALID" + resultSecurityId = "SEC-ACC-EARLY-INLINE-RESULT" + sourceIssuance = + (TestHelpers.defaultEquityCompensationIssuance "TX-ACC-EARLY-INLINE-SOURCE" sourceSecurityId "SH-1") with + early_exercisable = Some True + stock_class_id = Some "SC_COMMON" + vestings = [vestingOn firstVestingDate 40.0, vestingOn secondVestingDate 60.0] + invalidResultIssuance = + (TestHelpers.defaultStockIssuance "TX-ACC-EARLY-INLINE-INVALID" invalidResultSecurityId "SH-1" "SC_COMMON") with + date = earlyExerciseDate + quantity = 40.0 + vestings = [vestingOn firstVestingDate 20.0, vestingOn secondVestingDate 20.0] + resultIssuance = + (TestHelpers.defaultStockIssuance "TX-ACC-EARLY-INLINE-RESULT" resultSecurityId "SH-1" "SC_COMMON") with + date = earlyExerciseDate + quantity = 40.0 + vestings = [vestingOn firstVestingDate 16.0, vestingOn secondVestingDate 24.0] + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateEquityCompensationIssuance sourceIssuance] [] [] + accelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-EARLY-INLINE-SOURCE" sourceSecurityId 60.0)] + [] + [] + submitMustFail issuer do + exerciseCmd accelerationResult.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockIssuance invalidResultIssuance + , CT.OcfCreateEquityCompensationExercise EquityCompensationExerciseOcfData with + id = "TX-ACC-EARLY-INLINE-INVALID" + date = earlyExerciseDate + security_id = sourceSecurityId + quantity = 40.0 + consideration_text = None + resulting_security_ids = [invalidResultSecurityId] + comments = [] + ] + edits = [] + deletes = [] + exerciseResult <- applyUpdate + issuer + accelerationResult.updatedCapTableCid + [ CT.OcfCreateStockIssuance resultIssuance + , CT.OcfCreateEquityCompensationExercise EquityCompensationExerciseOcfData with + id = "TX-ACC-EARLY-INLINE" + date = earlyExerciseDate + security_id = sourceSecurityId + quantity = 40.0 + consideration_text = None + resulting_security_ids = [resultSecurityId] + comments = [] + ] + [] + [] + submitMustFail issuer do + exerciseCmd exerciseResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-EARLY-INLINE-RESULT-OVER" resultSecurityId 1.0) with + date = earlyExerciseDate)] + edits = [] + deletes = [] + +testVestingQuantityIntegrity_FullyVestedEarlyExercisableGrantCanSettleWithoutSchedule = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let sourceSecurityId = "SEC-ACC-FULLY-VESTED-EARLY-FLAG" + sourceIssuance = + (TestHelpers.defaultEquityCompensationIssuance "TX-ACC-FULLY-VESTED-EARLY-FLAG" sourceSecurityId "SH-1") with + early_exercisable = Some True + stock_class_id = Some "SC_COMMON" + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateEquityCompensationIssuance sourceIssuance] [] [] + _ <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [CT.OcfCreateEquityCompensationExercise EquityCompensationExerciseOcfData with + id = "TX-ACC-FULLY-VESTED-EARLY-FLAG-EXERCISE" + date = postSplitAccelerationDate + security_id = sourceSecurityId + quantity = 100.0 + consideration_text = None + resulting_security_ids = [] + comments = []] + [] + [] + pure () + +testVestingQuantityIntegrity_InlineAccelerationsCannotExceedFutureSchedule = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let stockIssuance = + (defaultStockIssuance "TX-ACC-INLINE-FUTURE-STOCK" "SEC-ACC-INLINE-FUTURE-STOCK" "SH-1" "SC_COMMON") with + vestings = [vestingOn firstVestingDate 40.0, vestingOn secondVestingDate 60.0] + equityCompensationIssuance = + (defaultEquityCompensationIssuance "TX-ACC-INLINE-FUTURE-EC" "SEC-ACC-INLINE-FUTURE-EC" "SH-1") with + stock_class_id = Some "SC_COMMON" + vestings = [vestingOn firstVestingDate 40.0, vestingOn secondVestingDate 60.0] + warrantIssuance = + (defaultWarrantIssuance "TX-ACC-INLINE-FUTURE-WARRANT" "SEC-ACC-INLINE-FUTURE-WARRANT" "SH-1") with + quantity = Some 100.0 + quantity_source = Some OcfQuantityInstrumentFixed + vestings = [vestingOn firstVestingDate 40.0, vestingOn secondVestingDate 60.0] + issuanceResult <- applyUpdate + issuer + capTableCid + [ CT.OcfCreateStockIssuance stockIssuance + , CT.OcfCreateEquityCompensationIssuance equityCompensationIssuance + , CT.OcfCreateWarrantIssuance warrantIssuance + ] + [] + [] + mapA_ + (\securityId -> submitMustFail issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration (acceleration ("ACC-INLINE-FUTURE-OVER-" <> securityId) securityId 61.0)] + edits = [] + deletes = []) + [stockIssuance.security_id, equityCompensationIssuance.security_id, warrantIssuance.security_id] + _ <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [ CT.OcfCreateVestingAcceleration (acceleration "ACC-INLINE-FUTURE-STOCK-EXACT" stockIssuance.security_id 60.0) + , CT.OcfCreateVestingAcceleration (acceleration "ACC-INLINE-FUTURE-EC-EXACT" equityCompensationIssuance.security_id 60.0) + , CT.OcfCreateVestingAcceleration (acceleration "ACC-INLINE-FUTURE-WARRANT-EXACT" warrantIssuance.security_id 60.0) + ] + [] + [] + pure () + +testVestingQuantityIntegrity_InheritedOnlySuccessorCannotDropSchedule = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let sourceSecurityId = "SEC-ACC-INHERITED-ONLY-SOURCE" + resultSecurityId = "SEC-ACC-INHERITED-ONLY-RESULT" + sourceIssuance = defaultStockIssuance "TX-ACC-INHERITED-ONLY-SOURCE" sourceSecurityId "SH-1" "SC_COMMON" + resultIssuance = (defaultStockIssuance "TX-ACC-INHERITED-ONLY-RESULT" resultSecurityId "SH-1" "SC_COMMON") with + date = postSplitAccelerationDate + fullyVestedResultIssuance = resultIssuance with + vesting_terms_id = None + vestings = [] + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateStockIssuance sourceIssuance] [] [] + accelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-INHERITED-ONLY-SOURCE" sourceSecurityId 80.0)] + [] + [] + transferResult <- applyUpdate + issuer + accelerationResult.updatedCapTableCid + [ CT.OcfCreateStockIssuance resultIssuance + , CT.OcfCreateStockTransfer StockTransferOcfData with + id = "TRANSFER-ACC-INHERITED-ONLY" + date = postSplitAccelerationDate + security_id = sourceSecurityId + quantity = 100.0 + resulting_security_ids = [resultSecurityId] + balance_security_id = None + consideration_text = None + comments = [] + ] + [] + [] + submitMustFail issuer do + exerciseCmd transferResult.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockIssuance fullyVestedResultIssuance] + deletes = [] + testVestingQuantityIntegrity_FullyVestedExerciseReleaseAndWarrantResultsCannotBeAccelerated = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableCid <- addPrerequisites issuer cap_table @@ -1561,3 +1787,113 @@ testVestingQuantityIntegrity_FullyVestedExerciseReleaseAndWarrantResultsCannotBe date = finalSuccessorAccelerationDate)] edits = [] deletes = [] + +testVestingQuantityIntegrity_PlanLinkedSuccessorIsNotDoubleScaledAcrossSameDaySplit = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let plan = defaultStockPlan "PLAN-ACC-SAME-DAY-SUCCESSOR" "SC_COMMON" + sourceSecurityId = "SEC-ACC-PLAN-SAME-DAY-SOURCE" + successorSecurityId = "SEC-ACC-PLAN-SAME-DAY-SUCCESSOR" + sourceIssuance = + (defaultEquityCompensationIssuance "TX-ACC-PLAN-SAME-DAY-SOURCE" sourceSecurityId "SH-1") with + quantity = 100.0 + stock_plan_id = Some plan.id + successorIssuance = + (defaultEquityCompensationIssuance "TX-ACC-PLAN-SAME-DAY-SUCCESSOR" successorSecurityId "SH-1") with + date = splitDate + quantity = 20.0 + stock_plan_id = Some plan.id + issuanceResult <- applyUpdate + issuer + capTableCid + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateEquityCompensationIssuance sourceIssuance + ] + [] + [] + accelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-PLAN-SAME-DAY-SOURCE" sourceSecurityId 50.0)] + [] + [] + successorResult <- applyUpdate + issuer + accelerationResult.updatedCapTableCid + [ CT.OcfCreateStockClassSplit (stockClassSplitOn "SPLIT-ACC-PLAN-SAME-DAY" "SC_COMMON" 2.0 1.0 splitDate) + , CT.OcfCreateEquityCompensationIssuance successorIssuance + , CT.OcfCreateEquityCompensationCancellation EquityCompensationCancellationOcfData with + id = "CANCEL-ACC-PLAN-SAME-DAY" + date = splitDate + security_id = sourceSecurityId + quantity = 80.0 + reason_text = "Partial cancellation after split" + balance_security_id = Some successorSecurityId + comments = [] + ] + [] + [] + exactResult <- applyUpdate + issuer + successorResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration ((acceleration "ACC-PLAN-SAME-DAY-SUCCESSOR-EXACT" successorSecurityId 20.0) with + date = successorAccelerationDate)] + [] + [] + submitMustFail issuer do + exerciseCmd exactResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-PLAN-SAME-DAY-SUCCESSOR-OVER" successorSecurityId 1.0) with + date = finalSuccessorAccelerationDate)] + edits = [] + deletes = [] + +testVestingQuantityIntegrity_ScheduledOrdinaryExerciseResultHasIndependentAccelerationHistory = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let sourceSecurityId = "SEC-ACC-ORDINARY-EXERCISE-SOURCE" + resultSecurityId = "SEC-ACC-ORDINARY-EXERCISE-RESULT" + sourceIssuance = + (defaultEquityCompensationIssuance "TX-ACC-ORDINARY-EXERCISE-SOURCE" sourceSecurityId "SH-1") with + early_exercisable = Some False + stock_class_id = Some "SC_COMMON" + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateEquityCompensationIssuance sourceIssuance] [] [] + accelerationResult <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration (acceleration "ACC-ORDINARY-EXERCISE-SOURCE" sourceSecurityId 60.0)] + [] + [] + exerciseResult <- applyUpdate + issuer + accelerationResult.updatedCapTableCid + [ stockSuccessorIssuance + "TX-ACC-ORDINARY-EXERCISE-RESULT" + resultSecurityId + "SH-1" + "SC_COMMON" + postSplitAccelerationDate + 100.0 + , CT.OcfCreateEquityCompensationExercise EquityCompensationExerciseOcfData with + id = "TX-ACC-ORDINARY-EXERCISE" + date = postSplitAccelerationDate + security_id = sourceSecurityId + quantity = 100.0 + consideration_text = None + resulting_security_ids = [resultSecurityId] + comments = [] + ] + [] + [] + exactResult <- applyUpdate + issuer + exerciseResult.updatedCapTableCid + [CT.OcfCreateVestingAcceleration ((acceleration "ACC-ORDINARY-EXERCISE-RESULT-EXACT" resultSecurityId 100.0) with + date = successorAccelerationDate)] + [] + [] + submitMustFail issuer do + exerciseCmd exactResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-ORDINARY-EXERCISE-RESULT-OVER" resultSecurityId 1.0) with + date = finalSuccessorAccelerationDate)] + edits = [] + deletes = [] diff --git a/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar index 372fae7c..bd5e16fd 100644 --- a/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:790047340fb41ca577aa9de9bf7f486850d49a8ae178c81ff66393b9f5db5869 -size 3418954 +oid sha256:26ea67433f003d79624701b08e6d59c0678983644974bb3accb9d15f50369307 +size 3439979 diff --git a/dars/dars.lock b/dars/dars.lock index ff952fbe..83c05464 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -61,8 +61,8 @@ "networks": [] }, "OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar": { - "sha256": "790047340fb41ca577aa9de9bf7f486850d49a8ae178c81ff66393b9f5db5869", - "size": 3418954, + "sha256": "26ea67433f003d79624701b08e6d59c0678983644974bb3accb9d15f50369307", + "size": 3439979, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-11T22:36:35.000Z", "networks": [] diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 2ab5920b..aee5b221 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, unique) import DA.Set (Set) import qualified DA.Set as Set -import DA.Action (foldlA, when) +import DA.Action (foldlA, unless, when) import DA.Foldable (mapA_) import DA.Optional (fromOptional, isNone, isSome) @@ -208,6 +208,10 @@ data SecurityQuantityEvent | SecurityQuantityRetraction with event_date: Time event_priority: Int + | SecurityQuantityEarlyExercise with + event_date: Time + event_priority: Int + quantity: Decimal deriving (Eq, Show) data VestingAccelerationTimelineEvent @@ -846,7 +850,7 @@ equityCompensationExerciseOutputShareLot : EquityCompensationExerciseOcfData -> Update (Optional StockClassShareLot) equityCompensationExerciseOutputShareLot maps issuanceData exerciseData = do - when (issuanceData.early_exercisable == Some True) do + when (hasEarlyExerciseVestingContinuity issuanceData) do assertMsg ("Early equity compensation exercise must materialize resulting stock so its vesting schedule remains in effect: " <> exerciseData.id) False @@ -931,6 +935,15 @@ hasDeclaredVestingSchedule : Optional Text -> [OcfVesting] -> Bool hasDeclaredVestingSchedule vestingTermsId vestings = isSome vestingTermsId || not (null vestings) +hasEarlyExerciseVestingContinuity : EquityCompensationIssuanceOcfData -> Bool +hasEarlyExerciseVestingContinuity issuanceData = + issuanceData.early_exercisable == Some True && + hasDeclaredVestingSchedule issuanceData.vesting_terms_id issuanceData.vestings + +scaledEarlyExerciseVestings : Decimal -> Decimal -> [OcfVesting] -> [OcfVesting] +scaledEarlyExerciseVestings sourceQuantity resultQuantity = + map (\vesting -> vesting with amount = vesting.amount * resultQuantity / sourceQuantity) + validateEarlyExerciseResultVestingSchedules : CapTableMaps -> Text -> @@ -938,16 +951,24 @@ validateEarlyExerciseResultVestingSchedules : [Text] -> Update () validateEarlyExerciseResultVestingSchedules maps label issuanceData securityIds = - when (issuanceData.early_exercisable == Some True) do + when (hasEarlyExerciseVestingContinuity issuanceData) do + assertMsg + (label <> " source must have a positive quantity for early-exercise vesting continuity") + (issuanceData.quantity > 0.0) mapA_ (\securityId -> case Map.lookup securityId maps.stock_issuances_by_security_id of Some stockIssuanceCid -> do stockIssuance <- fetch stockIssuanceCid assertMsg - (label <> " result must retain a vesting schedule for early-exercisable grant: " <> securityId) - (hasDeclaredVestingSchedule - stockIssuance.issuance_data.vesting_terms_id - stockIssuance.issuance_data.vestings) + (label <> " result must retain the source vesting terms for early-exercisable grant: " <> securityId) + (stockIssuance.issuance_data.vesting_terms_id == issuanceData.vesting_terms_id) + assertMsg + (label <> " result must retain the source inline vesting schedule pro rata for early-exercisable grant: " <> securityId) + (stockIssuance.issuance_data.vestings == + scaledEarlyExerciseVestings + issuanceData.quantity + stockIssuance.issuance_data.quantity + issuanceData.vestings) None -> pure ()) securityIds @@ -2069,6 +2090,7 @@ validateStockBalanceSuccessorQuantities eventIndex maps = do securityQuantityEventSortKey : SecurityQuantityEvent -> (Time, Int, Int) securityQuantityEventSortKey event = case event of SecurityQuantityDelta with event_date; event_priority -> (event_date, event_priority, 0) + SecurityQuantityEarlyExercise 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) @@ -2076,6 +2098,7 @@ securityQuantityEventSortKey event = case event of securityQuantityEventDate : SecurityQuantityEvent -> Time securityQuantityEventDate event = case event of SecurityQuantityDelta with event_date -> event_date + SecurityQuantityEarlyExercise with event_date -> event_date SecurityQuantityTerminalDelta with event_date -> event_date SecurityQuantityMultiplier with event_date -> event_date SecurityQuantityRetraction with event_date -> event_date @@ -2096,6 +2119,10 @@ validateSecurityQuantityBalanceEvent securityId currentOpt event = do assertMsg ("Security reduction quantity cannot be negative for " <> securityId <> ": derived delta is " <> show quantity_delta) (event_priority == 0 || quantity_delta <= 0.0) + SecurityQuantityEarlyExercise with quantity -> + assertMsg + ("Early exercise quantity cannot be negative for " <> securityId <> ": " <> show quantity) + (quantity >= 0.0) SecurityQuantityTerminalDelta with quantity_delta -> assertMsg ("Security terminal reduction quantity cannot be negative for " <> securityId <> ": derived delta is " <> show quantity_delta) @@ -2109,6 +2136,12 @@ validateSecurityQuantityBalanceEvent securityId currentOpt event = do ("Security balance cannot be negative for " <> securityId <> ": applying delta " <> show quantity_delta <> " leaves " <> show next) (next >= 0.0) pure (Some next) + SecurityQuantityEarlyExercise with quantity -> do + let next = current - quantity + assertMsg + ("Security balance cannot be negative for " <> securityId <> ": applying early exercise " <> show quantity <> " leaves " <> show next) + (next >= 0.0) + pure (Some next) SecurityQuantityTerminalDelta with quantity_delta -> do let next = current + quantity_delta assertMsg @@ -2128,6 +2161,8 @@ validateSecurityQuantityBalanceEvent securityId currentOpt event = do None -> case event of SecurityQuantityDelta {} -> pure None + SecurityQuantityEarlyExercise {} -> + pure None SecurityQuantityTerminalDelta {} -> pure (Some 0.0) SecurityQuantityMultiplier {} -> @@ -2560,12 +2595,26 @@ nonStockSecurityBalanceEventIndex maps = do assertNonNegativeQuantity ("Equity compensation exercise quantity for " <> exercise.exercise_data.id) exercise.exercise_data.quantity - pure (addSecurityQuantityEvent - exercise.exercise_data.security_id - (SecurityQuantityDelta with + event <- case Map.lookup exercise.exercise_data.security_id maps.equity_compensation_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + pure $ + if hasEarlyExerciseVestingContinuity issuance.issuance_data + then SecurityQuantityEarlyExercise with + event_date = exercise.exercise_data.date + event_priority = 1 + quantity = exercise.exercise_data.quantity + else SecurityQuantityDelta with + event_date = exercise.exercise_data.date + event_priority = 1 + quantity_delta = 0.0 - exercise.exercise_data.quantity + None -> pure SecurityQuantityDelta with event_date = exercise.exercise_data.date event_priority = 1 - quantity_delta = 0.0 - exercise.exercise_data.quantity) + quantity_delta = 0.0 - exercise.exercise_data.quantity + pure (addSecurityQuantityEvent + exercise.exercise_data.security_id + event index)) equityCancellationEvents (Map.values maps.equity_compensation_exercises) @@ -4455,8 +4504,8 @@ addEarlyExerciseStockVestingAccelerationPredecessors stockEventIndex balanceEven case Map.lookup exerciseData.security_id maps.equity_compensation_issuances_by_security_id of Some sourceIssuanceCid -> do sourceIssuance <- fetch sourceIssuanceCid - case sourceIssuance.issuance_data.early_exercisable of - Some True -> do + if hasEarlyExerciseVestingContinuity sourceIssuance.issuance_data + then do let label = "Equity compensation exercise " <> exerciseData.id sourceEventSortKey = (exerciseData.date, 1, 0) sourceQuantity <- equityCompensationSecurityQuantityBeforeSortKey @@ -4484,7 +4533,7 @@ addEarlyExerciseStockVestingAccelerationPredecessors stockEventIndex balanceEven None -> pure currentIndex) index exerciseData.resulting_security_ids - _ -> pure index + else pure index None -> pure index addStockConsolidationVestingAccelerationPredecessors : @@ -4674,10 +4723,17 @@ vestingAccelerationTimelineEvents inheritedSeed accelerations = (vestingAccelerationBuckets accelerations) in case inheritedSeed of Some (seedSortKey, seedQuantity) -> - (VestingAccelerationQuantity with - event_sort_key = seedSortKey - quantity = seedQuantity - report_total = False) :: directEvents + let (seedDate, _, _) = seedSortKey + in + [ VestingAccelerationQuantity with + event_sort_key = seedSortKey + quantity = seedQuantity + report_total = False + , VestingAccelerationQuantity with + event_sort_key = vestingAccelerationEvaluationSortKey seedDate + quantity = 0.0 + report_total = True + ] ++ directEvents None -> directEvents vestingAccelerationTimelineEventSortKey : VestingAccelerationTimelineEvent -> (Time, Int, Int) @@ -4782,6 +4838,7 @@ securityVestingAccelerationTimelineEvents quantityEvents inheritedSeed accelerat event_sort_key = (event_date, event_priority, 0) quantity_delta] else [] + SecurityQuantityEarlyExercise {} -> [] SecurityQuantityTerminalDelta with event_date; event_priority -> [VestingAccelerationRetirement with event_sort_key = (event_date, event_priority, 1)] @@ -4795,6 +4852,80 @@ securityVestingAccelerationTimelineEvents quantityEvents inheritedSeed accelerat quantityEvents in securityEvents ++ vestingAccelerationTimelineEvents inheritedSeed accelerations +securityQuantityEarlyExerciseBuckets : [SecurityQuantityEvent] -> [((Time, Int), Decimal)] +securityQuantityEarlyExerciseBuckets events = + sortOn + fst + (Map.toList (Map.fromListWithR (+) (concatMap + (\event -> case event of + SecurityQuantityEarlyExercise with event_date; event_priority; quantity -> + [((event_date, event_priority), quantity)] + _ -> []) + events))) + +equityCompensationVestingAccelerationTimelineEvents : + StockClassShareEventIndex -> + NonStockSecurityBalanceEventIndex -> + CapTableMaps -> + EquityCompensationIssuanceOcfData -> + Optional ((Time, Int, Int), Decimal) -> + [VestingAccelerationOcfData] -> + Update [VestingAccelerationTimelineEvent] +equityCompensationVestingAccelerationTimelineEvents stockEventIndex balanceEventIndex maps issuanceData inheritedSeed accelerations = do + quantityEvents <- equityCompensationSecurityQuantityEvents stockEventIndex balanceEventIndex maps issuanceData + earlyExerciseEvents <- foldlA + (\timelineEvents ((eventDate, eventPriority), exerciseQuantity) -> do + liveQuantityOpt <- securityQuantityBeforeEvent + issuanceData.security_id + (Some 0.0) + ((SecurityQuantityDelta with + event_date = issuanceData.date + event_priority = 0 + quantity_delta = issuanceData.quantity) + :: quantityEvents) + (eventDate, eventPriority, 0) + liveQuantity <- case liveQuantityOpt of + Some quantity -> pure quantity + None -> do + assertMsg ("Equity compensation quantity unexpectedly became unknown: " <> issuanceData.security_id) False + pure 0.0 + assertMsg + ("Early exercise requires a positive source quantity for vesting continuity: " <> issuanceData.security_id) + (liveQuantity > 0.0) + assertMsg + ("Early exercise quantity exceeds its source quantity for vesting continuity: " <> issuanceData.security_id) + (exerciseQuantity <= liveQuantity) + let ratio = OcfRatio with + numerator = liveQuantity - exerciseQuantity + denominator = liveQuantity + timelineEvent = VestingAccelerationSplit with + event_sort_key = (eventDate, eventPriority, 0) + ratio + pure (timelineEvent :: timelineEvents)) + [] + (securityQuantityEarlyExerciseBuckets quantityEvents) + let otherSecurityEvents = concatMap + (\event -> case event of + SecurityQuantityDelta with event_date; event_priority; quantity_delta -> + if quantity_delta < 0.0 + then [VestingAccelerationReduction with + event_sort_key = (event_date, event_priority, 1) + quantity_delta] + else [] + SecurityQuantityEarlyExercise {} -> [] + SecurityQuantityTerminalDelta with event_date; event_priority -> + [VestingAccelerationRetirement with + event_sort_key = (event_date, event_priority, 2)] + SecurityQuantityMultiplier with event_date; event_priority; ratio -> + [VestingAccelerationSplit with + event_sort_key = (event_date, event_priority, 3) + ratio] + SecurityQuantityRetraction with event_date; event_priority -> + [VestingAccelerationRetirement with + event_sort_key = (event_date, event_priority, 4)]) + quantityEvents + pure (earlyExerciseEvents ++ otherSecurityEvents ++ vestingAccelerationTimelineEvents inheritedSeed accelerations) + vestingAccelerationInheritedSeed : StockClassShareEventIndex -> NonStockSecurityBalanceEventIndex -> @@ -4861,9 +4992,8 @@ vestingAccelerationQuantityBefore stockEventIndex balanceEventIndex maps acceler None -> case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid - quantityEvents <- equityCompensationSecurityQuantityEvents - stockEventIndex balanceEventIndex maps issuance.issuance_data - let events = securityVestingAccelerationTimelineEvents quantityEvents inheritedSeed accelerations + events <- equityCompensationVestingAccelerationTimelineEvents + stockEventIndex balanceEventIndex maps issuance.issuance_data inheritedSeed accelerations pure (vestingAccelerationTimelineTotalBefore targetSortKey events) None -> case Map.lookup securityId maps.warrant_issuances_by_security_id of Some issuanceCid -> do @@ -4878,6 +5008,13 @@ vestingAccelerationQuantityBefore stockEventIndex balanceEventIndex maps acceler assertMsg ("Vesting acceleration predecessor security not found: " <> securityId) False pure 0.0 +inlineVestingQuantityAfter : Time -> [OcfVesting] -> Decimal +inlineVestingQuantityAfter date vestings = + foldl + (\total vesting -> if vesting.date > date then total + vesting.amount else total) + 0.0 + vestings + validateStockVestingAccelerationQuantities : StockClassShareEventIndex -> CapTableMaps -> @@ -4892,7 +5029,17 @@ validateStockVestingAccelerationQuantities eventIndex maps issuanceData inherite liveQuantity <- stockSecurityQuantityBeforeEvent eventIndex maps issuanceData targetSortKey assertMsg ("Vesting accelerations exceed live stock security quantity for " <> issuanceData.security_id <> " at " <> show accelerationDate) - (acceleratedQuantity <= liveQuantity)) + (acceleratedQuantity <= liveQuantity) + unless (null issuanceData.vestings) do + futureVestingQuantity <- stockClassQuantityAtSortKey + maps + issuanceData.stock_class_id + issuanceData.date + (inlineVestingQuantityAfter accelerationDate issuanceData.vestings) + targetSortKey + assertMsg + ("Vesting accelerations exceed stock quantity scheduled after " <> show accelerationDate <> " for " <> issuanceData.security_id) + (acceleratedQuantity <= futureVestingQuantity)) (stockVestingAccelerationTotals eventIndex issuanceData inheritedSeed accelerations) validateEquityCompensationVestingAccelerationQuantities : @@ -4905,8 +5052,9 @@ validateEquityCompensationVestingAccelerationQuantities : Update () validateEquityCompensationVestingAccelerationQuantities stockEventIndex balanceEventIndex maps issuanceData inheritedSeed accelerations = do quantityEvents <- equityCompensationSecurityQuantityEvents stockEventIndex balanceEventIndex maps issuanceData - let accelerationTotals = vestingAccelerationTimelineTotals - (securityVestingAccelerationTimelineEvents quantityEvents inheritedSeed accelerations) + timelineEvents <- equityCompensationVestingAccelerationTimelineEvents + stockEventIndex balanceEventIndex maps issuanceData inheritedSeed accelerations + let accelerationTotals = vestingAccelerationTimelineTotals timelineEvents mapA_ (\(accelerationDate, acceleratedQuantity) -> do let targetSortKey = vestingAccelerationEvaluationSortKey accelerationDate @@ -4926,7 +5074,30 @@ validateEquityCompensationVestingAccelerationQuantities stockEventIndex balanceE pure 0.0 assertMsg ("Vesting accelerations exceed live equity compensation security quantity for " <> issuanceData.security_id <> " at " <> show accelerationDate) - (acceleratedQuantity <= liveQuantity)) + (acceleratedQuantity <= liveQuantity) + unless (null issuanceData.vestings) do + let splitEvents = filter + (\event -> case event of + SecurityQuantityMultiplier {} -> True + _ -> False) + quantityEvents + futureVestingQuantityOpt <- securityQuantityBeforeEvent + issuanceData.security_id + (Some 0.0) + ((SecurityQuantityDelta with + event_date = issuanceData.date + event_priority = 0 + quantity_delta = inlineVestingQuantityAfter accelerationDate issuanceData.vestings) + :: splitEvents) + targetSortKey + futureVestingQuantity <- case futureVestingQuantityOpt of + Some quantity -> pure quantity + None -> do + assertMsg ("Equity compensation inline vesting quantity unexpectedly became unknown: " <> issuanceData.security_id) False + pure 0.0 + assertMsg + ("Vesting accelerations exceed equity compensation quantity scheduled after " <> show accelerationDate <> " for " <> issuanceData.security_id) + (acceleratedQuantity <= futureVestingQuantity)) accelerationTotals validateWarrantVestingAccelerationQuantities : @@ -4963,7 +5134,11 @@ validateWarrantVestingAccelerationQuantities eventIndex issuanceData inheritedSe pure 0.0 assertMsg ("Vesting accelerations exceed live warrant security quantity for " <> issuanceData.security_id <> " at " <> show accelerationDate) - (acceleratedQuantity <= liveQuantity)) + (acceleratedQuantity <= liveQuantity) + unless (null issuanceData.vestings) do + assertMsg + ("Vesting accelerations exceed warrant quantity scheduled after " <> show accelerationDate <> " for " <> issuanceData.security_id) + (acceleratedQuantity <= inlineVestingQuantityAfter accelerationDate issuanceData.vestings)) accelerationTotals None -> pure () @@ -4978,37 +5153,41 @@ validateVestingAccelerationQuantities : validateVestingAccelerationQuantities stockEventIndex balanceEventIndex maps accelerationIndex predecessorIndex (securityId, accelerations) = do inheritedSeed <- vestingAccelerationInheritedSeed stockEventIndex balanceEventIndex maps accelerationIndex predecessorIndex Set.empty securityId - case Map.lookup securityId maps.stock_issuances_by_security_id of - Some issuanceCid -> do - issuance <- fetch issuanceCid - assertMsg - ("Vesting acceleration requires stock security to declare vesting terms or inline vestings: " <> securityId) - (hasDeclaredVestingSchedule issuance.issuance_data.vesting_terms_id issuance.issuance_data.vestings) - validateStockVestingAccelerationQuantities stockEventIndex maps issuance.issuance_data inheritedSeed accelerations - None -> case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of + let hasPositiveInheritedSeed = case inheritedSeed of + Some (_, quantity) -> quantity > 0.0 + None -> False + when (not (null accelerations) || hasPositiveInheritedSeed) do + case Map.lookup securityId maps.stock_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid assertMsg - ("Vesting acceleration requires equity compensation security to declare vesting terms or inline vestings: " <> securityId) + ("Vesting acceleration requires stock security to declare vesting terms or inline vestings: " <> securityId) (hasDeclaredVestingSchedule issuance.issuance_data.vesting_terms_id issuance.issuance_data.vestings) - validateEquityCompensationVestingAccelerationQuantities - stockEventIndex - balanceEventIndex - maps - issuance.issuance_data - inheritedSeed - accelerations - None -> case Map.lookup securityId maps.warrant_issuances_by_security_id of + validateStockVestingAccelerationQuantities stockEventIndex maps issuance.issuance_data inheritedSeed accelerations + None -> case Map.lookup securityId maps.equity_compensation_issuances_by_security_id of Some issuanceCid -> do issuance <- fetch issuanceCid assertMsg - ("Vesting acceleration requires warrant security to declare vesting terms or inline vestings: " <> securityId) + ("Vesting acceleration requires equity compensation security to declare vesting terms or inline vestings: " <> securityId) (hasDeclaredVestingSchedule issuance.issuance_data.vesting_terms_id issuance.issuance_data.vestings) - validateWarrantVestingAccelerationQuantities balanceEventIndex issuance.issuance_data inheritedSeed accelerations - None -> - assertMsg - ("Vesting accelerations reference a missing or unsupported security: " <> securityId) - False + validateEquityCompensationVestingAccelerationQuantities + stockEventIndex + balanceEventIndex + maps + issuance.issuance_data + inheritedSeed + accelerations + None -> case Map.lookup securityId maps.warrant_issuances_by_security_id of + Some issuanceCid -> do + issuance <- fetch issuanceCid + assertMsg + ("Vesting acceleration requires warrant security to declare vesting terms or inline vestings: " <> securityId) + (hasDeclaredVestingSchedule issuance.issuance_data.vesting_terms_id issuance.issuance_data.vestings) + validateWarrantVestingAccelerationQuantities balanceEventIndex issuance.issuance_data inheritedSeed accelerations + None -> + assertMsg + ("Vesting accelerations reference a missing or unsupported security: " <> securityId) + False validateVestingQuantityIntegrity : StockClassShareEventIndex -> @@ -5019,9 +5198,19 @@ validateVestingQuantityIntegrity stockEventIndex balanceEventIndex maps = do validateInlineVestingQuantities maps accelerationIndex <- vestingAccelerationsBySecurityId maps predecessorIndex <- vestingAccelerationPredecessorIndex stockEventIndex balanceEventIndex maps + let securityIds = Map.keys (Map.fromList (map + (\securityId -> (securityId, True)) + (Map.keys accelerationIndex ++ Map.keys predecessorIndex))) mapA_ - (validateVestingAccelerationQuantities stockEventIndex balanceEventIndex maps accelerationIndex predecessorIndex) - (Map.toList accelerationIndex) + (\securityId -> + validateVestingAccelerationQuantities + stockEventIndex + balanceEventIndex + maps + accelerationIndex + predecessorIndex + (securityId, fromOptional [] (Map.lookup securityId accelerationIndex))) + securityIds validateTransactionLifecycleDates : CapTableMaps -> Update () validateTransactionLifecycleDates maps = do From b4e81c2f81dcc93e6b2869c5c5ba5df0d8222442 Mon Sep 17 00:00:00 2001 From: HardlyDifficult Date: Sat, 11 Jul 2026 19:55:57 -0400 Subject: [PATCH 10/10] fix: align inline vesting caps with quantity events --- .../TestVestingQuantityIntegrity.daml | 37 +++++++++++++++++++ .../0.0.17/OpenCapTable-v34.dar | 4 +- dars/dars.lock | 6 +-- .../codegen/templates/CapTable.daml.template | 32 +++++++--------- 4 files changed, 55 insertions(+), 24 deletions(-) diff --git a/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml b/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml index 41936bce..ca07fa32 100644 --- a/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml +++ b/Test/daml/OpenCapTable/TestVestingQuantityIntegrity.daml @@ -1562,6 +1562,12 @@ testVestingQuantityIntegrity_EarlyExerciseRetainsInlineScheduleProRata = script ] [] [] + submitMustFail issuer do + exerciseCmd exerciseResult.updatedCapTableCid CT.UpdateCapTable with + creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-EARLY-INLINE-SOURCE-OVER" sourceSecurityId 1.0) with + date = earlyExerciseDate)] + edits = [] + deletes = [] submitMustFail issuer do exerciseCmd exerciseResult.updatedCapTableCid CT.UpdateCapTable with creates = [CT.OcfCreateVestingAcceleration ((acceleration "ACC-EARLY-INLINE-RESULT-OVER" resultSecurityId 1.0) with @@ -1635,6 +1641,37 @@ testVestingQuantityIntegrity_InlineAccelerationsCannotExceedFutureSchedule = scr [] pure () +testVestingQuantityIntegrity_StockInlineCapAppliesSameDaySplit = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableCid <- addPrerequisites issuer cap_table + let securityId = "SEC-ACC-INLINE-SAME-DAY-SPLIT" + issuance = + (TestHelpers.defaultStockIssuance "TX-ACC-INLINE-SAME-DAY-SPLIT" securityId "SH-1" "SC_COMMON") with + date = splitDate + quantity = 100.0 + vestings = [vestingOn successorAccelerationDate 100.0] + sameDayAcceleration quantity = + (acceleration "ACC-INLINE-SAME-DAY-SPLIT" securityId quantity) with + date = splitDate + issuanceResult <- applyUpdate issuer capTableCid [CT.OcfCreateStockIssuance issuance] [] [] + submitMustFail issuer do + exerciseCmd issuanceResult.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassSplit (stockClassSplitOn "SPLIT-ACC-INLINE-SAME-DAY-OVER" "SC_COMMON" 2.0 1.0 splitDate) + , CT.OcfCreateVestingAcceleration (sameDayAcceleration 201.0) + ] + edits = [] + deletes = [] + _ <- applyUpdate + issuer + issuanceResult.updatedCapTableCid + [ CT.OcfCreateStockClassSplit (stockClassSplitOn "SPLIT-ACC-INLINE-SAME-DAY-EXACT" "SC_COMMON" 2.0 1.0 splitDate) + , CT.OcfCreateVestingAcceleration (sameDayAcceleration 200.0) + ] + [] + [] + pure () + testVestingQuantityIntegrity_InheritedOnlySuccessorCannotDropSchedule = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableCid <- addPrerequisites issuer cap_table diff --git a/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar index bd5e16fd..e883e710 100644 --- a/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26ea67433f003d79624701b08e6d59c0678983644974bb3accb9d15f50369307 -size 3439979 +oid sha256:a8ceb6a778f7fd9a4ce323b3588711ddd180a49088f3e372e6ace0d4b72ae4fe +size 3439832 diff --git a/dars/dars.lock b/dars/dars.lock index 83c05464..ffc9e98d 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -61,10 +61,10 @@ "networks": [] }, "OpenCapTable-v34/0.0.17/OpenCapTable-v34.dar": { - "sha256": "26ea67433f003d79624701b08e6d59c0678983644974bb3accb9d15f50369307", - "size": 3439979, + "sha256": "a8ceb6a778f7fd9a4ce323b3588711ddd180a49088f3e372e6ace0d4b72ae4fe", + "size": 3439832, "sdkVersion": "3.4.10", - "uploadedAt": "2026-07-11T22:36:35.000Z", + "uploadedAt": "2026-07-11T23:54:52.123Z", "networks": [] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index aee5b221..2bd66162 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -5031,10 +5031,11 @@ validateStockVestingAccelerationQuantities eventIndex maps issuanceData inherite ("Vesting accelerations exceed live stock security quantity for " <> issuanceData.security_id <> " at " <> show accelerationDate) (acceleratedQuantity <= liveQuantity) unless (null issuanceData.vestings) do - futureVestingQuantity <- stockClassQuantityAtSortKey + let scheduleSortKey = stockClassShareLotSplitCutoffSortKey eventIndex (stockIssuanceShareLot issuanceData) + futureVestingQuantity <- stockClassQuantityBetweenSortKeys maps issuanceData.stock_class_id - issuanceData.date + scheduleSortKey (inlineVestingQuantityAfter accelerationDate issuanceData.vestings) targetSortKey assertMsg @@ -5076,25 +5077,18 @@ validateEquityCompensationVestingAccelerationQuantities stockEventIndex balanceE ("Vesting accelerations exceed live equity compensation security quantity for " <> issuanceData.security_id <> " at " <> show accelerationDate) (acceleratedQuantity <= liveQuantity) unless (null issuanceData.vestings) do - let splitEvents = filter + let scheduleAdjustmentEvents = filter (\event -> case event of - SecurityQuantityMultiplier {} -> True + VestingAccelerationSplit {} -> True _ -> False) - quantityEvents - futureVestingQuantityOpt <- securityQuantityBeforeEvent - issuanceData.security_id - (Some 0.0) - ((SecurityQuantityDelta with - event_date = issuanceData.date - event_priority = 0 - quantity_delta = inlineVestingQuantityAfter accelerationDate issuanceData.vestings) - :: splitEvents) - targetSortKey - futureVestingQuantity <- case futureVestingQuantityOpt of - Some quantity -> pure quantity - None -> do - assertMsg ("Equity compensation inline vesting quantity unexpectedly became unknown: " <> issuanceData.security_id) False - pure 0.0 + timelineEvents + scheduleQuantityEvent = VestingAccelerationQuantity with + event_sort_key = (issuanceData.date, 0, 0) + quantity = inlineVestingQuantityAfter accelerationDate issuanceData.vestings + report_total = False + futureVestingQuantity = vestingAccelerationTimelineTotalBefore + targetSortKey + (scheduleQuantityEvent :: scheduleAdjustmentEvents) assertMsg ("Vesting accelerations exceed equity compensation quantity scheduled after " <> show accelerationDate <> " for " <> issuanceData.security_id) (acceleratedQuantity <= futureVestingQuantity))