URC-5: Token Venue Policy

Opening community discussion on URC-5: Token Venue Policy, following its merge into the URCs repository.

Some tokens recommend particular liquidity pools or restrict trading to supported venues, often because their mechanics depend on a Uniswap v4 hook (INDEX, STANDARD, FWA, etc). Without a shared way to discover those policies, routers and frontends need token-specific integrations and may otherwise offer unsupported routes. Anecdotally, STANDARD and FWA prevented a lot of trading volume due to routers routing to restricted pools and required bespoke integrations for both.

URC-5 defines IVenuePolicy, a token-level interface for declaring canonical v4 pools, distinguishing Preferred and Enforced policies, and exposing directional permissions for other liquidity venues. It also specifies how routers, aggregators, and frontends should interpret those declarations. Transfer-time enforcement remains token-specific.

We’d appreciate input from hook developers, token teams, routers, aggregators, and frontend implementers.

Thanks for opening this up. Some feedback from a routing perspective, partly mine and partly from others on the routing team at Uniswap Labs. Most of it is about scope.

Enforcement belongs on chain

In Enforced mode, for every hop that crosses the token’s boundary, consumers MUST restrict v4 candidates to canonicalPools() and MUST NOT use another v4 pool, even if simulation succeeds

Off-chain consumer behavior can’t be enforced, so the policy should describe what happens on chain. Enforced should mean “these transfers revert” and nothing more. That removes the “even if simulation succeeds” clause and the SHOULD NOT for net-zero intermediate hops. If a route through a non-canonical pool simulates and executes, the token did not enforce anything, and a router has no reason to drop that route because a view function asked it to.

A token that declares Enforced without reverting should be non-conforming. The reference implementation currently declares immutable Enforced mode and leaves enforcement out on both sides, because the token cannot see the pool behind a PoolManager transfer. If the spec’s own example can’t enforce the mode it declares, that is the part to solve first. The reference implementation should show how a token actually refuses transfers into non-canonical pools.

Consumer rules should be non-binding guidance

In Preferred mode, consumers MUST include canonical pools in candidate discovery when those pools meet the consumer’s hook-support, execution, and safety requirements.

In Enforced mode, a consumer MUST NOT offer add-liquidity on a v4 pool unless it is in canonicalPools()

This should be an interface standard only. It can’t prescribe how routers route. Consumers have different objectives, and not every consumer can include every canonical pool in candidate discovery. Routers route on the best price for the user, and being canonical doesn’t let a pool skip any filter. The same applies to the add-liquidity MUST NOT in rule 5. A token can also have a misconfigured or false policy, so a consumer can never fully trust the declared values. The token’s responsibility is to publish accurate information as guidance and warnings. The consumer rules section should shrink to non-binding guidance, and consumer conformance should be dropped.

Withdrawal exit fees are out of scope

A frontend quotes an LP withdrawal from the canonical pool using a fixed slippage default. The hook charges an exit fee through return deltas, the received amount is lower than the position’s principal, and the position manager reverts on slippage.

Exit fees on withdrawals affect any hooked pool with an exit fee, whether or not the token has a venue policy. Rule 6 fits better in a separate URC about hook behavior. We suggest removing it from this URC.

Drop canonicalPools() and the ordering

function canonicalPools() external view returns (PoolKey[] memory);

Preference order doesn’t change routing, since routers pick the best price regardless of the order a token declares. A map like canonicalPool(PoolId) returns (bool) plus indexing is enough, since the Initialize event already gives indexers the full key. In Enforced mode it is identical to venueAllowed(poolManager, poolId, *), so it only adds information in Preferred mode.

Events should carry state

event CanonicalPoolsChanged();
event ExternalVenuesChanged();

Events should be explicit so the full state can be indexed from events alone. These two force consumers to re-read state, which is an antipattern. Events that carry the changed pool or flag and its new value would replace them.

Launch periods and discovery

Launchpads usually want enforcement only during the launch period and open LPing after it. The merged spec only partly covers this. A token can switch from Enforced to Preferred and emit VenueEnforcementChanged, but Preferred is still a policy. The token keeps reporting the interface through ERC-165, it still declares canonical pools, and under the merged rules consumers stay bound by the Preferred rules for as long as the token exists. A launchpad token whose launch has ended has no venue policy at all, and the spec has no way to say so.

My suggestion is to add an explicit off state. That can be a third mode (None or Disabled), or a separate event VenuePolicyEnabled(bool enabled) with a matching view. In the off state venueAllowed returns true for everything, no pool is canonical, and externalVenues() returns no restrictions. Consumers then treat the token like any other token. The spec is still in Discussion, so the note that enum values are fixed in this version should not block this.

Every change to the policy state must be made in a transaction that emits the event. The spec should forbid time-based transitions such as “Enforced until block N, then open”. If venueEnforcement() changes its return value without an event, an indexer that builds state from events keeps treating the token as Enforced after the launch has ended. The only way to catch that is to re-read every policy token every block. The token requirements should say that the values returned by the views MUST NOT change unless an event is emitted in the same transaction.

The state event should also be emitted in the constructor, or in the upgrade transaction for upgradeable tokens. Then indexers can discover policy tokens by scanning logs for the event topic, instead of calling supportsInterface on every token they see. Indexers must only count logs where the emitting address is the token itself. Otherwise any contract could emit the event and claim a policy for a token it doesn’t control, which is the same copycat concern the spec already raises for hooks.

Nit

error VenueNotAllowed(address venue, bytes32 poolId, bool inbound);

A file-level error is fine, but it should follow the Solidity style guide order, which puts file-level errors after events and before the interface.