Skip to content

Commit 0808b4d

Browse files
authored
Merge branch 'dev' into dependabot/npm_and_yarn/dev/typescript-5.8.2
2 parents 815301d + e010076 commit 0808b4d

26 files changed

+398
-132
lines changed

.env.example

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Offchain db connection string for mongodb
2-
DATABASE_URL="mongodb://tap:tap@localhost:27017/mongo?authSource=admin&retryWrites=true&w=majority"
2+
DATABASE_URL="mongodb://ocp:ocp@localhost:27017/mongo?authSource=admin&retryWrites=true&w=majority"
33
DATABASE_REPLSET="0" # set to "1" if using --replSet option in mongo. this allows transactions
44

55
# RPC url for testnet (defaults to Anvil's http://127.0.0.1:8545)
@@ -20,6 +20,8 @@ ETHERSCAN_L1_API_KEY=UPDATE_ME
2020
PORT=8293
2121

2222
# FACET ADDRESSES
23+
FACTORY_ADDRESS=
24+
REFERENCE_DIAMOND=
2325
DIAMOND_CUT_FACET=
2426
ISSUER_FACET=
2527
STAKEHOLDER_FACET=

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ yarn-error.log*
2929

3030
!.vscode
3131

32+
# JetBrains
33+
.idea/
3234

3335
# TypeScript
3436
tsconfig.tsbuildinfo

README.md

Lines changed: 111 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,116 @@
1-
<div align="center">
2-
<a href="https://github.com/victormimo/open-captable-protocol/blob/main/LICENSE">
3-
<img alt="License" src="https://img.shields.io/github/license/victormimo/open-captable-protocol">
4-
</a>
5-
</div>
1+
# Open Cap Table Protocol (OCP)
62

7-
# Documentation
3+
This repository implements the **Open Cap Table Protocol (OCP)** for managing cap tables on-chain. It adheres to the **Open Cap Format (OCF)** standard for data modeling and validation. OCP supports managing cap tables across multiple **EVM-compatible chains**.
84

9-
This repo is based on the [Open Cap Table Coalition](https://github.com/Open-Cap-Table-Coalition/Open-Cap-Format-OCF) standard, with the license included in its entirety.
5+
## Repo Organization
106

11-
## License
7+
- **`src/`** → Server files (routes, MongoDB, etc.)
8+
- **`chain/`** → Smart contracts (Diamond pattern with facets)
9+
10+
## Prerequisites
11+
12+
Ensure you have the following installed:
13+
14+
- [Forge](https://book.getfoundry.sh/)
15+
- [Node.js](https://nodejs.org/)
16+
- [Yarn](https://yarnpkg.com/)
17+
18+
## Setup & Running Locally
19+
20+
1. Copy `.env.example` to `.env.local`. (The example file includes a local database setup for testing, which we recommend.)
21+
22+
### Setup
23+
24+
1. Install dependencies:
25+
26+
```sh
27+
yarn install
28+
```
29+
30+
2. You should see the factory contracts successfully deployed.
31+
32+
3. Start services:
33+
34+
- **Terminal 1:** Start Anvil (local blockchain)
35+
```sh
36+
anvil
37+
```
38+
- Take one of the output's "Private Keys" and set your env file's `PRIVATE_KEY`
39+
40+
- **Terminal 2:** Deploy contracts
41+
```sh
42+
yarn deploy:local
43+
```
44+
- Set your env file's variables using output of deploy script
45+
46+
```sh
47+
DIAMOND_CUT_FACET=
48+
ISSUER_FACET=
49+
STAKEHOLDER_FACET=
50+
STOCK_CLASS_FACET=
51+
STOCK_FACET=
52+
CONVERTIBLES_FACET=
53+
EQUITY_COMPENSATION_FACET=
54+
STOCK_PLAN_FACET=
55+
WARRANT_FACET=
56+
STAKEHOLDER_NFT_FACET=
57+
```
1258
13-
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
59+
- **Terminal 3:** Run the mongo instance
60+
```sh
61+
docker compose up
62+
```
63+
64+
- **Terminal 4:** Run the backend server
65+
```sh
66+
yarn dev
67+
```
68+
69+
## Multi-Chain Support
70+
71+
This repository supports deploying cap tables to different **EVM chains**.
72+
73+
- Check `/src/chains.js` and configure the required chain keys.
74+
- When making API requests:
75+
- **Issuer creation** → Pass `chainId` in the request body.
76+
- **Other transactions** (e.g., creating stakeholders, issuing stock) → Pass `issuerId` in the request body.
77+
- See `/src/routes` for implementation details.
78+
79+
## Usage
80+
81+
1. Create an **issuer** first.
82+
2. Add **stakeholders**, stock classes, and other relevant data.
83+
3. For quick testing, use the example script:
84+
```sh
85+
node src/examples/testTransfer.mjs
86+
```
87+
88+
## Resetting Local Testing
89+
90+
If you are frequently testing locally, reset the database before redeploying:
91+
92+
```sh
93+
yarn deseed
94+
```
95+
96+
## Deployment
97+
98+
Use the appropriate command to deploy contracts:
99+
100+
- **Local:**
101+
```sh
102+
# Clear envvars in .env.local if they exist from a previous deployment
103+
yarn deploy:local
104+
```
105+
- **Testnet:**
106+
```sh
107+
yarn deploy:testnet
108+
```
109+
- **Mainnet:**
110+
```sh
111+
yarn deploy:mainnet
112+
```
113+
114+
## License
14115
116+
This project is licensed under the MIT License. See the [`LICENSE`](LICENSE) file for details.

chain/script/DeployFactory.s.sol

Lines changed: 24 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { WarrantFacet } from "@facets/WarrantFacet.sol";
2020
import { StakeholderNFTFacet } from "@facets/StakeholderNFTFacet.sol";
2121
import { AccessControl } from "@libraries/AccessControl.sol";
2222
import { AccessControlFacet } from "@facets/AccessControlFacet.sol";
23+
import { Strings } from "openzeppelin-contracts/contracts/utils/Strings.sol";
2324

2425
library LibDeployment {
2526
uint256 constant FACET_COUNT = 11; // Number of enum values FacetType
@@ -56,16 +57,19 @@ library LibDeployment {
5657
return FacetCutInfo({ name: "DiamondLoupeFacet", selectors: selectors });
5758
}
5859
if (facetType == FacetType.Issuer) {
59-
bytes4[] memory selectors = new bytes4[](2);
60+
bytes4[] memory selectors = new bytes4[](3);
6061
selectors[0] = IssuerFacet.initializeIssuer.selector;
6162
selectors[1] = IssuerFacet.adjustIssuerAuthorizedShares.selector;
63+
selectors[2] = IssuerFacet.issuer.selector;
6264
return FacetCutInfo({ name: "IssuerFacet", selectors: selectors });
6365
}
6466
if (facetType == FacetType.Stakeholder) {
65-
bytes4[] memory selectors = new bytes4[](3);
67+
bytes4[] memory selectors = new bytes4[](5);
6668
selectors[0] = StakeholderFacet.createStakeholder.selector;
6769
selectors[1] = StakeholderFacet.getStakeholderPositions.selector;
6870
selectors[2] = StakeholderFacet.linkStakeholderAddress.selector;
71+
selectors[3] = StakeholderFacet.getStakeholderId.selector;
72+
selectors[4] = StakeholderFacet.getStakeholderIndex.selector;
6973
return FacetCutInfo({ name: "StakeholderFacet", selectors: selectors });
7074
}
7175
if (facetType == FacetType.StockClass) {
@@ -155,72 +159,50 @@ library LibDeployment {
155159
revert("Unknown selector");
156160
}
157161

162+
function _deployedHandler(string memory envName, address addr) internal returns (address) {
163+
console.log(string.concat(envName, "=", Strings.toHexString(addr)));
164+
return addr;
165+
}
166+
158167
function deployFacet(FacetType facetType) internal returns (address) {
159-
address facetAddress;
160168
if (facetType == FacetType.DiamondLoupe) {
161-
facetAddress = address(new DiamondLoupeFacet());
162-
console.log("DIAMOND_LOUPE_FACET=", facetAddress);
163-
return facetAddress;
169+
return _deployedHandler("DIAMOND_LOUPE_FACET", address(new DiamondLoupeFacet()));
164170
}
165171
if (facetType == FacetType.Issuer) {
166-
facetAddress = address(new IssuerFacet());
167-
console.log("ISSUER_FACET=", facetAddress);
168-
return facetAddress;
172+
return _deployedHandler("ISSUER_FACET", address(new IssuerFacet()));
169173
}
170174
if (facetType == FacetType.Stakeholder) {
171-
facetAddress = address(new StakeholderFacet());
172-
console.log("STAKEHOLDER_FACET=", facetAddress);
173-
return facetAddress;
175+
return _deployedHandler("STAKEHOLDER_FACET", address(new StakeholderFacet()));
174176
}
175177
if (facetType == FacetType.StockClass) {
176-
facetAddress = address(new StockClassFacet());
177-
console.log("STOCK_CLASS_FACET=", facetAddress);
178-
return facetAddress;
178+
return _deployedHandler("STOCK_CLASS_FACET", address(new StockClassFacet()));
179179
}
180180
if (facetType == FacetType.Stock) {
181-
facetAddress = address(new StockFacet());
182-
console.log("STOCK_FACET=", facetAddress);
183-
return facetAddress;
181+
return _deployedHandler("STOCK_FACET", address(new StockFacet()));
184182
}
185183
if (facetType == FacetType.Convertibles) {
186-
facetAddress = address(new ConvertiblesFacet());
187-
console.log("CONVERTIBLES_FACET=", facetAddress);
188-
return facetAddress;
184+
return _deployedHandler("CONVERTIBLES_FACET", address(new ConvertiblesFacet()));
189185
}
190186
if (facetType == FacetType.EquityCompensation) {
191-
facetAddress = address(new EquityCompensationFacet());
192-
console.log("EQUITY_COMPENSATION_FACET=", facetAddress);
193-
return facetAddress;
187+
return _deployedHandler("EQUITY_COMPENSATION_FACET", address(new EquityCompensationFacet()));
194188
}
195189
if (facetType == FacetType.StockPlan) {
196-
facetAddress = address(new StockPlanFacet());
197-
console.log("STOCK_PLAN_FACET=", facetAddress);
198-
return facetAddress;
190+
return _deployedHandler("STOCK_PLAN_FACET", address(new StockPlanFacet()));
199191
}
200192
if (facetType == FacetType.Warrant) {
201-
facetAddress = address(new WarrantFacet());
202-
console.log("WARRANT_FACET=", facetAddress);
203-
return facetAddress;
193+
return _deployedHandler("WARRANT_FACET", address(new WarrantFacet()));
204194
}
205195
if (facetType == FacetType.StakeholderNFT) {
206-
facetAddress = address(new StakeholderNFTFacet());
207-
console.log("STAKEHOLDER_NFT_FACET=", facetAddress);
208-
return facetAddress;
196+
return _deployedHandler("STAKEHOLDER_NFT_FACET", address(new StakeholderNFTFacet()));
209197
}
210198
if (facetType == FacetType.AccessControl) {
211-
facetAddress = address(new AccessControlFacet());
212-
console.log("ACCESS_CONTROL_FACET=", facetAddress);
213-
return facetAddress;
199+
return _deployedHandler("ACCESS_CONTROL_FACET", address(new AccessControlFacet()));
214200
}
215201
if (facetType == FacetType.MockFacet) {
216-
facetAddress = address(new MockFacet());
217-
console.log("MOCK_FACET=", facetAddress);
218-
return facetAddress;
202+
return _deployedHandler("MOCK_FACET", address(new MockFacet()));
219203
}
220204
if (facetType == FacetType.MockFacetV2) {
221-
facetAddress = address(new MockFacetV2());
222-
console.log("MOCK_FACET_V2=", facetAddress);
223-
return facetAddress;
205+
return _deployedHandler("MOCK_FACET_V2", address(new MockFacetV2()));
224206
}
225207
revert("Unknown facet type");
226208
}

chain/src/core/CapTableFactory.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ contract CapTableFactory is Ownable {
1919
// Reference diamond to copy facets from
2020
address public immutable referenceDiamond;
2121

22-
constructor(address _referenceDiamond) {
22+
constructor(address _referenceDiamond) Ownable(msg.sender) {
2323
require(_referenceDiamond != address(0), "Invalid referenceDiamond");
2424
referenceDiamond = _referenceDiamond;
2525
}

chain/src/facets/IssuerFacet.sol

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,9 @@ import { Issuer } from "@libraries/Structs.sol";
77
import { TxHelper, TxType } from "@libraries/TxHelper.sol";
88
import { AccessControl } from "@libraries/AccessControl.sol";
99
import { console } from "forge-std/console.sol";
10+
import { IIssuerFacet } from "@interfaces/IIssuerFacet.sol";
1011

11-
contract IssuerFacet {
12-
error IssuerAlreadyInitialized();
13-
error InvalidSharesAuthorized();
14-
15-
event IssuerAuthorizedSharesAdjusted(uint256 newSharesAuthorized);
16-
12+
contract IssuerFacet is IIssuerFacet {
1713
/// @notice Initialize the issuer with initial shares authorized
1814
/// @dev Can only be called once by an admin during setup
1915
function initializeIssuer(bytes16 id, uint256 initial_shares_authorized) external {
@@ -31,6 +27,10 @@ contract IssuerFacet {
3127
ds.issuer = Issuer({ id: id, shares_issued: 0, shares_authorized: initial_shares_authorized });
3228
}
3329

30+
function issuer() external view returns (Issuer memory) {
31+
return StorageLib.get().issuer;
32+
}
33+
3434
/// @notice Adjust the total number of authorized shares for the issuer
3535
/// @dev Only DEFAULT_ADMIN_ROLE can adjust authorized shares
3636
function adjustIssuerAuthorizedShares(bytes16 id, uint256 newSharesAuthorized) external {

chain/src/facets/StakeholderFacet.sol

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ contract StakeholderFacet {
1717

1818
error StakeholderAlreadyExists(bytes16 stakeholder_id);
1919
error AddressAlreadyLinked(address wallet_address);
20+
error NoStakeholder();
2021

2122
/// @notice Create a new stakeholder
2223
/// @dev Only OPERATOR_ROLE can create stakeholders
@@ -57,6 +58,23 @@ contract StakeholderFacet {
5758
emit StakeholderAddressLinked(stakeholder_id, wallet_address);
5859
}
5960

61+
/// @notice Get stakeholder id for a given address
62+
function getStakeholderId(address wallet_address, bool ensure_exists) external view returns (bytes16) {
63+
Storage storage ds = StorageLib.get();
64+
65+
// Check if address is linked to a stakeholder
66+
bytes16 stakeholder_id = ds.addressToStakeholderId[wallet_address];
67+
if (ensure_exists && stakeholder_id == bytes16(0)) {
68+
revert NoStakeholder();
69+
}
70+
return stakeholder_id;
71+
}
72+
73+
/// @notice Get stakeholder idx for a stakeholder id
74+
function getStakeholderIndex(bytes16 stakeholder_id) external view returns (uint256) {
75+
return StorageLib.get().stakeholderIndex[stakeholder_id];
76+
}
77+
6078
/// @notice Get all positions for a stakeholder
6179
/// @dev INVESTOR_ROLE can only view their own positions, OPERATOR_ROLE and above can view any
6280
function getStakeholderPositions(bytes16 stakeholder_id) external view returns (StakeholderPositions memory) {

chain/src/facets/StakeholderNFTFacet.sol

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ contract StakeholderNFTFacet is ERC721 {
2222

2323
constructor() ERC721("Stakeholder Position", "STKPOS") { }
2424

25+
// Internal function to check if a token exists by checking if the stakeholder ID exists
26+
function _exists(uint256 tokenId) internal view returns (bool) {
27+
Storage storage ds = StorageLib.get();
28+
bytes16 stakeholderId = bytes16(uint128(tokenId));
29+
return ds.stakeholderIndex[stakeholderId] != 0;
30+
}
31+
2532
/// @notice Mint an NFT representing a stakeholder's position
2633
/// @dev Only stakeholders with INVESTOR_ROLE can mint their own NFT
2734
function mint() external {
@@ -45,7 +52,7 @@ contract StakeholderNFTFacet is ERC721 {
4552
revert AlreadyMinted();
4653
}
4754

48-
_mint(msg.sender, tokenId);
55+
_safeMint(msg.sender, tokenId);
4956
}
5057

5158
/// @notice Get the URI for a token, containing metadata about stakeholder positions

chain/src/interfaces/ICapTable.sol

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import { IAccessControlFacet } from "./IAccessControlFacet.sol";
5+
import { ICapTableFactory } from "./ICaptableFactory.sol";
6+
import { ICapTableInitializer } from "./ICapTableInitializer.sol";
7+
import { IConvertiblesFacet } from "./IConvertiblesFacet.sol";
8+
import { IEquityCompensationFacet } from "./IEquityCompensationFacet.sol";
9+
import { IIssuerFacet } from "./IIssuerFacet.sol";
10+
import { IStakeholderFacet } from "./IStakeholderFacet.sol";
11+
import { IStakeholderNFTFacet } from "./IStakeholderNFTFacet.sol";
12+
import { IStockClassFacet } from "./IStockClassFacet.sol";
13+
import { IStockFacet } from "./IStockFacet.sol";
14+
import { IStockPlanFacet } from "./IStockPlanFacet.sol";
15+
import { IWarrantFacet } from "./IWarrantFacet.sol";
16+
17+
/* Consolidation of interfaces facet internally */
18+
interface ICapTable is
19+
IAccessControlFacet,
20+
IConvertiblesFacet,
21+
IEquityCompensationFacet,
22+
IIssuerFacet,
23+
IStakeholderFacet,
24+
IStakeholderNFTFacet,
25+
IStockClassFacet,
26+
IStockFacet,
27+
IStockPlanFacet,
28+
IWarrantFacet
29+
{ }

0 commit comments

Comments
 (0)