A Uniswap v4 hook that protects stablecoin liquidity providers from depeg events through real-time peg monitoring, graduated dynamic fees, and an automatic circuit breaker.
Built for the Atrium Academy UHI8 Hookathon · Deployed on Arc Testnet (Chain ID 5042002), Unichain Sepolia (Chain ID 1301), and Base Sepolia (Chain ID 84532)
Stablecoin pools on constant-product AMMs are especially vulnerable to depegging: as a stablecoin drifts from its $1 peg, arbitrageurs drain the pool at the expense of passive LPs. The Stable Protection Hook addresses this by:
- Monitoring real-time peg deviation on every swap using virtual reserves derived from
sqrtPriceX96and pool liquidity — no oracle required, no off-chain reliance. - Applying graduated dynamic fees that penalise swaps worsening the depeg and discount swaps that restore it, keeping LP revenue healthy under stress.
- Blocking swaps entirely via a circuit breaker when deviation exceeds 5% (500 bps), preventing catastrophic draining during a depeg crisis.
┌─────────────────────────────────────────┐
│ Uniswap v4 PoolManager │
└──────┬──────────────────────┬────────────┘
│ beforeSwap │ afterSwap
▼ ▼
┌────────────────────────────────────────┐
│ StableProtectionHook │
│ │
│ _getVirtualReservesNormalized() │
│ vr0 = L × Q96 / sqrtPriceX96 │
│ vr1 = L × sqrtPriceX96 / Q96 │
│ │ │
│ PegMonitor.classifyZone() │
│ deviation = |r0−r1|×10000 / avg │
│ │ │
│ ┌───────┴────────┐ │
│ │ │ │
│ CRITICAL? other zone │
│ revert CB dynamic fee │
│ OVERRIDE_FEE_FLAG │
└────────────────────────────────────────┘
| Hook Point | Action |
|---|---|
beforeInitialize |
Validates pool uses DYNAMIC_FEE_FLAG; stores default PoolConfig; seeds zone as HEALTHY |
beforeSwap |
Reads virtual reserves → classifies zone → blocks if CRITICAL → returns fee | OVERRIDE_FEE_FLAG |
afterSwap |
Re-reads reserves → reclassifies zone → emits ZoneChanged if shifted → snapshots state |
Deviation is computed as:
deviationBps = |reserve0 − reserve1| × 10000 / ((reserve0 + reserve1) / 2)
| Zone | Threshold | Base Fee | Dynamic A | Behaviour |
|---|---|---|---|---|
| HEALTHY | ≤ 0.10% (10 bps) | 1 bps | 100% of A | Normal trading |
| MINOR | ≤ 0.50% (50 bps) | 5 bps | 80% of A | Mild stress |
| MODERATE | ≤ 2.00% (200 bps) | 15 bps | 50% of A | Elevated risk |
| SEVERE | ≤ 5.00% (500 bps) | 50 bps | 25% of A | High risk |
| CRITICAL | > 5.00% (500 bps) | — | 10% of A | Swaps blocked |
Each zone's base fee is scaled by swap direction relative to the peg:
| Direction | Multiplier | Rationale |
|---|---|---|
| Toward-peg | × 0.5 | Discounted — incentivises peg restoration |
| Away-from-peg | × 3.0 | Premium — disincentivises worsening depeg |
| Zone | Toward-peg fee | Away-from-peg fee |
|---|---|---|
| HEALTHY | 0.5 bps | 3 bps |
| MINOR | 2.5 bps | 15 bps |
| MODERATE | 7.5 bps | 45 bps |
| SEVERE | 25 bps | 100 bps (capped) |
| CRITICAL | circuit breaker — swaps blocked |
The hook derives virtual reserves directly from Uniswap v4's on-chain state — no oracle, no incremental storage tracking:
virtual_r0 = L × 2^96 / sqrtPriceX96
virtual_r1 = L × sqrtPriceX96 / 2^96
This approximates the "as-if constant-product" reserves at the current price tick. Reserves are normalised to 18-decimal precision before zone classification, supporting any token decimal configuration (1–18).
src/
├── types/
│ └── SPTypes.sol Enums, structs, errors, events
├── libraries/
│ ├── StableSwapMath.sol StableSwap D-invariant, getY, swap output
│ ├── PegMonitor.sol Zone classification, fee calc, dynamic A
│ └── SPConfig.sol PoolConfig validation
├── interfaces/
│ └── IStableProtectionHook.sol External view interface
├── mocks/
│ └── MockStablecoin.sol ERC-20 for testnet deployments
└── StableProtectionHook.sol Main hook contract
test/
├── unit/
│ ├── StableSwapMath.t.sol 16 tests (incl. fuzz)
│ ├── PegMonitor.t.sol 30 tests (incl. fuzz)
│ └── StableProtectionHook.t.sol 26 tests (TestableHook harness)
└── integration/
└── StableProtectionHook.integration.t.sol 9 tests (full PoolManager stack)
script/
├── Deploy.s.sol Full deploy on Unichain Sepolia (mock tUSDC/tUSDT)
├── DeployBaseSepolia.s.sol Full deploy on Base Sepolia (real Circle USDC/EURC)
├── DeployArcTestnet.s.sol Arc Testnet: deploys v4 stack + hook + pool init
├── deploy_arc.sh Arc orchestrator: forge deploy + cast liquidity/swap
└── TestPoolSwap.s.sol Standalone: new pool + swap on existing hook
81 tests · 0 failures · 0 skipped
StableSwapMathTest 16 passed (incl. fuzz: getSwapOutput × 256 runs)
PegMonitorTest 30 passed (incl. fuzz: neverExceedsMaxFee × 256 runs)
StableProtectionHookTest 26 passed (TestableHook harness — no PoolManager needed)
StableProtectionHookIntegrationTest 9 passed (HookMiner + real PoolManager)
Run the full suite:
forge test --summary- Foundry
- Git (with submodule support)
git clone https://github.com/<your-username>/stableprotection-hook
cd stableprotection-hook
forge installforge buildforge test --summarycp .env.example .env # add PRIVATE_KEY and UNICHAIN_SEPOLIA_RPC
source .env
forge script script/Deploy.s.sol:Deploy \
--rpc-url unichain_sepolia \
--broadcast \
--verify \
-vvvvUses Circle's official testnet USDC and EURC on Base Sepolia — no mock tokens deployed. The deployer wallet must hold ≥ 60 USDC and ≥ 60 EURC (faucets: Circle USDC, Circle EURC).
cp .env.example .env # add PRIVATE_KEY and BASE_SEPOLIA_RPC
source .env
forge script script/DeployBaseSepolia.s.sol:DeployBaseSepolia \
--rpc-url base_sepolia \
--broadcast \
--slow \
-vvvvRequired .env variables:
PRIVATE_KEY=0x...
UNICHAIN_SEPOLIA_RPC=https://sepolia.unichain.org
BASE_SEPOLIA_RPC=https://sepolia.base.org
ARC_TESTNET_RPC_URL=https://rpc.testnet.arc.network
ETHERSCAN_API_KEY=<uniscan-api-key>
BASESCAN_API_KEY=<basescan-api-key>
Deployed on Arc, Circle's USDC-native L1 (gas is paid in USDC). Uses Circle's official Arc Testnet USDC and EURC. Arc has no public Uniswap v4 deployment, so this deployment includes a self-contained v4 stack (PoolManager + test routers) deployed alongside the hook.
| Contract | Address | Arcscan |
|---|---|---|
| StableProtectionHook | 0xF131A048875E578A0F89393e858C0442fcD7e0C0 |
view |
| USDC (Circle) | 0x3600000000000000000000000000000000000000 |
view |
| EURC (Circle) | 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a |
view |
| PoolManager (v4) | 0x15B5f2c054b9DC788250131FCD1bcfCC34080a59 |
view |
| PoolModifyLiquidityTest | 0x4f81385fa50336e4cbA6718A803f3e2Baa09D1c0 |
view |
| PoolSwapTest | 0xeA44982cB8b71A9BF69bfe3F3f5b43E1790be4d1 |
view |
Pool ID: 0xd96b50ff81c1381d3f5f6ee8ed0f8b57f3d94126e4e479021bae92cb90c3f48f
Pair: USDC / EURC (currency0 = USDC, currency1 = EURC; both 6-decimal)
Tick spacing: 1 · Fee: dynamic · Tick range at deploy: [-10, 10]
| Action | Arcscan |
|---|---|
| PoolManager deployed | 0x332a47… |
| Hook deployed (CREATE2) | 0xe5e32d… |
Pool created (initialize) |
0x5932d1… |
| Liquidity added (~15 USDC + ~15 EURC) | 0xbfed03… |
| Test swap (5 USDC → EURC) | 0x44e0a6… |
Post-swap on-chain state read from getZoneState(poolId): zone = HEALTHY,
currentDeviationBps = 3, confirming beforeSwap (dynamic fee applied via
OVERRIDE_FEE_FLAG) and afterSwap (zone snapshot updated) executed correctly
against the real Circle stablecoins on Arc.
Arc's USDC/EURC are native fiat tokens whose transfers run through chain-level
precompiles that Foundry's local EVM can't execute, so the deploy is split: a
forge script deploys the v4 stack + hook and initializes the pool (no token
moves), then cast send adds liquidity and runs the test swap against the live
node. The script/deploy_arc.sh wrapper orchestrates both.
export ARC_TESTNET_RPC_URL=https://rpc.testnet.arc.network
# Import the funded deployer once (gas + liquidity are paid in USDC/EURC):
cast wallet import arc-deployer --interactive
./script/deploy_arc.sh <deployer-address> arc-deployer| Contract | Address | Basescan |
|---|---|---|
| StableProtectionHook | 0xe5e6a9E09Ad1e536788f0c142AD5bc69e8B020C0 |
view |
| USDC (Circle) | 0x036CbD53842c5426634e7929541eC2318f3dCF7e |
view |
| EURC (Circle) | 0x808456652fdb597867f38412077A9182bf77359F |
view |
| PoolManager (v4) | 0x05E73354cFDd6745C338b50BcFDfA3Aa6fA03408 |
view |
Pool ID: 0x8e929e2af7edea4c196aa0718602f0892b7ecbc205491ffb170640ca75b3c4ab
Pair: USDC / EURC (currency0 = USDC, currency1 = EURC; both 6-decimal)
Tick spacing: 1 · Fee: dynamic · Tick range at deploy: [-10, 10]
| Action | Basescan |
|---|---|
| Hook deployed (CREATE2) | 0xad6032… |
Pool created (initialize) |
0x5cc896… |
| Liquidity added (~50 USDC + ~50 EURC) | 0xd93732… |
| Test swap (5 USDC → EURC) | 0x53c8f0… |
Post-swap on-chain state read from getZoneState(poolId): zone = HEALTHY, currentDeviationBps = 0, confirming beforeSwap (dynamic fee applied via OVERRIDE_FEE_FLAG) and afterSwap (zone snapshot updated) executed correctly against the real Circle stablecoins.
| Contract | Address | Uniscan |
|---|---|---|
| StableProtectionHook | 0x1510926ba6986cb3c93BFFF25839C0ef740820c0 |
view |
| tUSDC | 0x3D0aD0014933b87332BE00E832D16d219c65346c |
view |
| tUSDT | 0xEa3B5B015a5289bE6fFa7196aF5386A86E50a8c2 |
view |
| PoolManager | 0x00B036B58a818B1BC34d502D3fE730Db729e62AC |
view |
| Action | Uniscan |
|---|---|
| Hook deployed (CREATE2) | 0xb69507… |
Pool created (initialize) |
0x1ba84a… |
| Liquidity added | 0x057661… |
| Test swap (10 tUSDC → tUSDT) | 0x6af6e9… |
The swap exercised beforeSwap (HEALTHY zone, 0.5 bps fee applied via OVERRIDE_FEE_FLAG) and afterSwap (zone snapshot updated).
The hook address encodes permissions in its lowest 14 bits (Uniswap v4 convention). The required flags:
uint160 flags = uint160(
Hooks.BEFORE_INITIALIZE_FLAG | // bit 13
Hooks.BEFORE_SWAP_FLAG | // bit 7
Hooks.AFTER_SWAP_FLAG // bit 6
);Use HookMiner.find(CREATE2_PROXY, flags, creationCode, constructorArgs) to mine the correct CREATE2 salt. The Deploy.s.sol script handles this automatically.
| Parameter | Required value |
|---|---|
fee |
LPFeeLibrary.DYNAMIC_FEE_FLAG (0x800000) |
tickSpacing |
Any; 1 recommended for stablecoin pairs |
| Token decimals | 1–18 (hook normalises to 18 internally) |
Automated static analysis and security scanning of the hook contract and supporting libraries. Provides vulnerability detection and audit tooling across the Solidity codebase. https://github.com/trailofbits/skills
Research tooling and reference implementations from the Uniswap AI ecosystem. Contributed to advanced AMM pattern research and hook development workflow during the hackathon build. https://github.com/Uniswap/uniswap-ai
| Package | Version / Commit | Purpose |
|---|---|---|
uniswap/v4-core |
via v4-periphery |
PoolManager, hook interfaces, StateLibrary |
uniswap/v4-periphery |
eeb3eff |
BaseHook, HookMiner |
transmissions11/solmate |
via v4-core |
ERC-20 base, arithmetic |
foundry-rs/forge-std |
latest | Testing framework, scripting |
MIT