Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@
[submodule "contracts/lib/hookmate"]
path = contracts/lib/hookmate
url = https://github.com/akshatmittal/hookmate
[submodule "contracts/lib/reactive-lib"]
path = contracts/lib/reactive-lib
url = https://github.com/Reactive-Network/reactive-lib
113 changes: 106 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,21 @@ Always follow these when making contract changes:
- `_beforeSwap()` reads `sqrtPriceX96` + `liquidity` from PoolManager, derives virtual reserves, computes imbalance ratio (`max/min * 10000`), checks swap direction, and returns a fee with `LPFeeLibrary.OVERRIDE_FEE_FLAG`.
- `_afterSwap()` emits `SwapFeeApplied` event with post-swap state.

**Fee logic** (via `FeeCurve.sol` library):
- Zone 1 (ratio <= 12200, safe): 1bp flat
- Zone 2 (12200 < ratio <= 15000, warning): quadratic ramp, `100 + d^2/5600`
- Zone 3 (ratio > 15000, circuit breaker): linear, `1500 + (ratio - 15000)`
- Capped at MAX_FEE (500000 = 50%)
**Fee logic** (via `FeeCurve.sol` library -- 5-zone adaptive curve):
- Zone 1 Stable (ratio <= 10050, <=0.5% deviation): 1bp flat (BASE_FEE = 100)
- Zone 2 Drift (10050 < ratio <= 10100, 0.5-1%): linear ramp 1-5bp
- Zone 3 Stress (10100 < ratio <= 10300, 1-3%): quadratic 5-50bp
- Zone 4 Crisis (10300 < ratio <= 10500, 3-5%): quadratic 50-200bp
- Zone 5 Emergency (ratio > 10500, >5%): quadratic from 200bp, capped at MAX_FEE (500000 = 50%)
- Rebalancing swaps on imbalanced pools -> 0bp (incentivize recovery)

**Cross-chain contagion shield** (Phase 4):
- `AlertReceiver.sol` stores per-token cross-chain imbalance ratios from Reactive Network callbacks
- `DepegShieldHook` constructor takes `(IPoolManager, address alertReceiver)` -- address(0) disables
- Fee floor: `effectiveFee = max(localFee, FeeCurve.calculateFee(crossChainRatio))` -- same curve, earlier activation
- Rebalancing swaps stay 0bp regardless of cross-chain signals
- `ReactiveMonitor.sol` runs on Reactive Lasna, subscribes to V2/V3/V4 pool events, relays raw ratio

**Pool requirement:** Must initialize with `LPFeeLibrary.DYNAMIC_FEE_FLAG` (0x800000) in `PoolKey.fee`.

**Fee units:** Hundredths of a bip (100 = 1bp, 5000 = 50bp, 1000000 = 100%).
Expand All @@ -66,10 +74,101 @@ Tests use a custom `BaseTest` -> `Deployers` chain that deploys the full v4 stac
## Key Configuration

- **Solidity:** 0.8.30, cancun EVM, FFI enabled
- **Dependencies** are git submodules in `contracts/lib/` (forge-std, uniswap-hooks, hookmate)
- **Dependencies** are git submodules in `contracts/lib/` (forge-std, uniswap-hooks, hookmate, reactive-lib)
- **Remappings:** `@uniswap/v4-core/` and `@uniswap/v4-periphery/` resolve through `contracts/lib/uniswap-hooks/lib/`
- **Frontend:** Next.js 14+ (App Router), Tailwind CSS, viem + wagmi, RainbowKit, Recharts

## Implementation Plan

See `PLAN.md` for phased implementation spec. Phase 1 (directional fee hook) and Phase 2 (fee curve + depeg simulation) are complete. Phase 3 (frontend) is in progress. Phase 4 covers Reactive Network cross-chain integration.
See `PLAN.md` for phased implementation spec. All 4 phases are complete. Phase 4 added Reactive Network cross-chain early warning (AlertReceiver, ReactiveMonitor, hook fee floor via same curve).

## Testnet Deployment Guide

Deployment order matters. Scripts are in `contracts/script/` numbered 01-05.

### Step 1: Deploy Mock Tokens (01_DeployTokens.s.sol)
Uses CREATE2 so addresses are the same on all chains. Run on each chain.
```bash
forge script script/01_DeployTokens.s.sol --rpc-url <RPC> --broadcast
```
- mUSDC: `0x58C414Bd85bf1d39985476Dfa5fBd59af356E8f0`
- mUSDT: `0x2170d1eC7B1392611323A4c1793e580349CC5CC0`
- **Token ordering:** mUSDT < mUSDC, so currency0 = mUSDT, currency1 = mUSDC

