Skip to content
LogoLogo

AsyncSwap

Git Source

Inherits: BaseHook, EIP712

Title: AsyncSwap

Author: @msakiart

Uniswap v4 hook that converts an exact-input swap into an open async order: the maker deposits their input via _beforeSwap. Crossing orders settle directly at the resting limit; fillers settle unmatched orders.

Makers can also act by EIP-712 signature (setExecutorWithSig, updatePriceWithSig) so a relayer pays gas and the wallet shows a readable message. See OrderSignatures for types.

Constants

feeVault

Holds fee claims separately from open-order escrow.

FeeVault public immutable feeVault;

numeraire

Chain-specific USDC address. Every pool must include this exact currency.

Currency public immutable numeraire;

router

The only swap caller the hook accepts, fixed at deployment. Only the Router settles a new order's input and hands it to matchNewOrder, so an order opened through any other caller would leave the transient pending-match id set for the rest of the transaction, and matchBook would skip that pool.

address public immutable router;

minOrder

Minimum order in numeraire base units (0: none), fixed at deployment for every pool so the owner cannot change the rules under live orders.

uint256 public immutable minOrder;

priorityFeeCap

$0.10 in numeraire base units, 10^(decimals - 1): the most a priority-bid floor can require.

uint64 public immutable priorityFeeCap;

assetPrecision

Decimal places of a whole asset token an order may trade, fixed at deployment: with 4, every order trades at least 0.0001 token on its asset side. 36 or more leaves only a 1-unit floor.

uint8 public immutable assetPrecision;

PENDING_NATIVE_DEPOSIT_SLOT

Transient slot — keccak256("asyncswap.hook.pendingNativeDeposit") - 1. Tracks ETH deposited to this contract within the current tx via receive(), so executeOrder can settle native output only out of the filler's just-received deposit rather than the contract's accumulated balance (which may include donations, stranded ETH, or in-flight native-input escrow). Cleared after each settle.

bytes32 constant PENDING_NATIVE_DEPOSIT_SLOT = 0x9c2a7e4f3b1d8a5c6e0f2d4b7a9c1e3d5f8b6a0c2e4d7b9f1a3c5e7d9b1f3a50;

PENDING_MATCH_ID_SLOT

Transient hand-off from _beforeSwap to the Router after it settles the new input.

bytes32 constant PENDING_MATCH_ID_SLOT = 0x247d5e35be7fbfd4d95fd66ea940bfb4ee942bef70db6afae46dc83ce0f89eb2;

PENDING_MATCH_EXECUTOR_SLOT

bytes32 constant PENDING_MATCH_EXECUTOR_SLOT = 0x9583e36f799bab8c4d06dc676d85ba05b4dfbde5934d5684d8e431d0fd3948da;

State Variables

asyncOrders

Per-pool order book.

mapping(PoolId poolId => AsyncFiller.State) public asyncOrders;

priceBooks

mapping(PoolId poolId => AsyncFiller.Book) private priceBooks;

owner

Protocol owner authorized to collect vault fees and sweep stranded balances.

address public owner;

pendingOwner

Address nominated by transferOwnership; becomes owner on acceptOwnership.

address public pendingOwner;

nonceUsed

Unordered nonces consumed by signed maker actions (setExecutorWithSig, updatePriceWithSig).

mapping(address maker => mapping(uint256 nonce => bool)) public nonceUsed;

Functions

onlyOwner

Restricts a call to the protocol owner.

modifier onlyOwner();

constructor

constructor(
        IPoolManager _poolManager,
        Currency _numeraire,
        address _owner,
        address _router,
        uint256 _minOrderUsd,
        uint8 _assetPrecision
    ) BaseHook(_poolManager) EIP712("AsyncSwap", "1");

Parameters

NameTypeDescription
_poolManagerIPoolManager
_numeraireCurrency
_owneraddressExplicit protocol owner. Not msg.sender: CREATE2 deployments run the
constructor from the deterministic deployer, which could never act as owner.
_routeraddressThe Router, deployed after the hook (it takes the hook in its constructor): pass its
predicted address.
_minOrderUsduint256Minimum order in whole numeraire units, e.g. dollars (0: none). The constructor scales it
and the $0.10 bid cap by the numeraire's own decimals(), so neither can be mis-scaled.
_assetPrecisionuint8Decimal places of a whole asset token an order may trade (4: at least 0.0001 token).

