IL-Aware Limit Orders with Auto-Yield — a Uniswap V4 hook that turns every limit order into a yield-bearing DeFi position, and compensates LPs for impermanent loss from the yield earned.
Submitted to the UHI9 — Uniswap Hookathon · Deployed on Unichain
Demo Video (≤5 min): Watch on YouTube ↗
Live Frontend: il-aware-hook.vercel.app (Unichain mainnet)
- April 2026: LimitOrderHook v2 deployed on Base + Unichain mainnet (prior work, separate repo: github.com/impetus82/limit-order-hook-v4)
- May 17–19, 2026: ILAwareLimitOrderHook scaffolded as Hookathon preparation (pre-competition architecture work — IL rebate engine, SimulatedYieldVault, 46-test suite, Unichain mainnet deploy)
- May 25 – June 11, 2026: Active Hookathon development period (SimulatedYieldVault refinement, testing, demo, final submission)
No partner integrations. This project targets the Uniswap Impermanent Loss & Yield Systems theme directly, built on Uniswap V4 + OpenZeppelin primitives only (no third-party sponsor technology).
Traditional limit orders on-chain are capital-idle: your tokens sit in a contract earning nothing while you wait for the price to hit your target.
ILAwareLimitOrderHook changes this:
- Place a limit order — tokens are held in the hook
- Order executes automatically when a swap moves the pool price through your trigger level
- Deposit output to a yield vault (ERC-4626) — output tokens start earning yield immediately
- Claim with IL rebate — when you withdraw, the hook calculates how much impermanent loss your position suffered and rebates it from the accumulated yield
The result: you never leave money on the table. Every pending order earns yield, and executed orders get partial IL compensation — all without any external oracle.
User PoolManager ILAwareLimitOrderHook
| | |
|-- createLimitOrder() ------------------------------> mint ERC-721 NFT
| | | register trigger tick
| | |
|-- swap() ------------>| |
| |-- afterSwap() -------------> walk linked list
| | | execute eligible orders
| | | output held in hook
| | |
|-- depositToVault() --------------------------------> ERC-4626 deposit
| | | shares recorded in order
| | |
|-- claimOrder() -------------------------------------> redeem vault shares
| | | calculate IL rebate
| | | rebate = min(yield, IL)
| | | burn ERC-721 NFT
|<------ output + rebate ---------------------------|
Instead of scanning a fixed range of ticks on every swap (O(n) regardless of population), the hook maintains a sorted doubly-linked list of only active ticks — ticks that actually contain orders.
SENTINEL_MIN <-> tick(-500) <-> tick(0) <-> tick(300) <-> SENTINEL_MAX
- O(1) insertion at the correct sorted position
- O(1) removal when a tick's last order is filled or cancelled
- O(K) scan per swap where K = number of populated ticks crossed, not total tick range
This means a 10,000-tick price move costs the same as a 1-tick move if there are only 2 active ticks between them.
All token flows use Uniswap V4's native flash accounting (sync -> transfer -> settle -> take), ensuring:
- No ERC-20
transferFromoverhead during order execution - Atomic settlement within the PoolManager's unlock context
- Compatibility with V4's transient storage model (Cancun EVM)
_executeOrder returns bool success instead of reverting on slippage:
// Failed orders emit an event and stay in the bucket for next swap
if (!success) {
emit OrderExecutionFailed(orderId, "slippage");
continue; // don't revert the whole swap
}A single toxic order (extreme slippage) cannot block ALL swaps in the pool. This eliminates the critical DoS vector present in naive limit-order hook designs.
Gas metering prevents out-of-gas reverts when many orders queue up:
if (gasleft() < GAS_LIMIT_PER_ORDER) break; // resume next swapIL is estimated purely from sqrtPriceX96 delta — no Chainlink, no TWAP oracle needed:
sqrtR = sqrtPriceCurrent * 1e9 / sqrtPriceEntry
diff = |sqrtR - 1e9|
IL ~ size * diff^2 / (2 * 1e9^2) // size = the order's own output amount
This is a second-order Taylor approximation of the constant-product IL formula — no external feed, only the sqrtPriceX96 snapshots the hook already records (sqrtPriceBaseline at pool init, sqrtPriceAtFill on execution). The multiplier is the order's own output size, so ilAmount is a conservative rebate-sizing figure rather than a pool-wide LP-IL number — which is all it needs to be, since the rebate is hard-capped at the yield actually earned.
Every limit order is minted as an ERC-721 NFT at creation time (orderId == tokenId). This enables:
- Secondary market trading of pending orders (sell a 2000 USDC/WETH buy order at a discount if you need liquidity now)
- Access control without
creatorstorage —ownerOf(orderId)is always the canonical authority - Graceful cleanup —
_ownerOf(orderId) == address(0)detects burned (cancelled/claimed) orders without iterating
Short answer: nobody is made worse off.
When a limit order executes in a pool, the executing swap moves the price — that price impact is IL for existing LPs. The order creator also participates in this IL because their output tokens were worth more at the pre-swap price.
This hook addresses that loss with a two-sided mitigation:
| Actor | Without This Hook | With This Hook |
|---|---|---|
| Order creator | Receives output tokens, no yield while waiting | Output earns yield in ERC-4626 vault |
| Order creator after fill | Receives exactly amountOut, no IL recovery |
Receives amountOut + min(yield, IL) |
| LP providing liquidity | Earns fees, suffers full IL from limit executions | IL data available via lpPositions for external rebate programs |
The rebate formula rebate = min(yield, ilAmount) ensures:
- The creator can never receive more than their actual IL (no windfall)
- If
yield >= IL: creator is fully compensated, keeps any excess yield - If
yield < IL: creator keeps all yield and absorbs only the residual IL — net-positive versus an idle order whenever yield exceeds the small execution fee
Solvency is preserved by construction. The rebate is capped at min(yield, ilAmount), so the hook never pays out more than the yield it actually earned on tokens already owed to that user, and every order is isolated by orderId (no shared pot to drain). If the vault ever reverts on redeem, claimOrder degrades gracefully: it draws nothing from other orders' custody, leaves the position fully intact (NFT + vault shares), and lets the owner re-claim once the vault recovers — so a vault failure can neither make the hook insolvent nor trap a user's funds.
All 7 flags are enabled:
| Flag | Bit | Purpose |
|---|---|---|
afterInitialize |
12 | Record lastTick and sqrtPriceBaseline at pool creation |
afterAddLiquidity |
10 | Decode hookData to track real LP identity for IL |
beforeSwap |
7 | Passthrough (required for beforeSwapReturnDelta) |
afterSwap |
6 | Execute eligible orders, update lastTick |
beforeSwapReturnDelta |
3 | Future: dynamic fee hook pathway |
afterSwapReturnDelta |
2 | Enable precise output accounting |
afterAddLiquidityReturnDelta |
1 | Enable LP position recording |
// Place a limit order -- mints ERC-721 NFT to msg.sender
function createLimitOrder(
PoolKey calldata poolKey,
uint128 triggerPrice,
bool zeroForOne,
uint96 inputAmount
) external returns (uint256 orderId);
// Cancel active order -- burns NFT, refunds input tokens
function cancelOrder(uint256 orderId) external;
// After order fills: deposit output to ERC-4626 vault to earn yield
function depositToVault(uint256 orderId) external;
// Claim filled order output + optional IL rebate from yield
// Burns the ERC-721 NFT
function claimOrder(uint256 orderId, PoolKey calldata poolKey) external;# Install Foundry
curl -L https://foundry.paradigm.xyz | bash && foundryup
# Install dependencies
forge install
# Set environment variables
export DEPLOYER_PRIVATE_KEY=0x...
export VAULT_ASSET=0x4200000000000000000000000000000000000006 # WETH on Unichain# Unichain Mainnet
forge script script/DeployHookathon.s.sol:DeployHookathon \
--rpc-url https://mainnet.unichain.org \
--broadcast --verify -vvvv
# Unichain Testnet
forge script script/DeployHookathon.s.sol:DeployHookathon \
--rpc-url $UNICHAIN_TESTNET_RPC_URL \
--broadcast --verify -vvvvThe script automatically:
- Deploys
SimulatedYieldVault(ERC-4626 compatible, 3% APY viablock.timestamp) - Mines the CREATE2 salt via
HookMinerto satisfy all 7 permission flags - Deploys
ILAwareLimitOrderHookat the mined address
| Network | Contract | Address |
|---|---|---|
| Unichain Mainnet | ILAwareLimitOrderHook | 0x8C19f1641946c662308000bB4E2Eaf684c81d4CE |
| Unichain Mainnet | SimulatedYieldVault | 0xceee912C708516624E9aC5581c8FCC93eA8eE79d |
| Unichain Mainnet | USDC/WETH Pool | PoolId: 0xe1d695d4c147091549aeb6f9e78521a0184a1e7e272a71c12e708c881981f6ba |
Pool init tx: 0x3a082b9cb10f1c632502396116cf2b62280509f98d68e52a6db12cba6104f5a4
# Run all 53 tests
forge test -vvv
# Unit tests (pure functions, no deployment)
forge test --match-contract ILAwareLimitOrderHookTest -vvv
# Integration tests (full PoolManager + hook lifecycle)
forge test --match-contract ILAwareLimitOrderHookIntegrationTest -vvv
# Gas report
forge test --gas-reportTest coverage: 53 tests — 53 passing, 0 failing
Key scenarios covered:
test_AfterInitialize— baseline price recorded at pool creationtest_ILCalculation_PriceDoubled— IL approximation accuracytest_YieldRebate_OnClaim— end-to-end vault yield rebate flowtest_ERC721_Claim_After_Transfer— secondary market: new NFT owner claims filled ordertestGracefulExecutionOnSlippage— anti-DoS: failed orders do not block the pooltest_GracefulClaim_VaultReverts_JuneFix— vault redeem failure never traps funds; position preserved and re-claimabletestBatchExecution— multiple orders executed in a single swap
.
|-- src/
| |-- ILAwareLimitOrderHook.sol # Main hook contract
|-- script/
| |-- DeployHookathon.s.sol # One-shot Unichain deployment
| |-- HookMiner.sol # CREATE2 salt miner
| |-- AddLiquidityUnichain.s.sol
| |-- TriggerSwapUnichain.s.sol
| +-- RecoverPool.s.sol
|-- test/
| |-- ILAwareLimitOrderHook.t.sol # Unit tests
| +-- ILAwareLimitOrderHookIntegration.t.sol # Integration tests (45 tests)
+-- frontend/ # Next.js 16 + wagmi v2
+-- src/
|-- components/
| |-- OrderList.tsx # ERC-721 order UI with vault actions
| +-- CreateOrderForm.tsx
+-- config/
|-- abi.json # Auto-generated by forge build
+-- contracts.ts # Chain addresses, poolKey helpers
- Uniswap V4 — PoolManager, BaseHook, flash accounting
- Solidity 0.8.26 — via-IR optimizer, Cancun EVM (transient storage)
- OpenZeppelin — ERC-721, ERC-4626 (IERC4626), ReentrancyGuard, SafeERC20
- Foundry — forge build / test / script
- Next.js 16 + wagmi v2 + viem + RainbowKit — frontend
MIT
Built for UHI9 Uniswap Hookathon · May 2026