On-chain Employee Stock Option Plan (ESOP) built with Solidity. Option grants are represented as soulbound ERC-721 NFTs with linear vesting schedules, and employees exercise them by paying USDC to receive ERC-20 ESOP tokens.
| Contract | Standard | Purpose |
|---|---|---|
| ESOPToken | ERC-20 (capped) | Company equity token minted when employees exercise options |
| ESOPOptionNFT | ERC-721 (soulbound) | Represents individual option grants with vesting state |
| VestingMath | Library | Pure vesting calculation logic (cliff + linear) |
- Grant -- An admin with
GRANTOR_ROLEmints a soulbound NFT to an employee, encoding the strike price, cliff, vesting duration, and total options. - Vest -- Options vest linearly after the cliff period. Vesting math is calculated on-chain at read time.
- Exercise -- The employee calls
exercise(tokenId, amount), paysamount * strikePricein USDC (sent to the treasury), and receives ESOP tokens. - Terminate -- If employment ends, an admin calls
terminateGrant(). Unvested options are forfeited; vested options remain exercisable for a configurable post-termination window (default 90 days). - Burn -- Once a grant is fully exercised or expired, the NFT can be burned to clean up state.
- Soulbound NFTs -- Option grants are non-transferable. Wallet recovery is supported through an admin-approved two-step transfer process.
- USDC strike price -- Exercise payments are made in USDC (6 decimals) and routed to a configurable treasury address.
- Capped supply --
ESOPTokenenforces a hard cap on total minted tokens. - Access control -- Role-based permissions (
ADMIN_ROLE,GRANTOR_ROLE,MINTER_ROLE) via OpenZeppelinAccessControl. - Pausable -- Exercise operations can be paused in emergencies.
- Reentrancy guard -- All state-changing external calls are protected.
src/
ESOPToken.sol ERC-20 equity token
ESOPOptionNFT.sol Soulbound option grant NFT
interfaces/
IESOPToken.sol
IESOPOptionNFT.sol
libraries/
VestingMath.sol Vesting calculation library
test/
Base.t.sol Shared test setup and helpers
ESOPToken.t.sol Token unit tests
ESOPOptionNFT.t.sol Option NFT unit tests
VestingMath.t.sol Vesting math unit + fuzz tests
Integration.t.sol End-to-end lifecycle tests
invariant/
Invariant.t.sol Stateful invariant tests
Handler.t.sol Fuzzing handler
mocks/
MockERC20.sol Mock USDC for testing
script/
Deploy.s.sol Deployment script
git clone --recurse-submodules <repo-url>
cd web3-esop
forge buildforge buildforge testRun with verbose output:
forge test -vvvforge fmtSet environment variables and run the deploy script:
export ADMIN_ADDRESS=<multi-sig address>
export USDC_ADDRESS=<USDC token address>
export TREASURY_ADDRESS=<treasury address>
export MAX_SUPPLY=10000000000000000000000000 # 10M tokens (optional, default 10M)
forge script script/Deploy.s.sol:DeployScript \
--rpc-url <your_rpc_url> \
--private-key <your_private_key> \
--broadcastAfter deployment:
- Verify contracts on Etherscan
- Grant
GRANTOR_ROLEto the HR operator address via the admin multi-sig
- OpenZeppelin Contracts v5.5.0
- forge-std v1.14.0
MIT