transferOwnership

Start a two-step ownership transfer. The deployer (or a factory) should call this after deployment to hand control to the intended fee recipient / governance address, which then calls acceptOwnership.

function transferOwnership(address newOwner) external onlyOwner;

Parameters

NameTypeDescription
newOwneraddress

acceptOwnership

Complete a two-step ownership transfer. Only the nominated pendingOwner may call.

function acceptOwnership() external;

sweep

Sweep an accumulated balance of currency held by this hook to to.

Between transactions the hook's raw balance holds only stranded funds or donations; fees belong to feeVault. Maker input is escrowed as ERC-6909 claims in the PoolManager, and the transient native-deposit ledger is cleared at the end of every tx, so no per-order escrow lives in the hook's balance across calls.

function sweep(Currency currency, address to) external onlyOwner;

Parameters

NameTypeDescription
currencyCurrency
toaddress

_beforeInitialize

Anyone may open a pool that pairs the numeraire with a dynamic fee. No order check reads the pool price, so the initializer's choice of price cannot block orders.

function _beforeInitialize(address, PoolKey calldata key, uint160) internal virtual override returns (bytes4);

Parameters

NameTypeDescription
_address
keyPoolKey
_uint160

Returns

NameTypeDescription
<none>bytes4

getHookPermissions

Returns a struct of permissions to signal which hook functions are to be implemented

Used at deployment to validate the address correctly represents the expected permissions

function getHookPermissions() public pure override returns (Hooks.Permissions memory);

Returns

NameTypeDescription
<none>Hooks.Permissions

_beforeAddLiquidity

function _beforeAddLiquidity(address, PoolKey calldata, ModifyLiquidityParams calldata, bytes calldata)
        internal
        pure
        override
        returns (bytes4);

Parameters

NameTypeDescription
_address
_PoolKey
_ModifyLiquidityParams
_bytes

Returns

NameTypeDescription
<none>bytes4

_minAssetAmount

10^(decimals - assetPrecision) base units of asset, at least 1. Native ETH has 18 decimals. A token without a readable decimals() gets the 1-unit floor, and reported decimals are capped at 36. An asset with no code reverts: a pool opened before its token is deployed would otherwise keep a 1-unit floor.

function _minAssetAmount(Currency asset) private view returns (uint256);

Parameters

NameTypeDescription
assetCurrency

Returns

NameTypeDescription
<none>uint256

asyncOrder

Read an order's currently-open input amount. Returns 0 if the order does not exist.

function asyncOrder(PoolId poolId, bytes32 orderId) external view returns (uint256 amountIn);

Parameters

NameTypeDescription
poolIdPoolId
orderIdbytes32

Returns

NameTypeDescription
amountInuint256

asyncOrderInfo

Read both legs of an open order. Returns (0, 0) if the order does not exist.

function asyncOrderInfo(PoolId poolId, bytes32 orderId)
        external
        view
        returns (uint256 amountIn, uint256 amountOutMin);

Parameters

NameTypeDescription
poolIdPoolId
orderIdbytes32

Returns

NameTypeDescription
amountInuint256
amountOutMinuint256

asyncOrderPriorityFee

The priority bid an order escrowed when it was created.

function asyncOrderPriorityFee(PoolId poolId, bytes32 orderId) external view returns (uint256);

Parameters

NameTypeDescription
poolIdPoolId
orderIdbytes32

Returns

NameTypeDescription
<none>uint256

minAssetAmount

The least asset amount an order on this pool may trade (0 if not initialized).

function minAssetAmount(PoolId poolId) external view returns (uint256);

Parameters

NameTypeDescription
poolIdPoolId

Returns

NameTypeDescription
<none>uint256

bestOrder

Return the best live limit order for a pool side. isBuy is true when the maker escrows the pool numeraire to buy the other asset. priceKey is a WAD-scaled raw-amount ratio for sorting within this pool, not a human-readable price. Equal-price orders queue by bid, then submission order.

