$ uniscribe v1.0.0  —  UNI-20 spec frozen. Audit in progress.
// Documentation · v1.0.0

Uniscribe Protocol

An open inscription standard for Uniswap v4, with on-chain accounting and verifiable events.

Standard
UNI-20
Compiler
solc 0.8.26
License
MIT

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

ParameterValueNotes
TickerUNISprotocol-canonical, atomically inscribed at deploy
Max supply21,000,000 UNIShard cap, immutable
Per-mint cap1,000 UNISeach mint op claims up to this
Mint fee~0.000952 ETHper 1,000 UNIS (= 952_380_952_380_952 wei)
Deploy fee~0.00952 ETHonly for non-genesis tickers
Transfer fee0free, just emits events
Premine0%no team allocation, no treasury allocation
Mint ops to fully distribute21,000= 21M / 1,000
Total protocol fees at full distribution~20 ETHall routed to UniscribeTreasury
Floor price per UNIS~9.52 × 10⁻⁷ ETH= mint_fee / per_mint_cap
Floor FDV at full distribution~$60k @ $3k ETHprotocol 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:

FieldValue
tickUNIS
max21,000,000
lim1,000
Premine0 (fair launch)
deployerUniscribeTreasury

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.


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.

FieldTypeRequiredNotes
pstringyesMust be "uni-20".
opstringyesMust be "deploy".
tickstringyes1–8 chars, alphanumeric. Case-insensitive (normalized upper).
maxstring-uintyesTotal supply cap. Fits in uint256.
limstring-uintyesPer-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.

FieldTypeRequiredNotes
pstringyes"uni-20"
opstringyes"mint"
tickstringyesMust already exist.
amtstring-uintyes0 < amt ≤ lim

transfer

Moves amt tokens of tick from the sender to to. No fee. Reverts on insufficient balance.

FieldTypeRequiredNotes
pstringyes"uni-20"
opstringyes"transfer"
tickstringyesMust already exist.
amtstring-uintyes0 < amt ≤ balance
toaddressyesEIP-55 not required, checksum ignored.

Validation rules


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

ErrorWhen
InvalidProtocolThe p field is not "uni-20".
UnknownOpThe op field is not deploy/mint/transfer.
BadTickerTicker is empty, too long, or contains non-alphanumeric chars.
TickerExistsAttempted deploy of an already-registered ticker.
TickerNotFoundmint/transfer referenced an unknown ticker.
MintLimitExceededamt > lim on mint.
MaxSupplyExceededCumulative minted would exceed max.
InsufficientBalanceSender lacks tokens for transfer.
InsufficientFeemsg.value below the required fee.
MalformedJSONPayload failed strict parsing.

Fees

OpFeeWeiNotes
deploy~0.00952 ETH9_523_809_523_809_52010× mint fee
mint~0.000952 ETH952_380_952_380_952per 1,000 UNIS minted
transferfree0balances 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

  1. Mine a hook address whose lower bits match the permissions returned by getHookPermissions() (beforeSwap + afterSwap).
  2. Deploy Uniscribe.sol at that mined salt via CREATE2.
  3. Initialize a Uniswap v4 pool whose hooks field is the Uniscribe address.
  4. 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:

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

  1. User submits an ETH → wUNIS swap through any v4 router on the pool whose hooks field is UniscribeMintPool.
  2. The hook's beforeSwap fires. It computes ops = ethIn / 0.00021 ether (capped at 10 ops, and further capped by remaining UNIS supply).
  3. For each op, the hook calls uniscribe.inscribe() with a 1,000-unit mint payload, paying the standard 0.000952 ETH fee per op.
  4. It approves UNI20Wrapper and calls wrap(), converting the freshly-minted UNIS into wUNIS held by the hook.
  5. The hook returns a BeforeSwapDelta telling the v4 PoolManager: "consume ops × 0.000952 ETH (~$2.85 per 1,000 UNIS at $3k ETH) from the swapper, deliver ops × 1,000 × 1e18 wUNIS to them." Overpayment beyond ops × 0.000952 ETH (~$2.85 per 1,000 UNIS at $3k ETH) stays with the swapper.
  6. The protocol fees route to UniscribeTreasury in 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

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:

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.