# v4 quote and trade

A Signal v4 pool is an ordinary Uniswap v4 pool with a hook, so there are two ways to trade it: through Signal's router, or through any router that follows the PoolManager's rules. Keep every amount in integer base units and every identity verified as in [discovery](v4-discovery.md).

## Direction

| Trade | Input | Output | `zeroForOne` |
| --- | --- | --- | --- |
| Buy | Pair asset | Token | `true` when the pair asset is `currency0`, that is `!terms.tokenIs0` |
| Sale | Token | Pair asset | `terms.tokenIs0` |

Swaps are **exact input only**. There is no exact-output path.

## Quote

**With Uniswap's Quoter.** Call `quoteExactInputSingle` on the Quoter in the [deployment record](v4-deployments.md) with the pool key, `zeroForOne`, the input amount and empty `hookData`, as an `eth_call`:

```solidity
struct QuoteExactSingleParams { PoolKey poolKey; bool zeroForOne; uint128 exactAmount; bytes hookData; }
function quoteExactInputSingle(QuoteExactSingleParams params)
    returns (uint256 amountOut, uint256 gasEstimate);
```

The Quoter runs the swap with the hook in it, so `amountOut` is already **net of every hook fee** in force at that block: pool fee, tax, opening fee, size and deviation extras. Checked on 12 October 2026 against a live 4.2 pool.

Two limits:

- The Quoter is not one of Signal's approved routers. While a pool's Signal-only window or wallet cap runs, a buy quote through it reverts with `Gated` or `WhaleCap`. The revert data wraps the hook's error; look for its selector.
- It models the pool, not the pair asset. If the pair asset keeps a cut of its own transfers, the amount that reaches a wallet is lower. See [below](#pair-assets-that-tax-their-own-transfers).

**By simulating the real call.** An `eth_call` of the router method below, from the real wallet, with the real value or allowance, returns the output the user would receive and passes every opening rule exactly as a transaction would. Use this for the number you show before a signature.

A quote of zero, or a revert, is not an executable quote. Say so instead of sending.

## Trade through Signal's router

Router: `0xcf9d5f4dc9860060db528bc9722cc3d828fc807c` for releases 2, 4.1 and 4.2. It holds nothing between calls, has no owner, and anyone may call it. It pays the same fee as every other caller.

```solidity
// Buy with native HYPE on a pool paired with WHYPE. msg.value is the input.
function buyNative(PoolKey key, uint256 minOut, address to, address refundTo, uint256 deadline)
    payable returns (uint256 out);

// Any exact-input swap in an ERC-20. The input is pulled from the caller.
function swap(PoolKey key, bool zeroForOne, uint256 amountIn, uint256 minOut,
              address to, bool unwrap, uint256 deadline)
    returns (uint256 out);
```

| Trade | Call | Approval |
| --- | --- | --- |
| Buy with HYPE, WHYPE-paired pool | `buyNative` with `msg.value = amountIn` | None |
| Buy with the pair asset | `swap` with `zeroForOne = !tokenIs0` | The **pair asset** to the router |
| Sell for the pair asset | `swap` with `zeroForOne = tokenIs0`, `unwrap = false` | The **token** to the router |
| Sell for native HYPE, WHYPE-paired pool | `swap` with `zeroForOne = tokenIs0`, `unwrap = true` | The **token** to the router |

- The approval is a plain ERC-20 allowance to the router, not a Permit2 permit. Approve the amount of this trade.
- `to` receives the output. On `buyNative`, `refundTo` receives any native input the pool did not use.
- `minOut` is enforced on what was received: below it the call reverts `TooLittleReceived(got, min)`. Compute it from a fresh quote and the user's own slippage; refuse a minimum that rounds to zero.
- `deadline` is a unix time; past it the call reverts `Expired()`.
- `buyNative` on a pool that is not paired with WHYPE reverts `NotNativePool()`. For another pair asset, obtain that asset by any route you trust, then `swap`.

The router tells the hook who the swap is for: `hookData = abi.encode(uint8(1), to, msg.sender, bytes32[] proof)`. The hook believes this only from routers its factory approved, and uses it only to judge the wallet cap and to pay a pot. It never moves funds from either address. Because the router is approved on all three releases, **a buy through it passes a Signal-only window and can satisfy a wallet cap**; a buy through another router cannot while those windows run.

`buyNativeListed` and `swapListed` take an allowlist proof as a last argument. The releases since 4.1 admit no launch with an allowlist, and any older allowlist window (24 hours at most) has ended, so the plain forms are the ones to use.

## Trade through another router

Outside a launch's opening rules the pools accept swaps from any router: the hook has no router allowlist for sales at any time, nor for buys once the windows have ended. A Universal Router `V4_SWAP` with `SWAP_EXACT_IN_SINGLE`, the pool key and empty `hookData` is the standard shape. Three reminders specific to these pools:

- exact input only;
- a buy stopped by a price limit reverts `PartialFill`, so leave the price limit open and protect the trade with the minimum output;
- inside a Signal-only window or a wallet cap the buy reverts. Read `terms.blocks` and `terms.launchTs` first.

Simulate your exact route from the real wallet before you offer it. These docs verify quotes through the Quoter; they do not certify any third-party router.

## Pair assets that tax their own transfers

Some admitted pair assets keep a cut of every transfer that has a contract on either side. Today that is **PSTR** (`0xb9cd945141f7d99fd562a6222accde5043350ad4`), which keeps 10%. A router that sends the amount it owes cannot settle such an asset, so these pools trade through a router Signal deployed for them.

How to recognise one: in [live configuration](https://signal.family/api/config) the pair asset's row in `v4_lane_v42.quotes[]` carries `tax_bps` and `router`; a token's record carries `router_kind: "taxed"`, `quote_tax_bps`, and that router as `router`.

Taxed-pair router: `0x2fca8081d6bff127413c91108561ff0ddc978b74`.

```solidity
// Native HYPE in: buys the pair asset on its own V2 pool, which pays the PoolManager; then the pool trades.
function buyNative(PoolKey key, Currency quote, uint256 minOut, address to, uint256 deadline)
    payable returns (uint256 received);

// Token in, native HYPE out, through the same two steps in reverse.
function sellNative(PoolKey key, Currency quote, uint256 amountIn, uint256 minOut,
                    address payable to, uint256 deadline) returns (uint256 sent);

// Pair asset in or out directly.
function swap(PoolKey key, bool zeroForOne, uint256 amountIn, uint256 minOut,
              address to, uint256 deadline) returns (uint256 received);
```

`quote` is the pair asset's address. The router pays first and swaps what arrived, and it judges `minOut` on what the recipient's balance actually gained, so the minimum already accounts for the asset's cut. A HYPE buyer pays the asset's cut once on the way in; a seller pays it once on the way out. The hook's own fee is unchanged and comes on top.

Quote these pools by simulating the taxed router's call. The Uniswap Quoter's answer leaves the asset's cut out.

## Before handing a request to a wallet

1. Chain 999; token, pool id, pool key and hook verified at a recent block.
2. The launch's windows read from `terms`: opening fee, Signal-only, wallet cap, per-block budget.
3. The live rate shown to the user, with the base it falls to if an opening is running.
4. The exact call simulated from the real wallet: target, calldata, value, allowance, recipient.
5. A positive minimum output from that simulation and the user's slippage. A deadline.
6. The allowance bounded to this asset and this amount, to the router you are calling and nothing else.

After submission, reconcile the transaction hash before any retry. A successful simulation does not guarantee the fill: the rate falls every second during an opening and other trades move the pool. The confirmed receipt establishes what happened.

## Gas

Measured on a fork of HyperEVM at the 4.2 release: a 1 HYPE buy used about 252,000 gas, a sale about 168,000, a flush about 254,000. A launch with every rule switched on can be heavier. Estimate your own call.

A flush must be sent with a much higher limit than it ends up using: each payout needs about 405,000 gas still available when it starts, or the flush reverts `PayoutStarved`.
