Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion OpenCapTable-v34/daml.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion Test/daml.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion Test/daml/OpenCapTable/Setup.daml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions Test/daml/OpenCapTable/TestHelpers.daml
Original file line number Diff line number Diff line change
@@ -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(..))
Expand All @@ -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
Expand Down
134 changes: 134 additions & 0 deletions Test/daml/OpenCapTable/TestIssuerAuthorizedShares.daml
Original file line number Diff line number Diff line change
@@ -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 = []
9 changes: 7 additions & 2 deletions Test/daml/OpenCapTable/TestStockClassAuthorizedShares.daml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ()

4 changes: 2 additions & 2 deletions dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar
Git LFS file not shown
3 changes: 3 additions & 0 deletions dars/OpenCapTable-v34/0.0.4/OpenCapTable-v34.dar
Git LFS file not shown
11 changes: 9 additions & 2 deletions dars/dars.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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": []
Expand All @@ -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": []
}
}
}
59 changes: 59 additions & 0 deletions scripts/codegen/templates/CapTable.daml.template
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid rescanning adjustments per stock class

When the issuer has a finite cap, every UpdateCapTable folds over all stock classes and this call scans all stock-class authorization adjustments again for each class. A cap table with N stock classes and M authorization adjustments therefore performs O(N×M) contract fetches even for an unrelated update, which can make otherwise valid transactions exceed ledger limits; build the latest adjustment per class in one pass and sum from that index instead.

Useful? React with 👍 / 👎.

pure (addOptionalAuthorizedShares totalOpt stockClassLimitOpt))
Comment on lines +248 to +251
(Some 0.0)
(Map.values maps.stock_classes)

validateIssuerAuthorizedShares : CapTableMaps -> ContractId Issuer -> Update ()
validateIssuerAuthorizedShares maps issuerCid = do
issuerLimitOpt <- currentIssuerAuthorizedLimit maps issuerCid
stockClassTotalOpt <- currentStockClassAuthorizedTotal maps

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Short-circuit uncapped issuer validation

When the issuer has no finite cap (None/enum initial shares and no issuer adjustment), this still fetches every stock class and, for each one, scans all stock-class authorization adjustments before the None -> pure () branch returns. On large uncapped cap tables, unrelated updates pay O(stock classes × adjustments) ledger reads and can hit transaction limits even though there is no issuer ceiling to enforce; compute currentStockClassAuthorizedTotal only inside the Some issuerLimit branch.

Useful? React with 👍 / 👎.

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
Comment on lines +266 to +267

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Don't reject NOT_APPLICABLE as unlimited

For a finite issuer, this branch rejects any stock class whose effective authorization is None, but authorizedSharesLimit returns None for both OcfAuthorizedSharesUnlimited and OcfAuthorizedSharesNotApplicable. That means a valid OCF stock class using NOT_APPLICABLE is treated as if it authorizes unlimited shares and blocks all future updates under a finite issuer cap; distinguish the enum variants so only UNLIMITED triggers this failure.

Useful? React with 👍 / 👎.


validateIssuerReference : ContractId Issuer -> Text -> Update ()
validateIssuerReference issuerCid issuerId = do
issuerContract <- fetch issuerCid
Expand Down Expand Up @@ -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
Expand Down
Loading