Skip to content

Wucklace/OnchainTokenomist

Repository files navigation

OnchainTokenomist

Verifiable on-chain tokenomics — transforming token economies into programmable, executable systems.


Overview

OnchainTokenomist is a permissionless, modular smart contract protocol designed for tier-based token economies with category-based vesting. It transforms traditional off-chain, spreadsheet-based mechanics into fully programmable, composable, and executable tokenomics logic.

At its core, the protocol issues soulbound ERC-721 passes as the complete on-chain record of a holder's economic entitlement. Each pass carries its full state — allocation per pass, vesting schedule, amount claimed, and amount remaining — keyed by token ID, not by address. The holder's address is only verified at claim time to confirm pass ownership, never as the source of entitlement. Holding the pass is the right.

Each pass belongs to a tier — a defined allocation bucket within a category. Categories group related tiers under a shared vesting schedule, and together they compose the vault: the complete on-chain record of a project's entire token economy. The vault exposes a live, queryable summary of everything — total supply, per-category and per-tier allocations, amount claimed, and amount locked or unlocked. There is no off-chain source of truth — the vault is the source of truth.

Vaults accept either an ERC-20 token or the chain's native token (ETH, or equivalent) as the deposited asset. The same vault mechanics — tier structure, vesting schedules, pass issuance, and claim settlement — apply equally to both. Native token vaults require no approval step; the deposit is sent directly with the creation transaction.

What separates OnchainTokenomist from conventional tokenomics systems is that nothing lives off-chain. The entire economic logic — allocation, vesting, distribution authority — is encoded, enforced, and verifiable on-chain, with execution that can be delegated without introducing control or discretion.

That delegation is handled by an optional AI agent executor — a programmable address that performs operational tasks on behalf of the creator: composing recipient sets, generating Merkle proofs, submitting proposals, and triggering minting. Approval authority stays exclusively with human admin signers where configured.

This is what on-chain tokenomics infrastructure means: not a tool for sending tokens, but a system for encoding, enforcing, and verifying a project's entire economic logic on-chain — allocation rules, vesting schedules, distribution authority, and execution flow — all immutable, queryable, and autonomous-ready from day one.


Use Cases

OnchainTokenomist is built around a single flexible primitive — the vault. The same infrastructure serves three distinct use cases, each defined entirely by how the vault is configured at creation. All use cases support both ERC-20 and native token deposits.

1. Token Locker

Vesting disabled, startBlock set to the intended unlock block. The deposited amount is locked on-chain and released in full at the defined block — cryptographically enforced, with a verifiable on-chain record.

The tier structure within the vault determines how the locked amount is split across recipients. A single tier with one pass locks the full amount for one holder. Multiple tiers with different allocationPerPass and maxSupply values distribute the locked amount across many holders at different sizes — all unlocking at the same startBlock. Since startBlock is vault-level, all tiers and categories in a locker vault share the same unlock point.

Example:

  • Seed investors: 3 tiers (Large, Medium, Small) with different per-pass allocations, all unlocking at the same block
  • Treasury reserve: single tier, single pass, full amount released at a specific future block
  • Team allocation: multiple passes across tiers, all unlocking at the same future block

No vesting required — straightforward on-chain time lock with full allocation transparency. Admin configuration and the proposal workflow are optional across all vault types, including lockers.


2. Token Vesting Schedule

One or more categories, each with its own independent vesting schedule. Vesting is configured per category — cliff, duration, interval, and initial release are set independently for each group. A change in one category's schedule never affects another.

Within a category, all tiers share the same vesting timeline. Tiers differentiate by allocation size — different allocationPerPass values for different recipient groups — but vest according to the same cliff, duration, and interval defined for that category.

Across categories, each schedule is fully independent. A vault can carry multiple categories with entirely different timelines running simultaneously in the same vault.

Single-category example:

Team Category Vesting: 60d cliff | 360d duration | 30d interval | 0% TGE ├── Diamond tier: 40,000 tokens per pass | 1 pass └── Gold tier: 5,000 tokens per pass | 2 passes → All passes vest on the same Team schedule, different allocation sizes

Multi-category example:

Team Category Vesting: 60d cliff | 360d duration | 30d interval | 0% TGE ├── Diamond tier: 40,000 per pass └── Gold tier: 5,000 per pass Advisors Category Vesting: 30d cliff | 180d duration | 30d interval | 20% TGE └── Silver tier: 5,000 per pass Community Category Vesting: disabled (100% at startBlock) ├── Builder tier: 5,000 per pass ├── Supporter tier: 3,000 per pass └── Bronze tier: 2,600 per pass → Three independent timelines, one vault, one source of truth

