references/config-schema.md
# Configuration Schema
The canonical JSON shape produced by `permissioned-pools-configurator` and consumed by
`permissioned-pools-deployer`. Keyed by chain ID at the top level, mirroring the CCA
plugin's configuration file — one file can describe the same issuer's rollout across
several chains.
```json
{
"<chainId>": {
"permissionedToken": "0x...",
"allowlistChecker": {
"mode": "existing",
"address": "0x..."
},
"adapterOwner": "0x...",
"verificationDepositAmount": "1",
"allowedWrappers": {
"permissionedPositionManager": "0x...",
"universalRouterV2_2": "0x...",
"v4Quoter": "0x...",
"mixedRouteQuoterV2": "0x..."
},
"hook": "0x...",
"pool": {
"pairedCurrency": "native",
"feeTier": 3000,
"tickSpacing": 60,
"startingPriceRatio": 1
},
"seeding": {
"intent": "seed-now",
"wallet": "0x..."
}
}
}
```
`0x...` above is illustrative shorthand for "a 42-character address," not a literal
value — see [Address Validation](#address-validation) for the exact rule and for how an
unresolved address is represented instead.
## Top-Level Key
| Field | Type | Required | Validation |
| ----------- | ------------------ | -------- | --------------------------------------------------------------------------- |
| `<chainId>` | object key, string | yes | parses as a positive integer; one key per chain this issuer is deploying to |
The value under each chain-ID key is one complete configuration, described below. There
is no top-level array — this schema does not hardcode a supported-chain list, because
which chains have the permissioned-pools contracts deployed changes over time and is
resolved from the deploy guide's `#deployment-addresses` table, not from this file.
## Per-Chain Fields
| Field | Type | Required | Validation |
| ---------------------------- | ------ | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `permissionedToken` | string | yes | matches the address regex below, or the literal string `"RESOLVE"` (the unresolved sentinel, see below) |
| `allowlistChecker.mode` | string | yes | one of `existing`, `to-be-deployed` |
| `allowlistChecker.address` | string | required when `mode` is `existing`; otherwise `null` | matches the address regex when present |
| `adapterOwner` | string | yes | matches the address regex and is not the zero address, or the literal string `"RESOLVE"` (the unresolved sentinel, see below) |
| `verificationDepositAmount` | string | yes | a positive base-10 integer (greater than zero), as a string (to avoid JSON number precision loss), expressed in the underlying token's smallest unit |
| `allowedWrappers.*` (4 keys) | string | yes, all four keys present | matches the address regex, or the literal string `"RESOLVE"` (the unresolved sentinel, see below) |
| `hook` | string | yes | matches the address regex, or the literal string `"RESOLVE"` (the unresolved sentinel, see below) |
| `pool.pairedCurrency` | string | yes | the literal string `"native"`, or an address matching the regex below |
| `pool.feeTier` | number | yes | positive integer, in hundredths of a basis point (e.g. `3000` = 0.30%) |
| `pool.tickSpacing` | number | yes | positive integer |
| `pool.startingPriceRatio` | number | yes | positive number; paired-currency units per one permissioned-token unit |
| `seeding.intent` | string | yes | one of `seed-now`, `seed-later`, `none` |
| `seeding.wallet` | string | required when `intent` is `seed-now` or `seed-later`; otherwise `null` | matches the address regex when present |
### The four `allowedWrappers` keys
All four keys are always present in the object, whether or not their value is resolved
yet:
- `permissionedPositionManager`
- `universalRouterV2_2`
- `v4Quoter`
- `mixedRouteQuoterV2`
These are the only four keys this schema defines under `allowedWrappers`. If an issuer's
own custom router or quoter also needs registering (any contract that will call the
PoolManager on this pool needs to be on the adapter's `allowedWrappers` list — see
[Parameter Reference](./parameter-reference.md#the-four-wrappers-and-why-only-these-four)),
add it under an issuer-chosen key alongside the four above; the deployer skill registers
every key present in this object, not only the four named ones. **Never add a key named
after the factory or the hook here** — neither is a wrapper, and registering either would
be a configuration error the deployer skill's preflight should catch, not something this
schema silently accepts.
## Address Validation
Every address field validates against:
```regex
^0x[a-fA-F0-9]{40}$
```
The zero address never satisfies this check in practice: no address field in this schema
has meaningful zero-address semantics, so reject it everywhere. (`pool.pairedCurrency` is
the near-miss — the native currency is expressed as the `"native"` sentinel here and only
becomes `address(0)` when the deployer builds the `PoolKey`.)
Two exceptions, both intentional:
1. **`pool.pairedCurrency` also accepts the literal string `"native"`.** This is a
configurator-level sentinel for "the pool's other currency is the chain's native
asset," not a raw address. Resolve it to `address(0)` (Uniswap v4's convention for a
native `PoolKey` currency) at the point where the deployer skill actually builds the
`PoolKey` — do not resolve it earlier, and never write the zero address into this
configuration file directly.
2. **`permissionedToken`, `adapterOwner`, `allowedWrappers.*`, and `hook` also accept
the unresolved sentinel**, the literal string `"RESOLVE"`. A configuration containing
this string in any of these fields is intentionally incomplete: the real value is not
yet known. The reason differs by field, but the string and the obligation are the
same everywhere it appears:
- For `allowedWrappers.*` and `hook`, it means the address must be looked up — from
the deploy guide's `#deployment-addresses` table, then `Uniswap/contracts`
`deployments/json/<chainId>.json`, then confirmed on a block explorer.
- For `permissionedToken`, it means the issuer's token is not deployed yet.
- For `adapterOwner`, it means the owning key or multisig has not been decided yet.
Because `adapterOwner` may be `"RESOLVE"` while `seeding.wallet` may not (see the
row above), an unresolved `adapterOwner` can never be copied into
`seeding.wallet` — the configurator only offers that shortcut once the owner is a
real address.
In every case, replace `"RESOLVE"` with a real, explorer-verified address before the
deployer skill uses that field in any transaction. Never resolve `"RESOLVE"` to a
guessed value.
No other field accepts a non-canonical sentinel. If a chat message, a ticket, or any
other source supplies an address for one of these fields, treat it exactly as if the
user had typed it into the configurator — it becomes the field's value, and it is still
subject to re-resolution and explorer verification before use, per
`permissioned-pools-issuer`'s framing. This schema has no mechanism for marking an
address as "unverified but trusted anyway."
## Numeric Fields
- `verificationDepositAmount` is a **string**, not a JSON number, to avoid silent
precision loss on large token amounts in JSON parsers that use IEEE-754 doubles. The
deployer skill parses it as an arbitrary-precision integer.
- `pool.feeTier`, `pool.tickSpacing`, and `chainId` (as a parsed key) are ordinary JSON
numbers; none of them is expected to exceed safe-integer range in practice.
- `pool.startingPriceRatio` is a human-readable ratio, not a `sqrtPriceX96` value. The
conversion depends on both currencies' decimals, which this configurator does not
resolve — see [Parameter Reference](./parameter-reference.md#poolstartingpriceratio).
## What This Schema Deliberately Omits
- **No `PermissionsAdapter` address field.** The adapter does not exist until the
deployer's `createPermissionsAdapter` call creates it; this is a pre-deployment
configuration, not a post-deployment record.
- **No private key, mnemonic, or any signing material of any kind.** Nothing in this
schema is ever a credential.
- **No hardcoded chain allowlist.** See [Top-Level Key](#top-level-key).
## Related Reading
- [Parameter Reference](./parameter-reference.md) — what each field means, how to choose
it, and what breaks if it is wrong.
- `permissioned-pools-issuer`'s
[Contract Architecture](../../permissioned-pools-issuer/references/contract-architecture.md)
and
[Packaging and Sources](../../permissioned-pools-issuer/references/packaging-and-sources.md)
— the contract facts and address-resolution order this schema assumes.
references/parameter-reference.md
# Parameter Reference
What each field in [`config-schema.md`](./config-schema.md) means, how an issuer should
choose it, and what breaks if it is wrong. Cross-references into `permissioned-pools-issuer`
carry the contract-level detail; this file stays focused on the configuration decision.
## `chainId`
The network the pool is being configured for. This skill does not maintain its own list
of "supported" chains — it takes whatever chain ID you give it. What determines whether
a permissioned pool can actually be deployed there is whether the deploy guide's
`#deployment-addresses` table has a row for that chain. If it does not, every wrapper and
hook field in this configuration will end up marked `"RESOLVE"` with nothing to resolve
to, and that is the signal to check the chain rather than a bug in the configuration.
**What breaks if it is wrong:** every address you later resolve against the deploy
guide's table or `deployments/json/<chainId>.json` is for the wrong chain, and nothing in
the JSON structure itself will catch that — the mistake surfaces only when a resolved
address does not exist on the chain you actually deploy to.
## `permissionedToken`
The address of the issuer's own transfer-restricted ERC-20 — the token wallets and LPs
actually hold. This is **not** the pool currency; the pool currency is the
`PermissionsAdapter`, which does not exist until it is created. See
[Two Addresses, Not One](../../permissioned-pools-issuer/SKILL.md#two-addresses-not-one)
for the full distinction, which is the single most common integration error in this
whole flow.
Before configuring this field, confirm the token exposes `name()`, `symbol()`, and
`decimals()` — the adapter's virtual-token identity is derived from all three, falling
back to generic values when any is missing.
If the token is not deployed yet, store the literal string `"RESOLVE"` rather than
guessing an address — see [Address Validation](./config-schema.md#address-validation).
**What breaks if it is wrong:** every downstream step operates on the wrong token. The
allowlist checker will be asked about the wrong `tokenAddress`, and the adapter created
from this token address cannot be swapped for a different one later — the factory's
token mapping is write-once.
## `allowlistChecker`
Whether the issuer already has a deployed contract satisfying `IAllowlistChecker`, or
still needs to write and deploy one.
- **`mode: "existing"`** — the issuer has a checker deployed and verified against the
`IAllowlistChecker` interface via ERC-165. Record its address.
- **`mode: "to-be-deployed"`** — the checker does not exist yet. This configuration can
still be produced; the deployer skill's preflight is where the checker's own ERC-165
compliance actually gets checked, because `createPermissionsAdapter` reverts
`InvalidAllowListChecker` on the very first Uniswap call otherwise.
**What breaks if it is wrong:** a checker address that does not answer the ERC-165 probe
correctly makes adapter creation revert immediately — the first Uniswap transaction in
the whole journey. A checker that answers ERC-165 correctly but has a wrong or empty
allowlist inside it produces a pool that exists but that nobody (or everybody) can use,
which is a much quieter failure to notice.
## `adapterOwner`
The address that becomes `initialOwner` on `createPermissionsAdapter`. From the moment
the adapter is created, this key can:
- replace the allowlist checker (`updateAllowListChecker`)
- add or remove any wrapper, including the position manager itself (`updateAllowedWrapper`)
- pause or resume all swapping on every pool using this adapter (`updateSwappingEnabled`)
- approve or revoke a hook on the LP path (`setAllowedHook`, via the position manager)
- force-exit **any** LP position in a pool where this adapter is a currency
(`unwindPosition`, via the position manager)
Ownership transfer is two-step (`Ownable2Step`), so a mistyped new-owner address during a
later handover does not silently strand the adapter — but the _initial_ owner is set at
creation with no such protection, so get this one right the first time. The published
guide's own recommendation is a multisig.
If the owning key or multisig has not been decided yet, store the literal string
`"RESOLVE"` rather than picking an address just to complete the flow — the adapter is
not created until the deployer skill runs, so there is no urgency to fill this in early.
**What breaks if it is wrong:** every capability above lands with the wrong party (or an
address nobody controls, if it is mistyped and unrecoverable). There is no way to
"re-create" an adapter for the same token cheaply once one is verified — the factory
does not enforce a single adapter per token, but everything downstream (allowlisting,
verification, wrapper registration) has to be redone for a second one.
## `verificationDepositAmount`
The amount of the underlying permissioned token deposited into the not-yet-verified
adapter via `depositForVerification`, to satisfy the balance check that
`verifyPermissionsAdapter` reads.
**Three facts that should drive this choice, not intuition:**
1. **It is the mintable headroom, not a fee.** `wrapToPoolManager`'s available balance is
`balanceOf(adapter) - totalSupply()` — whatever you deposit here becomes exactly the
amount an allowed wrapper can convert into virtual tokens for the PoolManager. It is
not consumed, burned, or spent by verification itself.
2. **There is no withdraw function.** `PermissionsAdapter` has no withdraw, rescue, or
sweep path, and `Ownable2Step` gives the owner no token-moving power. Whatever is
deposited here is committed to the adapter permanently — it stays economically live as
mintable headroom, but it cannot be pulled back out directly.
3. **1 wei is enough.** The published documentation says so in multiple places, and
Uniswap's own test fixture deposits exactly 1 wei. Verification only reads whether the
balance is non-zero.
**What breaks if it is wrong:** depositing a large amount "to be safe" does not make
verification more secure — the check is a bare non-zero balance read — and it commits
real value to a contract with no exit path for it. Depositing zero (or forgetting this
step) makes `verifyPermissionsAdapter` revert `PermissionsAdapterNotVerified`, which
blocks three of the five contract-enforced ordering edges in the whole journey.
## `allowedWrappers`
The set of contracts registered on the adapter's `allowedWrappers` list via
`updateAllowedWrapper`. This is the real enforcement boundary of the whole system: the
hook and the adapter both check this list before letting anything reach the PoolManager
through a given caller.
### The four wrappers, and why only these four
1. `PermissionedPositionManager` — required before the first mint.
2. The Universal Router, specifically the `#v2.2` (or higher) deployment — required
before the first swap routed through it.
3. `V4Quoter` — required before the first quote simulation; without it, quote requests
revert even though the pool is live and mintable.
4. `MixedRouteQuoterV2` — same requirement, for mixed-route quoting.
**The rule behind the four, which generalizes past them:** register every contract that
will call the PoolManager on this pool. Each of the four qualifies because it correctly
reports the true originating caller through `msgSender()`, which is exactly what the
allowlist check consumes. If the issuer deploys a custom router or quoter of their own
that reaches the PoolManager on this pool, it needs the same property and the same
registration — add it to the configuration under its own key, alongside the four (see
[the schema's note on this](./config-schema.md#the-four-allowedwrappers-keys)).
**Never register the factory. Never register the hook.** The published guide's own
Step 5 table has six rows, and two of them — `PermissionsAdapterFactory` and
`PermissionedHooks` — are not wrappers. Registering either is not caught by any contract
check; it is simply wrong, and it does not do what a reader skimming the guide's table
might assume.
**What breaks if it is wrong:** an unregistered position manager blocks every mint with a
bare `Unauthorized()`. An unregistered router blocks every swap through it the same way.
An unregistered quoter makes the pool look broken to any interface pricing it, even
though it is live and mintable — this is, per the reference skill, the failure mode most
often mistaken for "the pool is broken" when it is a missing registration. Resolving the
plain `UniversalRouter` deployment instead of the `#v2.2` one registers a contract that
looks right and produces a router that cannot do permissioned routing at all.
## `hook`
The `PermissionedHooks` address, registered with `setAllowedHook` on the position
manager — not on the adapter, and not through `updateAllowedWrapper`. This is a separate
call from wrapper registration, gates the LP path only (mints and increases, not swaps,
not decreases, not burns), and is enforced against verification: calling it before the
adapter is verified reverts `NotPermissionsAdapterAdmin`, even for the genuine owner,
because the position manager cannot resolve an owner for an unverified adapter.
**What breaks if it is wrong:** an unapproved hook blocks every mint and increase with
`InvalidHook`, on that specific position manager. Approval is per position manager, so a
second deployment (or the same one on another chain) needs its own call — a value that
worked on one chain does not carry over.
## `pool.pairedCurrency`
The non-permissioned side of the pool. Use the literal `"native"` for the chain's native
asset (ETH, or the chain's equivalent), or a real ERC-20 address for anything else. The
permissioned side of the `PoolKey` is always the adapter — this field is never that side.
**What breaks if it is wrong:** using the underlying permissioned token's own address
here (instead of `"native"` or a genuinely separate paired asset) builds a `PoolKey`
where the currency ordering and the intended pair no longer make sense, and initializing
it either fails at the PoolManager level (equal currencies) or produces a pool nobody
intended to create.
## `pool.feeTier`
The pool's fee, in hundredths of a basis point (v4's standard convention — `3000` means
0.30%). This is an ordinary v4 pool parameter and carries no permissioned-pools-specific
behavior; choose it the way you would for any v4 pool.
**What breaks if it is wrong:** a fee tier mismatched to the pair's expected volatility
and volume profile produces a pool with wrong economics, but nothing in the permissioned-
pools contracts validates or corrects for that — it is a normal v4 liquidity decision the
issuer owns.
## `pool.tickSpacing`
The pool's tick spacing, paired with the fee tier in the same `PoolKey`. Also an
ordinary v4 parameter with no permissioned-pools-specific behavior.
**What breaks if it is wrong:** too fine a tick spacing for the intended fee tier is a gas
and liquidity-management inefficiency, not a permissioned-pools failure; get the pairing
right by the same conventions used for any v4 pool.
## `pool.startingPriceRatio`
Expressed here as a human-readable ratio — paired-currency units per one unit of the
permissioned token — rather than as `sqrtPriceX96`. That conversion is deliberately
deferred to the deployer skill, because it depends on the decimals of **both**
currencies, and this configurator does not resolve the paired currency's decimals (it
only validates the address shape or accepts the literal string `"native"`, the
native-currency sentinel). Recording a
ratio here keeps this configuration correct regardless of which decimals value ends up
resolved later.
**What breaks if it is wrong:** an incorrect starting price does not, by itself, violate
any permissioned-pools invariant — the pool initializes and it is a plain price-discovery
problem from there, same as it would be for an unrestricted v4 pool. The
permissioned-pools-specific failure mode to watch for instead is initializing with the
**wrong currency** in the adapter's slot (see `pool.pairedCurrency` above and
`permissioned-pools-issuer`'s enforced-ordering reference), which is a different mistake
that happens to be made at the same step.
## `seeding`
Whether, and from where, the issuer intends to seed the pool's first liquidity in this
same setup pass. This configurator's own question only ever produces `"seed-now"` (with
a `wallet`) or `"none"` (with `wallet: null`) — `config-schema.md`'s third enum value,
`"seed-later"`, is available for a configuration edited or extended outside this flow,
not something this skill's question set emits itself.
**The check this configuration cannot verify for you:** the wallet that sends the first
mint and the wallet that will own the resulting position both need `LIQUIDITY_ALLOWED`
on the issuer's own allowlist checker — and being the adapter owner grants nothing on
this path. This is, per the reference skill, the step that most often breaks an
otherwise-correct setup. If `seeding.intent` is anything other than `"none"`, treat
allowlisting the named wallet (and the recipient, if different) for `LIQUIDITY_ALLOWED`
as a precondition this configuration assumes has already been handled, not something it
performs.
**What breaks if it is wrong:** a seeding wallet without `LIQUIDITY_ALLOWED` produces a
bare `Unauthorized()` on the first mint attempt — indistinguishable, by selector alone,
from three other unrelated checks failing at the same call site (see
[Enforced Ordering and Reverts](../../permissioned-pools-issuer/references/enforced-ordering-and-reverts.md)
for how to disambiguate).
## Related Reading
- [Config Schema](./config-schema.md) — the exact JSON shape, types, and validation
rules for every field above.
- `permissioned-pools-issuer`'s
[Issuer Journey](../../permissioned-pools-issuer/references/issuer-journey.md) — where
each of these values gets used, in the order the setup sequence recommends.
- `permissioned-pools-issuer`'s
[Trust Model](../../permissioned-pools-issuer/references/trust-model.md) — what the
`adapterOwner` key and the `allowedWrappers` list actually control, in full.
SKILL.md
---
name: permissioned-pools-configurator
description: Interactively collect and validate the parameters for a Uniswap v4 Permissioned Pool setup - chain, underlying token, allowlist checker, adapter owner, verification deposit, the four wrapper registrations, hook, and pool pricing - and emit the JSON config the deployer skill consumes. Use when the user says "configure a permissioned pool", "permissioned pool config", "adapter parameters", "permissioned pool setup parameters", "PermissionsAdapter configuration", or asks to set up parameters for a permissioned pool before deploying. Covers contract mechanics only - does not constitute legal, financial, investment, tax, or compliance advice, and is not a compliance review.
allowed-tools: Read, Write, Edit, Glob, Grep, AskUserQuestion
model: opus
license: MIT
metadata:
author: uniswap
version: '0.1.0'
---
# Permissioned Pools Configurator
Interactive bulk-form configurator for a Uniswap v4 Permissioned Pool. Collects every
parameter the setup journey needs, validates each one, and displays a single JSON
configuration object that the `permissioned-pools-deployer` skill consumes to run the
on-chain sequence. For the contract mechanics behind any of these parameters, see
[`permissioned-pools-issuer`](../permissioned-pools-issuer/SKILL.md).
> **Runtime Compatibility:** This skill uses `AskUserQuestion` to collect parameters in
> batches. If `AskUserQuestion` is not available in your runtime, collect the same
> parameters through natural language conversation instead, in the same batch groupings
> and with the same validation after each group.
## Scope and Disclaimer
**This is a configuration tool, not deployment guidance and not a compliance review.**
- ✅ It collects and validates the inputs a permissioned-pool setup needs and produces a
JSON document. It does not call any contract, does not broadcast anything, and holds
no signing keys.
- ✅ **Treat this as contract mechanics only.** This skill does not constitute legal, financial, investment, or tax advice, and it is **not** a compliance review of your token, your allowlist, your KYC or AML program, or your configuration. It covers what each parameter means and what breaks if it is wrong.
- ✅ It never invents a contract address. Addresses you supply are recorded as given;
addresses you do not yet have — whether because they still need to be looked up
against the deploy guide's table or because the issuer has not decided on them yet —
are stored as the literal string `"RESOLVE"`, never guessed or recalled from memory.
- ✅ Review the emitted configuration yourself, and re-resolve every address it marks for
resolution, before handing it to a deployment flow.
- ✅ **Read the repo's usage guidelines.** The repo root `DISCLAIMER.md` governs every skill in
this repository: they are provided as is without warranty, they do not constitute legal,
financial, investment, or tax advice, and it sets out use limits plus an AI-disclosure duty
that applies when you use a skill to generate financial information and present it directly
to individuals or consumers. Point the user to it.
State this framing inline (quoted or in your own words) and continue with the
configuration flow in the same response — this is reference and data-collection
content, not an action-oriented deployment step, so no acknowledgment gate is needed
here. The deployer skill gates on acknowledgment before anything gas-spending.
## What This Config Is (and Isn't)
The output is a **pre-deployment plan**, not a record of what already exists on-chain.
- The **`PermissionsAdapter` address does not appear in this config.** It does not exist
until the deployer's Step 2 (`createPermissionsAdapter`) creates it. If the issuer
already has a deployed adapter for this token, that is a re-run/import scenario this
skill does not currently handle — collect its address as a note and confirm with the
deployer skill's preflight checks instead of adding it here. **This declines only the
adapter address itself — it does not block the rest of the request.** Continue
validating and including every other field the user supplied (or marked `"RESOLVE"`)
in the emitted configuration exactly as the flow below describes.
- The **underlying permissioned token is the only token address this config records.**
See the two-addresses distinction in `permissioned-pools-issuer` — the pool currency
the deployer builds later is the adapter, derived at deploy time, not configured here.
- Wrapper and hook addresses are **chain-specific and change over time.** This skill asks
whether you already have them resolved for your target chain; if not, it marks them
for resolution rather than filling in a value.
## Configuration Flow
Collect parameters in four batches of at most four questions each. After each batch,
validate every answer against [`config-schema.md`](./references/config-schema.md) before
moving on, and show a running summary of what has been collected and what remains.
Field-by-field meaning, how to choose each value, and what breaks if it is wrong are all
in [`parameter-reference.md`](./references/parameter-reference.md).
### Front-loaded or skip-ahead answers
A user may answer several batches' worth of questions in one free-form message, or ask
to skip the remaining questions and finish immediately. Do not withhold the whole
configuration while asking whether to proceed. Instead: validate everything the user did
supply against `config-schema.md`, apply the literal string `"RESOLVE"` (or `null` for
`allowlistChecker.address` when the checker is `to-be-deployed`) to every address-type
field left open, and produce that partial configuration in the same response. Fields with
no `"RESOLVE"` sentinel — the verification deposit amount, pool currency, fee tier, tick
spacing, starting price, and seeding — have no safe default; flag each one explicitly as
still needing an answer rather than guessing at it, and ask only about those. **This
includes the verification deposit amount even though Batch 2 lists "1 wei" as the
recommended option:** a menu option a user has not actually picked is not a value the
skill may silently apply on their behalf — treat an unanswered verification deposit
amount exactly like the other sentinel-less fields, never as an already-resolved `"1"`.
Skipping the batch-by-batch confirmation step never means skipping validation — every
rule above still applies to whatever the user did supply.
### Batch 1: Network, Token & Ownership (4 questions)
**Question 1 — Network**
- Prompt: "Which network is this permissioned pool for?"
- Options: "Ethereum Mainnet (chain ID 1)", "Unichain (chain ID 130)", "Sepolia (chain ID
11155111)", Custom chain ID (via "Other")
- Note in the prompt: availability of the permissioned-pools contracts on a given chain is
not something this skill asserts — confirm the chain has a row in the deploy guide's
`#deployment-addresses` table before proceeding.
- Store: `chainId`
**Question 2 — Underlying permissioned token**
- Prompt: "What is the address of the permissioned ERC-20 being listed?"
- Options: "Not deployed yet" (stores the literal string `"RESOLVE"`), custom address
(via "Other")
- Validation: `^0x[a-fA-F0-9]{40}$`, or the literal string `"RESOLVE"` if not yet deployed
- Store: `permissionedToken`
**Question 3 — Allowlist checker**
- Prompt: "Do you already have an allowlist checker deployed for this token?"
- Options, each mapped to the exact `allowlistChecker` value it produces:
- "Yes, use an existing address" (then ask for the address via "Other") →
`mode: "existing"`, `address` set to that address.
- "No, I still need to deploy one" → `mode: "to-be-deployed"`, `address` set to `null`.
- Validation: address form above when "existing" is chosen
- Store: `allowlistChecker.mode` (`existing` | `to-be-deployed`), `allowlistChecker.address`
**Question 4 — Adapter owner**
- Prompt: "What address should own the `PermissionsAdapter` (`initialOwner`)?"
- Options: "Not yet decided — mark for resolution" (stores the literal string
`"RESOLVE"`), custom address (via "Other")
- Note in the prompt: this key can change the allowlist checker, add or remove wrappers,
pause and resume swapping, and force-exit any LP position — the published guide
recommends a multisig. Do not pick this in a hurry just to finish the flow; use the
resolution marker instead if the multisig or key is not decided yet.
- Validation: `^0x[a-fA-F0-9]{40}$` and not the zero address, or the literal string
`"RESOLVE"`
- Store: `adapterOwner`
**Validate:** confirm chain ID is a positive integer, the token address is either a
valid address or the literal string `"RESOLVE"`, the checker address (if "existing" was
chosen) matches the address regex, and the adapter owner is either a valid non-zero
address or the literal string `"RESOLVE"`. Show a summary of the four collected values,
flagging any field still marked `"RESOLVE"`.
---
### Batch 2: Verification & Core Wrappers (4 questions)
**Question 1 — Verification deposit amount**
- Prompt: "How much of the underlying token should be deposited for verification?"
- Options: "1 wei (recommended)", Custom amount (via "Other")
- Note in the prompt: this deposit becomes the adapter's mintable headroom, there is no
withdraw function, and 1 wei is sufficient — see
[Parameter Reference](./references/parameter-reference.md#verificationdepositamount)
before choosing anything larger.
- "Recommended" describes the value if asked and picked — it is never a license to fill
in `"1"` on the user's behalf when this question was skipped. See
[Front-loaded or skip-ahead answers](#front-loaded-or-skip-ahead-answers) above.
- Store: `verificationDepositAmount`
**Question 2 — `PermissionedPositionManager` address**
- Prompt: "Do you have the `PermissionedPositionManager` address resolved for this
chain?"
- Options: "Not yet resolved — mark for resolution" (stores the literal string
`"RESOLVE"`), custom address (via "Other")
- Validation: address form, or the literal string `"RESOLVE"`
- Store: `allowedWrappers.permissionedPositionManager`
**Question 3 — Universal Router (v2.2+) address**
- Prompt: "Do you have the Universal Router `#v2.2` (or higher) address resolved for
this chain?"
- Options: "Not yet resolved — mark for resolution" (stores the literal string
`"RESOLVE"`), custom address (via "Other")
- Note in the prompt: the plain `UniversalRouter` deployments key is a different,
non-permissioned router — only the `#v2.2` deployment works here.
- Validation: address form, or the literal string `"RESOLVE"`
- Store: `allowedWrappers.universalRouterV2_2`
**Question 4 — `PermissionedHooks` address**
- Prompt: "Do you have the `PermissionedHooks` address resolved for this chain, for
`setAllowedHook`?"
- Options: "Not yet resolved — mark for resolution" (stores the literal string
`"RESOLVE"`), custom address (via "Other")
- Note in the prompt: this is the shared hook address, registered with
`setAllowedHook` on the position manager — it is never registered as a wrapper.
- Validation: address form, or the literal string `"RESOLVE"`
- Store: `hook`
**Validate:** confirm the deposit amount is a positive integer (not zero) expressed in
the token's smallest unit, and that every address field is either a valid address or the
literal string `"RESOLVE"` — never a guessed or invented value. Show a summary.
---
### Batch 3: Remaining Wrappers & Pool Currency (4 questions)
**Question 1 — `V4Quoter` address**
- Prompt: "Do you have the `V4Quoter` address resolved for this chain?"
- Options: "Not yet resolved — mark for resolution" (stores the literal string
`"RESOLVE"`), custom address (via "Other")
- Validation: address form, or the literal string `"RESOLVE"`
- Store: `allowedWrappers.v4Quoter`
**Question 2 — `MixedRouteQuoterV2` address**
- Prompt: "Do you have the `MixedRouteQuoterV2` address resolved for this chain?"
- Options: "Not yet resolved — mark for resolution" (stores the literal string
`"RESOLVE"`), custom address (via "Other")
- Validation: address form, or the literal string `"RESOLVE"`
- Store: `allowedWrappers.mixedRouteQuoterV2`
**Question 3 — Paired currency**
- Prompt: "What currency should the pool pair against the permissioned token?"
- Options: "Native ETH", custom ERC-20 address (via "Other")
- Note in the prompt: the permissioned token's side of the pool is always the adapter,
never the underlying token — this question is only about the _other_ currency.
- Validation: address form for an ERC-20 and not the zero address, or the literal string
`"native"` (the native-currency sentinel; see [Address Validation](./references/config-schema.md#address-validation)
for the full semantics). A user who supplies the zero address here means `"native"` —
record `"native"`, never the zero address.
- Store: `pool.pairedCurrency`
**Question 4 — Fee tier**
- Prompt: "What fee tier should the pool use?"
- Options: "0.05% (500)", "0.30% (3000)", "1.00% (10000)", Custom (via "Other")
- Validation: positive integer, hundredths of a basis point
- Store: `pool.feeTier`
**Validate:** confirm both remaining wrapper fields are a valid address or the literal
string `"RESOLVE"`, the paired currency is a valid non-zero address or the literal string
`"native"`, and the fee tier is a positive integer. When the paired currency is a resolved
address and `permissionedToken` is also a resolved address, confirm the two differ —
pairing the permissioned token against itself builds a `PoolKey` that either fails at the
PoolManager level or creates a pool nobody intended (see
[Parameter Reference](./references/parameter-reference.md#poolpairedcurrency)). Show a
summary of all four wrappers,
the hook, and the pool currency collected so far, and flag every field still marked
`"RESOLVE"`.
---
### Batch 4: Pricing & Seeding (3 questions)
**Question 1 — Tick spacing**
- Prompt: "What tick spacing should the pool use?"
- Options: "10 (stable-like pairs)", "60 (standard)", "200 (wide-range pairs)", Custom
(via "Other")
- Validation: positive integer
- Store: `pool.tickSpacing`
**Question 2 — Starting price**
- Prompt: "What should the pool's starting price be, expressed as paired-currency per
permissioned-token?"
- Options: "1:1 (equal value ratio)", Custom ratio (via "Other")
- Validation: positive number
- Store: `pool.startingPriceRatio` (a human-readable ratio; the deployer skill converts
this to `sqrtPriceX96` at initialization time, since that conversion depends on the
decimals of both currencies, which are not yet resolved at configuration time)
**Question 3 — Seeding intent**
- Prompt: "Do you plan to seed liquidity as part of this same setup?"
- Options, each mapped to the exact `seeding.intent` value it produces:
- "Yes, from the adapter owner's wallet" → `intent: "seed-now"`, `wallet` set to the
same address already collected as `adapterOwner` in Batch 1. **Offer this option
only when `adapterOwner` is a real address — not when it is the literal string
`"RESOLVE"`.** `seeding.wallet` must always be a real address (see Validation
below), so this option cannot copy forward an unresolved owner.
- "Yes, from a different wallet" (then ask for the address via "Other") →
`intent: "seed-now"`, `wallet` set to that address
- "Not yet — pool creation only" → `intent: "none"`, `wallet: null`
- If `adapterOwner` is `"RESOLVE"`, present only the second and third options — drop the
first option from the list entirely rather than showing it disabled.
- Note in the prompt: whichever wallet seeds liquidity, and the wallet that will receive
the position, both need `LIQUIDITY_ALLOWED` on the issuer's own allowlist before the
first mint — being the adapter owner grants nothing on the liquidity path.
- Validation: when `intent` is `"seed-now"`, `wallet` is required and must match the
address regex — never the literal string `"RESOLVE"` or any other sentinel; when
`intent` is `"none"`, `wallet` must be `null`. This flow never produces `"seed-later"`
— that enum value exists in the schema for a config edited or extended outside this
flow, not for output from these three options.
- Store: `seeding.intent`, `seeding.wallet`
**Validate:** confirm tick spacing is a positive integer, the starting price ratio is a
positive number, and — per the branch taken above — either `seeding.wallet` is a valid,
non-`"RESOLVE"` address (when `intent` is `"seed-now"`) or it is `null` (when `intent` is
`"none"`). `seeding.wallet` never accepts the literal string `"RESOLVE"`, even when it
was going to be copied from `adapterOwner` — see the option restriction above. Show the
full configuration collected across all four batches.
---
## Generate and Display the Configuration
Assemble everything collected into the JSON shape defined in
[`config-schema.md`](./references/config-schema.md), keyed by `chainId` at the top
level:
```json
{
"<chainId>": {
"permissionedToken": "...",
"allowlistChecker": { "mode": "...", "address": "..." },
"adapterOwner": "...",
"verificationDepositAmount": "...",
"allowedWrappers": {
"permissionedPositionManager": "...",
"universalRouterV2_2": "...",
"v4Quoter": "...",
"mixedRouteQuoterV2": "..."
},
"hook": "...",
"pool": {
"pairedCurrency": "native",
"feeTier": 3000,
"tickSpacing": 60,
"startingPriceRatio": 1
},
"seeding": { "intent": "seed-now", "wallet": "..." }
}
}
```
**Display this JSON directly in the transcript. Do not automatically create a file.**
Let the user copy it or choose a filepath in the next-steps question below.
## Display Summary
Immediately after the JSON, show a human-readable summary:
- Network and chain ID.
- Underlying token and allowlist-checker mode.
- Adapter owner.
- Verification deposit amount, with a one-line reminder that it is not recoverable.
- Every wrapper and the hook, each flagged **resolved** or **needs resolution**.
- Pool currency, fee tier, tick spacing, starting price ratio.
- Seeding intent and wallet, with a reminder that the wallet needs `LIQUIDITY_ALLOWED`.
- A validation checklist: which rules from `config-schema.md` passed, and which fields
still need a real address before this config can be used to deploy anything.
## Next Steps
Ask the user what they want to do:
- "Save to file" — ask for a filepath, default `permissioned-pool-config.json`. Require
a relative path ending in `.json`; reject an absolute path (leading `/`), reject a
leading `~` (a runtime that expands it can write outside the working directory —
including over Claude Code's own config store), and reject any path containing a `..`
segment. Resolve the path and confirm it still stays within the current working
directory before writing. If the resolved path already exists, ask for confirmation
before overwriting it — including a file that looks unrelated to this skill, such as
`package.json` or `tsconfig.json`. Re-prompt once on a rejected path rather than
silently substituting the default. Then write exactly the JSON object shown above.
(This validation is intentionally stricter than other skills' "save to file" steps in
this repo — the `~` and `..` rejections were added in response to a security audit
finding that a leading `~` could target `~/.claude.json`, so keep them here rather
than trimming to match a lighter-weight equivalent elsewhere.)
- "View the setup walkthrough" — point at `permissioned-pools-deployer`.
- "Modify configuration" — re-run the relevant batch above.
- "Exit" — end here; the user can copy the JSON from the transcript.
## Notes for Implementers of This Flow
- **Never fill in a token, wrapper, hook, or adapter-owner address from memory.** If the
user does not supply one, store the exact literal string `"RESOLVE"` in that field —
full stop. This includes not "helpfully" completing a partially-typed address.
- **Batch, validate, summarize — every time.** Do not run all four batches back to back
without the validation and summary steps in between; that is what keeps a bad input
from propagating into the next batch's defaults.
- **This skill does not read chain state, fetch an RPC, or consult a deployments JSON
file.** It has no `Bash` or `WebFetch` in its tool list, on purpose — resolving
addresses against `Uniswap/contracts` `deployments/json/<chainId>.json` and the deploy
guide's table is the user's job, or a later step in the deployer skill's preflight.