Skip to content

Enforce stock plan reserved share ceilings#252

Closed
HardlyDifficult wants to merge 15 commits into
codex/ocf-reference-integrityfrom
codex/ocf-stock-plan-pool-ceiling-v2
Closed

Enforce stock plan reserved share ceilings#252
HardlyDifficult wants to merge 15 commits into
codex/ocf-reference-integrityfrom
codex/ocf-stock-plan-pool-ceiling-v2

Conversation

@HardlyDifficult

@HardlyDifficult HardlyDifficult commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

Stacked on #251.

This adds a final-state stock-plan pool invariant: securities issued from a stock plan cannot leave the plan with more net allocated shares than its effective reserved pool.

The effective reserved pool is:

  • latest StockPlanPoolAdjustment.shares_reserved, when present;
  • otherwise StockPlan.initial_shares_reserved.

The net allocated amount is stock issuances plus equity-compensation issuances tied to the plan, minus explicit StockPlanReturnToPool quantities. Returns are also validated so they cannot manufacture capacity: each return must reference a stock or equity-compensation security from the same plan, and cumulative returns for a security cannot exceed that security's original allocation.

The check runs after deletes, edits, issuer edit, and creates have all been processed, so a single batch can raise the pool or return shares and issue from the corrected pool while still rejecting invalid final state.

OCF documentation

Tests

Added TestStockPlanReservedShares coverage for:

  • stock issuance exceeding the initial pool;
  • equity compensation issuance exceeding the initial pool;
  • same-batch pool adjustment allowing additional issuance;
  • same-date lower pool adjustment taking precedence;
  • returns to pool allowing reuse;
  • returns exceeding the security allocation;
  • returns to a different plan than the source security;
  • pool adjustment dropping below allocated shares;
  • deleting a pool adjustment that would invalidate allocated shares;
  • stock plan edit dropping below allocated shares.

Updated existing return-to-pool smoke tests to create real plan-backed securities before returning them to pool.

Validation

  • NODE_PATH=/Users/fairnick/Documents/code/fairmint/open-captable-protocol-daml/node_modules PATH="/Users/fairnick/Documents/code/fairmint/open-captable-protocol-daml/node_modules/.bin:$HOME/.dpm/bin:$PATH" npm run build
  • From Test/: JAVA_HOME=/opt/homebrew/opt/openjdk@17 PATH="/Users/fairnick/Documents/code/fairmint/open-captable-protocol-daml/node_modules/.bin:$HOME/.dpm/bin:$PATH" dpm test --show-coverage --color --coverage-ignore-choice 'splice-amulet:.*'
  • NODE_PATH=/Users/fairnick/Documents/code/fairmint/open-captable-protocol-daml/node_modules PATH="/Users/fairnick/Documents/code/fairmint/open-captable-protocol-daml/node_modules/.bin:$HOME/.dpm/bin:$PATH" npm run lint:daml
  • NODE_PATH=/Users/fairnick/Documents/code/fairmint/open-captable-protocol-daml/node_modules PATH="/Users/fairnick/Documents/code/fairmint/open-captable-protocol-daml/node_modules/.bin:$HOME/.dpm/bin:$PATH" npm run verify-dars
  • JAVA_HOME=/opt/homebrew/opt/openjdk@17 NODE_PATH=/Users/fairnick/Documents/code/fairmint/open-captable-protocol-daml/node_modules PATH="/Users/fairnick/Documents/code/fairmint/open-captable-protocol-daml/node_modules/.bin:/opt/homebrew/opt/openjdk@17/bin:$HOME/.dpm/bin:$PATH" npm run check-upgrade-compat

Note

Medium Risk
Changes core cap-table mutation validation for equity plan pools and returns, which can block previously accepted UpdateCapTable batches; scope is bounded to stock-plan accounting with broad script coverage.

Overview
Cap table UpdateCapTable now rejects invalid final state where net plan allocations exceed the effective reserved pool (latest StockPlanPoolAdjustment.shares_reserved, else StockPlan.initial_shares_reserved). Net allocation sums stock and equity-compensation issuances tied to the plan and subtracts StockPlanReturnToPool amounts.

Return-to-pool rules are tightened: returns must reference a plan-backed security on the same plan, and cumulative returns per security cannot exceed that security’s issuance quantity. Pool adjustments use same-date precedence where the lower shares_reserved wins (mirroring other adjustment types). Validation runs after the full batch (creates, edits, deletes), so a single update can raise the pool or return shares and then issue from the corrected ceiling.

OpenCapTable-v34 is bumped to 0.0.5 with an updated packaged DAR. Tests add TestStockPlanReservedShares for over-issuance, same-batch adjustments, return reuse/limits, and edits/deletes that would shrink the pool below outstanding allocations; existing return-to-pool smoke tests now create real plan-backed securities first.

