LimitOrderReactor plus Permit2, vendored byte for byte
at tag v2.1.0 and checked against upstream in CI. Panofx adds three small contracts. None of them holds
user funds between transactions.
| Contract | Origin | What it does | Holds funds? |
|---|---|---|---|
LimitOrderReactor | Uniswap, vendored at v2.1.0, deployed by Panofx | Decodes a signed limit order, injects protocol fees, validates, pulls the swapper’s input via Permit2 to the filler, hands control to the filler’s callback, pulls the filler’s outputs to their recipients, emits Fill. The owner can only set the fee controller | Never; tokens pass through inside one call |
Permit2 | Uniswap, canonical, at 0x000000000022D473030F116dDEE9F6B43aC78BA3 on every EVM chain | Moves a token holder’s tokens when shown a valid signature; enforces one-time nonces and deadlines; accepts contract signers via EIP-1271. Used twice per fill: once for the resolver’s input, once for the taker’s | Never |
PanofxValidation | Panofx, about 40 lines | Every offer names it. Reverts unless the filler is the executor and the offer’s deadline has not passed, and carries the network pause behind the owner | Never |
PanofxFeeController | Panofx, about 80 lines | Returns the protocol fee output: bps (at most 5, the reactor’s hard cap) of the corridor’s fee token to the treasury. Fee token and bps are set per corridor | Never |
PanofxExecutor | Panofx, about 200 lines | The filler for every fill. Verifies the taker’s TakerAuth covers exactly these offers (execute), or for a limit order that every offer pays in the buy token the LimitAuth names and nets at least its minimum (executeLimit); pulls the taker’s sell token via Permit2 inside the reactor callback, delivers proceeds net of fee and gas, pays the submitter the capped gas fee, and leaves itself empty. Anyone may call it | Never; zero balance after every call, enforced by test |
Who signs what
| Party | Signs | Purpose | Lifetime |
|---|---|---|---|
| Resolver node | EIP-712 PermitWitnessTransferFrom with the LimitOrder as witness, under Permit2’s domain | The binding offer | At most offer_ttl_seconds, one nonce |
| Taker (sponsored) | EIP-712 PermitWitnessTransferFrom with a TakerAuth witness: offers hash, recipient, minimum out, maximum gas fee, fee token | Authorizes exactly this settlement and nothing else; no transaction, no gas | The quote’s 20 seconds, one nonce |
| Taker (limit order) | The same Permit2 message with a LimitAuth witness: buy token, recipient, minimum out, maximum gas fee, fee token; no offers | Authorizes whichever offers deliver the minimum in the named token before the deadline. Permit2 checks it under its own witness type, so it can never be submitted as a swap | 5 minutes to 30 days, one nonce; cancelled on the relay by a signed message, or revoked in Permit2 |
| Taker (self-execute) | An ordinary transaction calling PanofxExecutor.execute with a gas fee of zero | Same settlement, taker pays gas | Until mined |
| Keeper | The submitting transaction | Pays gas; receives the capped gas fee in the fee token | Until mined |
| Keyless taker (API) | An EIP-191 taker proof | Proves they own the wallet the RFQ names | 24 hours |
| Multisig | Admin transactions only | Set fee token and bps per corridor, pause, set fee controller |
Facts from the source that constrain the design
Read directly from UniswapX v2.1.0 and Permit2 on 11 September 2026.| Fact | Where | Consequence |
|---|---|---|
MAX_FEE_BPS = 5; each fee output is checked against 5 bps of that token’s total in the order | ProtocolFees.sol | 5 bps is the hard ceiling |
| A fee token must appear in the order as input or output, and fees may not be taken on both in one order | ProtocolFees.sol | The fee is taken in the dollar token only, whichever side it is on |
Outputs are pulled from msg.sender with safeTransferFrom | CurrencyLibrary.transferFill | The filler is whoever calls execute; the executor approves the reactor for the outputs it is about to pull |
Input tokens are pulled via permitWitnessTransferFrom with the order hash as witness and the reactor as spender | LimitOrderReactor._transferInputTokens | The resolver approves Permit2 once; the offer signature commits to the exact order |
Permit2 calls isValidSignature when the claimed signer has code | SignatureVerification.verify | A contract can be the swapper, which is what a future operator vault needs |
| Permit2 nonces are an unordered bitmap; each nonce is one bit in a 256-bit word | SignatureTransfer._useUnorderedNonce | Sequential nonces per node pack one storage word; random nonces cost a fresh word each time |
ExclusiveFillerValidation reverts if lastExclusiveTimestamp >= block.timestamp and the filler differs | ExclusiveFillerValidation.sol | The timestamp is set to the deadline: exclusive for the offer’s whole life |
executeWithCallback hands control to the filler after inputs are pulled and before outputs are pulled | BaseReactor.executeWithCallback | The hook the executor uses to pull the taker’s leg and pay proceeds inside the same atomic call |
USDC on Base implements EIP-2612 permit; cNGN on Base does not, but implements ERC-2771 | Bytecode and eth_call, 11 Sep 2026 | USDC approvals are gasless in-transaction; first-time cNGN approvals need one transaction |
| Solidity 0.8.24, optimizer runs 1,000,000 | foundry.toml | The same toolchain is pinned so bytecode matches upstream |

