Skip to content

Kaushik2003/para-dex

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Adaptive Perpetual DEX

A perpetual futures market implemented inside a Uniswap v4 hook: traders act through the v4 unlock-callback path, and per-pool OI skew drives on-chain fee overrides and exposure limits. Funding settlement and liquidations are automated by Reactive Network reactors subscribed to Unichain events.

Uniswap Hookathon 2026

Theme alignment — Specialized Markets This repo is a hook template for perp markets on any v4 pool: market identity is the pool (PoolId) and all perp state is keyed by it. The design is Unichain-native because execution runs inside v4 hook callbacks on Unichain’s PoolManager.

Project tags Perpetual Futures Dynamic Fees Hook-Native Perps Skew Engine Reactive Automation Unichain Pyth Oracle Non-Custodial Margin

Docs: contracts.md (protocol + contracts) · frontend.md (UI + wagmi/Pyth wiring)


Problem / Background

Perp DEXs and spot AMMs are usually separate systems: perps depend on external oracles/keepers, while AMMs do not price the risk of one-sided perp demand to LPs.

This separation produces oracle manipulation/staleness risk, invisible LP directional exposure, and no native skew deterrent; keepers add liveness/incentive failure modes.

Uniswap v4 hooks allow the perp lifecycle to run inside swap execution: perp actions are v4 “virtual swaps” (sentinel amountSpecified + encoded hookData) so margin checks, OI updates, funding accrual, and automation events are atomic.


Architecture

The Two-Way Coupling

Perp state is pool-native (PoolId), and PerpHook.beforeSwap() converts skew into a fee override and an exposure block using RiskEngine:

skewFactor = |longOI - shortOI| / (longOI + shortOI)   (WAD, 1e18)
worsensSkew fee:  fee = baseFee * (1 + skewFactor * 3.0), capped at baseFee * 3.0
improvesSkew fee: fee = baseFee * max(0.2, 1 - skewFactor * 0.8), floored at baseFee * 0.2

Spot swaps currently use a fixed fee override (DEFAULT_BASE_FEE = 3000 pips); skew-fees are scoped to perp “virtual swaps” in this demo.

Contract Architecture

PerpHook.sol

0x34d737160c8DE1f1a24978cae0B5a62b828190c0 v4 hook callbacks + perp coordination (margin, skew, funding, events).

PerpController.sol

0xc71c82c0a4c4D7C0D4beEc6E96668fC5439032F5 User entrypoint; routes actions through v4 unlock-callback.

PositionManager.sol

0x0DA5A02762146f5697F951d423B200d419736046 Per-trader positions keyed by PoolId (WAD units).

MarginVault.sol

0x0207149577b97c305C275694a3f5D89a826e8c85 USDC custody (6 decimals): free/locked collateral accounting.

RiskEngine.sol (library)

[address — see unichain-1301.json] Pure math: margin, skew, fee multipliers, max leverage (20×), and 80% dominant-side exposure cap threshold.

FundingEngine.sol

0x70177cbA4c0491865F6b29Aad1eaaa81Ba731B18 Funding index accrual + lazy settlement + Reactive settlement hook.

LiquidationEngine.sol

0x9E7bC7BA424E0EEFf7a25F2637da0c544A9B0B00 Maintenance margin check + close; permissionless + Reactive entrypoints.

LiquidationReactor.sol

0xcAb594a7b304a9C8BC989fA4CDF9fd042d458909 Lasna reactor: PositionUnderwater → liquidation callback.

FundingReactor.sol

0x487FAFf3Fdf0a2e01E28120529Ece386EA74740A Lasna reactor: FundingDue → funding settlement callback.

MockUSDC.sol

0x94d48675B76CFB487feB95225a1c5D54c4f2Ee0D 6-decimal mock token with public mint() (testnet/demo only).

MockWBTC.sol

0x674f676860c182Ac1D04F20FeFD392B56845DfC8 8-decimal mock token with public mint() (testnet/demo only).

Data Flow

  1. MarginVault.deposit()
  2. PerpController.openPosition(..., priceUpdateData) → Pyth update + PoolManager.unlock()
  3. PerpController.unlockCallback → sentinel PoolManager.swap()PerpHook.beforeSwap/afterSwap
  4. FundingEngine.accrue + PerpHook emits FundingDue (interval)
  5. FundingReactor / LiquidationReactor relay callbacks to Unichain engines
  6. PerpController.closePosition(..., priceUpdateData) → Pyth update + settle + unlock collateral

Unichain Integration

  1. Why Unichain: v4 PoolManager is deployed natively on Unichain Sepolia at 0x00B036B58a818B1BC34d502D3fE730Db729e62AC (from contracts/deployments/unichain-1301.json), and the hook is embedded in the pool’s immutable PoolKey.
  2. Chain-specific code: frontend/lib/wagmi.ts (chain 1301), contracts/deployments/unichain-1301.json (addresses), frontend/scripts/sync-sepolia-env.mjs (writes .env.local), and NEXT_PUBLIC_CHAIN_ID=1301 gates the faucet in frontend/components/SpotSwap.tsx.
  3. Pool initialization: dedicated hook pool (MockWBTC/MockUSDC + PerpHook); pool ID 0xcffe71c8d7ada94270e05e66cf99b6fff73f64ae44ebe0a78de2b19788c0eb31.
  4. Explorer: https://sepolia.uniscan.xyz (PerpHook and PerpController below).

