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
279 changes: 174 additions & 105 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,153 +1,222 @@
# OscillonHook
# Oscillon Hook

Inventory-risk protection hook for Uniswap v4 stable pools.
**Inventory-risk protection for Uniswap v4 stable pools.**

/\*
Oscillon is a dynamic-fee hook that monitors oracle prices on the token being sold, detects depeg conditions, and applies a **base fee plus depeg surcharge** so liquidity providers are compensated when traders drain below-peg stablecoins.

- ╔══════════════════════════════════════════════════════════════════╗
- ║ OscillonHook.sol v2.0 — Multi-Pool ║
- ║ ║
- ║ ARCHITECTURE CHANGE vs v1.x: ║
- ║ v1: immutable ORACLE0/ORACLE1/STABLE0/STABLE1 ║
- ║ → hardcoded to ONE pool forever at deploy ║
- ║ ║
- ║ v2: mapping(PoolId => PoolConfig) ║
- ║ → owner calls registerPool() for each stable pair ║
- ║ → supports USDC/USDT, USDC/DAI, USDT/DAI, ║
- ║ USDC/crvUSD, any stable pair with Chainlink feeds ║
- ║ ║
- ║ FREEZE REMOVED: ║
- ║ v1: severe depeg → revert PoolFrozen() → pool bricked ║
- ║ v2: severe depeg → fee capped at MAX_FEE_PIPS (50 bps) ║
- ║ swaps still flow, LPs still protected, nothing breaks ║
- ║ ║
- ║ Supported pools (register after deploy): ║
- ║ • USDC / USDT ║
- ║ • USDC / DAI ║
- ║ • USDT / DAI ║
- ║ • USDC / crvUSD ║
- ║ • any stable pair with a Chainlink USD feed ║
- ╚══════════════════════════════════════════════════════════════════╝
\*/
Designed for stable/stable pools such as USDC/USDT, USDC/DAI, and USDT/DAI.

## Overview
---

`OscillonHook` is a `beforeSwap` hook that protects LP inventory during stablecoin depeg events.
It monitors token-specific oracle prices, computes off-peg severity, and applies defensive fee/circuit-breaker policy.
## Why Oscillon Exists

Designed for stable/stable pools such as USDC/USDT and USDC/DAI.
Static swap fees (e.g. 3 bps flat) do not scale with depeg risk. When a stablecoin trades below $1, arbitrageurs can extract value from the pool while LPs absorb impaired inventory. Oscillon raises fees on **drain swaps** (selling the below-peg asset) while keeping normal swaps competitive at peg.

## What It Does
---

Implementation: `src/OscillonHook.sol`
## How It Works

1. Validates pool is composed of configured stables (`STABLE0`, `STABLE1`).
2. Reads the oracle for the input stable token.
3. Computes absolute depeg in basis points from $1.
4. Applies dynamic fee policy with deep-depeg protections.
5. Emits `DepegDetected(depegBps, fee, swapSize)`.
```
Swap (user sells tokenIn)
┌───────────────────────────────────────┐
│ OscillonHook.beforeSwap │
│ 1. Identify input token (token0/1) │
│ 2. Read Chainlink + pool TWAP │
│ 3. Compute depegBps + isDrain │
│ 4. Select fee = base + surcharge │
│ 5. Emit DepegDetected │
└───────────────────────────────────────┘
Uniswap v4 applies override fee
```

## Policy
| Component | Role |
|-----------|------|
| `ChainlinkOracleAdapter` | Primary USD price per token |
| `OscillonTwapOracle` | 30-minute pool TWAP fallback |
| `OscillonPriceEngine` | Blends sources; disagreement guard at 20 bps |
| `OscillonFeePolicy` | Hybrid piecewise + quadratic surcharge curve |
| `OscillonHook` | Orchestrates policy and returns dynamic fee |

Constants:
**Drain swap** = user sells the token that is below $1 (`pegBelow = true`).
**Restore swap** = user sells the above-peg token → base fee only.

- `SMALL_DEPEG_BPS = 7`
- `DRAIN_DEPEG_BPS = 20`
- `FREEZE_DEPEG_BPS = 60`
- `RESTORE_WINDOW = 1 hours`
---

Fee tiers (LP fee override pips):
## Fee Model

- Base: `100` (~1 bps)
- Small depeg: `800` (~8 bps)
- Drain tier: `2800` (~28 bps)
- Restore tier: `30` (~0.3 bps)
Oscillon uses **base fee + depeg surcharge** (not a replacement curve):

Notes:
```
total fee = BASE_FEE (3 bps) + depeg surcharge (hybrid curve)
```

- Severe depeg (`>= FREEZE_DEPEG_BPS`) freezes the path with `PoolFrozen()`.
- Deep-depeg swap-size cap now applies to both exact-in and exact-out requests.
- `RESTORE_FEE_PIPS` is intentionally lower than base to incentivize post-stress rebalancing flow.
| Scenario | Fee (approx.) |
|----------|----------------|
| At peg, any direction | **3 bps** base |
| 6 bps drain (sell below-peg USDT) | **4 bps** (3 + 1) |
| 20 bps drain | **9 bps** (3 + 6) |
| Restore direction (sell above-peg) | **3 bps** base |
| Max cap | **50 bps** |

Constants (`OscillonConstants.sol`):

- `BASE_FEE_PIPS = 300` (3 bps)
- `SMALL_DEPEG_BPS = 3` — dynamic path starts here
- `MAX_FEE_PIPS = 5000` (50 bps)
- `MAX_ORACLE_AGE = 25 hours`
- `ORACLE_DISAGREE_BPS = 20` — Chainlink vs TWAP blend threshold

On each swap the hook emits:

```solidity
event DepegDetected(
PoolId indexed poolId,
uint256 depegBps,
uint24 feeApplied,
uint256 swapSize,
bool isDrain,
bool usingFallback
);
```

## Security
---

- Hook entrypoints are restricted by `onlyPoolManager` (via `BaseHook`).
- Direct non-manager calls revert.
- Oracle sanity checks include:
- positive price
- not future timestamp
- fresh data within 1 hour
## Backtest Results

Liquidity operations remain available:
Historical simulations on USDC/USDT swap data compare Oscillon against a static 3 bps fee. Two periods are shown: a calm month (May 2026) and the USDC depeg crisis (March 2023).

- `beforeAddLiquidity = false`
- `beforeRemoveLiquidity = false`
- `afterAddLiquidity = false`
- `afterRemoveLiquidity = false`
### May 2026 — normal market conditions

## Testing
~18,500 chronological swaps with low volatility. Oscillon keeps fees near the 3 bps base while static and dynamic models earn similar total income (~$65k). The benefit shows up in the **3–7 bps deviation** bucket, where static LP capture drops to ~85% and Oscillon stays near 100%.

Tests: `test/OscillonHook.t.sol`
![Backtest May 2026](docs/assets/backtest-2026-05.png)

- `test_beforeSwap_AppliesPolicyLadder_WhenUSDTDepegs()`
- `test_beforeSwap_AppliesPolicyLadder_WhenUSDCDepegs()`
- `test_beforeSwap_Reverts_WhenCallerIsNotPoolManager()`
- `test_beforeSwap_Reverts_WhenInputStableIsAbovePegByFreezeThreshold()`
- `test_beforeSwap_Reverts_WhenExactOutputExceedsDeepDepegCap()`
### March 2023 — USDC depeg crisis

Run:
#### Minute-by-minute fee response

```bash
forge test -vvv
Chainlink-matched replay from March 10–16, 2023. As USDC depegged to ~1,200 bps, Oscillon scaled fees from 3 bps to the 50 bps cap while static stayed flat.

![Depeg timeline March 2023](docs/assets/depeg-timeline-2023-03.png)

*Top: oracle depeg magnitude. Middle: static 3 bps vs Oscillon hybrid. Bottom: remaining arb gap after fee (drain protection proxy).*

#### Full-period dashboard (~6,200 swaps)

Fee curves, LVR distribution, cumulative LP income, and LP capture % over the March 2023 stress window. Income spikes around swap 2,500–3,500 when the depeg hit. Oscillon hybrid ends ~2× static (~$450k vs ~$215k) and holds 25–50% LP capture in the 15–30+ bps ranges where static collapses below 10%.

![Backtest March 2023](docs/assets/backtest-2023-03.png)

#### Model comparison (same period)

Side-by-side fee models including hybrid, piecewise, quadratic (K=45), and additive (base + drain tax). Additive and paper-minimum benchmarks show the upper bound on LP protection.

![Backtest results March 2023](docs/assets/backtest-results.png)

> Backtests are illustrative simulations on historical data. On-chain behavior depends on oracle freshness, pool liquidity, and swap direction. See [Known Limitations](#known-limitations).

---

## Architecture

```
src/
├── OscillonHook.sol # beforeSwap fee override + pool registry
├── oracle/
│ ├── OscillonPriceEngine.sol # Chainlink + TWAP cascade
│ └── adapters/
│ └── ChainlinkOracleAdapter.sol
├── libraries/
│ ├── OscillonFeePolicy.sol # Hybrid surcharge curve
│ ├── OscillonDepegMath.sol # Depeg bps + disagreement guard
│ └── OscillonTwapOracle.sol # In-pool TWAP ring buffer
└── constants/
└── OscillonConstants.sol
```

## Deployment Script
**Multi-pool support:** owner calls `registerPool()` per stable pair with Chainlink adapters for each token. No pool freeze — severe depeg caps fee at 50 bps; swaps continue.

A CREATE2 + HookMiner deployment script is included:
---

- `script/DeployOscillonHook.s.sol`
## Quick Start

Required env vars:
### Build & test

- `POOL_MANAGER`
- `ORACLE0`
- `STABLE0`
- `STABLE0_DECIMALS`
- `ORACLE1`
- `STABLE1`
- `STABLE1_DECIMALS`
```bash
forge build
forge test --match-contract OscillonHook -vv
```

Example:
### Deploy (Anvil local)

```bash
forge script script/DeployOscillonHook.s.sol:DeployOscillonHookScript --broadcast --rpc-url <RPC_URL>
# Terminal 1
anvil --chain-id 31337

# Terminal 2
source .env # PRIVATE_KEY must have 0x prefix
forge script script/DeployOscillon.s.sol:DeployOscillon \
--rpc-url http://127.0.0.1:8545 \
--broadcast
```

## Build Commands
Deployment addresses are written to `oscillon-ui/src/deployment.json`.

### Deploy (Arbitrum Sepolia)

```bash
forge build
forge test
forge fmt
forge snapshot
forge script script/DeployOscillon.s.sol:DeployOscillon \
--rpc-url "$RPC_URL" \
--broadcast
```

## Known Limitations (Pre-Audit)
---

## Frontend Integration

The UI should show **three linked values** per swap:

1. **Hook price** — effective USD price for the input token (Chainlink + TWAP logic)
2. **Depeg deviation** — bps from $1 for that token
3. **Total fee** — base (3 bps) + surcharge, from `DepegDetected` or swap simulation

Use `getDeployment(chainId)` from `oscillon-ui/src/deployment.config.ts`.
Fee quotes must be **per swap direction** — `getPoolState()` only reflects token0.

---

## Security

- Hook callbacks restricted to `PoolManager` via `BaseHook`
- Chainlink staleness: `block.timestamp > updatedAt + 25h` → TWAP fallback
- Oracle disagreement > 20 bps → conservative price (closer to $1)
- Exact-output swaps disabled when `depegBps >= 3`
- Rolling drain multiplier increases fee under sustained drain pressure

Liquidity add/remove is unrestricted (no hook on LP operations).

---

**Surplus accounting (indicative only)**
## Known Limitations

`surplusAccrued` and `protocolAccrued` track theoretical LP surplus from dynamic fees but are not connected to actual v4 fee settlement. These values are meaningful as indicators of mechanism activity but `collectProtocolFees` is not safe for production use without implementing proper fee skimming via `donate()` or `afterSwapReturnDelta`. This is a known P0 for mainnet — deferred for POC submission.
**Surplus accounting (indicative only)**
`surplusAccrued` and `protocolAccrued` track theoretical surplus from dynamic fees but are not connected to actual v4 fee settlement. `collectProtocolFees` requires proper fee skimming via `donate()` or `afterSwapReturnDelta` for production.

**Rounding direction**
**Oracle latency**
TWAP window is 30 minutes. During fast depegs, TWAP can lag spot. Chainlink freshness uses a 25-hour `maxAge`.

Fee surplus math uses `mulDivDown` throughout. This is conservative (slightly under-charges). A production deployment would implement `mulDivUp` on surplus accrual per the rounding policy documented in the audit report.
**K parameter**
`K_STANDARD = 45` used universally; thin-pool tier not yet wired to live liquidity reads.

**K parameter liquidity threshold**
**POC scope**
Static thresholds; single oracle per token; economic calibration from backtests, not live mainnet stress.

`THIN_POOL_LIQUIDITY` comparison uses incorrect units (USDC atoms vs AMM liquidity units). Fixed by using `K_STANDARD=45` universally in this version.
---

## MVP Limitations
## License

- Static thresholds and fee tiers (not governance-tunable yet)
- Single oracle feed per token (no fallback aggregation)
- Economic parameter calibration should be validated with deeper simulation
MIT
Loading