Uniscribe Protocol
An open inscription standard for Uniswap v4, with on-chain accounting and verifiable events.
Uniscribe defines a compact JSON format — UNI-20 — that rides inside Uniswap v4 swap hookData, plus a single reference hook contract that parses those payloads on-chain and maintains a global registry of tickers and balances.
Status. UNI-20 v1.0.0 is frozen. The reference hook contract is shipping to testnet. Mainnet deployment follows third-party audit.
Tokenomics
| Parameter | Value | Notes |
|---|---|---|
| Ticker | UNIS | protocol-canonical, atomically inscribed at deploy |
| Max supply | 21,000,000 UNIS | hard cap, immutable |
| Per-mint cap | 1,000 UNIS | each mint op claims up to this |
| Mint fee | ~0.000952 ETH | per 1,000 UNIS (= 952_380_952_380_952 wei) |
| Deploy fee | ~0.00952 ETH | only for non-genesis tickers |
| Transfer fee | 0 | free, just emits events |
| Premine | 0% | no team allocation, no treasury allocation |
| Mint ops to fully distribute | 21,000 | = 21M / 1,000 |
| Total protocol fees at full distribution | ~20 ETH | all routed to UniscribeTreasury |
| Floor price per UNIS | ~9.52 × 10⁻⁷ ETH | = mint_fee / per_mint_cap |
| Floor FDV at full distribution | ~$60k @ $3k ETH | protocol minimum; secondary market unbounded |
All numbers are baked into the contract as constants. There's no admin function to modify any of them — the parameters are fixed from the moment the contract is deployed, for the lifetime of UNIS.
Genesis ticker
The Uniscribe contract inscribes a canonical protocol-owned UNI-20
ticker, UNIS, atomically in its constructor as inscription
number 1. Parameters are baked into the contract as constants:
| Field | Value |
|---|---|
tick | UNIS |
max | 21,000,000 |
lim | 1,000 |
| Premine | 0 (fair launch) |
deployer | UniscribeTreasury |
Because the deploy happens inside the constructor, no one can
front-run the ticker registration. The team mints alongside everyone
else via the standard mint op — roughly 21,000
mint inscriptions are required to fully distribute the supply.
Quickstart
The fastest path to your first inscription:
1. Build a payload
{
"p": "uni-20",
"op": "deploy",
"tick": "UNIS",
"max": "21000000",
"lim": "1000"
}
2. Encode and attach to a v4 swap
const payload = ethers.toUtf8Bytes(JSON.stringify(op));
await poolManager.swap(
poolKey,
{ zeroForOne: true, amountSpecified: 1n, sqrtPriceLimitX96 },
payload // ← hookData
);
3. Or call the contract directly
For environments without an active swap context, use the bare inscribe() entrypoint:
await uniscribe.inscribe(payload, { value: ethers.parseEther("0.00952") });
Architecture
Uniscribe is one contract that wears two hats. As a Uniswap v4 BaseHook it intercepts beforeSwap, reads the hookData field, and delegates to its parser. As a standalone registry it exposes inscribe() for off-pool callers and view functions for indexers and wallets.
- Storage. Tickers, deployer addresses, supply caps, balances.
- Parser. Pure-Solidity strict JSON reader, calldata-only.
- Events. A single
Inscriptionevent per op plus typed siblings (Mint,Transfer,TickerDeployed). - Fees.
0.00952 ETHper deploy,0.000952 ETHper mint, routed to a transparent treasury.
UNI-20 Specification
A UNI-20 inscription is a UTF-8 encoded JSON object that conforms to the schema below. Whitespace outside of strings is permitted but discouraged. Strings must be quoted with " (no single quotes). Numeric fields must be quoted to preserve precision under 256-bit arithmetic.
JSON schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://uniscribe.xyz/schema/uni-20.json",
"title": "UNI-20 Inscription",
"type": "object",
"required": ["p", "op", "tick"],
"properties": {
"p": { "const": "uni-20" },
"op": { "enum": ["deploy", "mint", "transfer"] },
"tick": { "type": "string", "pattern": "^[A-Za-z0-9]{1,8}$" },
"max": { "type": "string", "pattern": "^[0-9]{1,78}$" },
"lim": { "type": "string", "pattern": "^[0-9]{1,78}$" },
"amt": { "type": "string", "pattern": "^[0-9]{1,78}$" },
"to": { "type": "string", "pattern": "^0x[0-9a-fA-F]{40}$" }
}
}
deploy
Registers a new ticker. Reverts if the ticker already exists, if max or lim are zero, or if lim > max.
| Field | Type | Required | Notes |
|---|---|---|---|
p | string | yes | Must be "uni-20". |
op | string | yes | Must be "deploy". |
tick | string | yes | 1–8 chars, alphanumeric. Case-insensitive (normalized upper). |
max | string-uint | yes | Total supply cap. Fits in uint256. |
lim | string-uint | yes | Per-mint cap. Must be ≤ max. |
mint
Mints up to lim tokens of tick to the transaction sender. Reverts if total minted would exceed max or if amt > lim.
| Field | Type | Required | Notes |
|---|---|---|---|
p | string | yes | "uni-20" |
op | string | yes | "mint" |
tick | string | yes | Must already exist. |
amt | string-uint | yes | 0 < amt ≤ lim |
transfer
Moves amt tokens of tick from the sender to to. No fee. Reverts on insufficient balance.
| Field | Type | Required | Notes |
|---|---|---|---|
p | string | yes | "uni-20" |
op | string | yes | "transfer" |
tick | string | yes | Must already exist. |
amt | string-uint | yes | 0 < amt ≤ balance |
to | address | yes | EIP-55 not required, checksum ignored. |
Validation rules
- Strict parser. Nested objects, escape sequences, and trailing commas are rejected.
- Unknown keys are ignored. This preserves forward-compatibility with future op extensions.
- Tickers are case-insensitive. Internally normalized to upper-case ASCII and packed into
bytes32. - Numeric overflow. Any value exceeding
2^256-1causes a parse revert.
Contract ABI reference
Selected callable surface from Uniscribe.sol:
Entry points
function inscribe(bytes calldata payload) external payable;
function beforeSwap(
address sender,
PoolKey calldata key,
SwapParams calldata params,
bytes calldata hookData
) external returns (bytes4, int256, uint24);
function afterSwap(...) external returns (bytes4, int128);
Views
function tickerInfo(string calldata ticker)
external view returns (Ticker memory);
function balanceOfTicker(string calldata ticker, address holder)
external view returns (uint256);
function totalInscriptions() external view returns (uint64);
Events
event Inscription(
uint64 indexed inscriptionNo,
address indexed from,
bytes32 indexed tick,
string op,
uint256 amount,
address to,
bytes raw
);
event TickerDeployed(bytes32 indexed tick, address indexed deployer, uint256 max, uint256 lim);
event Mint (bytes32 indexed tick, address indexed to, uint256 amount);
event Transfer (bytes32 indexed tick, address indexed from, address indexed to, uint256 amount);
Errors
| Error | When |
|---|---|
InvalidProtocol | The p field is not "uni-20". |
UnknownOp | The op field is not deploy/mint/transfer. |
BadTicker | Ticker is empty, too long, or contains non-alphanumeric chars. |
TickerExists | Attempted deploy of an already-registered ticker. |
TickerNotFound | mint/transfer referenced an unknown ticker. |
MintLimitExceeded | amt > lim on mint. |
MaxSupplyExceeded | Cumulative minted would exceed max. |
InsufficientBalance | Sender lacks tokens for transfer. |
InsufficientFee | msg.value below the required fee. |
MalformedJSON | Payload failed strict parsing. |
Fees
| Op | Fee | Wei | Notes |
|---|---|---|---|
deploy | ~0.00952 ETH | 9_523_809_523_809_520 | 10× mint fee |
mint | ~0.000952 ETH | 952_380_952_380_952 | per 1,000 UNIS minted |
transfer | free | 0 | balances move on-chain |
Fees are calibrated so that fully minting all 21M UNIS costs
~20 ETH in protocol fees over the lifetime of the genesis
ticker (21,000 mint ops × MINT_FEE). The entirety routes to the
immutable treasury address set at deployment.
Overpayment is refunded in the same transaction.
At ETH = $3,000, that's a floor FDV of approximately $60,000 at full distribution — the minimum anyone could buy out the supply for in protocol fees alone (gas not included). The secondary market can trade at any multiple above that floor.
Integration guide
There are two paths to integrate Uniscribe:
A. Routing through a Uniswap v4 pool
- Mine a hook address whose lower bits match the permissions returned by
getHookPermissions()(beforeSwap+afterSwap). - Deploy
Uniscribe.solat that mined salt viaCREATE2. - Initialize a Uniswap v4 pool whose
hooksfield is the Uniscribe address. - Compose swaps with your UNI-20 payload in
hookData.
B. Direct inscription
If you don't need a swap, call inscribe(bytes) on the contract with the UTF-8 JSON payload. Send enough ETH to cover the op fee. The hook's swap entrypoints are unused in this mode.
const payload = ethers.toUtf8Bytes(JSON.stringify({
p: "uni-20", op: "mint", tick: "UNIS", amt: "1000"
}));
await uniscribe.inscribe(payload, {
value: ethers.parseEther("0.000952")
});
Indexer notes
The on-chain registry is the source of truth. Indexers exist to give apps and wallets a friendly query surface. Recommended approach:
- Subscribe to the
Inscriptionevent — it carries the monotonicinscriptionNo, the op string, and the raw payload bytes. That's enough to reconstruct the global timeline without reading per-op storage. - Cross-check totals against
tickerInfo(tick).mintedandbalanceOf[tick][holder]for any chain reorg recovery. - Unpack
bytes32 tickby trimming trailing zero bytes and ASCII-decoding the remainder.
Note. Reverted inscriptions never emit events. Unlike Bitcoin ordinals, there is no "invalid inscription" state to reconcile — if your indexer sees the event, the op succeeded.
Example transactions
deploy Register the UNIS ticker with a 21M cap and 1k per-mint limit.
// payload
{"p":"uni-20","op":"deploy","tick":"UNIS","max":"21000000","lim":"1000"}
// call
uniscribe.inscribe(payloadBytes, { value: parseEther("0.00952") }); // deploy fee
// emitted
Inscription(1, msg.sender, 0x554e495300...000, "deploy", 21000000, 0x0, payloadBytes);
TickerDeployed(0x554e495300...000, msg.sender, 21000000, 1000);
mint Claim 1,000 UNIS to yourself.
{"p":"uni-20","op":"mint","tick":"UNIS","amt":"1000"}
uniscribe.inscribe(payloadBytes, { value: parseEther("0.000952") });
Inscription(2, msg.sender, 0x554e495300...000, "mint", 1000, msg.sender, payloadBytes);
Mint(0x554e495300...000, msg.sender, 1000);
transfer Move 500 UNIS to another address.
{"p":"uni-20","op":"transfer","tick":"UNIS","amt":"500","to":"0x0000000000000000000000000000000000000abc"}
uniscribe.inscribe(payloadBytes); // no fee on transfer
Inscription(3, msg.sender, 0x554e495300...000, "transfer", 500, 0x...abc, payloadBytes);
Transfer(0x554e495300...000, msg.sender, 0x...abc, 500);
Mint pool (v4)
The launch ships a Uniswap v4 pool whose hook is the
UniscribeMintPool contract. Swapping ETH for
wUNIS through this pool directly mints UNI-20
inscriptions — the user never sees JSON, never calls
inscribe(), never touches a wrapper. They just trade.
How a mint-swap works
- User submits an ETH → wUNIS swap through any v4 router on the
pool whose
hooksfield isUniscribeMintPool. - The hook's
beforeSwapfires. It computesops = ethIn / 0.00021 ether(capped at 10 ops, and further capped by remaining UNIS supply). - For each op, the hook calls
uniscribe.inscribe()with a 1,000-unitmintpayload, paying the standard 0.000952 ETH fee per op. - It approves
UNI20Wrapperand callswrap(), converting the freshly-minted UNIS into wUNIS held by the hook. - The hook returns a
BeforeSwapDeltatelling the v4 PoolManager: "consumeops × 0.000952 ETH(~$2.85 per 1,000 UNIS at $3k ETH) from the swapper, deliverops × 1,000 × 1e18wUNIS to them." Overpayment beyondops × 0.000952 ETH(~$2.85 per 1,000 UNIS at $3k ETH) stays with the swapper. - The protocol fees route to
UniscribeTreasuryin the same transaction.
Pricing. Pass-through. One mint op = 1,000 UNIS =
0.000952 ETH, identical to calling inscribe() directly.
The mint pool is a UX layer, not a price layer. There's no
AMM-style price impact — the hook is an infinite-depth book at the
protocol's mint fee until supply hits 21M, at which point swaps revert
and the secondary market (regular wUNIS pools) takes over.
Why it has a separate hook address
Uniswap v4 encodes hook permissions in the lower 14 bits of the hook's
contract address. The main Uniscribe contract uses
BEFORE_SWAP | AFTER_SWAP = 0x00C0. The mint pool needs
BEFORE_SWAP | BEFORE_SWAP_RETURNS_DELTA = 0x0088 so it can
override the swap's accounting. Both addresses are mined via
HookMiner at deployment.
Mint pool methods (frontends + indexers)
function remainingSupply() external view returns (uint256);
function quoteCost(uint256 units) external pure returns (uint256 ethCost);
function quoteMint(uint256 ethIn) external view returns (uint256 units, uint256 ethCost);
event MintedViaSwap(address indexed recipient, uint256 ethPaid, uint256 unisMinted, uint256 wUnisDelivered);
Security model
- Permissionless deploy. First-come-first-served ticker registration is intentional. Build wallets and marketplaces assuming squatters; rely on community-curated allow-lists for UX.
- Reentrancy. Treasury payout uses a low-level call; balances are updated before any external interaction in the same op.
- Fee underpayment. Reverts with
InsufficientFeebefore any state mutation. - Pool manager trust. Swap-routed inscriptions require
msg.sender == poolManager. Directinscribe()bypasses this — both paths run the same parser and registry logic.
Audit status
The reference implementation has undergone an independent code review prior to launch. 14 findings (3 critical, 4 high, 7 medium) were identified and fixed:
- Reentrancy guard added to
inscribe(); event ordering moved before external calls - Stuck-ETH-on-transfer-op vulnerability closed (reverts on
msg.value > 0) - Deployer hook-flag check tightened to strict equality
- ERC-20 wrapper rejects
transfertoaddress(0) - Mint pool now uses
senderfrombeforeSwap, nottx.origin - Mint pool enforces exact-multiple ETH input (prevents residual-swap routing)
- JSON parser requires closing brace (rejects malformed payloads)
- Treasury supports clean cancellation of pending ownership transfer
A formal third-party audit by a reputable firm is recommended before mainnet deployment with material TVL. The current implementation is suitable for testnet, launch on cheap L2s, and small-cap mainnet deployments at the team's discretion.
Not financial advice. UNI-20 is a token standard. Anything deployed under it has no inherent value and no relationship to the UNI token, Uniswap Labs, or the Uniswap DAO.