Skip to content

Whitelist cap table creators via Merkle allowlist (KYB-ready) #234

@ThatAlexPalmer

Description

@ThatAlexPalmer

Summary

Any wallet can call CapTableFactory.createCapTable(), which lets it deploy a cap table. We want to restrict creation to a vetted set of wallets (start with 0x3601a913fD3466f30f5ABb978E484d1B37Ce995D) and leave room to enforce KYB in the future.

Current State (verified)

  • Factory is Ownable and deploys BeaconProxy instances; CapTable.initialize sets admin = msg.sender. Owner can upgrade the beacon implementation (onlyOwner). There is no access control on createCapTable.
  • Tests assert permissionless creation and that the caller becomes ADMIN of their own cap table (e.g., testNonOwnerCanCreateCapTable, testNonOwnerIsCapTableAdmin, testNonOwnerCannotManageOthersCapTable).

ABI Impact

  • The change is ABI-affecting (adds a proof parameter to createCapTable) and will require a factory re-deploy and env updates.

Proposal (Phase 1: Merkle allowlist, owner-managed, KYB‑ready)

  • Gate createCapTable behind Merkle proof verification against a stored root.
  • Keep an owner bypass (break-glass).
  • Emit events for observability.
  • Use domain-separated leaves to prevent cross-contract/chain proof reuse.

Contract Changes (CapTableFactory.sol)

  • Imports: OpenZeppelin MerkleProof (v5).
  • Storage:
  • bytes32 public creatorAllowlistRoot; bool public whitelistEnabled = true;
    Events:
  • event CreatorAllowlistRootUpdated(bytes32 indexed root, address indexed by);
  • event WhitelistEnabledSet(bool enabled, address indexed by);
    Admin (onlyOwner):
  • setCreatorAllowlistRoot(bytes32 root)
  • setWhitelistEnabled(bool enabled)
    View:
  • currentAllowlistRoot() -> bytes32
    Gate (createCapTable):
  • Append proof param
      bytes16 id,
      string memory name,
      uint256 initial_shares_authorized,
      address operator,
      bytes32[] calldata proof
    ) external returns (address)
  • If whitelistEnabled: allow owner OR require valid proof:
      if (msg.sender != owner()) {
        bool ok = MerkleProof.verify(proof, creatorAllowlistRoot, _leaf(msg.sender));
        if (!ok) revert CreatorNotAllowed(msg.sender);
      }
    }

Deployment / Migration

  • Re-deploy the factory with the updated ABI.
  • Owner sets the initial root (setCreatorAllowlistRoot) computed from an allowlist that includes: 0x3601a913fD3466f30f5ABb978E484d1B37Ce995D
  • Update NEXT_PUBLIC_FACTORY_ADDRESS (frontend) and corresponding server config.

Frontend / Server Integration

  • Frontend:
  • Fetch Merkle proof for the connected wallet before calling createCapTable; pass proof to the contract call.
  • Handle CreatorNotAllowed revert with a clear “not allowed to create” message.
  • Owner-bypass remains for internal testing.
  • Server:
  • Provide GET /allowlist/proof?address=0x... returning { root, proof } from a maintained JSON/CSV allowlist.
  • Root rotation flow: publish new root + call setCreatorAllowlistRoot.
  • Build tooling:
  • Script to compute the Merkle root and per-address proofs from CSV/JSON.
  • Foundry script to set the root on-chain.

Tests (add/modify)

  • Revert when: whitelistEnabled AND (no proof | bad proof | wrong root).
  • Success when: valid proof; owner without proof; whitelistEnabled == false.
  • Root rotation: old proofs fail after root update; new proofs succeed.
  • Update existing factory tests to supply a valid proof (or prank as owner).

Security / Ops Notes

  • Gas: Merkle adds O(log N) hashing per call (paid by caller); on-chain state stays O(1) (single root). A mapping is cheaper per-call but requires 20k gas per address to add and grows state; Merkle avoids that and scales.
  • Domain separation prevents cross-chain/contract proof reuse.
  • Revocation requires rotating the root; clients must re-fetch proofs. In the future, consider supporting a temporary secondary root to smooth rotations.

Acceptance Criteria

  • Only wallets with a valid Merkle proof (or the owner) can create a cap table.
  • Tests cover deny/allow, owner bypass, bad proof, and root rotation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions