diff --git a/OpenCapTable-v34/daml.yaml b/OpenCapTable-v34/daml.yaml index 0e021f17..236edb72 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.17 +version: 0.0.18 build-options: - -Wno-crypto-text-is-alpha - --typecheck-upgrades=no diff --git a/Test/daml.yaml b/Test/daml.yaml index 36057ec4..8fcddbad 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.17.dar + - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.18.dar build-options: - -Wno-template-interface-depends-on-daml-script diff --git a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml index 2f7b7515..7929c7f1 100644 --- a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml @@ -133,44 +133,87 @@ testIssuerAuthorizedSharesAdjustmentCannotDropBelowStockClasses = script do edits = [] deletes = [] -testSameDateLowerIssuerAuthorizedSharesAdjustmentWins = script do +testConflictingSameOcfDateIssuerAuthorizedSharesAdjustmentsFail = script do TestOcp{issuer, cap_table} <- setupTestOcp - capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 - (capTableWithClass, _) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_ISSUER_SAME_DATE" 100.0) - result <- submit issuer do - exerciseCmd capTableWithClass CT.UpdateCapTable with + let morning = DT.time (DA.date 2024 Jan 01) 9 0 0 + let evening = DT.time (DA.date 2024 Jan 01) 17 0 0 + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with creates = - [ CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_SAME_DATE_LOW" 100.0) - , CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_SAME_DATE_HIGH" 150.0) + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment ((issuerAuthorizedSharesAdjustment "TX_ISSUER_SAME_DATE_LOW" 100.0) with date = morning) + , CT.OcfCreateIssuerAuthorizedSharesAdjustment ((issuerAuthorizedSharesAdjustment "TX_ISSUER_SAME_DATE_HIGH" 150.0) with date = evening) ] edits = [] deletes = [] - submitMustFail issuer do - exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockClass (limitedStockClass "SC_ISSUER_SAME_DATE_EXTRA" 50.0)] +testIdenticalSameOcfDateIssuerAuthorizedSharesAdjustmentsSucceed = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let morning = DT.time (DA.date 2024 Jan 01) 9 0 0 + let evening = DT.time (DA.date 2024 Jan 01) 17 0 0 + + _ <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment ((issuerAuthorizedSharesAdjustment "TX_ISSUER_SAME_DATE_FIRST" 150.0) with date = morning) + , CT.OcfCreateIssuerAuthorizedSharesAdjustment ((issuerAuthorizedSharesAdjustment "TX_ISSUER_SAME_DATE_SECOND" 150.0) with date = evening) + ] edits = [] deletes = [] + pure () -testSameDateLowerIssuerAuthorizedSharesAdjustmentWinsWithReversedCreateOrder = script do +testIssuerAuthorizedAdjustmentAppliesForEntireOcfDate = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 - (capTableWithClass, _) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_ISSUER_SAME_DATE_REVERSED" 100.0) + let morning = DT.time (DA.date 2024 Jan 01) 9 0 0 + let evening = DT.time (DA.date 2024 Jan 01) 17 0 0 + let stockClass = (limitedStockClass "SC_ISSUER_SAME_OCF_DATE" 150.0) with + board_approval_date = Some morning + let adjustment = (issuerAuthorizedSharesAdjustment "TX_ISSUER_SAME_OCF_DATE" 150.0) with + date = evening + + _ <- submit issuer do + exerciseCmd capTableLimited CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass stockClass + , CT.OcfCreateIssuerAuthorizedSharesAdjustment adjustment + ] + edits = [] + deletes = [] + pure () + +testIssuerAdjustmentConflictValidationUsesFinalState = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let morning = DT.time (DA.date 2024 Jan 01) 9 0 0 + let evening = DT.time (DA.date 2024 Jan 01) 17 0 0 + let firstId = "TX_ISSUER_FINAL_STATE_FIRST" + let secondId = "TX_ISSUER_FINAL_STATE_SECOND" + let first = (issuerAuthorizedSharesAdjustment firstId 150.0) with date = morning + let second = (issuerAuthorizedSharesAdjustment secondId 150.0) with date = evening + let conflictingEdit = second with new_shares_authorized = 200.0 + result <- submit issuer do - exerciseCmd capTableWithClass CT.UpdateCapTable with + exerciseCmd cap_table CT.UpdateCapTable with creates = - [ CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_SAME_DATE_HIGH_FIRST" 150.0) - , CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_SAME_DATE_LOW_SECOND" 100.0) + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment first + , CT.OcfCreateIssuerAuthorizedSharesAdjustment second ] edits = [] deletes = [] submitMustFail issuer do exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockClass (limitedStockClass "SC_ISSUER_SAME_DATE_REVERSED_EXTRA" 50.0)] - edits = [] + creates = [] + edits = [CT.OcfEditIssuerAuthorizedSharesAdjustment conflictingEdit] deletes = [] + _ <- submit issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditIssuerAuthorizedSharesAdjustment conflictingEdit] + deletes = [CT.OcfDeleteIssuerAuthorizedSharesAdjustment firstId] + pure () + testIssuerEditCannotDropBelowStockClasses = script do TestOcp{issuer, cap_table} <- setupTestOcp capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index d47a07cf..50281a90 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -472,45 +472,58 @@ testStockClassDeleteWithAuthorizedSharesAdjustmentFailsForCeiling = script do edits = [] deletes = [CT.OcfDeleteStockClass stockClassId] -testSameDateLowerStockClassAuthorizedSharesAdjustmentWins = script do +testConflictingSameOcfDateStockClassAuthorizedSharesAdjustmentsFail = script do TestOcp{issuer, cap_table} <- setupTestOcp - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_SAME_DATE" 100.0) - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_SAME_DATE" "Same Date Holder" - result <- submit issuer do - exerciseCmd capTableReady CT.UpdateCapTable with + let morning = DT.time (DA.date 2024 Jan 01) 9 0 0 + let evening = DT.time (DA.date 2024 Jan 01) 17 0 0 + + submitMustFail issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with creates = - [ CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_SAME_DATE_HIGH" stockClassId 200.0) - , CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_SAME_DATE_LOW" stockClassId 100.0) + [ CT.OcfCreateStockClassAuthorizedSharesAdjustment ((stockClassAuthorizedSharesAdjustment "TX_AUTH_SAME_DATE_HIGH" stockClassId 200.0) with date = morning) + , CT.OcfCreateStockClassAuthorizedSharesAdjustment ((stockClassAuthorizedSharesAdjustment "TX_AUTH_SAME_DATE_LOW" stockClassId 100.0) with date = evening) ] edits = [] deletes = [] - submitMustFail issuer do - exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_SAME_DATE_ISSUED" "SEC_SAME_DATE_ISSUED" stakeholderId stockClassId) with quantity = 150.0)] +testIdenticalSameOcfDateStockClassAuthorizedSharesAdjustmentsSucceed = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_SAME_DATE_REVERSED" 100.0) + let morning = DT.time (DA.date 2024 Jan 01) 9 0 0 + let evening = DT.time (DA.date 2024 Jan 01) 17 0 0 + + _ <- submit issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassAuthorizedSharesAdjustment ((stockClassAuthorizedSharesAdjustment "TX_AUTH_SAME_DATE_FIRST" stockClassId 200.0) with date = morning) + , CT.OcfCreateStockClassAuthorizedSharesAdjustment ((stockClassAuthorizedSharesAdjustment "TX_AUTH_SAME_DATE_SECOND" stockClassId 200.0) with date = evening) + ] edits = [] deletes = [] + pure () -testSameDateLowerStockClassAuthorizedSharesAdjustmentWinsWithReversedCreateOrder = script do +testStockClassAuthorizedAdjustmentAppliesForEntireOcfDate = script do TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_AUTH_SAME_OCF_DATE" 100.0) + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_AUTH_SAME_OCF_DATE" "Same OCF Date Holder" + let morning = DT.time (DA.date 2024 Jan 01) 9 0 0 + let evening = DT.time (DA.date 2024 Jan 01) 17 0 0 + let adjustment = (stockClassAuthorizedSharesAdjustment "TX_AUTH_SAME_OCF_DATE" stockClassId 200.0) with + date = evening + let issuance = (defaultStockIssuance "TX_ISSUANCE_SAME_OCF_DATE" "SEC_ISSUANCE_SAME_OCF_DATE" stakeholderId stockClassId) with + date = morning + quantity = 150.0 - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_SAME_DATE_REVERSED" 100.0) - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_SAME_DATE_REVERSED" "Same Date Reversed Holder" - result <- submit issuer do + _ <- submit issuer do exerciseCmd capTableReady CT.UpdateCapTable with creates = - [ CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_SAME_DATE_LOW_FIRST" stockClassId 100.0) - , CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_AUTH_SAME_DATE_HIGH_SECOND" stockClassId 200.0) + [ CT.OcfCreateStockClassAuthorizedSharesAdjustment adjustment + , CT.OcfCreateStockIssuance issuance ] edits = [] deletes = [] - - submitMustFail issuer do - exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with - creates = [CT.OcfCreateStockIssuance ((defaultStockIssuance "TX_SAME_DATE_REVERSED_ISSUED" "SEC_SAME_DATE_REVERSED_ISSUED" stakeholderId stockClassId) with quantity = 150.0)] - edits = [] - deletes = [] + pure () testStockClassSplitCannotLeaveIssuedSharesAboveAuthorizedShares = script do TestOcp{issuer, cap_table} <- setupTestOcp @@ -1190,28 +1203,77 @@ testStockConversionUsesRatioAdjustmentForDestinationCeiling = script do edits = [] deletes = [] -testSameDateHigherStockConversionRatioAdjustmentWinsForCeiling = script do +testStockConversionRatioAdjustmentAppliesForEntireOcfDate = script do TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_RATIO_SAME_DATE_COMMON" 200.0) + let preferredClassId = "SC_CONVERSION_RATIO_SAME_DATE_SOURCE" + let preferredClass = (limitedStockClass preferredClassId 200.0) with + conversion_rights = [successorStockClassConversionRightTo commonClassId] + (capTableWithPreferred, _) <- setupStockClassWith issuer capTableWithCommon preferredClass + (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_RATIO_SAME_DATE" "Same OCF Date Conversion Holder" + capTableIssued <- createStockIssuanceWith issuer capTableReady + ((defaultStockIssuance "TX_CONVERSION_RATIO_SAME_DATE_ISSUED" "SEC_CONVERSION_RATIO_SAME_DATE" stakeholderId preferredClassId) with quantity = 75.0) + let morning = DT.time (DA.date 2024 Jan 01) 9 0 0 + let evening = DT.time (DA.date 2024 Jan 01) 17 0 0 + let conversionId = "TX_CONVERSION_RATIO_SAME_DATE" + _ <- submit issuer do + exerciseCmd capTableIssued CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassConversionRatioAdjustment + ((stockClassConversionRatioAdjustment "TX_CONVERSION_RATIO_SAME_DATE_ADJUSTMENT" preferredClassId 2.0) with date = evening) + , stockSuccessorIssuance + "TX_CONVERSION_RATIO_SAME_DATE_OUTPUT" + (conversionId <> "_RESULT") + stakeholderId + commonClassId + morning + 150.0 + , CT.OcfCreateStockConversion ((stockConversion conversionId "SEC_CONVERSION_RATIO_SAME_DATE" 75.0) with date = morning) + ] + edits = [] + deletes = [] + pure () + +testConflictingSameOcfDateStockClassConversionRatioAdjustmentsFail = script do + TestOcp{issuer, cap_table} <- setupTestOcp (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_RATIO_TIE_COMMON" 100.0) let preferredClassId = "SC_CONVERSION_RATIO_TIE_SOURCE" let preferredClass = (limitedStockClass preferredClassId 200.0) with conversion_rights = [successorStockClassConversionRightTo commonClassId] (capTableWithPreferred, _) <- setupStockClassWith issuer capTableWithCommon preferredClass - (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithPreferred "SH_CONVERSION_RATIO_TIE" "Conversion Ratio Tie Holder" - capTableIssued <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_CONVERSION_RATIO_TIE_ISSUED" "SEC_CONVERSION_RATIO_TIE" stakeholderId preferredClassId) with quantity = 75.0) + let morning = DT.time (DA.date 2024 Jan 01) 9 0 0 + let evening = DT.time (DA.date 2024 Jan 01) 17 0 0 submitMustFail issuer do - exerciseCmd capTableIssued CT.UpdateCapTable with + exerciseCmd capTableWithPreferred CT.UpdateCapTable with creates = - [ CT.OcfCreateStockClassConversionRatioAdjustment (stockClassConversionRatioAdjustment "TX_CONVERSION_RATIO_TIE_LOW" preferredClassId 1.0) - , CT.OcfCreateStockClassConversionRatioAdjustment (stockClassConversionRatioAdjustment "TX_CONVERSION_RATIO_TIE_HIGH" preferredClassId 2.0) - , stockSuccessorIssuance "TX_CONVERSION_RATIO_TIE_OUTPUT" "TX_CONVERSION_RATIO_TIE_RESULT" stakeholderId commonClassId defaultTestDate 150.0 - , CT.OcfCreateStockConversion (stockConversion "TX_CONVERSION_RATIO_TIE" "SEC_CONVERSION_RATIO_TIE" 75.0) + [ CT.OcfCreateStockClassConversionRatioAdjustment ((stockClassConversionRatioAdjustment "TX_CONVERSION_RATIO_TIE_LOW" preferredClassId 1.0) with date = morning) + , CT.OcfCreateStockClassConversionRatioAdjustment ((stockClassConversionRatioAdjustment "TX_CONVERSION_RATIO_TIE_HIGH" preferredClassId 2.0) with date = evening) ] edits = [] deletes = [] +testIdenticalSameOcfDateStockClassConversionRatioAdjustmentsSucceed = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableWithCommon, commonClassId) <- setupStockClassWith issuer cap_table (limitedStockClass "SC_CONVERSION_RATIO_IDENTICAL_COMMON" 100.0) + let preferredClassId = "SC_CONVERSION_RATIO_IDENTICAL_SOURCE" + let preferredClass = (limitedStockClass preferredClassId 200.0) with + conversion_rights = [successorStockClassConversionRightTo commonClassId] + (capTableWithPreferred, _) <- setupStockClassWith issuer capTableWithCommon preferredClass + let morning = DT.time (DA.date 2024 Jan 01) 9 0 0 + let evening = DT.time (DA.date 2024 Jan 01) 17 0 0 + + _ <- submit issuer do + exerciseCmd capTableWithPreferred CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassConversionRatioAdjustment ((stockClassConversionRatioAdjustment "TX_CONVERSION_RATIO_IDENTICAL_FIRST" preferredClassId 2.0) with date = morning) + , CT.OcfCreateStockClassConversionRatioAdjustment ((stockClassConversionRatioAdjustment "TX_CONVERSION_RATIO_IDENTICAL_SECOND" preferredClassId 2.0) with date = evening) + ] + edits = [] + deletes = [] + pure () + testStockClassWithNonRatioConversionRightRejected = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/Test/daml/OpenCapTable/TestStockClassConversionRatioAdjustment.daml b/Test/daml/OpenCapTable/TestStockClassConversionRatioAdjustment.daml index 40a19114..68036245 100644 --- a/Test/daml/OpenCapTable/TestStockClassConversionRatioAdjustment.daml +++ b/Test/daml/OpenCapTable/TestStockClassConversionRatioAdjustment.daml @@ -4,6 +4,7 @@ import Fairmint.OpenCapTable.OCF.StockClass import Fairmint.OpenCapTable.OCF.StockClassConversionRatioAdjustment import qualified Fairmint.OpenCapTable.CapTable as CT import OpenCapTable.Setup +import OpenCapTable.TestHelpers (defaultStockClass, successorStockClassConversionRightTo) import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..)) import Fairmint.OpenCapTable.Types.Stock (OcfStockClassType(..), OcfRatio(..), OcfInitialSharesAuthorized(..)) import Fairmint.OpenCapTable.Types.Conversion (OcfRatioConversionMechanism(..), OcfRoundingType(..)) @@ -12,13 +13,27 @@ import DA.Date (Month(..)) import qualified DA.Time as DT import Daml.Script +conversionRatioAdjustment : Text -> Text -> Time -> Decimal -> Decimal -> OcfRoundingType -> StockClassConversionRatioAdjustmentOcfData +conversionRatioAdjustment adjustmentId stockClassId adjustmentDate conversionPrice numerator roundingType = + StockClassConversionRatioAdjustmentOcfData with + id = adjustmentId + date = adjustmentDate + stock_class_id = stockClassId + new_ratio_conversion_mechanism = OcfRatioConversionMechanism with + conversion_price = OcfMonetary with amount = conversionPrice, currency = "USD" + ratio = OcfRatio with numerator = numerator, denominator = 1.0 + rounding_type = roundingType + comments = [] + -- Test: Create and archive conversion ratio adjustment (anti-dilution scenario) testCreateAndArchiveStockClassConversionRatioAdjustment = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp result <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with - creates = [CT.OcfCreateStockClass StockClassOcfData with + creates = + [ CT.OcfCreateStockClass (defaultStockClass "SC_COMMON_FOR_SERIES_A") + , CT.OcfCreateStockClass StockClassOcfData with id = "SC_SERIES_A" name = "Series A Preferred" class_type = OcfStockClassTypePreferred @@ -32,7 +47,7 @@ testCreateAndArchiveStockClassConversionRatioAdjustment = script do price_per_share = Some (OcfMonetary with amount = 1.00; currency = "USD") liquidation_preference_multiple = Some 1.0 participation_cap_multiple = None - conversion_rights = [] + conversion_rights = [successorStockClassConversionRightTo "SC_COMMON_FOR_SERIES_A"] comments = []] edits = [] deletes = [] @@ -58,7 +73,9 @@ testCreateAndArchiveStockClassConversionRatioAdjustmentBuiltIn = script do TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp result <- submit issuer do exerciseCmd cap_table CT.UpdateCapTable with - creates = [CT.OcfCreateStockClass StockClassOcfData with + creates = + [ CT.OcfCreateStockClass (defaultStockClass "SC_COMMON_FOR_SERIES_B") + , CT.OcfCreateStockClass StockClassOcfData with id = "SC_SERIES_B" name = "Series B Preferred" class_type = OcfStockClassTypePreferred @@ -72,7 +89,7 @@ testCreateAndArchiveStockClassConversionRatioAdjustmentBuiltIn = script do price_per_share = Some (OcfMonetary with amount = 2.50; currency = "USD") liquidation_preference_multiple = Some 1.0 participation_cap_multiple = None - conversion_rights = [] + conversion_rights = [successorStockClassConversionRightTo "SC_COMMON_FOR_SERIES_B"] comments = []] edits = [] deletes = [] @@ -91,3 +108,94 @@ testCreateAndArchiveStockClassConversionRatioAdjustmentBuiltIn = script do edits = [] deletes = [] pure () + +testStockClassConversionRatioAdjustmentRequiresRatioRight = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let stockClassId = "SC_RATIO_ADJUSTMENT_WITHOUT_RIGHT" + result <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [CT.OcfCreateStockClass (defaultStockClass stockClassId)] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassConversionRatioAdjustment + (conversionRatioAdjustment "TX_RATIO_ADJUSTMENT_WITHOUT_RIGHT" stockClassId (DT.time (DA.date 2024 Jan 01) 0 0 0) 0.75 1.333 OcfRoundingFloor) + ] + edits = [] + deletes = [] + +testStockClassConversionRatioAdjustmentMissingTargetFails = script do + TestOcp{issuer, cap_table} <- setupTestOcp + + submitMustFail issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassConversionRatioAdjustment + (conversionRatioAdjustment "TX_RATIO_ADJUSTMENT_MISSING_TARGET" "SC_RATIO_ADJUSTMENT_MISSING" (DT.time (DA.date 2024 Jan 01) 0 0 0) 0.75 1.333 OcfRoundingFloor) + ] + edits = [] + deletes = [] + +testStockClassConversionRatioAdjustmentTargetEditUsesFinalState = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let commonClassId = "SC_RATIO_ADJUSTMENT_EDIT_COMMON" + let sourceClassId = "SC_RATIO_ADJUSTMENT_EDIT_SOURCE" + let sourceClass = (defaultStockClass sourceClassId) with + conversion_rights = [successorStockClassConversionRightTo commonClassId] + let adjustmentId = "TX_RATIO_ADJUSTMENT_EDIT" + let adjustment = conversionRatioAdjustment adjustmentId sourceClassId (DT.time (DA.date 2024 Jan 01) 0 0 0) 0.75 1.333 OcfRoundingFloor + + result <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass (defaultStockClass commonClassId) + , CT.OcfCreateStockClass sourceClass + , CT.OcfCreateStockClassConversionRatioAdjustment adjustment + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockClass (sourceClass with conversion_rights = [])] + deletes = [] + + _ <- submit issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditStockClass (sourceClass with conversion_rights = [])] + deletes = [CT.OcfDeleteStockClassConversionRatioAdjustment adjustmentId] + pure () + +testSameOcfDateRatioAdjustmentsCompareFullMechanism = script do + TestOcp{issuer, cap_table} <- setupTestOcp + let commonClassId = "SC_RATIO_MECHANISM_COMMON" + let sourceClassId = "SC_RATIO_MECHANISM_SOURCE" + let sourceClass = (defaultStockClass sourceClassId) with + conversion_rights = [successorStockClassConversionRightTo commonClassId] + let morning = DT.time (DA.date 2024 Jan 01) 9 0 0 + let evening = DT.time (DA.date 2024 Jan 01) 17 0 0 + + result <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClass (defaultStockClass commonClassId) + , CT.OcfCreateStockClass sourceClass + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockClassConversionRatioAdjustment + (conversionRatioAdjustment "TX_RATIO_MECHANISM_FIRST" sourceClassId morning 0.75 2.0 OcfRoundingFloor) + , CT.OcfCreateStockClassConversionRatioAdjustment + (conversionRatioAdjustment "TX_RATIO_MECHANISM_SECOND" sourceClassId evening 0.50 2.0 OcfRoundingFloor) + ] + edits = [] + deletes = [] diff --git a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml index cb5eccea..07adf16a 100644 --- a/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml +++ b/Test/daml/OpenCapTable/TestStockPlanReservedShares.daml @@ -626,16 +626,15 @@ testStockPlanReservedShares_FutureAdjustmentCannotAuthorizeBackdatedIssuance = s edits = [] deletes = [] -testStockPlanReservedShares_SameDateLowerAdjustmentWins = script do +testStockPlanReservedShares_ConflictingSameOcfDateAdjustmentsFail = script do TestOcp{issuer, cap_table} <- setupTestOcp - (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + (capTableReady, stockClassId, _) <- setupForStockIssuance issuer cap_table let stockPlanId = "PLAN_SAME_DATE" let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 - let higherAdjustment = stockPlanPoolAdjustment "TX_POOL_HIGH" stockPlanId 200.0 - let lowerAdjustment = stockPlanPoolAdjustment "TX_POOL_LOW" stockPlanId 120.0 - let issuance = (defaultStockIssuance "TX_STOCK_SAME_DATE_POOL" "SEC-STOCK-SAME-DATE-POOL" stakeholderId stockClassId) with - stock_plan_id = Some stockPlanId - quantity = 150.0 + let morning = DT.time (DA.date 2024 Jan 01) 9 0 0 + let evening = DT.time (DA.date 2024 Jan 01) 17 0 0 + let higherAdjustment = (stockPlanPoolAdjustment "TX_POOL_HIGH" stockPlanId 200.0) with date = morning + let lowerAdjustment = (stockPlanPoolAdjustment "TX_POOL_LOW" stockPlanId 120.0) with date = evening submitMustFail issuer do exerciseCmd capTableReady CT.UpdateCapTable with @@ -643,10 +642,54 @@ testStockPlanReservedShares_SameDateLowerAdjustmentWins = script do [ CT.OcfCreateStockPlan plan , CT.OcfCreateStockPlanPoolAdjustment higherAdjustment , CT.OcfCreateStockPlanPoolAdjustment lowerAdjustment + ] + edits = [] + deletes = [] + +testStockPlanReservedShares_IdenticalSameOcfDateAdjustmentsSucceed = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, _) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_IDENTICAL_SAME_DATE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let morning = DT.time (DA.date 2024 Jan 01) 9 0 0 + let evening = DT.time (DA.date 2024 Jan 01) 17 0 0 + let firstAdjustment = (stockPlanPoolAdjustment "TX_POOL_IDENTICAL_FIRST" stockPlanId 200.0) with date = morning + let secondAdjustment = (stockPlanPoolAdjustment "TX_POOL_IDENTICAL_SECOND" stockPlanId 200.0) with date = evening + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockPlanPoolAdjustment firstAdjustment + , CT.OcfCreateStockPlanPoolAdjustment secondAdjustment + ] + edits = [] + deletes = [] + pure () + +testStockPlanReservedShares_AdjustmentAppliesForEntireOcfDate = script do + TestOcp{issuer, cap_table} <- setupTestOcp + (capTableReady, stockClassId, stakeholderId) <- setupForStockIssuance issuer cap_table + let stockPlanId = "PLAN_SAME_OCF_DATE" + let plan = (defaultStockPlan stockPlanId stockClassId) with initial_shares_reserved = 100.0 + let morning = DT.time (DA.date 2024 Jan 01) 9 0 0 + let evening = DT.time (DA.date 2024 Jan 01) 17 0 0 + let adjustment = (stockPlanPoolAdjustment "TX_POOL_SAME_OCF_DATE" stockPlanId 200.0) with date = evening + let issuance = (defaultStockIssuance "TX_POOL_ISSUANCE_SAME_OCF_DATE" "SEC_POOL_ISSUANCE_SAME_OCF_DATE" stakeholderId stockClassId) with + date = morning + stock_plan_id = Some stockPlanId + quantity = 150.0 + + _ <- submit issuer do + exerciseCmd capTableReady CT.UpdateCapTable with + creates = + [ CT.OcfCreateStockPlan plan + , CT.OcfCreateStockPlanPoolAdjustment adjustment , CT.OcfCreateStockIssuance issuance ] edits = [] deletes = [] + pure () testStockPlanReservedShares_ReturnToPoolAllowsReuse = script do TestOcp{issuer, cap_table} <- setupTestOcp diff --git a/dars/OpenCapTable-v34/0.0.18/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.18/OpenCapTable-v34.dar new file mode 100644 index 00000000..a49709d1 --- /dev/null +++ b/dars/OpenCapTable-v34/0.0.18/OpenCapTable-v34.dar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f73752ab485c0926f61bd0d4c5b1be9b4a7aff35902d170189238c2424650805 +size 3449292 diff --git a/dars/dars.lock b/dars/dars.lock index ffc9e98d..03ba7c86 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -67,6 +67,13 @@ "uploadedAt": "2026-07-11T23:54:52.123Z", "networks": [] }, + "OpenCapTable-v34/0.0.18/OpenCapTable-v34.dar": { + "sha256": "f73752ab485c0926f61bd0d4c5b1be9b4a7aff35902d170189238c2424650805", + "size": 3449292, + "sdkVersion": "3.4.10", + "uploadedAt": "2026-07-12T00:40:48.908Z", + "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 2bd66162..4c648795 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -13,6 +13,7 @@ module Fairmint.OpenCapTable.CapTable where import DA.Map (Map) import qualified DA.Map as Map import DA.List (sortOn, unique) +import qualified DA.Date as Date import DA.Set (Set) import qualified DA.Set as Set import DA.Action (foldlA, unless, when) @@ -289,46 +290,62 @@ issuerAuthorizedSharesLimit : Optional OcfInitialSharesAuthorized -> Optional De issuerAuthorizedSharesLimit initialSharesAuthorized = authorizedSharesLimit =<< initialSharesAuthorized +-- OCF Date is date-only; the DAML Time transport must not introduce ordering +-- between declarations that occur on the same calendar date. +ocfDate : Time -> Date +ocfDate = Date.toDateUTC + +ocfDateOnOrBefore : Time -> Time -> Bool +ocfDateOnOrBefore candidate asOf = ocfDate candidate <= ocfDate asOf + +-- Adjustments declare absolute state as of their OCF date. Repeated declarations +-- may agree, but conflicting state on one date has no deterministic OCF meaning. +recordAdjustmentState : + Eq a => + Text -> + Text -> + Time -> + a -> + Map Text (Map Date (Text, a)) -> + Update (Map Text (Map Date (Text, a))) +recordAdjustmentState label targetId adjustmentDate state index = do + let dateKey = ocfDate adjustmentDate + let targetHistory = fromOptional Map.empty (Map.lookup targetId index) + case Map.lookup dateKey targetHistory of + Some (existingLabel, existingState) -> + assertMsg + (label <> " conflicts with " <> existingLabel <> " for " <> targetId <> " on OCF date " <> show dateKey) + (state == existingState) + None -> pure () + pure (Map.insert targetId (Map.insert dateKey (label, state) targetHistory) index) + stockClassAuthorizedAdjustmentTakesPrecedence : StockClassAuthorizedSharesAdjustmentOcfData -> StockClassAuthorizedSharesAdjustmentOcfData -> Bool stockClassAuthorizedAdjustmentTakesPrecedence candidate current = - candidate.date > current.date || - (candidate.date == current.date && candidate.new_shares_authorized < current.new_shares_authorized) + ocfDate candidate.date > ocfDate current.date issuerAuthorizedAdjustmentTakesPrecedence : IssuerAuthorizedSharesAdjustmentOcfData -> IssuerAuthorizedSharesAdjustmentOcfData -> Bool issuerAuthorizedAdjustmentTakesPrecedence candidate current = - candidate.date > current.date || - (candidate.date == current.date && candidate.new_shares_authorized < current.new_shares_authorized) + ocfDate candidate.date > ocfDate current.date stockPlanPoolAdjustmentTakesPrecedence : StockPlanPoolAdjustmentOcfData -> StockPlanPoolAdjustmentOcfData -> Bool stockPlanPoolAdjustmentTakesPrecedence candidate current = - candidate.date > current.date || - (candidate.date == current.date && candidate.shares_reserved < current.shares_reserved) + ocfDate candidate.date > ocfDate current.date stockClassConversionRatioAdjustmentTakesPrecedence : StockClassConversionRatioAdjustmentOcfData -> StockClassConversionRatioAdjustmentOcfData -> Bool stockClassConversionRatioAdjustmentTakesPrecedence candidate current = - candidate.date > current.date || - (candidate.date == current.date && stockClassConversionRatioAdjustmentIncreasesShares candidate current) - -stockClassConversionRatioAdjustmentIncreasesShares : - StockClassConversionRatioAdjustmentOcfData -> - StockClassConversionRatioAdjustmentOcfData -> - Bool -stockClassConversionRatioAdjustmentIncreasesShares candidate current = - let candidateRatio = candidate.new_ratio_conversion_mechanism.ratio in - let currentRatio = current.new_ratio_conversion_mechanism.ratio in - candidateRatio.numerator * currentRatio.denominator > currentRatio.numerator * candidateRatio.denominator + ocfDate candidate.date > ocfDate current.date addDateById : Text -> Time -> Map Text [Time] -> Map Text [Time] addDateById key date index = @@ -476,7 +493,7 @@ stockClassConversionRatioAdjustmentAsOf maps stockClassId asOf = (\latest adjustmentCid -> do adjustment <- fetch adjustmentCid let candidate = adjustment.adjustment_data - if candidate.stock_class_id == stockClassId && candidate.date <= asOf + if candidate.stock_class_id == stockClassId && ocfDateOnOrBefore candidate.date asOf then case latest of Some current -> if stockClassConversionRatioAdjustmentTakesPrecedence candidate current @@ -1259,7 +1276,7 @@ stockClassAuthorizedAdjustmentAsOf : stockClassAuthorizedAdjustmentAsOf adjustmentHistory stockClassId asOf = foldl (\latest candidate -> - if candidate.date <= asOf + if ocfDateOnOrBefore candidate.date asOf then case latest of Some current -> if stockClassAuthorizedAdjustmentTakesPrecedence candidate current @@ -1341,7 +1358,7 @@ issuerAuthorizedAdjustmentAsOf : issuerAuthorizedAdjustmentAsOf adjustmentHistory issuerId asOf = foldl (\latest candidate -> - if candidate.date <= asOf + if ocfDateOnOrBefore candidate.date asOf then case latest of Some current -> if issuerAuthorizedAdjustmentTakesPrecedence candidate current @@ -1393,7 +1410,7 @@ stockPlanPoolAdjustmentAsOf : stockPlanPoolAdjustmentAsOf adjustmentHistory stockPlanId asOf = foldl (\latest candidate -> - if candidate.date <= asOf + if ocfDateOnOrBefore candidate.date asOf then case latest of Some current -> if stockPlanPoolAdjustmentTakesPrecedence candidate current @@ -3798,6 +3815,92 @@ validateIssuerAuthorizedShareAdjustmentReferences maps issuerCid = do (adjustment.adjustment_data.issuer_id == issuerContract.issuer_data.id)) (Map.values maps.issuer_authorized_shares_adjustments) +validateIssuerAuthorizedAdjustmentStates : CapTableMaps -> Update () +validateIssuerAuthorizedAdjustmentStates maps = do + _ <- foldlA + (\index adjustmentCid -> do + adjustment <- fetch adjustmentCid + let data_ = adjustment.adjustment_data + recordAdjustmentState + ("Issuer authorized shares adjustment " <> data_.id) + data_.issuer_id + data_.date + data_.new_shares_authorized + index) + Map.empty + (Map.values maps.issuer_authorized_shares_adjustments) + pure () + +validateStockClassAuthorizedAdjustmentStates : CapTableMaps -> Update () +validateStockClassAuthorizedAdjustmentStates maps = do + _ <- foldlA + (\index adjustmentCid -> do + adjustment <- fetch adjustmentCid + let data_ = adjustment.adjustment_data + recordAdjustmentState + ("Stock class authorized shares adjustment " <> data_.id) + data_.stock_class_id + data_.date + data_.new_shares_authorized + index) + Map.empty + (Map.values maps.stock_class_authorized_shares_adjustments) + pure () + +validateStockPlanPoolAdjustmentStates : CapTableMaps -> Update () +validateStockPlanPoolAdjustmentStates maps = do + _ <- foldlA + (\index adjustmentCid -> do + adjustment <- fetch adjustmentCid + let data_ = adjustment.adjustment_data + recordAdjustmentState + ("Stock plan pool adjustment " <> data_.id) + data_.stock_plan_id + data_.date + data_.shares_reserved + index) + Map.empty + (Map.values maps.stock_plan_pool_adjustments) + pure () + +stockClassHasRatioConversionRight : StockClassOcfData -> Bool +stockClassHasRatioConversionRight stockClassData = + any + (\right -> case right.conversion_mechanism of + OcfConversionMechanismRatioConversion -> True + _ -> False) + stockClassData.conversion_rights + +validateStockClassConversionRatioAdjustmentStates : CapTableMaps -> Update () +validateStockClassConversionRatioAdjustmentStates maps = do + _ <- foldlA + (\index adjustmentCid -> do + adjustment <- fetch adjustmentCid + let data_ = adjustment.adjustment_data + case Map.lookup data_.stock_class_id maps.stock_classes of + Some stockClassCid -> do + stockClass <- fetch stockClassCid + assertMsg + ("Stock class conversion ratio adjustment " <> data_.id <> " targets a stock class without a ratio conversion right: " <> data_.stock_class_id) + (stockClassHasRatioConversionRight stockClass.stock_class_data) + None -> pure () + recordAdjustmentState + ("Stock class conversion ratio adjustment " <> data_.id) + data_.stock_class_id + data_.date + data_.new_ratio_conversion_mechanism + index) + Map.empty + (Map.values maps.stock_class_conversion_ratio_adjustments) + pure () + +validateAdjustmentStateIntegrity : CapTableMaps -> Update () +validateAdjustmentStateIntegrity maps = do + validateIssuerAuthorizedAdjustmentStates maps + validateStockClassAuthorizedAdjustmentStates maps + validateStockPlanPoolAdjustmentStates maps + validateStockClassConversionRatioAdjustmentStates maps + validateIssuerReference : ContractId Issuer -> Text -> Update () validateIssuerReference issuerCid issuerId = do issuerContract <- fetch issuerCid @@ -8466,6 +8569,7 @@ template CapTable validateDocumentRelatedObjects finalMaps newIssuerCid validateStockClassConversionRightReferences finalMaps + validateAdjustmentStateIntegrity finalMaps validateConvertibleIssuanceReferences finalMaps validateConvertibleConversionRightReferences finalMaps validateWarrantConversionRightReferences finalMaps