Perpetual Futures DEX built entirely as a Uniswap v4 Hook on Unichain.
There is no native on-chain perpetual futures DEX on Unichain. LPs providing liquidity to Uniswap v4 pools have no way to hedge their impermanent loss, and traders looking for leveraged exposure must bridge funds to other chains. Meanwhile, Unichain's 200ms Flashblocks provide uniquely fast settlement — an ideal environment for derivatives — but no protocol takes advantage of this.
"Perp DEXs built with hooks" — one of Unichain's most-requested application types.
PerpHinge is a perpetual futures DEX built entirely as a Uniswap v4 Hook, leveraging the existing pool infrastructure for mark price discovery. Traders open long/short positions with up to 10x leverage, backed by USDC margin. The hook intercepts afterSwap callbacks to continuously update a TWAP oracle and settle funding rates — no separate keeper infrastructure needed for core operations.
- 200ms Flashblocks = more TWAP data points per minute = more accurate mark price
- Low gas fees = viable for frequent position management and funding settlement
- Native Uniswap v4 = deep hook integration with the DEX's own liquidity pools
- Update-on-demand model = traders submit fresh price data with each transaction
- Real-time index price = sub-second price feeds for accurate PnL calculations
- Staleness protection = contract enforces 5-minute max age on oracle data, with graceful fallback so stale prices never brick the pool
Unichain (L2) — 200ms Flashblocks
┌──────────────────────────────────────────────────────────────┐
│ │
│ Trader ──┬── openPosition(key, isLong, margin, lev, pyth) │
│ │ → USDC margin transferred to Hook │
│ │ → Pyth index price updated on-chain │
│ │ → Position recorded with entry price │
│ │ │
│ ├── closePosition(key, pyth) │
│ │ → Funding settled, PnL computed, payout │
│ │ │
│ └── liquidate(key, trader, pyth) ← anyone │
│ → 2.5% reward + insurance fund │
│ │
│ Uniswap v4 PoolManager │
│ └── afterSwap() → PerpHingeHook │
│ → TWAP observation written (180-slot ring buffer) │
│ → Funding rate settled if interval elapsed │
│ │
│ Pyth Network Oracle │
│ → Index price for PnL, entry/exit, liquidation checks │
│ → Graceful fallback on stale data (never bricks swaps) │
│ │
└──────────────────────────────────────────────────────────────┘
Frontend (Next.js + wagmi + RainbowKit)
→ Real-time Pyth price via Hermes API
→ Live P&L, position health, funding rate display
→ Keyboard shortcuts [L]ong / [S]hort / [Enter] submit
| Feature | Description |
|---|---|
| Leveraged Trading | 1x–10x leverage on ETH/USD perpetual futures |
| Pyth Oracle | Real-time index prices via Pyth Network (5-min staleness check) |
| TWAP Mark Price | 30-minute TWAP from Uniswap v4 pool observations (180-slot ring buffer) |
| Funding Rate | Hourly funding payments between longs and shorts based on mark–index spread |
| Liquidations | Anyone can call liquidate() and earn 2.5% of notional as reward |
| Automated Liquidations | Optional Reactive Network RSC for cross-chain auto-liquidation callbacks |
| Insurance Fund | Absorbs remaining margin from liquidated positions to cover bad debt |
| Open Interest Caps | $1M per-side OI cap to manage protocol risk |
| Contract | Address | Verified |
|---|---|---|
| PerpHingeHook | 0x187cf339772ecD2c572AfbD2631b76e68D7db4ED |
Blockscout |
| PerpHingeCallback | 0x2c68f7504dD832f9DdBAB88b4c16E493A924647E |
— |
| PoolManager | 0x00B036B58a818B1BC34d502D3fE730Db729e62AC |
— |
| Pyth Oracle | 0x2880aB155794e7179c9eE2e38200202908C17B43 |
— |
| tUSDC (Test USDC) | 0xDFe9b0627e0ec2b653FaDe125421cc32575631FC |
— |
| WETH | 0x4200000000000000000000000000000000000006 |
— |
| Contract | Address | Explorer |
|---|---|---|
| PerpHingeReactive (RSC) | 0x95EAb385c669aca31C0d406c270d6EdDFED0D1ee |
ReactScan |
| RVM Dashboard | 0x3C343AD077983371b29fee386bdBC8a92E934C51 |
ReactScan |
- 5 event subscriptions registered (PositionOpened, PositionClosed, Liquidated, AutoLiquidated, Cron100)
- 55+ RVM transactions processed (cron-based position monitoring)
- Cross-chain event delivery from Unichain Sepolia is pending Reactive Network indexer support for chain 1301
The hook address encodes permission bits: AFTER_INITIALIZE | BEFORE_SWAP | AFTER_SWAP. Address is mined via CREATE2 salt search against Forge's deterministic deployer (0x4e59b44847b379578588920cA78FbF26c0B4956C).
- Connect wallet — Switch to Unichain Sepolia (chain ID 1301)
- Get testnet ETH — Use the Unichain Sepolia faucet
- Mint tUSDC — Click "Mint tUSDC" in the trading panel (1,000 tUSDC per mint)
- Open a position — Choose Long or Short, set margin and leverage (1–10x), submit
- Watch your P&L — Real-time unrealized PnL updates via Pyth price feed
- Close position — Click "Close Position" to settle and receive your payout
- Check the leaderboard — View your trade on the
/leaderboardpage
src/
PerpHingeHook.sol # Main hook (IHooks + perp trading engine)
PerpHingeCallback.sol # Unichain callback for Reactive Network liquidations
PerpHingeReactive.sol # RSC on Reactive Network for position monitoring
interfaces/
IPerpHinge.sol # Position/PoolPerp structs, external interface
IPyth.sol # Pyth Network oracle interface
libraries/
PerpMath.sol # PnL, margin, leverage, liquidation fee math
FundingMath.sol # Funding rate calculations (mark vs index)
TWAPOracle.sol # TWAP observation ring buffer (180 slots)
script/
00_DeployHook.s.sol # Deploy hook with CREATE2 salt mining
01_DeployReactive.s.sol # Deploy callback + RSC (two-phase, optional)
02_OpenPosition.s.sol # Demo: open position and check PnL
03_DeployTestUSDC.s.sol # Deploy test USDC, mint, and enable pool
04_InitializePool.s.sol # Initialize pool, add liquidity, trigger first swap
test/
PerpHingeHook.t.sol # 33 tests (unit + integration + fuzz)
mocks/
MockPyth.sol # Mock Pyth oracle with staleness checks
MockERC20.sol # Configurable-decimal ERC20 for testing
frontend/
src/
app/page.tsx # Trading interface
app/leaderboard/page.tsx # Top PnL traders
components/ # TradingPanel, PriceChart, PositionPanel, etc.
hooks/ # usePythPrice, usePerpHinge, usePosition
lib/ # Contract ABIs, addresses, Pyth helpers
forge buildforge test --match-path test/PerpHingeHook.t.sol -vvforge test --match-path test/PerpHingeHook.t.sol --gas-report| Function | Gas (approx) |
|---|---|
openPosition (long) |
~195,600 |
openPosition (short) |
~210,900 |
closePosition |
~172,700 |
liquidate |
~196,500 |
afterSwap (TWAP update) |
~239,000 |
| Full lifecycle (open → fund → close) | ~373,300 |
cd frontend
npm install
npm run dev# Set up .env with PRIVATE_KEY, USDC, WETH, REACTIVE_CALLBACK
source .env
# 1. Deploy hook (auto-detects Sepolia vs Mainnet via chain ID)
forge script script/00_DeployHook.s.sol --rpc-url https://sepolia.unichain.org --broadcast
# → Note the HOOK_ADDRESS from output, update .env
# 2. Deploy test USDC, mint tokens, enable pool on the hook
HOOK_ADDRESS=<hook_from_step_1> \
forge script script/03_DeployTestUSDC.s.sol --rpc-url https://sepolia.unichain.org --broadcast
# → Note the tUSDC address from output, update .env
# 3. Initialize pool on PoolManager, add liquidity, trigger first swap
HOOK_ADDRESS=<hook> USDC=<tusdc> \
forge script script/04_InitializePool.s.sol --rpc-url https://sepolia.unichain.org --broadcast
# 4. (Optional) Verify on Blockscout
forge verify-contract <HOOK_ADDRESS> src/PerpHingeHook.sol:PerpHingeHook \
--chain-id 1301 \
--constructor-args $(cast abi-encode "constructor(address,address,address,address)" \
0x00B036B58a818B1BC34d502D3fE730Db729e62AC \
0x2880aB155794e7179c9eE2e38200202908C17B43 \
$REACTIVE_CALLBACK \
$(cast wallet address $PRIVATE_KEY)) \
--verifier blockscout \
--verifier-url https://unichain-sepolia.blockscout.com/api/- Trader calls
openPosition(key, isLong, margin, leverage, pythUpdateData)with ETH for Pyth fee - Pyth price feed is updated on-chain (update-on-demand model)
- Funding is settled for the pool before recording the new position
- USDC margin is transferred from trader to hook via
safeTransferFrom - Position is recorded with entry price from Pyth, cumulative funding index snapshot
- Open interest is increased for the corresponding side (long/short)
- Trader calls
closePosition(key, pythUpdateData) - Funding is settled for the trader's position (margin adjusted up/down)
- PnL is calculated:
notional * (exitPrice - entryPrice) / entryPrice(negated for shorts) - Payout = margin + PnL (clamped to zero — trader can't lose more than margin)
- Open interest is decreased, position is deleted, payout transferred
A position is liquidatable when effective margin drops below maintenance margin:
effective_margin = margin + unrealized_pnl - funding_owed
maintenance_margin = notional * 5% (500 BPS)
liquidatable when: effective_margin < maintenance_margin
Two liquidation paths:
- Manual: Anyone calls
liquidate(key, trader, pyth)— earns 2.5% of notional as reward, rest goes to insurance fund - Automated (optional): Reactive Network RSC detects breach, emits callback,
PerpHingeCallback.autoLiquidate()is called — entire remaining margin goes to insurance fund (no liquidator reward)
Funding settles every hour, driven by mark-index price spread:
funding_rate_per_second = (mark_price - index_price) / index_price / FUNDING_INTERVAL
accrued_funding = rate * elapsed_seconds
- When mark > index: longs pay shorts (market is overheated)
- When mark < index: shorts pay longs (market is oversold)
- Funding is settled automatically on every
afterSwapcallback if the interval has elapsed
The hook maintains a 180-slot ring buffer of tick observations, updated on every swap. Mark price is computed as a 30-minute TWAP:
avg_tick = (latest_tickCumulative - oldest_tickCumulative) / elapsed_time
mark_price = 1.0001^avg_tick (converted to WAD via TickMath)
Unichain's 200ms Flashblocks mean ~300 potential observations per minute vs ~5 on Ethereum mainnet — significantly more accurate TWAP.
| Check | Status | Details |
|---|---|---|
nonReentrant on all user-facing functions |
Done | openPosition, closePosition, liquidate use OZ ReentrancyGuard |
| OI cap enforced | Done | $1M per side, checked before position creation |
| Pyth price freshness | Done | getPriceNoOlderThan(feedId, 300) — 5-minute max age, try/catch in afterSwap so stale prices never brick pool swaps |
| Funding settled before position changes | Done | _updateFunding() called before open/close/liquidate |
| Insurance fund for bad debt | Done | Remaining margin from liquidations stored in insuranceFund[poolId] |
| Only reactiveCallback can auto-liquidate | Done | autoLiquidate() checks msg.sender == reactiveCallback |
| No double-open positions | Done | openPosition reverts with PositionAlreadyOpen() if pos.isOpen |
| SafeERC20 for token transfers | Done | All transfer/transferFrom use OZ SafeERC20 |
| FullMath for overflow safety | Done | _getMarkPrice uses FullMath.mulDiv for 512-bit intermediate precision |
| Ownable admin functions | Done | enablePool restricted to onlyOwner |
| PnL clamped to prevent underflow | Done | Payout = max(0, margin + pnl) — trader can't owe the protocol |
- Single position per pool per address: Cannot open multiple positions in the same pool
- No partial close: Must close entire position at once
- USDC 6-decimal compatibility: Internal math uses WAD (1e18) for prices; position sizes/margins use native token decimals (6 for USDC)
- No position modification: Cannot add/remove margin or adjust leverage on existing position
- TWAP fallback: If no TWAP data, falls back to index price for mark price
PerpHinge integrates Reactive Network for automated cross-chain liquidation monitoring:
- PerpHingeReactive (RSC) deployed on Reactive Lasna with 5 event subscriptions watching the hook on Unichain Sepolia
- PerpHingeCallback deployed on Unichain Sepolia to receive liquidation callbacks from the RSC
- RVM active with 55+ cron transactions processed for periodic position health checks
- Frontend status panel displays live subscription count, RVM transaction count, and callback history — all verified on-chain via RNK RPC
- Cross-chain event delivery from Unichain Sepolia (chain 1301) to RSCs is a Reactive Network infrastructure dependency — subscriptions are registered and the RVM is operational, but the indexer for Unichain Sepolia events is not yet delivering to RSCs
Current implementation prioritizes correctness and readability for hackathon judging. Production optimizations would include:
- Pack Position struct —
side(uint8) +isOpen(bool) can share a slot withentryPriceby using uint128 for prices - Cache storage reads —
_poolPerps[poolId]is read multiple times in some functions; could use a memory copy - Batch funding settlement — Currently settles on every swap; could batch to reduce writes
- Use uint128 for prices — Pyth prices fit in int64; WAD prices fit in uint128 (saves 50% on price storage)
- Observation array packing —
TWAPOracle.Observationuses 3 slots; could packblockTimestamp+initializedinto one
- Solidity 0.8.26 (Cancun EVM, via Foundry)
- Uniswap v4 (IHooks, StateLibrary, PoolManager, TickMath)
- Pyth Network (real-time price oracle, update-on-demand)
- OpenZeppelin (ReentrancyGuard, Ownable, SafeERC20)
- Reactive Network (optional — automated cross-chain liquidations via RSC + Callback)
- Next.js 16 + wagmi v2 + RainbowKit (trading frontend)
- Recharts (price charting)
A complete perpetual futures engine built as a single Uniswap v4 Hook — a new DeFi primitive, not a fork. The hook implements afterInitialize, beforeSwap, and afterSwap to power: a custom 180-slot TWAP ring buffer oracle (v4 has no built-in oracle), hourly funding rate settlement driven by natural pool activity, full position management with 1–10x leverage, a liquidation engine with 2.5% rewards, an insurance fund for bad debt protection, and $1M per-side open interest caps. The mark price TWAP is computed entirely from afterSwap tick observations — no external keeper or oracle needed for the mark price. 33 tests (unit + integration + fuzz) with full gas benchmarks. Hook address mined via CREATE2 salt search for correct permission bit encoding.
PerpHinge is exactly what Unichain's Request for Hooks asked for: "Perp DEXs built with hooks, including margin management, liquidations, and incentive mechanisms." Deployed and live on Unichain Sepolia at 0x187cf339772ecD2c572AfbD2631b76e68D7db4ED with an initialized ETH/USDC pool, real liquidity, and verified contracts on Blockscout. Leverages Unichain's 200ms Flashblocks for ~300 TWAP observations per minute (vs ~5 on Ethereum mainnet) — a 60x improvement in mark price accuracy that makes Unichain uniquely suited for on-chain derivatives. Full trading frontend at perphinge.vercel.app.
Automated cross-chain liquidation monitoring via a Reactive Smart Contract (RSC) deployed on Reactive Lasna at 0x95EAb385c669aca31C0d406c270d6EdDFED0D1ee. The RSC subscribes to 5 hook events on Unichain Sepolia (PositionOpened, PositionClosed, Liquidated, AutoLiquidated, Cron100) and maintains a shadow position registry in ReactVM state. Every 100 blocks, the Cron100 subscription triggers a health check across all tracked positions — if margin is breached, the RSC emits a callback that PerpHingeCallback.autoLiquidate() executes on Unichain. This replaces off-chain keeper bots with fully on-chain cross-chain automation: no AWS lambdas, no centralized infrastructure. RVM is active with 55+ cron transactions processed. Frontend includes a live Reactive Network status panel showing subscription count, RVM activity, and callback history via RNK RPC queries.
MIT