### Step 2: Deploy AlertReceiver (02_DeployAlertReceiver.s.sol)
Run on each destination chain. Constructor takes the chain's callback proxy address.
```bash
CALLBACK_PROXY=<proxy_addr> forge script script/02_DeployAlertReceiver.s.sol --rpc-url <RPC> --broadcast
```
Callback proxy addresses (Reactive Network infrastructure):
- Sepolia: `0xc9f36411C9897e7F959D99ffca2a0Ba7ee0D7bDA`
- Base Sepolia: `0xa6eA49Ed671B8a4dfCDd34E36b7a75Ac79B8A5a6`
- Unichain Sepolia: `0x9299472A6399Fd1027ebF067571Eb3e3D7837FC4`

### Step 3: Deploy Hook (03_DeployHook.s.sol)
Run on each chain. Needs AlertReceiver address from step 2.
```bash
ALERT_RECEIVER=<addr> forge script script/03_DeployHook.s.sol --rpc-url <RPC> --broadcast
```

### Step 4: Create Pool + Seed Liquidity (04_CreatePool.s.sol)
Run on each chain. Needs hook address from step 3.
```bash
HOOK=<addr> forge script script/04_CreatePool.s.sol --rpc-url <RPC> --broadcast
```

### Step 5: Deploy ReactiveMonitor on Reactive Lasna

**CRITICAL: Reactive Network has dual-state (RNK + ReactVM). They share bytecode but NOT storage. Only the constructor runs in both environments. Any state needed by `react()` (which runs in ReactVM) MUST be set in the constructor. Post-deploy external calls like `addMonitoredPool()` only write to RNK state and will cause "Unknown pool" errors in ReactVM.**

**CRITICAL: `forge create` times out on Reactive Lasna. Use `cast send --create` instead.**

```bash
# 1. Get bytecode
BYTECODE=\$(forge inspect src/reactive/ReactiveMonitor.sol:ReactiveMonitor bytecode)

# 2. Encode constructor args: PoolConfig[] and DestConfig[]
ARGS=\$(cast abi-encode "constructor((uint256,address,bytes32,uint8,bytes32)[],(uint256,address)[])" \
"[(chainId,poolManagerAddr,pairId,poolType,poolId),...]" \
"[(chainId,alertReceiverAddr),...]")

# 3. Deploy (concat bytecode + args without 0x prefix)
cast send --rpc-url https://lasna-rpc.rnk.dev/ --private-key \$PK \
--legacy --value 0.1ether --gas-limit 3000000 \
--create "\${BYTECODE}\${ARGS#0x}"
```

- PoolType enum: 0 = UNISWAP_V4, 1 = UNISWAP_V3, 2 = UNISWAP_V2
- poolId: only for V4 pools (the PoolKey hash). For V3/V2, use bytes32(0).
- pairId: owner-assigned label, e.g. `keccak256("USDC/USDT")`. Must be consistent across all chains.
- `--value 0.1ether` funds the REACT subscription balance
- `--legacy` required for Reactive Lasna (no EIP-1559)

See `script/05_DeployReactive.s.sol` for the current pool/destination config values.

### Step 6: Fund Callback Proxies
Each destination chain's callback proxy must be funded for the ReactiveMonitor address:
```bash
cast send --rpc-url <dest_chain_rpc> --private-key \$PK \
<callback_proxy_addr> "depositTo(address)" <reactive_monitor_addr> --value 0.001ether
```
Do this for all 3 destination chains (Sepolia, Base Sepolia, Unichain Sepolia).

### Step 7: Register Pairs on AlertReceivers
Each AlertReceiver needs local token addresses mapped to the pairId:
```bash
cast send --rpc-url <chain_rpc> --private-key \$PK \
<alert_receiver_addr> "registerPair(bytes32,address,address)" \
<pairId> <localToken0> <localToken1>
```

### Step 8: Trigger and Verify
Do a swap on any chain to trigger a V4 Swap event. The ReactiveMonitor should:
1. Detect the event on Reactive Lasna
2. Decode the imbalance ratio
3. Emit Callback events to other chains' AlertReceivers
4. Callback proxies deliver to AlertReceivers
5. AlertReceivers store the alert
6. Hooks on other chains read the cross-chain ratio as fee floor