Reviewed by Cursor Bugbot for commit c21a636. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Added stock plan reserved-share accounting and validation, enforcing issuance ceilings, same-batch adjustment precedence, and correct reuse of shares returned to the pool.
  • Bug Fixes

    • Prevented reserved pools from being reduced below already-allocated/issued quantities.
    • Tightened return-to-pool checks so returned amounts can’t exceed the referenced security’s allowed allocation.
  • Tests

    • Added a new reserved-share test suite covering failure and success scenarios.
    • Refreshed return-to-pool tests to use shared setup and parameterized identifiers.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds reserved-share pool ceiling validation to the CapTable DAML template, bumps OpenCapTable-v34 to 0.0.5, and adds/updates DAML tests for stock plan pool adjustments, return-to-pool behavior, and issuance allocation limits.

Changes

Stock plan pool ceiling validation

Layer / File(s) Summary
Pool adjustment precedence and reserved-share computation
scripts/codegen/templates/CapTable.daml.template
Adds precedence rules and per-plan indices to derive each stock plan’s effective reserved-share ceiling from pool adjustments or the initial reserved amount.
Allocation indices and reserved-share validation
scripts/codegen/templates/CapTable.daml.template
Adds quantity aggregation helpers, stock- and equity-compensation allocation indices, return-to-pool accounting, security allocation resolution, reserved-share ceiling validation, and the new batch-validation hook.
Package version and dar artifact bump
OpenCapTable-v34/daml.yaml, Test/daml.yaml, dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar
Bumps the package version to 0.0.5, updates the test dependency to the new DAR, and refreshes the DAR pointer.
New reserved-shares test suite
Test/daml/OpenCapTable/TestStockPlanReservedShares.daml
Adds a test module with helper constructors, a shared setup helper, and reserved-share tests for issuance limits, pool adjustments, return-to-pool reuse, and invalid adjustment/edit/delete cases.
Return-to-pool test refactor
Test/daml/OpenCapTable/TestStockPlanReturnToPool.daml
Refactors the existing return-to-pool tests to use setupForStockIssuance and variable-driven identifiers instead of hardcoded setup values.

Estimated code review effort: 4 (Complex) | ~60 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: enforcing stock plan reserved share ceilings.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/ocf-stock-plan-pool-ceiling-v2

Comment @coderabbitai help to get the list of available commands.

@HardlyDifficult

Copy link
Copy Markdown
Collaborator Author

@copilot review

@HardlyDifficult
HardlyDifficult marked this pull request as ready for review July 9, 2026 01:09

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 87737fa232

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

case issuance.issuance_data.stock_plan_id of
Some planId ->
if planId == stockPlanId
then pure (total + issuance.issuance_data.quantity)

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 Reject negative equity quantities before summing pool allocations

When an EquityCompensationIssuance has a negative quantity (currently not rejected by validateEquityCompensationIssuanceOcfData), adding it here reduces the computed allocation for the stock plan. That lets a batch pair an oversized positive plan issuance with a negative equity-compensation issuance for the same plan and still pass the new allocatedQuantity <= reservedShares check, bypassing the reserved-share ceiling this change is trying to enforce.

Useful? React with 👍 / 👎.

assertMsg
("Stock plan reserved shares exceeded for " <> stockPlan.plan_data.id <> ": allocated " <> show allocatedQuantity <> " exceeds reserved " <> show reservedShares)
(allocatedQuantity <= reservedShares))
(Map.values maps.stock_plans)

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 Validate plan-backed stock issuances even if the plan is deleted

When a batch deletes a StockPlan that is still referenced by StockIssuance.stock_plan_id, this loop only visits surviving stock plans, so the deleted plan id is never passed to stockPlanNetAllocatedQuantity and its allocations disappear from the reserved-share check. Unlike equity compensation, stock issuances are not revalidated after deletes, so this leaves orphaned plan-backed stock and bypasses the new pool ceiling entirely.

Useful? React with 👍 / 👎.

case issuance.issuance_data.stock_plan_id of
Some planId ->
if planId == stockPlanId
then pure (total + issuance.issuance_data.quantity)

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 Apply stock splits when counting plan-backed stock

When a plan-backed stock issuance is followed by a StockClassSplit for its class, this adds the original issuance quantity rather than the split-adjusted current quantity already computed for authorized-share checks. A 100-share plan issuance from a 100-share pool followed by a 2-for-1 split still contributes only 100 here, so the cap table can end with 200 plan-backed stock shares while passing the new reserved-pool ceiling unless a separate return/issuance changes the original quantity.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Implements a final-state invariant in UpdateCapTable to ensure plan-backed issuances (stock + equity compensation), net of valid returns-to-pool, never exceed a stock plan’s effective reserved share pool (latest pool adjustment by date, same-date ties choosing the lower reserve; otherwise the plan’s initial reserve). This tightens correctness around stock plan capacity accounting and return-to-pool integrity.