This is the core vesting primitive — composable, category-isolated, and entirely on-chain.


3. Full Tokenomics / Distribution

The complete protocol — vaults that combine token lockers, vesting schedules, and multi-category distribution into a single on-chain tokenomics system. Every allocation, every vesting schedule, every distribution rule encoded in one atomic vault creation.

This is the use case for projects that need their entire token economy on-chain: team allocations vesting over years, advisor tranches with initial releases, community distributions unlocking at TGE, and treasury reserves time-locked for future use — all configured at creation, all immutable, all queryable from day one.

What makes this distinct from the vesting use case is scope: not one or two categories, but the full token economy. The vault becomes the authoritative on-chain record of everything the project has committed — total supply, per-category and per-tier allocations, how much has vested, how much has been claimed, and how much remains locked. No spreadsheet, no off-chain tracker, no operator trust required.


The Problem It Solves

Traditional tokenomics is broken:

  • Allocations live in spreadsheets, not on-chain — unverifiable and mutable
  • Vesting is enforced off-chain, relying on team trust with no cryptographic guarantee
  • Economic records are opaque — there is no verifiable on-chain source of truth for what was promised, how many tokens are locked, how many are vesting, and how many have been claimed
  • No on-chain checks and balances — the same party that controls the treasury also executes distributions unilaterally, with no independent approval or verifiable audit trail

OnchainTokenomist replaces all of this with a verifiable on-chain tokenomics infrastructure — one where every allocation, every vesting schedule, and every claim is recorded, enforced, and queryable entirely on-chain.


Key Concepts

Vault

A vault is the root tokenomics object — the complete on-chain record of a project's entire token economy, exposing a live, queryable summary of every allocation, vesting schedule, claim, and locked or unlocked amount across every category and tier.

Before creation, the creator defines every parameter — token address (ERC-20 contract address or native token sentinel), total amount, tier structure, vesting schedules, optional executor, and admin configuration — and submits them in a single atomic transaction. A small registration fee in the chain's native token is required at creation time.

At creation, the contract locks the full token amount and configuration atomically. From this point everything is immutable — allocations, tiers, vesting parameters, and admin roles are permanently fixed on-chain and cannot be changed.

Post-creation, the only action available to the creator is pass distribution — minting passes to recipients within the defined tier supply caps. Once all passes across all categories are fully distributed, the vault status automatically transitions to finalized and no further minting is possible.

A vault is structured around categories — logical groupings of tiers such as Team, Investors, Community, or Builder. Each category contains tiers: defined allocation buckets with a per-pass allocation and max supply (e.g. Diamond, Gold, Silver). Every category carries its own vesting schedule — cliff, duration, interval, and initial release — making each one fully self-contained. A change in one category's vesting never affects another.

Vaults are optionally configured with up to two admins, forming a dual-approval governance layer over pass distribution. Where no admins are configured, the creator mints directly. An AI agent executor can also be configured — handling operational execution within the bounds defined at creation.

Soulbound Pass

Each pass is the complete on-chain record of a holder's economic entitlement within their tier — carrying its full state from the moment of minting until the final claim.

The pass is non-transferable — transferFrom and safeTransferFrom revert unconditionally — because the entitlement belongs to the pass, not the address. All state is tracked by token ID, never by address; the holder's address is only relevant at claim time to confirm ownership. Once the full allocation has been claimed, the pass burns automatically — the record is complete and nothing remains to track.

The pass is the only proof of entitlement.

Proposal Workflow (Team Vaults)

In team-mode vaults, minting requires an approved proposal. The creator or executor submits a proposal via proposeMintCategory() — each tier within the proposal carries its own Merkle root, with recipient verification handled on-chain at mint time. The proposal requires approval from all configured admins before mintPasses() can be called. Proposals expire if not fully approved within the deadline window.

Creator / Executor
       │
       ▼
proposeMintCategory()   ─────────────────────────────────────┐
       │                                                     │
       ▼                                                     │
  Pending Proposal                                 Auto-expires at deadline
       │
  ┌────┴────┐
  │         │
Admin1   Admin2
approve  approve
  │         │
  └────┬────┘
       │
  Fully Approved
       │
       ▼
mintPasses()  →  ERC-721 issued to recipients via Merkle proof

Direct Mint (Creator-Mode Vaults)

Vaults with no admins configured bypass the proposal workflow entirely. The creator or executor calls mintDirect() — no approval needed, no Merkle verification required.

Vesting & Claiming