function bestOrder(PoolId poolId, bool isBuy)
        external
        view
        returns (bool found, uint128 priceKey, bytes32 orderId, address maker);

Parameters

NameTypeDescription
poolIdPoolId
isBuybool

Returns

NameTypeDescription
foundbool
priceKeyuint128
orderIdbytes32
makeraddress

orderBookLevel

Return the first and last order under the pool's configured ordering mode, plus count.

function orderBookLevel(PoolId poolId, bool isBuy, uint128 priceKey)
        external
        view
        returns (bytes32 head, bytes32 tail, uint32 count);

Parameters

NameTypeDescription
poolIdPoolId
isBuybool
priceKeyuint128

Returns

NameTypeDescription
headbytes32
tailbytes32
countuint32

orderBookOrder

Read one live order: its terms, price key, queue position (sequence) and bid.

function orderBookOrder(PoolId poolId, bytes32 orderId)
        external
        view
        returns (
            address maker,
            uint256 amountIn,
            uint256 amountOutMin,
            uint128 priceKey,
            bool isBuy,
            uint64 sequence,
            uint64 priorityFee
        );

Parameters

NameTypeDescription
poolIdPoolId
orderIdbytes32

Returns

NameTypeDescription
makeraddress
amountInuint256
amountOutMinuint256
priceKeyuint128
isBuybool
sequenceuint64
priorityFeeuint64

orderCreatedSequence

Immutable incarnation of a live order, used to bind cancellation signatures.

function orderCreatedSequence(PoolId poolId, bytes32 id) external view returns (uint64);

Parameters

NameTypeDescription
poolIdPoolId
idbytes32

Returns

NameTypeDescription
<none>uint64

orderReprices

How many times the order has been repriced: a signed UpdatePrice commits to it.

function orderReprices(PoolId poolId, bytes32 id) external view returns (uint32);

Parameters

NameTypeDescription
poolIdPoolId
idbytes32

Returns

NameTypeDescription
<none>uint32

isExecutor

function isExecutor(PoolId poolId, address maker, address executor) external view returns (bool);

Parameters

NameTypeDescription
poolIdPoolId
makeraddress
executoraddress

Returns

NameTypeDescription
<none>bool

calculateHookFee

function calculateHookFee(uint256) public pure returns (uint256);

Parameters

NameTypeDescription
_uint256

Returns

NameTypeDescription
<none>uint256

calculatePoolFee

Surcharge a filler pays on top of the maker's minimum: none. The 0.02% trade fee comes out of what each side receives instead (AsyncFiller.tradeFee).

function calculatePoolFee(uint24, uint256) public pure virtual returns (uint256);

Parameters

NameTypeDescription
_uint24
_uint256

Returns

NameTypeDescription
<none>uint256

executeOrder

Fill order in full. Must be called from an authorized executor (router) that has already paid the filler's gross output within the same unlock: for ERC20 output via PoolManager.settleFor(hook), for native output by sending ETH to this hook.

function executeOrder(AsyncOrder calldata order, bytes calldata data) external;

Parameters

NameTypeDescription
orderAsyncOrder
databytesabi.encode(filler) — the address that should receive the maker's input.

matchNewOrder

Match the order just created by the current Router unlock callback.

The Router calls this only after settling the maker's input. The transient hand-off prevents stale order ids or a different executor from triggering a match.

function matchNewOrder(AsyncOrder calldata order) external returns (uint8 matched);

Parameters

NameTypeDescription
orderAsyncOrder

Returns

NameTypeDescription
matcheduint8

tryFillOrderFromPoolLiquidity

Permissionless executor entry for retrying a resting order against current pool LPs.

Must run inside PoolManager.unlock. Router calls this after validating the request path.

function tryFillOrderFromPoolLiquidity(AsyncOrder calldata order) external returns (bool filled);

Parameters

NameTypeDescription
orderAsyncOrder

Returns

NameTypeDescription
filledbool

fillOrderFromPoolLiquidity

The self-call boundary makes a failed AMM attempt atomic while preserving the resting order. PoolManager skips hook callbacks when the hook itself is the swap caller, so this swap does not create an order. sqrtPriceLimitX96 is a price the swap may not cross (0 for none). Without one the whole order fills or the call reverts; with one the pool may fill part and leave the rest on the book.

function fillOrderFromPoolLiquidity(AsyncOrder calldata order, bytes32 id, uint160 sqrtPriceLimitX96) external;

Parameters

NameTypeDescription
orderAsyncOrder
idbytes32
sqrtPriceLimitX96uint160

matchBook

Permissionlessly continue a crossed book inside a PoolManager unlock.

The Router exposes the unlock wrapper; each call processes at most eight pairs.

function matchBook(PoolKey calldata key, uint8 maxMatches) external returns (uint8 matched);

Parameters

NameTypeDescription
keyPoolKey
maxMatchesuint8

Returns

NameTypeDescription
matcheduint8

updatePrice

Maker updates the limit price of an unfilled order. Direct call from the maker — pure state mutation, no PM interaction, so no unlock or router needed.

function updatePrice(AsyncOrder calldata order, uint256 newAmountOutMin) external;

Parameters

NameTypeDescription
orderAsyncOrder
newAmountOutMinuint256

topUpPriorityFee

Increase an existing bid through the maker-authenticating Router inside an unlock.

The Router must settle the additional numeraire debt before the unlock ends.

function topUpPriorityFee(AsyncOrder calldata order, uint256 addedFee) external;

Parameters

NameTypeDescription
orderAsyncOrder
addedFeeuint256

cancelOrder

Maker cancels an unfilled order. Must be called via an authorized executor inside an IPoolManager.unlock context. The executor must have asserted maker identity.

The trusted Router needs no grant: it cancels only for order.owner or with the owner's signature, and a cancel only returns input to the owner, so revoking the Router never traps a maker's orders.

function cancelOrder(AsyncOrder calldata order) external;

Parameters

NameTypeDescription
orderAsyncOrder

_beforeSwap

function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata hookParams)
        internal
        override
        returns (bytes4, BeforeSwapDelta, uint24);

Parameters

NameTypeDescription
senderaddress
keyPoolKey
paramsSwapParams
hookParamsbytes

Returns

NameTypeDescription
<none>bytes4
<none>BeforeSwapDelta
<none>uint24

_createOrder

Extracted to keep _beforeSwap stack-shallow.

function _createOrder(PoolId poolId, bool zeroForOne, uint256 amountIn, UserParams memory hookData, bool isBuy)
        private;

Parameters

NameTypeDescription
poolIdPoolId
zeroForOnebool
amountInuint256
hookDataUserParams
isBuybool

setExecutor

Maker explicitly authorises an executor (typically the Router) to submit and fill orders on their behalf. Without this grant, _createOrder reverts on the maker's first swap. Pass allow = false to revoke an executor.

function setExecutor(PoolId poolId, address executor, bool allow) external;

Parameters

NameTypeDescription
poolIdPoolId
executoraddress
allowbool

setExecutorWithSig

Signed setExecutor: anyone may submit the maker's EIP-712 SetExecutor grant. Lets a relayer authorize the Router before the maker's first gasless order on a pool.

function setExecutorWithSig(
        PoolKey calldata key,
        address maker,
        address executor,
        bool allow,
        uint256 nonce,
        uint256 deadline,
        bytes calldata signature
    ) external;

Parameters

NameTypeDescription
keyPoolKey
makeraddress
executoraddress
allowbool
nonceuint256
deadlineuint256
signaturebytes

updatePriceWithSig

Signed updatePrice: anyone may submit the maker's EIP-712 UpdatePrice message.

function updatePriceWithSig(
        AsyncOrder calldata order,
        uint256 amountIn,
        uint256 newAmountOutMin,
        uint256 nonce,
        uint256 deadline,
        bytes calldata signature
    ) external;

Parameters