Check reactscan: `https://lasna.reactscan.net/address/<deployer>/contract/<monitor_addr>`
6 changes: 3 additions & 3 deletions PLAN.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# DepegShield — Implementation Spec

> This file is the source of truth for implementation. Each phase builds on the last.
> Status: Phase 1 ✅ | Phase 2 ✅ | Phase 3 ✅ | Phase 4 🔲
> Status: Phase 1 ✅ | Phase 2 ✅ | Phase 3 ✅ | Phase 4

---

Expand Down Expand Up @@ -151,8 +151,8 @@ Zone 3: fee = 1500 + (ratio - 15000), capped at 500000

**New file: `contracts/src/AlertReceiver.sol`** (deployed on Unichain)
- `setAlert(address token, uint8 severity)` — callable only by Reactive Network
- `clearAlert(address token)` — callable by Reactive Network or after TTL expiry
- Stores: `mapping(address => Alert)` where Alert = {severity, timestamp, ttl}
- `clearAlert(address token)` — callable by Reactive Network when source pool recovers
- Stores: `mapping(address => Alert)` where Alert = {severity, timestamp}
- `getAlertSeverity(address token) → uint8` — view for hook to read

**Modify: `contracts/src/DepegShieldHook.sol`**
Expand Down
97 changes: 79 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ DepegShield closes this gap using [Reactive Network](https://reactive.network/),
```

- **ReactiveMonitor** (Reactive Network) - Subscribes to swap events on stablecoin pools across any supported chain. Tracks cumulative sell pressure over a rolling window. When imbalance crosses a configurable threshold, it fires a cross-chain callback to the protected pool's chain.
- **AlertReceiver** (deployed alongside the hook) - Receives callbacks and stores alert state with TTL-based expiry. Alerts decay automatically if not refreshed.
- **AlertReceiver** (deployed alongside the hook) - Receives callbacks and stores alert state. Alerts persist until the source pool recovers (ratio returns to balanced), ensuring continuous protection during ongoing depegs.
- **DepegShieldHook** reads the alert state in `beforeSwap`. When an alert is active, the hook multiplies its fee curve by a severity factor. Even a locally-balanced pool charges elevated fees if a cross-chain depeg is underway.

No off-chain bots. No centralized keepers. Fully on-chain. The source chains to monitor and the alert thresholds are configurable per deployment.
Expand Down Expand Up @@ -304,19 +304,29 @@ depegShield/
│ ├── src/
│ │ ├── DepegShieldHook.sol # Core hook: beforeSwap fee logic, afterSwap events
│ │ ├── FeeCurve.sol # 5-zone fee curve library
│ │ └── MockStablecoin.sol # Free-mint ERC20 for testnet demos
│ │ ├── AlertReceiver.sol # Cross-chain alert storage (per destination chain)
│ │ ├── MockStablecoin.sol # Free-mint ERC20 for testnet demos
│ │ ├── interfaces/
│ │ │ └── IAlertReceiver.sol
│ │ └── reactive/
│ │ └── ReactiveMonitor.sol # Reactive Network cross-chain monitor
│ ├── test/
│ │ ├── DepegShieldHook.t.sol # Hook behavior tests
│ │ ├── FeeCurve.t.sol # Fee curve unit + fuzz tests
│ │ └── DepegScenario.t.sol # Depeg simulation scenarios
│ │ ├── DepegScenario.t.sol # Depeg simulation scenarios
│ │ ├── AlertReceiver.t.sol # Alert receiver + pair registry tests
│ │ └── CrossChainFee.t.sol # Cross-chain fee floor tests
│ └── script/
│ ├── DeployAll.s.sol # Full deployment: tokens + hook + pool + liquidity
│ └── 00_DeployHook.s.sol # Hook-only CREATE2 deployment
│ ├── 01_DeployTokens.s.sol # Deploy mUSDC + mUSDT via CREATE2
│ ├── 02_DeployAlertReceiver.s.sol # Deploy AlertReceiver + register pair
│ ├── 03_DeployHook.s.sol # Mine salt + deploy hook (CREATE2)
│ ├── 04_CreatePool.s.sol # Init pool at 1:1 + seed liquidity
│ └── 05_DeployReactive.s.sol # ReactiveMonitor config reference
├── frontend/ # Next.js app
│ └── src/
│ ├── app/ # Landing page + Explore page
│ ├── components/ # FeeCurveChart, SimulationReplay, PoolHealthGauge
│ ├── components/ # FeeCurveChart, SimulationReplay, PoolHealthGauge, CrossChainAlert
│ └── lib/ # Fee curve math, simulation data
```

Expand Down Expand Up @@ -356,44 +366,95 @@ npm run dev

### Deploy

The `DeployAll` script deploys mock stablecoins (mUSDC, mUSDT), the hook, creates the pool, and adds initial liquidity in a single transaction.
Deployment uses 5 isolated scripts, run in order per chain. Each script is self-contained with its own env vars.

```bash
cd contracts
cp .env.example .env # Fill in PRIVATE_KEY, fund the wallet on target chains
source .env
forge script script/DeployAll.s.sol --rpc-url $UNICHAIN_SEPOLIA_RPC_URL --private-key $PRIVATE_KEY --broadcast

# Step 1: Deploy mock tokens (once per chain, deterministic addresses via CREATE2)
forge script script/01_DeployTokens.s.sol --rpc-url <RPC_URL> --private-key "\$PRIVATE_KEY" --broadcast

# Step 2: Deploy AlertReceiver + register mUSDC/mUSDT pair
# CALLBACK_PROXY varies per chain (see script comments for addresses)
CALLBACK_PROXY=0x... forge script script/02_DeployAlertReceiver.s.sol --rpc-url <RPC_URL> --private-key "\$PRIVATE_KEY" --broadcast

# Step 3: Deploy DepegShieldHook (mines CREATE2 salt for flag-encoded address)
ALERT_RECEIVER=0x... forge script script/03_DeployHook.s.sol --rpc-url <RPC_URL> --private-key "\$PRIVATE_KEY" --broadcast

# Step 4: Create pool at 1:1 price + seed 100K liquidity per side
HOOK=0x... forge script script/04_CreatePool.s.sol --rpc-url <RPC_URL> --private-key "\$PRIVATE_KEY" --broadcast

# Step 5: Deploy ReactiveMonitor on Reactive Lasna
# All pool/destination config MUST be in the constructor because the ReactVM
# creates an isolated state copy at deploy time. Post-deploy calls only
# affect the RNK chain state, not the ReactVM.
# See 05_DeployReactive.s.sol for the full constructor args with all 3 chains.
#
# Use cast send --create (forge create may time out on Reactive Lasna):
BYTECODE=\$(forge inspect src/reactive/ReactiveMonitor.sol:ReactiveMonitor bytecode)
ARGS=\$(cast abi-encode "c((uint256,address,bytes32,uint8,bytes32)[],(uint256,address)[])" \
"[(chainId,poolManager,pairId,poolType,poolId),...]" \
"[(chainId,alertReceiver),...]")
cast send --rpc-url https://lasna-rpc.rnk.dev/ --private-key "\$PRIVATE_KEY" \
--legacy --value 0.1ether --gas-limit 3000000 \
--create "\${BYTECODE}\${ARGS#0x}"

# After deployment, fund the callback proxies on each destination chain:
# cast send <CALLBACK_PROXY> "depositTo(address)" <MONITOR_ADDR> --value 0.01ether
```

---

## Testnet Deployments

All contracts are verified on their respective block explorers.

### Mock Tokens (same address on all chains via CREATE2)

| Token | Address | Decimals |
|-------|---------|----------|
| mUSDC | `0xD6E322dE450F9A276f2F3AFe72bC0C93D5284Ef0` | 6 |
| mUSDT | `0xf02383D4eBcF11016Df5AdAEB5899B947bcC0098` | 6 |
| mUSDC | [`0x58C414Bd85bf1d39985476Dfa5fBd59af356E8f0`](https://sepolia.etherscan.io/address/0x58C414Bd85bf1d39985476Dfa5fBd59af356E8f0) | 6 |
| mUSDT | [`0x2170d1eC7B1392611323A4c1793e580349CC5CC0`](https://sepolia.etherscan.io/address/0x2170d1eC7B1392611323A4c1793e580349CC5CC0) | 6 |

Both have a public `mint(address, uint256)` function for testing.

### Hook Addresses
### Sepolia (Chain ID: 11155111)

| Contract | Address | Explorer |
|----------|---------|----------|
| DepegShieldHook | `0xEDfFdabADd4263836403BF0D5F92a613Fc9f00C0` | [View](https://sepolia.etherscan.io/address/0xEDfFdabADd4263836403BF0D5F92a613Fc9f00C0) |
| AlertReceiver | `0x6bFe889e87A51634194B9447201548BEc8D825C3` | [View](https://sepolia.etherscan.io/address/0x6bFe889e87A51634194B9447201548BEc8D825C3) |

### Base Sepolia (Chain ID: 84532)

| Contract | Address | Explorer |
|----------|---------|----------|
| DepegShieldHook | `0xf8Fd12C76C606cA9bc3dAdeE9706B4357e6780c0` | [View](https://sepolia.basescan.org/address/0xf8Fd12C76C606cA9bc3dAdeE9706B4357e6780c0) |
| AlertReceiver | `0x92a8497C788d43572Fe29f144E6FF015AE3Ff22d` | [View](https://sepolia.basescan.org/address/0x92a8497C788d43572Fe29f144E6FF015AE3Ff22d) |

| Chain | Chain ID | Hook Address |
|-------|----------|-------------|
| Unichain Sepolia | 1301 | `0x3B101a77A6467E457b3CEFa7Fb4964Da1FBD40c0` |
| Sepolia | 11155111 | `0x06AAaA578EFe1A6ACbE78DAB5cdE791a0BF040C0` |
| Base Sepolia | 84532 | `0x1CF03b90D93D33C73d3215Ba73003C69EF6040c0` |
### Unichain Sepolia (Chain ID: 1301)

| Contract | Address | Explorer |
|----------|---------|----------|
| DepegShieldHook | `0x05e5c38f6ca3e76c30145eb73f1128B7749140C0` | [View](https://sepolia.uniscan.xyz/address/0x05e5c38f6ca3e76c30145eb73f1128B7749140C0) |
| AlertReceiver | `0xfe8BA3Fa183C98d637fd549f579670b3cB63b199` | [View](https://sepolia.uniscan.xyz/address/0xfe8BA3Fa183C98d637fd549f579670b3cB63b199) |

### Reactive Lasna (Chain ID: 5318007)

| Contract | Address | Explorer |
|----------|---------|----------|
| ReactiveMonitor | `0xfa5eeb94A58e5E83451C90E0915705E2d3a8EBA1` | [View](https://lasna.reactscan.net/address/0xf30180b9cec36f5a3762332c0f102fe8c024d64e/contract/0xfa5eeb94A58e5E83451C90E0915705E2d3a8EBA1) |

### Pool Configuration

| Parameter | Value |
|-----------|-------|
| currency0 | `0xD6E322dE450F9A276f2F3AFe72bC0C93D5284Ef0` (mUSDC) |
| currency1 | `0xf02383D4eBcF11016Df5AdAEB5899B947bcC0098` (mUSDT) |
| fee | `0x800000` (DYNAMIC_FEE_FLAG) |
| tickSpacing | 10 |
| LP range | +/- 1000 ticks (~+/-10% price range) |
| Initial price | 1:1 (sqrtPriceX96 = 2^96) |
| Initial liquidity | 100K per side |

Mock tokens have a public `mint(address, uint256)` function for testing.
9 changes: 8 additions & 1 deletion contracts/.env.example
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
# Private key for deployment (with 0x prefix)
PRIVATE_KEY=

# RPC URLs (public endpoints, no API key needed)
# RPC URLs
UNICHAIN_SEPOLIA_RPC_URL=https://sepolia.unichain.org
SEPOLIA_RPC_URL=https://ethereum-sepolia-rpc.publicnode.com
BASE_SEPOLIA_RPC_URL=https://sepolia.base.org
REACTIVE_RPC_URL=https://lasna-rpc.rnk.dev/

# Reactive Network Callback Proxy addresses (per destination chain)
# See https://dev.reactive.network/origins-and-destinations
CALLBACK_PROXY_SEPOLIA=0xc9f36411C9897e7F959D99ffca2a0Ba7ee0D7bDA
CALLBACK_PROXY_BASE_SEPOLIA=0xa6eA49Ed671B8a4dfCDd34E36b7a75Ac79B8A5a6
CALLBACK_PROXY_UNICHAIN_SEPOLIA=0x9299472A6399Fd1027ebF067571Eb3e3D7837FC4

# Block explorer API key (for contract verification, optional)
ETHERSCAN_API_KEY=
Loading
Loading