-
Notifications
You must be signed in to change notification settings - Fork 0
Enforce issuer authorized share ceiling #243
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
96aef65
ca1821f
7ba294d
6685101
ac22e27
c923ede
b5f8bbc
237a7e9
98ee063
f469eb8
68b6b25
50e106d
14f3e5b
84cce5c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 = [] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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)) | ||
|
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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the issuer has no finite cap ( 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For a finite issuer, this branch rejects any stock class whose effective authorization is Useful? React with 👍 / 👎. |
||
|
|
||
| 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 | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the issuer has a finite cap, every
UpdateCapTablefolds 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 👍 / 👎.