Each category carries its own independent vesting schedule — configured at vault creation and enforced entirely on-chain. Parameters are defined per category:

Parameter Description
cliff Blocks after startBlock before any interval unlocks begin
duration Total vesting period in blocks, measured from startBlock
interval Unlock frequency — tokens vest in discrete steps every N blocks after the cliff
initialRelease Percentage unlocked immediately at startBlock, before the cliff (basis points — 2000 = 20%)
enabled Toggle — disabled means 100% liquid at startBlock

Pass holders call claim(tokenIds[]) to collect vested tokens. The contract calculates the vested amount at the current block, subtracts previously claimed amounts, and transfers the difference. Claiming is cumulative — missed intervals are always collectable in the next claim. Once fully claimed, the pass burns automatically.


Native Token Support

Vaults accept either an ERC-20 token or the chain's native token (ETH, NEX, or equivalent) as the deposited asset. All vault mechanics — tier structure, vesting schedules, pass issuance, and on-chain claim settlement — are identical for both. The only differences are at creation and claim time.

Sentinel Address

The native token is identified by a standard sentinel address used across the protocol:

address constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

Pass this as tokenAddress when calling createVault to create a native token vault. No ERC-20 approval is needed.

msg.value at Creation

For ERC-20 vaults, msg.value must equal exactly the registration fee: msg.value = registrationFee

The token deposit is pulled separately via transferFrom — the creator must approve the contract for amount before calling createVault.

For native token vaults, msg.value must cover both the registration fee and the full deposit in a single transaction: msg.value = registrationFee + amount

The contract splits these on receipt — the fee is forwarded to the fee receiver and the deposit is retained in the contract. No approval step is needed.

Claiming Native Tokens

Claims work identically for native and ERC-20 vaults from the holder's perspective — call claim(tokenIds[]) and the vested amount is transferred. Internally the contract routes through a unified _transferOut helper:

  • ERC-20: SafeERC20.safeTransfer
  • Native: .call{value: amount}("") — reverts on failure, safe for smart contract wallet recipients

Example: Creating a Native Token Vault

const NATIVE_TOKEN = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
const depositAmount = ethers.parseEther("100000");
const registrationFee = await contract.registrationFee(); // same fee for both
const totalMsgValue = registrationFee + depositAmount;

await contract.createVault(
  NATIVE_TOKEN,       // native token sentinel — no approval needed
  depositAmount,
  admin1.address,
  admin2.address,
  executor.address,
  startBlock,
  tierConfigs,
  vestingConfigs,
  { value: totalMsgValue }  // fee + deposit in one shot
);

Architecture

The contract is fully modular. All logic is split across inherited abstract modules with no delegatecall or proxy patterns. A single BaseStorage contract declares the shared storage struct s, all custom errors, all events, and the NATIVE_TOKEN sentinel constant. Solidity's C3 linearization guarantees all modules read and write the same storage slot in the final deployed contract.

BaseStorage.sol
│  Shared storage `s`, all events, all errors, NATIVE_TOKEN constant
│
├── TransferModule.sol
│      _transferOut() — unified ERC-20 and native token transfer helper
│      └── VaultModule.sol       createVault()
│      └── ClaimModule.sol       claim()
│
├── ProposalModule.sol       proposeMintCategory(), approveMintProposal(), rejectMintProposal()
├── MintModule.sol           mintPasses(), mintDirect()
└── ViewModule.sol           all read-only queries and return structs

OnchainTokenomist.sol
   Inherits all modules + FeeManager
   Constructor + soulbound transfer overrides

Abstract Modules

Module Responsibility
BaseStorage Single source of truth for storage layout s, all custom errors, all events, and NATIVE_TOKEN sentinel
TransferModule Unified _transferOut — routes ERC-20 via SafeERC20.safeTransfer and native via .call{value}
FeeManager Registration fee collection, fee receiver management, fee update timelock, governance transfer (max 3 transfers)
VaultModule createVault — fee validation, deposit handling, tier and vesting config storage
ProposalModule Proposal lifecycle — create, dual-admin approve, reject, expiry enforcement
MintModule mintPasses (Merkle-gated), mintDirect (creator-only)
ClaimModule claim — vesting calculation, transfer, pass burn on full claim
ViewModule All read-only queries — pass info, vault summaries, category allocations, tier details, vesting schedules

Libraries (compiler-inlined, no runtime delegation)

Library Purpose
StorageLib All storage structs and TokenomicsStorage definition
VestingCalculator Vesting math — calculateVested, getNextUnlockBlock, getVestedPercentage
ValidationLib Input validation — addresses, amounts, tier configs, vesting configs

Contract Interface

Vault

/**
 * @param tokenAddress ERC-20 contract address, or NATIVE_TOKEN sentinel for native deposits
 * @param amount       Total tokens to deposit
 *
 * msg.value (ERC-20 vault):     registrationFee exactly
 * msg.value (native token vault): registrationFee + amount exactly
 *
 * Query exact cost with getERC20VaultCost() or getNativeVaultCost(amount) before calling.
 */
function createVault(
    address tokenAddress,
    uint256 amount,
    address admin1,
    address admin2,
    address executor,
    uint256 startBlock,
    TierConfigInput[] calldata tierConfigs,
    VestingConfigInput[] calldata vestingConfigs
) external payable returns (uint256 vaultId);

Proposals

function proposeMintCategory(
    uint256 vaultId,
    bytes32 category,
    TierBatch[] calldata tierBatches
) external returns (uint256 proposalId);

function approveMintProposal(uint256 proposalId) external;
function rejectMintProposal(uint256 proposalId) external;

Minting

// Team vaults — requires approved proposal + Merkle proof per recipient
function mintPasses(
    uint256 proposalId,
    bytes32 tier,
    address[] calldata recipients,
    bytes32[][] calldata proofs
) external;

// Creator-only vaults — no proposal, no proof required
function mintDirect(
    uint256 vaultId,
    bytes32 category,
    bytes32 tier,
    address[] calldata recipients
) external;

Claiming

// Works identically for ERC-20 and native token vaults
function claim(uint256[] calldata tokenIds) external;

Fee Queries

// Registration fee in native token — same for all vault types
// For native token vaults, msg.value = registrationFee + depositAmount
// For ERC-20 vaults, msg.value = registrationFee
function registrationFee() external view returns (uint256);

Views

function getUserPasses(uint256[] calldata tokenIds) external view returns (UserPassInfo[] memory);
function getVaultSummary(uint256 vaultId) external view returns (VaultSummary memory);
function getVaultSummaries(uint256 startId, uint256 count) external view returns (VaultSummary[] memory);
function getVaultSummariesByCreator(address creator) external view returns (VaultSummary[] memory);
function getVaultCategoryAllocations(uint256 vaultId) external view returns (CategoryAllocation[] memory);
function getTierDetails(uint256 vaultId, bytes32 category, bytes32 tier) external view returns (TierDetails memory);
function getCategoryTierDetails(uint256 vaultId, bytes32 category) external view returns (TierDetails[] memory);
function getVestingSchedule(uint256 vaultId, bytes32 category) external view returns (VestingSchedule memory);
function getAdminPendingProposals(address admin) external view returns (ProposalInfo[] memory);
function getInvolvedProposals(address account) external view returns (ProposalInfo[] memory);
function getPlatformStats() external view returns (PlatformStats memory);
function getProposalTiers(uint256 proposalId) external view returns (
    bytes32[] memory tiers,
    bytes32[] memory merkleRoots,
    uint256[] memory remainingSupplies,
    uint256[] memory supplyCounts,
    uint256[] memory mintedCounts,
    bool admin1Approved,
    bool admin2Approved,
    bool rejected,
    bool expired,
    bool executed
);

// Discovery & Helper Views
function getOwnerTokenIds(address owner) external view returns (uint256[] memory);
function getVaultCategories(uint256 vaultId) external view returns (bytes32[] memory);
function getCategoryTiers(uint256 vaultId, bytes32 category) external view returns (bytes32[] memory);
function getNextVaultId() external view returns (uint256);
function getNextProposalId() external view returns (uint256);

Security Properties

Every guarantee made by the protocol — immutability, verifiability, and trustless distribution — is backed by a specific on-chain mechanism. No property relies on off-chain enforcement or operator trust.