https://sepolia.uniscan.xyz/address/0x34d737160c8DE1f1a24978cae0B5a62b828190c0
https://sepolia.uniscan.xyz/address/0xc71c82c0a4c4D7C0D4beEc6E96668fC5439032F5


Reactive Network Integration

Why Reactive: funding settlement and liquidations are usually keeper jobs; here they are event-driven callbacks (no bot, no incentive token).

LiquidationReactor (contracts/src/reactive/LiquidationReactor.sol): Lasna 5318007, watches PerpHook.PositionUnderwater on Unichain 1301, callbacks LiquidationEngine.liquidateFromReactive(reactVmId, trader, pool), address 0xcAb594a7b304a9C8BC989fA4CDF9fd042d458909.

FundingReactor (contracts/src/reactive/FundingReactor.sol): Lasna 5318007, watches PerpHook.FundingDue on Unichain 1301, callbacks FundingEngine.settleFromReactive(reactVmId, pool), address 0x487FAFf3Fdf0a2e01E28120529Ece386EA74740A.

Constraints: ReactVM react() only emits Callback; constructors run twice (if (!vm) subscribe); callback arg0 is overwritten with ReactVM ID; reactors must be deployed funded (--value 0.01ether).

Deployment: contracts/script/deploy-reactive-lasna.sh. Lasna explorer: https://lasna.reactscan.net.


Pyth Oracle Integration

Pull oracle: PerpHook._getSpotPrice() uses pyth.getPriceNoOlderThan(priceFeedId, 60), and PerpController updates Pyth from bytes[] priceUpdateData fetched from Hermes (usePythPriceData.ts). Feed: BTC/USD 0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43.


Getting Started

Prerequisites

Node.js >=20, forge, cast, jq, and a wallet funded with Unichain Sepolia ETH.

Clone and Install

git clone [repo]
cd adaptive-perp-dex
cd contracts && forge install
cd ../frontend && npm install

Run Demo (Unichain Sepolia — Testnet)

Step 1: Sync contract addresses

cd frontend
npm run demo:sepolia-env
# Reads ../contracts/deployments/unichain-1301.json
# Writes .env.local with all NEXT_PUBLIC_ vars

Step 2: Start frontend

npm run dev
# Open http://localhost:3000

Step 3: Connect and test

  1. Connect wallet via RainbowKit
  2. Switch to Unichain Sepolia (chainId 1301) — auto-prompted
  3. SPOT tab → "Get test tokens" → mint 100k USDC + 10 MockWBTC
  4. PERPS tab → Deposit → approve USDC → deposit collateral
  5. Set size (BTC, 8 decimals) + leverage → Open Long / Open Short
  6. Watch position in PositionsPanel + skew bar update

Note about Pyth: every open/close fetches Hermes update bytes and bundles them with the tx; PerpHook enforces maxPriceAge.

Step 4: Watch Reactive automation

After a FundingDue event fires (once fundingInterval elapses), both reactors are visible at https://lasna.reactscan.net.

Deploy Your Own (Advanced)

# Deploy contracts to Unichain Sepolia
cd contracts
cp .env.example .env.sepolia
# fill: PRIVATE_KEY, UNICHAIN_SEPOLIA_RPC, PYTH_CONTRACT

forge script script/DeployUnichain.s.sol \
  --rpc-url $UNICHAIN_SEPOLIA_RPC \
  --private-key $PRIVATE_KEY \
  --broadcast

# Deploy Reactive reactors to Lasna
# (ensure REACTIVE_RPC_URL and ETHEREUM_SEPOLIA_RPC are set)
bash script/deploy-reactive-lasna.sh

Contract Reference

PerpController (contracts/src/core/PerpController.sol):

function openPosition(PoolKey calldata poolKey, bool isLong, int256 sizeDelta, uint256 leverage) external payable;
function openPosition(PoolKey calldata poolKey, bool isLong, int256 sizeDelta, uint256 leverage, bytes[] memory priceUpdateData)
    public payable;
function closePosition(PoolKey calldata poolKey, int256 sizeDelta) external payable;
function closePosition(PoolKey calldata poolKey, int256 sizeDelta, bytes[] memory priceUpdateData) public payable;
function closePositionInternal(address trader, PoolKey calldata key) external;
function liquidate(PoolKey calldata key, address trader) external;
function unlockCallback(bytes calldata data) external returns (bytes memory);
function addCollateral(PoolKey calldata key, uint256 amount) external;
function removeCollateral(PoolKey calldata key, uint256 amount) external;
function setLiquidationEngine(address _liquidationEngine) external;
function setFundingEngine(address _fundingEngine) external;
function setPerpHook(address _perpHook) external;
function setMarginVault(address _marginVault) external;
function setPositionManager(address _positionManager) external;
function setPyth(address _pyth) external;
Function What it does Emits
openPosition (2 overloads) Optional Pyth update; routes OPEN through v4 unlock path. via hook: PositionOpened
closePosition (2 overloads) Optional Pyth update; routes CLOSE through v4 unlock path. via hook: PositionClosed
closePositionInternal Liquidation-only close path. via hook: PositionClosed
liquidate Routes LIQUIDATE through v4 unlock path. via engine: PositionLiquidated (if triggered)
unlockCallback PoolManager-only; executes sentinel swap.
addCollateral Claims funding; locks more USDC via hook. CollateralAdded
removeCollateral Claims funding; releases USDC via hook. CollateralRemoved
set* Admin wiring (one-time) + setPyth.

