Skip to content

big14way/PerpHinge

Repository files navigation

PerpHinge

Perpetual Futures DEX built entirely as a Uniswap v4 Hook on Unichain.

The Problem

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.

The Solution

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.

Why Unichain?

  • 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

Why Pyth?

  • 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

Architecture

                     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

Key Features

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

Deployments

Unichain Sepolia (Chain ID: 1301)

Contract Address Verified
PerpHingeHook 0x187cf339772ecD2c572AfbD2631b76e68D7db4ED Blockscout
PerpHingeCallback 0x2c68f7504dD832f9DdBAB88b4c16E493A924647E
PoolManager 0x00B036B58a818B1BC34d502D3fE730Db729e62AC
Pyth Oracle 0x2880aB155794e7179c9eE2e38200202908C17B43
tUSDC (Test USDC) 0xDFe9b0627e0ec2b653FaDe125421cc32575631FC
WETH 0x4200000000000000000000000000000000000006

Reactive Network Lasna (Chain ID: 5318007)

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

Hook Flags

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).

Try the Live Demo

perphinge.vercel.app

  1. Connect wallet — Switch to Unichain Sepolia (chain ID 1301)
  2. Get testnet ETH — Use the Unichain Sepolia faucet
  3. Mint tUSDC — Click "Mint tUSDC" in the trading panel (1,000 tUSDC per mint)
  4. Open a position — Choose Long or Short, set margin and leverage (1–10x), submit
  5. Watch your P&L — Real-time unrealized PnL updates via Pyth price feed
  6. Close position — Click "Close Position" to settle and receive your payout
  7. Check the leaderboard — View your trade on the /leaderboard page

Project Structure

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

Quick Start

Build

forge build

Test (33 tests, 256 fuzz runs)

forge test --match-path test/PerpHingeHook.t.sol -vv

Gas Report

forge 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

Run Frontend

cd frontend
npm install
npm run dev

Deploy to Unichain Sepolia

# 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/

How It Works

Opening a Position

  1. Trader calls openPosition(key, isLong, margin, leverage, pythUpdateData) with ETH for Pyth fee
  2. Pyth price feed is updated on-chain (update-on-demand model)
  3. Funding is settled for the pool before recording the new position
  4. USDC margin is transferred from trader to hook via safeTransferFrom
  5. Position is recorded with entry price from Pyth, cumulative funding index snapshot
  6. Open interest is increased for the corresponding side (long/short)

Closing a Position

  1. Trader calls closePosition(key, pythUpdateData)
  2. Funding is settled for the trader's position (margin adjusted up/down)
  3. PnL is calculated: notional * (exitPrice - entryPrice) / entryPrice (negated for shorts)
  4. Payout = margin + PnL (clamped to zero — trader can't lose more than margin)
  5. Open interest is decreased, position is deleted, payout transferred

Liquidation

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 Rate

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 afterSwap callback if the interval has elapsed

TWAP Oracle

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.

Security Audit Checklist

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

Known Limitations (Hackathon Scope)

  • 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

Reactive Network Integration

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

Gas Optimization Notes

Current implementation prioritizes correctness and readability for hackathon judging. Production optimizations would include:

  1. Pack Position structside (uint8) + isOpen (bool) can share a slot with entryPrice by using uint128 for prices
  2. Cache storage reads_poolPerps[poolId] is read multiple times in some functions; could use a memory copy
  3. Batch funding settlement — Currently settles on every swap; could batch to reduce writes
  4. Use uint128 for prices — Pyth prices fit in int64; WAD prices fit in uint128 (saves 50% on price storage)
  5. Observation array packingTWAPOracle.Observation uses 3 slots; could pack blockTimestamp + initialized into one

Tech Stack

  • 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)

Prize Tracks

Uniswap v4 Hooks (Main Track)

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.

Unichain (Sponsor Track)

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.

Reactive Network (Sponsor Track)

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.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors