diff --git a/OpenCapTable-v34/daml.yaml b/OpenCapTable-v34/daml.yaml index 2403aff1..4d300d29 100644 --- a/OpenCapTable-v34/daml.yaml +++ b/OpenCapTable-v34/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.4.10 name: OpenCapTable-v34 source: daml -version: 0.0.3 +version: 0.0.4 build-options: - -Wno-crypto-text-is-alpha - --typecheck-upgrades=no diff --git a/Test/daml.yaml b/Test/daml.yaml index 151e762a..c6e7f2d6 100644 --- a/Test/daml.yaml +++ b/Test/daml.yaml @@ -7,6 +7,6 @@ dependencies: - daml-stdlib - daml-script data-dependencies: - - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.3.dar + - ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.4.dar build-options: - -Wno-template-interface-depends-on-daml-script diff --git a/Test/daml/OpenCapTable/Setup.daml b/Test/daml/OpenCapTable/Setup.daml index 764e6754..246c5146 100644 --- a/Test/daml/OpenCapTable/Setup.daml +++ b/Test/daml/OpenCapTable/Setup.daml @@ -54,7 +54,7 @@ setupTestOcp = script do email = None phone = None address = None - initial_shares_authorized = Some (OcfInitialSharesNumeric 1000.0) + initial_shares_authorized = Some (OcfInitialSharesNumeric 100_000_000.0) comments = [] return TestOcp diff --git a/Test/daml/OpenCapTable/TestHelpers.daml b/Test/daml/OpenCapTable/TestHelpers.daml index d8d1bf34..a781a0d2 100644 --- a/Test/daml/OpenCapTable/TestHelpers.daml +++ b/Test/daml/OpenCapTable/TestHelpers.daml @@ -1,6 +1,7 @@ module OpenCapTable.TestHelpers where import DA.Assert (assertMsg) +import Fairmint.OpenCapTable.OCF.Issuer import Fairmint.OpenCapTable.OCF.Stakeholder import Fairmint.OpenCapTable.OCF.StockClass import Fairmint.OpenCapTable.OCF.StockIssuance (StockIssuanceOcfData(..)) @@ -27,6 +28,23 @@ import DA.Time (time) -- Functions that return valid default OCF data with sensible defaults. -- Use these to reduce boilerplate in tests that don't care about specific values. +-- | Returns valid IssuerOcfData with sensible defaults. +defaultIssuerWithAuthorizedShares : Text -> Optional OcfInitialSharesAuthorized -> IssuerOcfData +defaultIssuerWithAuthorizedShares issuerId initialSharesAuthorized = IssuerOcfData with + id = issuerId + legal_name = "TestIssuer Inc." + formation_date = DT.time (DA.date 2020 Jan 15) 12 0 0 + country_of_formation = "US" + dba = None + country_subdivision_of_formation = None + country_subdivision_name_of_formation = None + tax_ids = [] + email = None + phone = None + address = None + initial_shares_authorized = initialSharesAuthorized + comments = [] + -- | Default initial shares authorized for test stock classes. -- Configurable constant to avoid magic numbers. defaultInitialSharesAuthorized : Decimal diff --git a/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml new file mode 100644 index 00000000..a375a0f6 --- /dev/null +++ b/Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml @@ -0,0 +1,134 @@ +module OpenCapTable.TestIssuerAuthorizedShares where + +import Fairmint.OpenCapTable.OCF.IssuerAuthorizedSharesAdjustment +import Fairmint.OpenCapTable.OCF.StockClass +import Fairmint.OpenCapTable.OCF.StockClassAuthorizedSharesAdjustment +import Fairmint.OpenCapTable.Types.Stock (OcfAuthorizedShares(..), OcfInitialSharesAuthorized(..)) +import qualified Fairmint.OpenCapTable.CapTable as CT +import OpenCapTable.Setup +import OpenCapTable.TestHelpers +import Daml.Script + +limitedStockClass : Text -> Decimal -> StockClassOcfData +limitedStockClass classId authorizedShares = + (defaultStockClass classId) with + initial_shares_authorized = OcfInitialSharesNumeric authorizedShares + +issuerAuthorizedSharesAdjustment : Text -> Decimal -> IssuerAuthorizedSharesAdjustmentOcfData +issuerAuthorizedSharesAdjustment adjustmentId sharesAuthorized = + IssuerAuthorizedSharesAdjustmentOcfData with + id = adjustmentId + date = defaultTestDate + issuer_id = "ISSUER_1" + new_shares_authorized = sharesAuthorized + board_approval_date = None + stockholder_approval_date = None + comments = [] + +stockClassAuthorizedSharesAdjustment : Text -> Text -> Decimal -> StockClassAuthorizedSharesAdjustmentOcfData +stockClassAuthorizedSharesAdjustment adjustmentId stockClassId authorizedShares = + StockClassAuthorizedSharesAdjustmentOcfData with + id = adjustmentId + date = defaultTestDate + stock_class_id = stockClassId + new_shares_authorized = authorizedShares + board_approval_date = None + stockholder_approval_date = None + comments = [] + +setIssuerAuthorizedShares : Party -> ContractId CT.CapTable -> Decimal -> Script (ContractId CT.CapTable) +setIssuerAuthorizedShares issuer capTable sharesAuthorized = do + result <- submit issuer do + exerciseCmd capTable CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditIssuer (defaultIssuerWithAuthorizedShares "ISSUER_1" (Some (OcfInitialSharesNumeric sharesAuthorized)))] + deletes = [] + pure result.updatedCapTableCid + +testStockClassCannotExceedIssuerInitialAuthorizedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + + submitMustFail issuer do + exerciseCmd capTableLimited CT.UpdateCapTable with + creates = [CT.OcfCreateStockClass (limitedStockClass "SC_TOO_MANY_FOR_ISSUER" 101.0)] + edits = [] + deletes = [] + +testIssuerAuthorizedSharesAdjustmentAllowsSameBatchStockClassIncrease = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + + _ <- submit issuer do + exerciseCmd capTableLimited CT.UpdateCapTable with + creates = + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_FOR_CLASS" 150.0) + , CT.OcfCreateStockClass (limitedStockClass "SC_ALLOWED_BY_ISSUER_RAISE" 150.0) + ] + edits = [] + deletes = [] + pure () + +testIssuerAuthorizedSharesAdjustmentCannotDropBelowStockClasses = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + (capTableWithClass, _) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_ISSUER_DROP" 100.0) + + submitMustFail issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with + creates = [CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_DROP_TOO_LOW" 99.0)] + edits = [] + deletes = [] + +testIssuerEditCannotDropBelowStockClasses = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + (capTableWithClass, _) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_ISSUER_EDIT_DROP" 100.0) + + submitMustFail issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditIssuer (defaultIssuerWithAuthorizedShares "ISSUER_1" (Some (OcfInitialSharesNumeric 99.0)))] + deletes = [] + +testStockClassAuthorizedSharesAdjustmentCannotExceedIssuerAuthorizedShares = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + (capTableWithClass, stockClassId) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_CLASS_RAISE_TOO_HIGH" 100.0) + + submitMustFail issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with + creates = [CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_CLASS_RAISE_TOO_HIGH" stockClassId 101.0)] + edits = [] + deletes = [] + +testIssuerAuthorizedSharesAdjustmentDeleteCannotInvalidateStockClasses = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + (capTableWithClass, stockClassId) <- setupStockClassWith issuer capTableLimited (limitedStockClass "SC_ISSUER_DELETE_DROP" 100.0) + result <- submit issuer do + exerciseCmd capTableWithClass CT.UpdateCapTable with + creates = + [ CT.OcfCreateIssuerAuthorizedSharesAdjustment (issuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_DELETE" 150.0) + , CT.OcfCreateStockClassAuthorizedSharesAdjustment (stockClassAuthorizedSharesAdjustment "TX_CLASS_RAISE_FOR_DELETE" stockClassId 150.0) + ] + edits = [] + deletes = [] + + submitMustFail issuer do + exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with + creates = [] + edits = [] + deletes = [CT.OcfDeleteIssuerAuthorizedSharesAdjustment "TX_ISSUER_RAISE_DELETE"] + +testFiniteIssuerRejectsUnlimitedStockClassAuthorization = script do + TestOcp{issuer, cap_table} <- setupTestOcp + capTableLimited <- setIssuerAuthorizedShares issuer cap_table 100.0 + let unlimitedClass = (defaultStockClass "SC_UNLIMITED_ISSUER_REJECT") with + initial_shares_authorized = OcfInitialSharesEnum OcfAuthorizedSharesUnlimited + + submitMustFail issuer do + exerciseCmd capTableLimited CT.UpdateCapTable with + creates = [CT.OcfCreateStockClass unlimitedClass] + edits = [] + deletes = [] diff --git a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml index 65fb0073..d2968965 100644 --- a/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml +++ b/Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml @@ -100,11 +100,16 @@ testStockClassAuthorizedSharesAdjustmentDeleteCannotInvalidateIssuedShares = scr testUnlimitedStockClassAuthorizedSharesAllowsLargeIssuance = script do TestOcp{issuer, cap_table} <- setupTestOcp + issuerResult <- submit issuer do + exerciseCmd cap_table CT.UpdateCapTable with + creates = [] + edits = [CT.OcfEditIssuer (defaultIssuerWithAuthorizedShares "ISSUER_1" None)] + deletes = [] + let unlimitedClass = (defaultStockClass "SC_UNLIMITED") with initial_shares_authorized = OcfInitialSharesEnum OcfAuthorizedSharesUnlimited - (capTableWithClass, stockClassId) <- setupStockClassWith issuer cap_table unlimitedClass + (capTableWithClass, stockClassId) <- setupStockClassWith issuer issuerResult.updatedCapTableCid unlimitedClass (capTableReady, stakeholderId) <- setupStakeholderWithId issuer capTableWithClass "SH_UNLIMITED" "Unlimited Holder" _ <- createStockIssuanceWith issuer capTableReady ((defaultStockIssuance "TX_UNLIMITED" "SEC_UNLIMITED" stakeholderId stockClassId) with quantity = 2_000_000.0) pure () - diff --git a/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar index 4e44774f..d388a4c2 100644 --- a/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar +++ b/dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a83b3fdecb2a6796ff63f353c06c9c13d171062a2a9afa3894f789f18a4fa72 -size 2462750 +oid sha256:0746dde9ed79da54e1bb5e92a90c2da5af05ea0594b20f48416020c0af32790c +size 2476037 diff --git a/dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar b/dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar new file mode 100644 index 00000000..01a6f8b8 --- /dev/null +++ b/dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f46eecafe83dbad4780782c91cd38fd162b162828e12b03027ee6f109835ee7a +size 2511187 diff --git a/dars/dars.lock b/dars/dars.lock index 16aa1a3d..dc2e8754 100644 --- a/dars/dars.lock +++ b/dars/dars.lock @@ -12,8 +12,8 @@ ] }, "OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": { - "sha256": "5a83b3fdecb2a6796ff63f353c06c9c13d171062a2a9afa3894f789f18a4fa72", - "size": 2462750, + "sha256": "0746dde9ed79da54e1bb5e92a90c2da5af05ea0594b20f48416020c0af32790c", + "size": 2476037, "sdkVersion": "3.4.10", "uploadedAt": "2026-07-07T21:50:59.634Z", "networks": [] @@ -24,6 +24,13 @@ "sdkVersion": "3.4.10", "uploadedAt": "2026-07-09T00:16:37.252Z", "networks": [] + }, + "OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar": { + "sha256": "f46eecafe83dbad4780782c91cd38fd162b162828e12b03027ee6f109835ee7a", + "size": 2511187, + "sdkVersion": "3.4.10", + "uploadedAt": "2026-07-09T00:24:49.407Z", + "networks": [] } } } diff --git a/scripts/codegen/templates/CapTable.daml.template b/scripts/codegen/templates/CapTable.daml.template index 6bea5c7f..2a4891f4 100644 --- a/scripts/codegen/templates/CapTable.daml.template +++ b/scripts/codegen/templates/CapTable.daml.template @@ -117,6 +117,19 @@ latestStockClassAuthorizedAdjustment adjustments = None adjustments +latestIssuerAuthorizedAdjustment : [IssuerAuthorizedSharesAdjustmentOcfData] -> Optional IssuerAuthorizedSharesAdjustmentOcfData +latestIssuerAuthorizedAdjustment adjustments = + foldl + (\latest adjustment -> case latest of + None -> Some adjustment + Some current -> if adjustment.date >= current.date then Some adjustment else latest) + None + adjustments + +issuerAuthorizedSharesLimit : Optional OcfInitialSharesAuthorized -> Optional Decimal +issuerAuthorizedSharesLimit initialSharesAuthorized = + authorizedSharesLimit =<< initialSharesAuthorized + fetchStockClassData : CapTableMaps -> Text -> Optional StockClassOcfData -> Update StockClassOcfData fetchStockClassData maps stockClassId stockClassOverride = case stockClassOverride of Some stockClassData -> pure stockClassData @@ -208,6 +221,51 @@ validateCurrentStockClassAuthorizedShares maps stockClassId excludedAdjustmentId limitOpt <- currentStockClassAuthorizedLimit maps stockClassId excludedAdjustmentId stockClassOverride additionalAdjustment assertWithinStockClassAuthorizedShares stockClassId currentIssued limitOpt +currentIssuerAuthorizedLimit : CapTableMaps -> ContractId Issuer -> Update (Optional Decimal) +currentIssuerAuthorizedLimit maps issuerCid = do + issuerContract <- fetch issuerCid + adjustments <- foldlA + (\acc adjustmentCid -> do + adjustment <- fetch adjustmentCid + if adjustment.adjustment_data.issuer_id == issuerContract.issuer_data.id + then pure (adjustment.adjustment_data :: acc) + else pure acc) + [] + (Map.values maps.issuer_authorized_shares_adjustments) + let latestAdjustment = latestIssuerAuthorizedAdjustment adjustments + pure $ case latestAdjustment of + Some adjustment -> Some adjustment.new_shares_authorized + None -> issuerAuthorizedSharesLimit issuerContract.issuer_data.initial_shares_authorized + +addOptionalAuthorizedShares : Optional Decimal -> Optional Decimal -> Optional Decimal +addOptionalAuthorizedShares totalOpt sharesOpt = case (totalOpt, sharesOpt) of + (Some total, Some shares) -> Some (total + shares) + _ -> None + +currentStockClassAuthorizedTotal : CapTableMaps -> Update (Optional Decimal) +currentStockClassAuthorizedTotal maps = + foldlA + (\totalOpt stockClassCid -> do + stockClass <- fetch stockClassCid + stockClassLimitOpt <- currentStockClassAuthorizedLimit maps stockClass.stock_class_data.id None None None + pure (addOptionalAuthorizedShares totalOpt stockClassLimitOpt)) + (Some 0.0) + (Map.values maps.stock_classes) + +validateIssuerAuthorizedShares : CapTableMaps -> ContractId Issuer -> Update () +validateIssuerAuthorizedShares maps issuerCid = do + issuerLimitOpt <- currentIssuerAuthorizedLimit maps issuerCid + stockClassTotalOpt <- currentStockClassAuthorizedTotal maps + case issuerLimitOpt of + None -> pure () + Some issuerLimit -> case stockClassTotalOpt of + Some stockClassTotal -> + assertMsg + ("Issuer authorized shares exceeded: stock classes authorize " <> show stockClassTotal <> " shares but issuer authorizes " <> show issuerLimit) + (stockClassTotal <= issuerLimit) + None -> + assertMsg "Issuer authorized shares exceeded: stock class authorization is unlimited but issuer authorization is finite" False + validateIssuerReference : ContractId Issuer -> Text -> Update () validateIssuerReference issuerCid issuerId = do issuerContract <- fetch issuerCid @@ -458,6 +516,7 @@ template CapTable validateValuationStockClassReferences finalMaps validateEquityCompensationIssuanceReferences finalMaps validateWarrantIssuanceReferences finalMaps + validateIssuerAuthorizedShares finalMaps newIssuerCid -- Create new CapTable with updated maps and potentially new issuer newCapTableCid <- create this with