Property Mechanism
Pass-keyed entitlement All distribution state is keyed by token ID — allocation, vesting, and claimed amounts are tied to the pass, never to an address
Address touch on mint only Recipient addresses are written once to a reverse lookup index at mint time — no address is ever read for entitlement, access control, or claim calculation
Event-level Privacy Economic context (vault, category, tier) is never logged in transaction events; data is fragmented across global IDs to ensure indexer-resistance and prevent mass-monitoring
Soulbound passes transferFrom and safeTransferFrom unconditionally revert
Reentrancy protection All stateful functions use OpenZeppelin ReentrancyGuard
Merkle-gated minting Each recipient verified against admin-approved Merkle root at mint time
Dual-admin approval Both admins must independently approve before any mint is permitted
Proposal deadlines Proposals auto-expire after a fixed block window — no stale approvals
Proposal rejection Either admin can permanently reject a proposal — rejected proposals cannot be executed
Supply enforcement Strict invariant check: mintedCount + batch.length > maxSupply reverts before any mint
Immutable vault config Allocations, tiers, vesting parameters, and admin roles are permanently fixed at creation — nothing can be changed post-deploy
Deterministic Cost The registration fee is paid upfront at creation. Once a vault is deployed, it has no ongoing financial dependency on the protocol or any central admin
Pass auto-burn Pass burns on full claim — no double-claim possible
ERC-20 token safety All ERC-20 transfers use OpenZeppelin SafeERC20
Native transfer safety Native token payouts use .call{value: amount}("") — reverts on failure, safe for smart contract wallet recipients. .transfer is never used
Fee and deposit separation For native token vaults, the registration fee and token deposit arrive in the same transaction but are validated and forwarded independently — the fee is routed to the fee receiver, the deposit is retained. No cross-contamination is possible
Exact fee enforcement msg.value must equal the required amount exactly for both vault types — overpayment reverts to prevent funds being permanently locked
Fee timelock Registration fee updates require a 604800 block timelock before taking effect
Category isolation Vesting schedules and supply caps are enforced per category — exhaustion or misconfiguration in one category cannot affect another
Executor bounds The executor cannot alter allocations, vesting parameters, or admin roles — it can only perform operational tasks within the bounds defined at creation
Vault finalization Once all passes across all categories are fully distributed, the vault status transitions to finalized — no further minting is possible under any condition

Deployment

constructor(
    address _feeReceiver,    // Address that receives vault registration fees
    uint256 _registrationFee // Fee in native token (wei) per vault creation
)

The constructor initializes all counters to 1 (nextVaultId, nextProposalId, nextTokenId) — ID 0 is reserved as a sentinel null value throughout the contract.


Testing

The test suite verifies every protocol guarantee end-to-end — from vault creation and proposal flows to vesting enforcement, claim mechanics, and access control across both ERC-20 and native token vaults. Scripts are split into sequential flow tests and standalone scenario tests. Each script prints a full summary of what was tested at the end of its run.

What's Covered

Script Description
step1 ERC-20 vault creation — full multisig (admin1 + admin2 + executor), token deployment, tier and vesting config
step2 Proposal flow — propose, dual-admin approve, Merkle-gated mint across Team, Advisors, and Community categories
step3 Vesting and claim — block progression to trigger interval unlocks, partial claims, full claims, pass burn confirmation
native-vault Native token vault creation — sentinel address, msg.value = fee + deposit, no approval step
native-mint Native vault proposal flow — propose, dual-admin approve, Merkle-gated mint across all categories and tiers
native-claim Native vault vesting lifecycle — block mining to TGE, cliff, and each interval; partial claims, cumulative claims, full claim and burn confirmation; contract balance verified at zero after all claims
creator-vault Creator-only vault — no admins, executor-enabled
direct-mint Direct mint — creator and executor mint via mintDirect, no proposal or Merkle proof required
comprehensive-test Multi-vault scenarios — single admin, approve proposal, mint proposal, pending proposal, expired proposal, rejected proposal, cross-role minting, view function coverage
fee-manager Fee management and governance — propose/execute/cancel fee updates, timelock enforcement, fee receiver updates, governance transfer up to max limit
access-control Permission enforcement — NotAdmin, NotAuthorized, NothingToClaim reverts, pre-startBlock claim guard, burn confirmation

Sequential Flow (ERC-20)

Runs as a single session — each step depends on state from the previous.

npx hardhat node
npm run deploy
npm run step1
npm run step2
npm run step3

Sequential Flow (Native Token)

npx hardhat node
npm run deploy
npm run native-vault
npm run native-mint
npm run native-claim

Standalone Tests

Each requires a fresh node and redeploy — they create their own vaults and mine blocks independently.

# Creator-only vault flow
npx hardhat node
npm run deploy
npm run creator-vault
npm run direct-mint

# Fee & Governance
npx hardhat node
npm run deploy
npm run fee-manager

# Access Control
npx hardhat node
npm run deploy
npm run access-control

# Comprehensive multi-vault
npx hardhat node
npm run deploy
npm run comprehensive-test

Note: Always restart the Hardhat node and redeploy before each standalone test to ensure clean contract state, correct token IDs, and accurate block numbers.


Dependencies


License

MIT

Releases

No releases published

Packages

 
 
 

Contributors