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
45 changes: 45 additions & 0 deletions Test/daml/OpenCapTable/TestStockPlanReferences.daml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
module OpenCapTable.TestStockPlanReferences where

import qualified Fairmint.OpenCapTable.CapTable as CT
import OpenCapTable.Setup
import OpenCapTable.TestHelpers
import Daml.Script

testStockPlan_MissingStockClassReferenceFails = script do
TestOcp{issuer, cap_table} <- setupTestOcp

submitMustFail issuer do
exerciseCmd cap_table CT.UpdateCapTable with
creates = [CT.OcfCreateStockPlan (defaultStockPlan "PLAN_MISSING_CLASS" "SC_MISSING")]
edits = []
deletes = []

testStockPlan_StockClassCreatedInSameBatchSucceeds = script do
TestOcp{issuer, cap_table} <- setupTestOcp

_ <- submit issuer do
exerciseCmd cap_table CT.UpdateCapTable with
creates =
[ CT.OcfCreateStockClass (defaultStockClass "SC_VALID")
, CT.OcfCreateStockPlan (defaultStockPlan "PLAN_VALID" "SC_VALID")
]
edits = []
deletes = []
pure ()

testStockPlan_EditMissingStockClassReferenceFails = script do
TestOcp{issuer, cap_table} <- setupTestOcp
(capTableCid, classId) <- setupStockClass issuer cap_table
let plan = defaultStockPlan "PLAN_EDIT_CLASS" classId
createResult <- submit issuer do
exerciseCmd capTableCid CT.UpdateCapTable with
creates = [CT.OcfCreateStockPlan plan]
edits = []
deletes = []
let editedPlan = plan with stock_class_ids = ["SC_MISSING_ON_EDIT"]

submitMustFail issuer do
exerciseCmd createResult.updatedCapTableCid CT.UpdateCapTable with
creates = []
edits = [CT.OcfEditStockPlan editedPlan]
deletes = []
7 changes: 5 additions & 2 deletions Test/daml/OpenCapTable/TestStockPlanReturnToPool.daml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Fairmint.OpenCapTable.OCF.StockPlan
import Fairmint.OpenCapTable.OCF.StockPlanReturnToPool
import qualified Fairmint.OpenCapTable.CapTable as CT
import OpenCapTable.Setup
import OpenCapTable.TestHelpers (addDefaultStockClass)
import qualified DA.Date as DA
import DA.Date (Month(..))
import qualified DA.Time as DT
Expand All @@ -13,8 +14,9 @@ import Daml.Script
-- Test: Create and archive return to pool (termination scenario)
testCreateAndArchiveStockPlanReturnToPool = script do
TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp
capTableCid <- addDefaultStockClass issuer cap_table
result <- submit issuer do
exerciseCmd cap_table CT.UpdateCapTable with
exerciseCmd capTableCid CT.UpdateCapTable with
creates = [CT.OcfCreateStockPlan StockPlanOcfData with
id = "PLAN_2021"
plan_name = "2021 Equity Incentive Plan"
Expand Down Expand Up @@ -45,8 +47,9 @@ testCreateAndArchiveStockPlanReturnToPool = script do
-- Test: Create and archive with Archive choice (expiration scenario)
testCreateAndArchiveStockPlanReturnToPoolExpiration = script do
TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp
capTableCid <- addDefaultStockClass issuer cap_table
result <- submit issuer do
exerciseCmd cap_table CT.UpdateCapTable with
exerciseCmd capTableCid CT.UpdateCapTable with
creates = [CT.OcfCreateStockPlan StockPlanOcfData with
id = "PLAN_2015"
plan_name = "2015 Stock Option Plan"
Expand Down
4 changes: 2 additions & 2 deletions dars/OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar
Git LFS file not shown
4 changes: 2 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": "f77df928bb56c75578fc8cf35ca0853aca80d8ae61e6d533f5578bd54c48b6f9",
"size": 2465540,
"sha256": "2afe248b351f9b4e7b84bf2b23d6c5d8d20308126de6c694ab95d21490b0d82b",
"size": 2466418,
"sdkVersion": "3.4.10",
"uploadedAt": "2026-07-07T21:50:59.634Z",
"networks": []
Expand Down
3 changes: 3 additions & 0 deletions scripts/codegen/captable-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ tiers:
# that the security exists in the correct issuance type. This ensures type safety - a
# StockTransfer can only reference a stock security, not a warrant security.
validations:
# Stock plans - validate referenced stock classes exist
StockPlan: ['stock_class_ids[]:stock_classes']

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same-batch plan before class fails

Medium Severity

New StockPlan reference checks require each stock_class_ids entry to exist in stock_classes before the plan is created. UpdateCapTable only sorts creates by tier, and both types are tier 1, so a batch that lists OcfCreateStockPlan before OcfCreateStockClass fails even when the class is included in the same batch.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 98ee063. Configure here.

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 Move StockPlan into a later create tier

With this validation, StockPlan now depends on StockClass, but both remain tier 1 (scripts/codegen/captable-config.yaml lines 13-19) while UpdateCapTable sorts creates only by getCreateTier (scripts/codegen/templates/CapTable.daml.template line 330). If a client submits a valid same-batch create with OcfCreateStockPlan before the referenced OcfCreateStockClass, the stock-class lookup runs before the class is inserted and the batch fails even though the reference is present in the batch. Put StockPlan in a later tier, and move its dependent plan transactions after it, or otherwise sort same-tier creates by dependency.

Useful? React with 👍 / 👎.


Comment on lines +78 to +80
# Issuances - validate stakeholder exists
StockIssuance:
[
Expand Down
Loading