PerpHook (contracts/src/core/PerpHook.sol):

function setAdmin(address newAdmin) external;
function getHookPermissions() public pure returns (Hooks.Permissions memory);
function claimFees(PoolId poolId) external;
function adjustCollateral(address trader, PoolId pool, int256 delta) external;
function emitPositionUnderwater(address trader, bytes32 poolId, uint256 marginRatio) external;
function checkAndEmitUnderwaterIfNeeded(address trader, bytes32 poolIdBytes) external;
function setLiquidationEngine(address _le) external;
function setPositionManager(address _pm) external;
function setMarginVault(address _mv) external;
function setFundingEngine(address _fe) external;
function setPerpController(address _pc) external;
function setPyth(address _pyth) external;
function setPriceFeedId(bytes32 _feedId) external;
function setMaxPriceAge(uint256 _age) external;
function getMarkPrice(bytes32) external view returns (uint256);
Function What it does Emits
setAdmin / getHookPermissions Admin / v4 permissions.
claimFees Transfers accrued protocol fees to protocolFeeRecipient. FeesWithdrawn
adjustCollateral Controller-only lock/release + position metadata update.
emitPositionUnderwater / checkAndEmitUnderwaterIfNeeded FundingEngine-only underwater signaling. PositionUnderwater
setLiquidationEngine/setPositionManager/setMarginVault/setFundingEngine/setPerpController One-time wiring (admin-only).
setPyth/setPriceFeedId/setMaxPriceAge Oracle config (admin-only).
getMarkPrice Returns Pyth mark price (WAD).

Environment Variables

Variable Source field Required Description
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID (preserved) Optional WalletConnect project ID.
NEXT_PUBLIC_CHAIN_ID (script constant) Required Default chain selection + UI gating.
NEXT_PUBLIC_RPC_URL (script constant) Required Frontend RPC.
NEXT_PUBLIC_TOKEN0 token0 (or derived) Required v4 currency0.
NEXT_PUBLIC_TOKEN1 token1 (or derived) Required v4 currency1.
NEXT_PUBLIC_POOL_MANAGER poolManager Required v4 PoolManager.
NEXT_PUBLIC_PERP_HOOK perpHook Required PerpHook address.
NEXT_PUBLIC_PERP_CONTROLLER perpController Required PerpController address.
NEXT_PUBLIC_MARGIN_VAULT marginVault Required MarginVault address.
NEXT_PUBLIC_POSITION_MANAGER positionManager Required PositionManager address.
NEXT_PUBLIC_FUNDING_ENGINE fundingEngine Required FundingEngine address.
NEXT_PUBLIC_LIQUIDATION_ENGINE liquidationEngine Required LiquidationEngine address.
NEXT_PUBLIC_USDC usdc Required MockUSDC address.
NEXT_PUBLIC_WBTC wbtc Required MockWBTC address.
NEXT_PUBLIC_POOL_ID poolId Optional Pool ID (display/scripts).
NEXT_PUBLIC_PYTH_CONTRACT pythContract Required Pyth contract.
NEXT_PUBLIC_PRICE_FEED_ID priceFeedId Required BTC/USD feed ID.
NEXT_PUBLIC_INVERT_PRICE invertPrice Optional Deployment flag.

Project Structure

adaptive-perp-dex/
├── contracts/
│   ├── src/
│   │   ├── core/          # PerpHook, PerpController, PositionManager, MarginVault
│   │   ├── engines/       # RiskEngine, FundingEngine, LiquidationEngine
│   │   ├── reactive/      # LiquidationReactor, FundingReactor
│   │   └── mocks/         # MockUSDC, MockWBTC
│   ├── script/
│   │   ├── DeployUnichain.s.sol
│   │   └── deploy-reactive-lasna.sh
│   ├── test/
│   └── deployments/
│       ├── unichain-1301.json   # live testnet addresses
│       └── anvil-31337.json     # local dev addresses
└── frontend/
    ├── app/               # Next.js pages and layout
    ├── components/        # UI components
    ├── context/           # MarketContext (skew, fees, prices)
    ├── hooks/             # usePythPrice, usePythPriceData, etc.
    ├── lib/               # contracts.ts, abis.ts, wagmi.ts, format.ts
    └── scripts/           # sync-sepolia-env.mjs, reactive-demo.mjs

License

MIT

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors