Reveal

Overview

Reveal is a set of contracts that apply configurable selling rules to tokens launched through it.

The protocol does not custody funds, does not execute trades on your behalf, and does not decide who may sell. It computes, per position, how much may be released right now — and rejects anything above that.

Three rules do the work: a time-based unlock, relief that accelerates when a position is underwater, and a cap on how much any single wallet can move the pool within a window. Everything else is configuration.

Quickstart

A launch is a single transaction against the factory.

import { createToken } from "@reveal/sdk";

const token = await createToken({
  name: "Reveal",
  symbol: "REVEAL",
  supply: 1_000_000_000n,
  liquidity: parseEther("40"),

  rules: {
    initialUnlock: 1000,      // 10% sellable from block one
    unlockDuration: 86_400,   // fully unlocked after 24h
    sizePenalty: 2500,        // large positions unlock slower
    impactCap: 100,           // 1% of liquidity...
    impactWindow: 300,        // ...per 5 minute window
    launchDelay: 30,          // no buys for the first 30s
  },
});

Every field in rules is optional and falls back to the defaults listed under Parameters. Omitting a field is a choice like any other — the resulting values are public before the first buy either way.

Launch lifecycle

What happens between deployment and a fully liquid market.

1 · Deploy
The factory deploys the token, creates the pool, and writes the rules into immutable storage. Nothing can trade yet.
2 · Delay window
For launchDelay seconds, buys revert. This removes the same-block advantage that lets a bot own the first print.
3 · Buy ramp
Trading opens with a cap on individual buy size that grows over buyRamp seconds until it disappears.
4 · Discovery
Buys become positions. Sells are metered per position and per wallet window, so pressure arrives spread out rather than at once.
5 · Steady state
Once every position has passed unlockDuration, only the impact cap remains active.

Positions, not balances

The unit of accounting is the buy, not the wallet.

Every buy creates a position. Selling limits are computed per position, so two buys made hours apart unlock on their own schedules even though they sit in the same wallet.

struct Position {
    address owner;
    uint128 amount;        // tokens bought
    uint128 sold;          // tokens already released
    uint128 entryPrice;    // TWAP at entry
    uint128 entryLiquidity;// pool depth at entry, sizes the penalty
    uint64  openedAt;
}

A sell consumes positions oldest-first. That ordering is deliberate: it releases the tokens that have waited longest, which is both the friendlier default for holders and the harder one to game.

Computing the sellable amount

Three inputs, then a cap.

sellable(position) =
    amount
  * max( timeUnlock(elapsed, duration, sizePenalty),
         reliefUnlock(drawdown) )
  - sold

executable = min( sellable, remainingWindowAllowance(wallet) )

Relief is a floor, not an addition. A position deep in drawdown never unlocks less than its schedule already permits, and a position in profit is never penalised for it.

Where the rules live

In the token's transfer hook, not only in the router.

If limits only existed in the launchpad's router, moving tokens to a second wallet or trading them on another pool would bypass everything. The check therefore sits in the token itself.

Whitelisted pools
Pools created by the factory are known to the token. Transfers into them are treated as sells and metered.
Wallet to wallet
Transfers to an unknown address consume sellable amount exactly as a sale would, so splitting a position does not reset its schedule.
Contracts
Transfers to unrecognised contracts revert with UnknownDestination until the destination is registered.

Price oracle

Drawdown is measured against a time-weighted average, never spot.

This is the single most important implementation detail in the protocol. Reading spot price would let anyone crash the market for one block, unlock their entire position under maximum relief, and sell into the recovery.

// 30 minute TWAP, read from the pool's accumulator
uint256 reference = pool.consult(TWAP_WINDOW);
uint256 drawdown  = entryPrice > reference
    ? (entryPrice - reference) * 10_000 / entryPrice
    : 0;

A longer window costs responsiveness during a genuine crash; a shorter one lowers the cost of manipulating relief. Thirty minutes is the current default and is expected to move before audit.

Unlock schedule

Basis points throughout, so 10000 bps equals 100%.

ParameterTypeRangeDefault
initialUnlockShare of a position sellable the moment it is bought. Set it too low and holders feel trapped before the chart has moved.uint160 – 10000 bps1000
unlockDurationSeconds for a position to reach fully sellable, before any size or drawdown adjustment.uint321h – 30d86400
unlockCurveShape of the schedule. Ease-out front-loads liquidity, which suits launches where early buyers take the most risk.uint80 linear · 1 ease-out1
sizePenaltyHow much a position's share of liquidity stretches its own unlock duration. A wallet holding 10% of the pool waits materially longer than one holding 0.1%.uint160 – 10000 bps2500

sizePenalty is what stops a whale from holding the same schedule as a small buyer. At the default of 2500 bps, a position worth 10% of pool liquidity takes roughly a quarter longer to unlock than a negligible one.

Drawdown relief

Tiers that raise the floor when a position is underwater.

ParameterTypeRangeDefault
reliefThresholdsDrawdown levels, measured against entry price, at which relief tiers activate.uint16[4]0 – 10000 bps[1000, 2000, 4000]
reliefUnlockSellable share granted at each tier. Values are floors: a position never unlocks less than its time-based schedule already allows.uint16[4]0 – 10000 bps[3000, 5000, 9500]

Impact caps

A ceiling on how much one wallet can move the pool at once.

ParameterTypeRangeDefault
impactCapMaximum share of pool liquidity one wallet may move within a window.uint1610 – 2000 bps100
impactWindowRolling window over which impactCap is measured.uint3260 – 3600 s300

The cap applies to unlocked positions too. It is measured on a rolling window per wallet, so a refused remainder becomes available again as the window slides rather than at a fixed reset.

Anti-sniper

Rules that only apply to the opening minutes.

ParameterTypeRangeDefault
launchDelaySeconds after deployment before the first buy is accepted. Removes the same-block advantage.uint320 – 600 s30
buyRampPeriod during which the maximum buy size grows from buyFloor to unlimited.uint320 – 1800 s600
buyFloorStarting cap on a single buy, as a share of liquidity, at the opening of the ramp.uint161 – 500 bps50

These reduce the advantage of being first; they do not remove it. Ordering ultimately belongs to whoever sequences the chain.

Interface

The surface an integration needs.

interface IRevealToken {
    function sellableOf(uint256 id) external view returns (uint256);
    function positionsOf(address owner)
        external view returns (uint256[] memory);
    function windowAllowance(address wallet)
        external view returns (uint256 remaining, uint64 resetsAt);
    function rules() external view returns (Rules memory);
}

sellableOf is the call a front end should make before enabling a sell button. It already accounts for time, size, relief and prior sales — but not for the wallet's window allowance, which windowAllowance returns separately.

Events

What an indexer should listen to.

PositionOpened(address owner, uint256 amount, uint256 price)
Emitted on every buy. Carries the entry price and the liquidity snapshot used to size the unlock.
PositionReduced(address owner, uint256 id, uint256 amount)
Emitted on every accepted sell, partial or full.
SellRejected(address owner, uint256 requested, uint256 allowed)
Emitted when a sell exceeds the sellable amount. Useful for surfacing why a trade failed.
ReliefTierReached(uint256 id, uint8 tier)
Emitted the first time a position crosses a drawdown tier.

Errors

Custom errors, so a failed trade explains itself.

ExceedsSellable(uint256 allowed)
The sell is larger than what the position may currently release. The returned value is the amount that would succeed.
ExceedsImpactCap(uint256 remaining)
The wallet has already used its share of the current window.
LaunchNotOpen(uint256 opensAt)
A buy arrived before launchDelay elapsed.
BuyAboveRamp(uint256 max)
The buy exceeds the size allowed at this point in buyRamp.
UnknownDestination()
A transfer targets an address that is neither a whitelisted pool nor an account passing the sellable-amount check.

Fees

Charged at launch and on trades routed through the protocol.

Launch fee
A flat fee at deployment, paid by the creator. It does not scale with supply or liquidity.
Trade fee
A share of each swap routed through a protocol pool, split between the pool and the treasury.
No sell penalty
Selling is never taxed more heavily than buying. Asymmetric taxes push holders toward the exits they can still use, which is the opposite of the intent here.

Indexing

Reconstructing state from events.

Positions are the unit of state worth indexing. Track PositionOpened and PositionReduced, then recompute sellable amounts client-side rather than storing them — they change with every block through elapsed time and price.

// derived, never stored
const sellable = positions
  .map(p => sellableAt(p, now, twap))
  .reduce((a, b) => a + b, 0n);

Known limits

Where the protocol stops helping.

Multi-wallet splitting
Buying through many wallets sidesteps per-position sizing and per-wallet windows. The aim is to make that costly and visible, not impossible — any real fix would require identity, which this protocol will not do.
MEV
Sandwiching and front-running are reduced by the opening delay and buy ramp, never eliminated. Ordering belongs to the sequencer.
Oracle manipulation
A sufficiently capitalised actor can move a 30 minute TWAP. Deeper initial liquidity raises that cost more than any parameter here.
Immutability cuts both ways
Parameters cannot be fixed after deployment. A misconfigured launch stays misconfigured.