Skip to content
Draft
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.15
version: 0.0.16
build-options:
- -Wno-crypto-text-is-alpha
- --typecheck-upgrades=no
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary, validateOcfMonetary)
import Fairmint.OpenCapTable.Types.Stock (OcfSecurityExemption, validateOcfSecurityExemption)
import Fairmint.OpenCapTable.Types.Conversion (OcfConvertibleType, OcfConversionTriggerType(..), OcfConvertibleConversionRight(..), validateOcfConvertibleConversionRight, validateOcfConversionTriggerFields)
import Fairmint.Shared.TypeHelpers (validateOptionalText)
import DA.List (unique)


template ConvertibleIssuance
Expand Down Expand Up @@ -85,6 +86,7 @@ validateConvertibleIssuanceOcfData d =
validateOptionalText d.consideration_text &&
-- Required arrays per schema
not (null d.conversion_triggers) &&
unique (map (\trigger -> trigger.trigger_id) d.conversion_triggers) &&
all validateOcfConvertibleConversionTrigger d.conversion_triggers


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Fairmint.OpenCapTable.Types.Stock (OcfQuantitySourceType, OcfSecurityExem
import Fairmint.OpenCapTable.Types.Vesting (OcfVesting, validateOcfVesting)
import Fairmint.OpenCapTable.Types.Conversion (OcfConversionTrigger, validateOcfConversionTrigger)
import Fairmint.Shared.TypeHelpers (validateOptionalText)
import DA.List (unique)

template WarrantIssuance
with
Expand Down Expand Up @@ -95,6 +96,7 @@ validateOcfWarrantIssuanceDataTx d =
optional True validateOcfMonetary d.exercise_price &&
validateOptionalText d.consideration_text &&
-- exercise_triggers: OCF schema requires array but no minItems, so empty array is valid
unique (map (\trigger -> trigger.trigger_id) d.exercise_triggers) &&
all validateOcfConversionTrigger d.exercise_triggers &&
validateOptionalText d.vesting_terms_id &&
-- If vestings array present, enforce items are valid and minItems >= 1
Expand Down
26 changes: 23 additions & 3 deletions OpenCapTable-v34/daml/Fairmint/OpenCapTable/Types/Conversion.daml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module Fairmint.OpenCapTable.Types.Conversion where
-- OCF: https://raw.githubusercontent.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF/main/schema/types/

import DA.Optional (isNone, isSome)
import DA.List (sortOn)
import Fairmint.Shared.TypeHelpers (validateOptionalText, validateRequiredText)
import Fairmint.OpenCapTable.Types.Validation (validateOcfPercentage)
import Fairmint.OpenCapTable.Types.Monetary (OcfMonetary, validateOcfMonetary)
Expand Down Expand Up @@ -72,7 +73,23 @@ data OcfInterestRate = OcfInterestRate
deriving (Eq, Show)

validateOcfInterestRate : OcfInterestRate -> Bool
validateOcfInterestRate r = validateOcfPercentage r.rate
validateOcfInterestRate r =
validateOcfPercentage r.rate &&
optional True (\endDate -> endDate >= r.accrual_start_date) r.accrual_end_date

validateSortedOcfInterestRateSchedule : [OcfInterestRate] -> Bool
validateSortedOcfInterestRateSchedule rates = case rates of
[] -> True
[rate] -> validateOcfInterestRate rate
current :: next :: rest ->
validateOcfInterestRate current &&
current.accrual_start_date < next.accrual_start_date &&
optional True (\endDate -> endDate < next.accrual_start_date) current.accrual_end_date &&
Comment thread
HardlyDifficult marked this conversation as resolved.
validateSortedOcfInterestRateSchedule (next :: rest)

validateOcfInterestRateSchedule : [OcfInterestRate] -> Bool
validateOcfInterestRateSchedule rates =
validateSortedOcfInterestRateSchedule (sortOn (\rate -> rate.accrual_start_date) rates)

-- =============================================================================
-- CAPITALIZATION DEFINITION TYPES
Expand Down Expand Up @@ -360,7 +377,7 @@ data OcfNoteConversionMechanism = OcfNoteConversionMechanism

validateOcfNoteConversionMechanism : OcfNoteConversionMechanism -> Bool
validateOcfNoteConversionMechanism m =
all validateOcfInterestRate m.interest_rates &&
validateOcfInterestRateSchedule m.interest_rates &&
optional True (\d -> d >= (0.0) && d < (1.0 : Decimal)) m.conversion_discount &&
optional True validateOcfMonetary m.conversion_valuation_cap &&
optional True validateOcfRatio m.exit_multiple &&
Expand Down Expand Up @@ -611,6 +628,9 @@ validateOcfConversionTriggerFields triggerType startDate endDate triggerConditio
noTriggerDate = isNone triggerDate
hasStartDate = isSome startDate
hasEndDate = isSome endDate
hasValidRange = case (startDate, endDate) of
(Some start, Some end) -> start <= end
_ -> False
noRange = isNone startDate && isNone endDate
in case triggerType of
OcfTriggerTypeTypeAutomaticOnDate ->
Expand All @@ -620,7 +640,7 @@ validateOcfConversionTriggerFields triggerType startDate endDate triggerConditio
OcfTriggerTypeTypeElectiveOnCondition ->
hasCondition && noTriggerDate && noRange
OcfTriggerTypeTypeElectiveInRange ->
hasStartDate && hasEndDate && noCondition && noTriggerDate
hasStartDate && hasEndDate && hasValidRange && noCondition && noTriggerDate
OcfTriggerTypeTypeElectiveAtWill ->
noCondition && noTriggerDate && noRange
OcfTriggerTypeTypeUnspecified ->
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.15.dar
- ../OpenCapTable-v34/.daml/dist/OpenCapTable-v34-0.0.16.dar
build-options:
- -Wno-template-interface-depends-on-daml-script
59 changes: 55 additions & 4 deletions Test/daml/OpenCapTable/TestConvertibleSecurityBalances.daml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ convertibleConversion transactionId securityId triggerId date resultingSecurityI

convertibleIssuance : Text -> Text -> Text -> Time -> Decimal -> ConvertibleIssuanceOcfData
convertibleIssuance issuanceId securityId stakeholderId date amount =
(defaultConvertibleIssuance issuanceId securityId stakeholderId ("TRIGGER-" <> issuanceId)) with
(defaultConvertibleIssuance issuanceId securityId stakeholderId "TRIGGER-CONVERTIBLE-CONTINUITY") with
date = date
investment_amount = OcfMonetary with amount = amount, currency = "USD"

Expand Down Expand Up @@ -282,6 +282,57 @@ testConvertibleSecurityBalance_TransferCannotChangeSuccessorConversionTerms = sc
edits = []
deletes = []

testConvertibleSecurityBalance_TransferCannotRenameSuccessorTrigger = script do
TestOcp{issuer, cap_table} <- setupTestOcp
capTableReady <- addDefaultStakeholder issuer cap_table
let source = convertibleIssuance "CV-ISS-TRIGGER-ID-SOURCE" "CSEC-TRIGGER-ID-SOURCE" "SH-1" jan01 100.0
baseResult = convertibleIssuance "CV-ISS-TRIGGER-ID-RESULT" "CSEC-TRIGGER-ID-RESULT" "SH-1" jan02 60.0
result = baseResult with
conversion_triggers = map
(\trigger -> trigger with trigger_id = "TRIGGER-CONVERTIBLE-RENAMED")
baseResult.conversion_triggers

submitMustFail issuer do
exerciseCmd capTableReady CT.UpdateCapTable with
creates =
[ CT.OcfCreateConvertibleIssuance source
, CT.OcfCreateConvertibleIssuance result
, CT.OcfCreateConvertibleTransfer (convertibleTransfer
"TX-CV-TRIGGER-ID-TRANSFER"
source.security_id
60.0
jan02
[result.security_id]
None)
]
edits = []
deletes = []

testConvertibleSecurityBalance_CancellationCannotRenameBalanceTrigger = script do
TestOcp{issuer, cap_table} <- setupTestOcp
capTableReady <- addDefaultStakeholder issuer cap_table
let source = convertibleIssuance "CV-ISS-CANCEL-TRIGGER-ID-SOURCE" "CSEC-CANCEL-TRIGGER-ID-SOURCE" "SH-1" jan01 100.0
baseBalance = convertibleIssuance "CV-ISS-CANCEL-TRIGGER-ID-BALANCE" "CSEC-CANCEL-TRIGGER-ID-BALANCE" "SH-1" jan02 60.0
balance = baseBalance with
conversion_triggers = map
(\trigger -> trigger with trigger_id = "TRIGGER-CONVERTIBLE-RENAMED")
baseBalance.conversion_triggers

submitMustFail issuer do
exerciseCmd capTableReady CT.UpdateCapTable with
creates =
[ CT.OcfCreateConvertibleIssuance source
, CT.OcfCreateConvertibleIssuance balance
, CT.OcfCreateConvertibleCancellation (convertibleCancellation
"TX-CV-CANCEL-TRIGGER-ID"
source.security_id
40.0
jan02
(Some balance.security_id))
]
edits = []
deletes = []

testConvertibleSecurityBalance_PartialTransferScalesSuccessorProRata = script do
TestOcp{issuer, cap_table} <- setupTestOcp
capTableReady <- addDefaultStakeholder issuer cap_table
Expand Down Expand Up @@ -420,7 +471,7 @@ testConvertibleSecurityBalance_ConversionWithBalanceRetiresSource = script do
, CT.OcfCreateConvertibleConversion (convertibleConversion
"TX-CV-CONVERSION-BALANCE"
"CSEC-CONVERSION-SOURCE"
"TRIGGER-CV-ISS-CONVERSION-SOURCE"
"TRIGGER-CONVERTIBLE-SUCCESSOR"
jan02
["SSEC-CONVERSION-RESULT"]
(Some "CSEC-CONVERSION-BALANCE")
Expand All @@ -447,7 +498,7 @@ testConvertibleSecurityBalance_ConversionWithoutBalanceRetiresSource = script do
, CT.OcfCreateConvertibleConversion (convertibleConversion
"TX-CV-FULL-CONVERSION"
"CSEC-FULL-CONVERSION-SOURCE"
"TRIGGER-CV-ISS-FULL-CONVERSION-SOURCE"
"TRIGGER-CONVERTIBLE-SUCCESSOR"
jan02
["SSEC-FULL-CONVERSION-RESULT"]
None
Expand Down Expand Up @@ -535,7 +586,7 @@ testConvertibleSecurityBalance_ConversionCannotCreateZeroBalance = script do
, CT.OcfCreateConvertibleConversion (convertibleConversion
"TX-CV-CONVERSION-ZERO-BALANCE"
"CSEC-CONVERSION-ZERO-SOURCE"
"TRIGGER-CV-ISS-CONVERSION-ZERO-SOURCE"
"TRIGGER-CONVERTIBLE-SUCCESSOR"
jan02
["SSEC-CONVERSION-ZERO-RESULT"]
(Some "CSEC-CONVERSION-ZERO-BALANCE")
Expand Down
34 changes: 33 additions & 1 deletion Test/daml/OpenCapTable/TestConvertibleTransfer.daml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ testConvertibleTransfer_NoteFullAssignment = script do
(capTableCid, assigneeId) <- setupStakeholderWithId issuer capTableCid "SH-ASSIGNEE" "Convertible Assignee"
let securityId = "CSEC-NOTE-001"
-- Create a convertible note issuance
let noteTriggerDate = DT.time (DA.date 2025 Jun 15) 0 0 0
let noteTriggerDate = DT.time (DA.date 2026 Jun 15) 0 0 0
result <- submit issuer do
exerciseCmd capTableCid CT.UpdateCapTable with
creates = [CT.OcfCreateConvertibleIssuance (convertibleTransferIssuance
Expand All @@ -133,3 +133,35 @@ testConvertibleTransfer_NoteFullAssignment = script do
edits = []
deletes = []
pure ()

testConvertibleTransfer_PreservesRootReachableTriggers = script do
TestOcp{issuer, cap_table} <- setupTestOcp
capTableCid <- addDefaultStakeholder issuer cap_table
(capTableCid, assigneeId) <- setupStakeholderWithId issuer capTableCid "SH-ASSIGNEE" "Convertible Assignee"
let securityId = "CSEC-NOTE-EXPIRED-TRIGGER"
triggerDate = DT.time (DA.date 2025 Jun 15) 0 0 0
transferDate = DT.time (DA.date 2025 Dec 01) 0 0 0
result <- submit issuer do
exerciseCmd capTableCid CT.UpdateCapTable with
creates = [CT.OcfCreateConvertibleIssuance (convertibleTransferIssuance
"TX_NOTE_EXPIRED_TRIGGER_ISSUANCE" securityId "SH-1" 250000.0
(DT.time (DA.date 2023 Jun 15) 0 0 0) OcfConvertibleNote triggerDate)]
edits = []
deletes = []
_ <- submit issuer do
exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with
creates = [CT.OcfCreateConvertibleIssuance (convertibleTransferIssuance
"TX_NOTE_EXPIRED_TRIGGER_SUCCESSOR" "CSEC-NOTE-EXPIRED-TRIGGER-SUCCESSOR" assigneeId 250000.0
transferDate OcfConvertibleNote triggerDate)
, CT.OcfCreateConvertibleTransfer ConvertibleTransferOcfData with
id = "TX_NOTE_EXPIRED_TRIGGER_TRANSFER"
date = transferDate
amount = OcfMonetary with amount = 250000.0, currency = "USD"
security_id = securityId
resulting_security_ids = ["CSEC-NOTE-EXPIRED-TRIGGER-SUCCESSOR"]
comments = []
balance_security_id = None
consideration_text = None]
edits = []
deletes = []
pure ()
17 changes: 10 additions & 7 deletions Test/daml/OpenCapTable/TestEquityCompensationTransfer.daml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import DA.Date (Month(..))
import qualified DA.Time as DT
import Daml.Script

equityCompensationExpiration : Time
equityCompensationExpiration = DT.time (DA.date 2032 Jan 15) 0 0 0

-- Create an equity compensation issuance to transfer (follows real workflow)
createEquityCompensationIssuance issuer capTableCid securityId quantity issuanceDate =
submit issuer do
Expand All @@ -33,7 +36,7 @@ createEquityCompensationIssuance issuer capTableCid securityId quantity issuance
base_price = None
exercise_price = Some (OcfMonetary with amount = 0.50, currency = "USD")
early_exercisable = None
expiration_date = Some (DT.time (DA.date 2032 Jan 15) 0 0 0)
expiration_date = Some equityCompensationExpiration
stock_class_id = None
stock_plan_id = None
vesting_terms_id = None
Expand All @@ -54,7 +57,7 @@ testEquityCompensationTransfer_NSOToFamilyTrust = script do
balanceResult <- createEquityCompensationIssuance issuer result.updatedCapTableCid "ESEC-NSO-REMAIN" 5000.0 transferDate
_ <- submit issuer do
exerciseCmd balanceResult.updatedCapTableCid CT.UpdateCapTable with
creates = [equityCompensationSuccessorIssuance "EC-ISS-TRANSFERRED-001" "ESEC-NSO-TRANSFERRED-001" transfereeId transferDate 5000.0
creates = [equityCompensationSuccessorIssuance "EC-ISS-TRANSFERRED-001" "ESEC-NSO-TRANSFERRED-001" transfereeId transferDate 5000.0 (Some equityCompensationExpiration)
, CT.OcfCreateEquityCompensationTransfer EquityCompensationTransferOcfData with
id = "TX_EQUITY_COMP_TRANSFER_1"
date = transferDate
Expand All @@ -78,7 +81,7 @@ testEquityCompensationTransfer_ResultQuantityMustMatchTransfer = script do
submitMustFail issuer do
exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with
creates =
[ equityCompensationSuccessorIssuance "EC-ISS-QUANTITY-MISMATCH" "ESEC-QUANTITY-MISMATCH-RESULT" transfereeId (DT.time (DA.date 2025 Jun 01) 0 0 0) 59.0
[ equityCompensationSuccessorIssuance "EC-ISS-QUANTITY-MISMATCH" "ESEC-QUANTITY-MISMATCH-RESULT" transfereeId (DT.time (DA.date 2025 Jun 01) 0 0 0) 59.0 (Some equityCompensationExpiration)
, CT.OcfCreateEquityCompensationTransfer EquityCompensationTransferOcfData with
id = "TX-EC-TRANSFER-QUANTITY-MISMATCH"
date = DT.time (DA.date 2025 Jun 01) 0 0 0
Expand All @@ -103,8 +106,8 @@ testEquityCompensationTransfer_BalanceMustKeepSourceStakeholder = script do
submitMustFail issuer do
exerciseCmd sourceResult.updatedCapTableCid CT.UpdateCapTable with
creates =
[ equityCompensationSuccessorIssuance "EC-ISS-BALANCE-HOLDER-RESULT" "ESEC-BALANCE-HOLDER-RESULT" transfereeId transferDate 60.0
, equityCompensationSuccessorIssuance "EC-ISS-BALANCE-HOLDER-REMAINDER" "ESEC-BALANCE-HOLDER-REMAINDER" transfereeId transferDate 40.0
[ equityCompensationSuccessorIssuance "EC-ISS-BALANCE-HOLDER-RESULT" "ESEC-BALANCE-HOLDER-RESULT" transfereeId transferDate 60.0 (Some equityCompensationExpiration)
, equityCompensationSuccessorIssuance "EC-ISS-BALANCE-HOLDER-REMAINDER" "ESEC-BALANCE-HOLDER-REMAINDER" transfereeId transferDate 40.0 (Some equityCompensationExpiration)
, CT.OcfCreateEquityCompensationTransfer EquityCompensationTransferOcfData with
id = "TX-EC-BALANCE-HOLDER-MISMATCH"
date = transferDate
Expand All @@ -127,7 +130,7 @@ testEquityCompensationTransfer_FullTransfer = script do
result <- createEquityCompensationIssuance issuer capTableCid securityId 2500.0 (DT.time (DA.date 2022 Jan 15) 0 0 0)
_ <- submit issuer do
exerciseCmd result.updatedCapTableCid CT.UpdateCapTable with
creates = [equityCompensationSuccessorIssuance "EC-ISS-NEWHOLDER" "ESEC-NSO-NEWHOLDER" transfereeId (DT.time (DA.date 2025 Sep 15) 0 0 0) 2500.0
creates = [equityCompensationSuccessorIssuance "EC-ISS-NEWHOLDER" "ESEC-NSO-NEWHOLDER" transfereeId (DT.time (DA.date 2025 Sep 15) 0 0 0) 2500.0 (Some equityCompensationExpiration)
, CT.OcfCreateEquityCompensationTransfer EquityCompensationTransferOcfData with
id = "TX_EQUITY_COMP_TRANSFER_FULL"
date = DT.time (DA.date 2025 Sep 15) 0 0 0
Expand All @@ -152,7 +155,7 @@ testEquityCompensationTransfer_WithApprovalComments = script do
balanceResult <- createEquityCompensationIssuance issuer result.updatedCapTableCid "ESEC-NSO-REMAINING" 7000.0 transferDate
_ <- submit issuer do
exerciseCmd balanceResult.updatedCapTableCid CT.UpdateCapTable with
creates = [equityCompensationSuccessorIssuance "EC-ISS-TRUST" "ESEC-NSO-TRUST" transfereeId transferDate 8000.0
creates = [equityCompensationSuccessorIssuance "EC-ISS-TRUST" "ESEC-NSO-TRUST" transfereeId transferDate 8000.0 (Some equityCompensationExpiration)
, CT.OcfCreateEquityCompensationTransfer EquityCompensationTransferOcfData with
id = "TX_EQUITY_COMP_TRANSFER_APPROVED"
date = transferDate
Expand Down
9 changes: 5 additions & 4 deletions Test/daml/OpenCapTable/TestHelpers.daml
Original file line number Diff line number Diff line change
Expand Up @@ -345,15 +345,16 @@ createWarrantIssuance issuer capTableCid securityId quantity issuanceDate =

convertibleSuccessorIssuance : Text -> Text -> Text -> Time -> Decimal -> CT.OcfCreateData
convertibleSuccessorIssuance issuanceId securityId stakeholderId date investmentAmount =
CT.OcfCreateConvertibleIssuance ((defaultConvertibleIssuance issuanceId securityId stakeholderId ("TRIGGER-" <> issuanceId)) with
CT.OcfCreateConvertibleIssuance ((defaultConvertibleIssuance issuanceId securityId stakeholderId "TRIGGER-CONVERTIBLE-SUCCESSOR") with
date = date
investment_amount = OcfMonetary with amount = investmentAmount, currency = "USD")

Comment thread
coderabbitai[bot] marked this conversation as resolved.
equityCompensationSuccessorIssuance : Text -> Text -> Text -> Time -> Decimal -> CT.OcfCreateData
equityCompensationSuccessorIssuance issuanceId securityId stakeholderId date quantity =
equityCompensationSuccessorIssuance : Text -> Text -> Text -> Time -> Decimal -> Optional Time -> CT.OcfCreateData
equityCompensationSuccessorIssuance issuanceId securityId stakeholderId date quantity expirationDate =
CT.OcfCreateEquityCompensationIssuance ((defaultEquityCompensationIssuance issuanceId securityId stakeholderId) with
date = date
quantity = quantity)
quantity = quantity
expiration_date = expirationDate)

-- | Returns valid StockLegendTemplateOcfData with sensible defaults.
defaultStockLegendTemplate : Text -> StockLegendTemplateOcfData
Expand Down
Loading
Loading