NameTypeDescription
orderAsyncOrder
amountInuint256The order's input the maker signed against. If matches have since shrunk the order, the
signed price applies to the remainder: its minimum is newAmountOutMin scaled down, rounded up.
newAmountOutMinuint256
nonceuint256
deadlineuint256
signaturebytes

_verifyReprice

Verifies a signed UpdatePrice against the order's creation sequence and reprice count, and returns its minimum for the order's live input: orders only shrink, so a live input above the signed one means the signature was for another order, and a smaller one gets the signed price, rounded up.

function _verifyReprice(
        AsyncOrder calldata order,
        bytes32 id,
        uint256 amountIn,
        uint256 newAmountOutMin,
        uint256 nonce,
        uint256 deadline,
        bytes calldata signature
    ) private returns (uint256);

Parameters

NameTypeDescription
orderAsyncOrder
idbytes32
amountInuint256
newAmountOutMinuint256
nonceuint256
deadlineuint256
signaturebytes

Returns

NameTypeDescription
<none>uint256

invalidateNonce

Revoke a signed action before a relayer submits it.

function invalidateNonce(uint256 nonce) external;

Parameters

NameTypeDescription
nonceuint256

useNonce

Burns maker's nonce for a signature the Router verified itself (cancelWithSig). Every maker signature shares this one nonce space, so invalidateNonce revokes those signatures too, and only the trusted Router may burn one: any other executor could void the maker's signed actions on every pool. The Router needs no grant here, so a maker who revoked it can still cancel by signature.

function useNonce(address maker, uint256 nonce) external;

Parameters

NameTypeDescription
makeraddress
nonceuint256

DOMAIN_SEPARATOR

EIP-712 domain separator for this hook's signed actions.

function DOMAIN_SEPARATOR() external view returns (bytes32);

Returns

NameTypeDescription
<none>bytes32

_verify

Checks deadline, burns the nonce, then validates an EOA or ERC-1271 signature.

function _verify(address maker, bytes32 structHash, uint256 nonce, uint256 deadline, bytes calldata signature)
        private;

Parameters

NameTypeDescription
makeraddress
structHashbytes32
nonceuint256
deadlineuint256
signaturebytes

_useNonce

function _useNonce(address maker, uint256 nonce) private;

Parameters

NameTypeDescription
makeraddress
nonceuint256

receive

Accept ETH transfers. The Router (or any caller paying for a native-output fill) forwards msg.value to the hook before triggering executeOrder. The hook records the deposit on a transient slot so executeOrder settles only against the filler's same-transaction deposit. ETH sent without a follow-up executeOrder stays in the hook balance and is recoverable only through sweep.

receive() external payable;

Events

HookSwap

Pool-level swap-event mirror; bytes32 poolId for indexer compatibility.

See https://github.com/OpenZeppelin/uniswap-hooks BaseAsyncSwap for the source pattern.

event HookSwap(
        bytes32 indexed id,
        address indexed sender,
        int128 amount0,
        int128 amount1,
        uint128 hookLPfeeAmount0,
        uint128 hookLPfeeAmount1
    );

Parameters

NameTypeDescription
idbytes32
senderaddress
amount0int128
amount1int128
hookLPfeeAmount0uint128
hookLPfeeAmount1uint128

OwnershipTransferred

Emitted when ownership is transferred.

event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

Parameters

NameTypeDescription
previousOwneraddress
newOwneraddress

OwnershipTransferStarted

Emitted when the owner nominates a new owner.

event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner);

Parameters

NameTypeDescription
previousOwneraddress
newOwneraddress

Swept

Emitted when the owner sweeps an accumulated balance.

event Swept(Currency indexed currency, address indexed to, uint256 amount);

Parameters

NameTypeDescription
currencyCurrency
toaddress
amountuint256

NonceInvalidated

Emitted when a maker burns a nonce, consumed or revoked.

event NonceInvalidated(address indexed maker, uint256 nonce);

Parameters

NameTypeDescription
makeraddress
nonceuint256

ExecutorSet

Emitted after each successful direct or signed executor authorization update.

allow is the resulting permission, including when the value is unchanged.

event ExecutorSet(PoolId indexed poolId, address indexed maker, address indexed executor, bool allow);

Parameters