Changes:

  • Add stock plan pool ceiling validation (validateStockPlanPoolCeilings) to the cap table update flow, including stricter return-to-pool constraints.
  • Add a comprehensive TestStockPlanReservedShares suite and update existing return-to-pool smoke tests to use actual plan-backed securities.
  • Bump OpenCapTable-v34 to 0.0.5 and update test DAR/data dependencies and lockfiles accordingly.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
Test/daml/OpenCapTable/TestStockPlanReturnToPool.daml Updates return-to-pool smoke tests to create real plan-backed issuances before returning shares.
Test/daml/OpenCapTable/TestStockPlanReservedShares.daml Adds new test coverage for reserved-share ceiling enforcement, adjustments precedence, and return-to-pool constraints.
Test/daml.yaml Updates test data-dependency DAR to OpenCapTable-v34-0.0.5.
scripts/codegen/templates/CapTable.daml.template Adds pool adjustment precedence/indexing plus final-state stock plan pool ceiling and return-to-pool validations, wired into UpdateCapTable.
OpenCapTable-v34/daml.yaml Bumps package version from 0.0.4 to 0.0.5.
dars/OpenCapTable-v34/0.0.5/OpenCapTable-v34.dar Adds the new immutable DAR artifact (LFS pointer).
dars/dars.lock Records the new DAR’s sha/size/sdkVersion/upload timestamp.

Comment on lines +504 to +509
stockPlanNetAllocatedQuantity : CapTableMaps -> Text -> Update Decimal
stockPlanNetAllocatedQuantity maps stockPlanId = do
stockAllocation <- stockIssuancePlanAllocation maps stockPlanId
equityCompensationAllocation <- equityCompensationPlanAllocation maps stockPlanId
returnedQuantity <- stockPlanReturnedQuantity maps stockPlanId
pure (stockAllocation + equityCompensationAllocation - returnedQuantity)
Comment on lines +447 to +456
stockPlanReturnedQuantityForSecurity : CapTableMaps -> Text -> Update Decimal
stockPlanReturnedQuantityForSecurity maps securityId =
foldlA
(\total returnCid -> do
returnToPool <- fetch returnCid
if returnToPool.return_data.security_id == securityId
then pure (total + returnToPool.return_data.quantity)
else pure total)
0.0
(Map.values maps.stock_plan_return_to_pools)
…nto codex/ocf-stock-plan-pool-ceiling-v2

# Conflicts:
#	scripts/codegen/templates/CapTable.daml.template

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b7debe1. Configure here.

Comment thread scripts/codegen/templates/CapTable.daml.template
@HardlyDifficult

Copy link
Copy Markdown
Collaborator Author

@copilot review

Copilot AI commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

@copilot review

Addressed with commit 9fb27ae (plus follow-up optimization commits ecd2422, 73ad3fc, and 5fa7204). The stock-plan ceiling checks now precompute per-security return totals and per-plan net allocations, removing repeated full-map scans in the validation paths.

@HardlyDifficult

Copy link
Copy Markdown
Collaborator Author

CI is green on the latest head (771f7b8). @copilot review

@HardlyDifficult

Copy link
Copy Markdown
Collaborator Author

CI is green on the latest head (c21a636). @copilot review

@HardlyDifficult

Copy link
Copy Markdown
Collaborator Author

Closed as superseded by #238. The stock plan reserved-share ceiling work is now folded into the authorized/reserved share ceiling category branch.

I carried over the actionable review feedback before closing this PR:

  • plan-backed stock allocations remain validated even if the StockPlan is deleted, with regression test testStockPlanReservedShares_DeletePlanCannotOrphanStockIssuance
  • negative plan-backed equity-comp quantities can no longer offset an overallocated plan pool, with regression test testStockPlanReservedShares_NegativeEquityCompensationCannotOffsetOverallocatedPool
  • plan-backed stock allocations are split-adjusted before checking the reserved pool, with regression test testStockPlanReservedShares_StockSplitCountsAgainstPlanPool
  • stock return-to-pool reuse now requires actual returned stock, with regression tests testStockPlanReservedShares_StockReturnRequiresReturnedShares and testStockPlanReservedShares_StockCancellationReturnAllowsReuse

Local validation on #238: npm run build, npm run backup-dar -- --package OpenCapTable-v34 --version 0.0.5, npm run verify-dars, focused stock-plan tests, npm test, npm run lint:daml, npm run check-upgrade-compat, and git diff --check all pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants