Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8f498c3
Enforce global security id uniqueness
HardlyDifficult Jul 9, 2026
69cae78
Merge remote-tracking branch 'origin/codex/ocf-vesting-graph-integrit…
HardlyDifficult Jul 9, 2026
019d401
Refresh security id DAR after vesting merge
HardlyDifficult Jul 9, 2026
f1c8eee
Merge remote-tracking branch 'origin/codex/ocf-vesting-graph-integrit…
HardlyDifficult Jul 9, 2026
b0c4766
Refresh security id DAR after vesting update
HardlyDifficult Jul 9, 2026
3b83a24
Merge remote-tracking branch 'origin/codex/ocf-vesting-graph-integrit…
HardlyDifficult Jul 9, 2026
bb52fcd
Refresh security id DAR after vesting fixes
HardlyDifficult Jul 9, 2026
7a105ca
Merge remote-tracking branch 'origin/codex/ocf-vesting-graph-integrit…
HardlyDifficult Jul 9, 2026
dfa0f14
Refresh security id DAR after reference fold
HardlyDifficult Jul 9, 2026
1ae9713
Validate final security ID uniqueness
HardlyDifficult Jul 9, 2026
70b1f1d
Merge remote-tracking branch 'origin/codex/ocf-vesting-graph-integrit…
HardlyDifficult Jul 9, 2026
5fd583a
Refresh security ID DAR after reference updates
HardlyDifficult Jul 9, 2026
9139d35
Merge remote-tracking branch 'origin/codex/ocf-vesting-graph-integrit…
HardlyDifficult Jul 9, 2026
133300b
Refresh security ID DAR after vesting updates
HardlyDifficult Jul 9, 2026
5121feb
Merge commit 'a5fbd9d' into codex/ocf-security-id-uniqueness
HardlyDifficult Jul 10, 2026
bae63d4
feat: enforce lifecycle successor ID uniqueness
HardlyDifficult Jul 10, 2026
0901de0
Merge commit '8b1992f' into codex/ocf-security-id-uniqueness
HardlyDifficult Jul 10, 2026
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.13
version: 0.0.14
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.13.dar
- ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.14.dar
build-options:
- -Wno-template-interface-depends-on-daml-script
172 changes: 171 additions & 1 deletion Test/daml/OpenCapTable/TestSecurityIdValidation.daml
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,40 @@ module OpenCapTable.TestSecurityIdValidation where

import qualified Fairmint.OpenCapTable.OCF.StockTransfer as ST
import qualified Fairmint.OpenCapTable.OCF.WarrantTransfer as WT
import qualified Fairmint.OpenCapTable.OCF.WarrantIssuance as WI
import qualified Fairmint.OpenCapTable.OCF.ConvertibleTransfer as CVT
import qualified Fairmint.OpenCapTable.OCF.EquityCompensationTransfer as ECT
import qualified Fairmint.OpenCapTable.CapTable as CT
import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary(..))
import OpenCapTable.Setup
import OpenCapTable.TestHelpers (addPrerequisites, addDefaultStockIssuance, addDefaultWarrantIssuance, addDefaultConvertibleIssuance, addDefaultEquityCompensationIssuance)
import OpenCapTable.TestHelpers (addPrerequisites, addDefaultStockIssuance, addDefaultWarrantIssuance, addDefaultConvertibleIssuance, addDefaultEquityCompensationIssuance, addStockIssuance, defaultStockIssuance)
import qualified DA.Date as DA
import DA.Date (Month(..))
import qualified DA.Map as Map
import qualified DA.Time as DT
import Daml.Script

warrantIssuanceData : Text -> Text -> Text -> WI.WarrantIssuanceOcfData
warrantIssuanceData issuanceId securityId stakeholderId = WI.WarrantIssuanceOcfData with
id = issuanceId
custom_id = "WR-" <> issuanceId
date = DT.time (DA.date 2024 Jan 01) 0 0 0
purchase_price = OcfMonetary with amount = 1.0, currency = "USD"
security_id = securityId
stakeholder_id = stakeholderId
comments = []
exercise_triggers = []
security_law_exemptions = []
vestings = []
board_approval_date = None
consideration_text = None
exercise_price = None
quantity = Some 100.0
quantity_source = None
stockholder_approval_date = None
vesting_terms_id = None
warrant_expiration_date = None


-- =============================================================================
-- Test: Non-existent security_id fails with clear error
Expand Down Expand Up @@ -204,6 +227,153 @@ testEquityCompensationTransfer_WrongSecurityTypeFails = script do
deletes = []
pure ()

-- =============================================================================
-- Test: security_id values are globally unique across security issuance types
-- =============================================================================

testSecurityIssuance_DuplicateSecurityIdAcrossTypesFails = script do
TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp
cap_table <- addPrerequisites issuer cap_table
cap_table <- addDefaultStockIssuance issuer cap_table

submitMustFail issuer do
exerciseCmd cap_table CT.UpdateCapTable with
creates = [CT.OcfCreateWarrantIssuance (warrantIssuanceData "WR-DUP-STOCK-SECURITY" "SEC-STOCK-1" "SH-1")]
edits = []
deletes = []
pure ()

testSecurityIssuance_DuplicateSecurityIdAcrossTypesFailsInSameBatch = script do
TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp
cap_table <- addPrerequisites issuer cap_table

submitMustFail issuer do
exerciseCmd cap_table CT.UpdateCapTable with
creates =
[ CT.OcfCreateStockIssuance (defaultStockIssuance "TX-BATCH-STOCK" "SEC-BATCH-DUP" "SH-1" "SC_COMMON")
, CT.OcfCreateWarrantIssuance (warrantIssuanceData "TX-BATCH-WARRANT" "SEC-BATCH-DUP" "SH-1")
]
edits = []
deletes = []
pure ()

testSecurityIssuance_EditCannotReuseSecurityIdFromAnotherType = script do
TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp
cap_table <- addPrerequisites issuer cap_table
cap_table <- addDefaultWarrantIssuance issuer cap_table
cap_table <- addStockIssuance issuer cap_table "STOCK-EDIT-SRC" "SEC-STOCK-EDIT" "SH-1" "SC_COMMON"

submitMustFail issuer do
exerciseCmd cap_table CT.UpdateCapTable with
creates = []
edits = [CT.OcfEditStockIssuance (defaultStockIssuance "STOCK-EDIT-SRC" "SEC-WR-1" "SH-1" "SC_COMMON")]
deletes = []
pure ()

-- A final-state validation must reject legacy or upgraded cap tables whose
-- security ID index is stale and therefore hides a duplicate issuance.
testSecurityIssuance_FinalStateDuplicateSecurityIdsFail = script do
TestOcp{system_operator, issuer, ctx, cap_table} <- setupTestOcp
cap_table <- addPrerequisites issuer cap_table
cap_table <- addDefaultStockIssuance issuer cap_table
capTableDataOpt <- queryContractId issuer cap_table
let Some capTableData = capTableDataOpt

duplicateWarrantCid <- submit (actAs issuer <> actAs system_operator) $ createCmd $ WI.WarrantIssuance with
context = ctx
issuance_data = warrantIssuanceData "WR-UPGRADED-DUPLICATE" "SEC-STOCK-1" "SH-1"

let corruptedCapTable = capTableData with
warrant_issuances = Map.insert "WR-UPGRADED-DUPLICATE" duplicateWarrantCid capTableData.warrant_issuances
corruptedCapTableCid <- submit (actAs issuer <> actAs system_operator) $ createCmd corruptedCapTable

submitMustFail issuer do
exerciseCmd corruptedCapTableCid CT.UpdateCapTable with
creates = []
edits = []
deletes = []
pure ()

-- Lifecycle outputs identify newly created securities, so an output cannot
-- reuse the source security's identity.
testSuccessorSecurityId_CannotReuseSourceSecurityId = script do
TestOcp{issuer, cap_table} <- setupTestOcp
cap_table <- addPrerequisites issuer cap_table
cap_table <- addStockIssuance issuer cap_table "STOCK-SOURCE-REUSE" "SEC-SOURCE-REUSE" "SH-1" "SC_COMMON"

submitMustFail issuer do
exerciseCmd cap_table CT.UpdateCapTable with
creates = [CT.OcfCreateStockTransfer ST.StockTransferOcfData with
id = "TX-SOURCE-REUSE"
date = DT.time (DA.date 2024 Jan 01) 0 0 0
security_id = "SEC-SOURCE-REUSE"
quantity = 100.0
comments = []
resulting_security_ids = ["SEC-SOURCE-REUSE"]
balance_security_id = None
consideration_text = None]
edits = []
deletes = []
pure ()

-- A partial transaction creates distinct recipient and remainder securities.
testSuccessorSecurityId_BalanceCannotAlsoBeResult = script do
TestOcp{issuer, cap_table} <- setupTestOcp
cap_table <- addPrerequisites issuer cap_table
cap_table <- addStockIssuance issuer cap_table "STOCK-BALANCE-RESULT-SOURCE" "SEC-BALANCE-RESULT-SOURCE" "SH-1" "SC_COMMON"

submitMustFail issuer do
exerciseCmd cap_table CT.UpdateCapTable with
creates =
[ CT.OcfCreateStockIssuance
((defaultStockIssuance "STOCK-BALANCE-RESULT" "SEC-BALANCE-RESULT" "SH-1" "SC_COMMON") with quantity = 50.0)
, CT.OcfCreateStockTransfer ST.StockTransferOcfData with
id = "TX-BALANCE-RESULT"
date = DT.time (DA.date 2024 Jan 01) 0 0 0
security_id = "SEC-BALANCE-RESULT-SOURCE"
quantity = 50.0
comments = []
resulting_security_ids = ["SEC-BALANCE-RESULT"]
balance_security_id = Some "SEC-BALANCE-RESULT"
consideration_text = None
]
edits = []
deletes = []
pure ()

-- Even when successor issuances are not materialized, one security identity
-- cannot be claimed as the output of multiple lifecycle transactions.
testSuccessorSecurityId_CannotBeProducedByMultipleTransactionTypes = script do
TestOcp{issuer, cap_table} <- setupTestOcp
cap_table <- addPrerequisites issuer cap_table
cap_table <- addStockIssuance issuer cap_table "STOCK-SHARED-RESULT-A" "SEC-SHARED-RESULT-A" "SH-1" "SC_COMMON"
cap_table <- addDefaultWarrantIssuance issuer cap_table

submitMustFail issuer do
exerciseCmd cap_table CT.UpdateCapTable with
creates =
[ CT.OcfCreateStockTransfer ST.StockTransferOcfData with
id = "TX-SHARED-RESULT-A"
date = DT.time (DA.date 2024 Jan 01) 0 0 0
security_id = "SEC-SHARED-RESULT-A"
quantity = 100.0
comments = []
resulting_security_ids = ["SEC-SHARED-RESULT"]
balance_security_id = None
consideration_text = None
, CT.OcfCreateWarrantTransfer WT.WarrantTransferOcfData with
id = "TX-SHARED-RESULT-B"
date = DT.time (DA.date 2024 Jan 01) 0 0 0
security_id = "SEC-WR-1"
quantity = 100.0
comments = []
resulting_security_ids = ["SEC-SHARED-RESULT"]
balance_security_id = None
consideration_text = None
]
edits = []
deletes = []
pure ()

-- =============================================================================
-- Test: Valid security_id succeeds
Expand Down
3 changes: 3 additions & 0 deletions dars/OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar
Git LFS file not shown
7 changes: 7 additions & 0 deletions dars/dars.lock
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@
"uploadedAt": "2026-07-09T18:48:49.697Z",
"networks": []
},
"OpenCapTable-v34/0.0.14/OpenCapTable-v34.dar": {
"sha256": "8d173853748b69a7b4bbb1c291cdf27bde89bcdce1f29ccd98c7601521e28050",
"size": 3094585,
"sdkVersion": "3.4.10",
"uploadedAt": "2026-07-09T18:50:58.571Z",
"networks": []
},
"OpenCapTable-v34/0.0.2/OpenCapTable-v34.dar": {
"sha256": "2ce462b8afcb1bcdbe351c56b0a47de478477f3c655da00e41cb397f63e997d6",
"size": 2446238,
Expand Down
Loading
Loading