NameTypeDescription
poolIdPoolId
makeraddress
executoraddress
allowbool

AsyncOrdersMatched

Pairwise direct escrow match; emitted by the linked matcher at the hook address.

event AsyncOrdersMatched(
        PoolId indexed poolId,
        bytes32 indexed sellOrderId,
        bytes32 indexed buyOrderId,
        uint256 assetAmount,
        uint256 numeraireAmount
    );

Parameters

NameTypeDescription
poolIdPoolId
sellOrderIdbytes32
buyOrderIdbytes32
assetAmountuint256
numeraireAmountuint256

AsyncOrderLiquidityFilled

An open order was fully executed against active Uniswap v4 pool liquidity.

event AsyncOrderLiquidityFilled(
        PoolId indexed poolId, bytes32 indexed orderId, address indexed maker, uint256 amountIn, uint256 amountOut
    );

Parameters

NameTypeDescription
poolIdPoolId
orderIdbytes32
makeraddress
amountInuint256
amountOutuint256

DynamicFeeCharged

Mirror of DynamicFeeOrdering.DynamicFeeCharged, emitted by the linked library in this contract's context, so the hook's ABI decodes it.

event DynamicFeeCharged(
        PoolId indexed poolId,
        bytes32 indexed orderId,
        Currency indexed currency,
        uint256 toLps,
        uint256 toFeeVault,
        int24 refTick
    );

Parameters

NameTypeDescription
poolIdPoolId
orderIdbytes32
currencyCurrency
toLpsuint256
toFeeVaultuint256
refTickint24

PoolPriceObserved

Mirror of DynamicFeeOrdering.PoolPriceObserved, emitted by the linked library in this contract's context so indexers can reconstruct completed pool-fill price movements.

event PoolPriceObserved(
        PoolId indexed poolId,
        bytes32 indexed orderId,
        int24 tickBefore,
        int24 tickAfter,
        int24 refTick,
        uint64 observedBlock
    );

Parameters

NameTypeDescription
poolIdPoolId
orderIdbytes32
tickBeforeint24
tickAfterint24
refTickint24
observedBlockuint64

AsyncOrderPriorityFeeUpdated

event AsyncOrderPriorityFeeUpdated(
        PoolId indexed poolId, bytes32 indexed orderId, uint256 addedFee, uint256 newPriorityFee
    );

Parameters

NameTypeDescription
poolIdPoolId
orderIdbytes32
addedFeeuint256
newPriorityFeeuint256

Errors

ExactInputOnly

error ExactInputOnly();

UnaccountedNativeOutput

error UnaccountedNativeOutput();

NotOwner

error NotOwner();

ZeroAddress

error ZeroAddress();

UnsupportedNumeraireDecimals

error UnsupportedNumeraireDecimals();

AssetHasNoCode

error AssetHasNoCode();

SignatureExpired

error SignatureExpired();

InvalidSignature

error InvalidSignature();

NonceAlreadyUsed

error NonceAlreadyUsed();

InvalidNumeraire

error InvalidNumeraire();

NumeraireRequired

error NumeraireRequired();

DynamicFeeRequired

error DynamicFeeRequired();

NotSelf

error NotSelf();

NotTrustedRouter

error NotTrustedRouter();

CannotRevokeTrustedRouter

Revoking the Router would strand the maker's live orders: Router.cancelOrder, cancelWithSig, fillOrder and fillFromPoolLiquidity all need the grant, and an EOA can't cancel from its own unlock. So the trusted Router can never be revoked; any other executor can.

error CannotRevokeTrustedRouter();

Structs

UserParams

Hook params decoded from the hookData of IPoolManager.swap. The maker sets amountOutMin (their limit price); executor is the address authorized to call fill. Salt is contract-assigned at create time (see _createOrder) and emitted in AsyncOrderCreated — the maker fetches it from the event log to reference the order in cancel / updatePrice later.

struct UserParams {
        address user;
        address executor;
        uint256 amountOutMin;
        uint256 priorityFee;
    }

Properties

NameTypeDescription
useraddress
executoraddress
amountOutMinuint256
priorityFeeuint256