From 64855d21293d8bd49f1145b38f21b02f24c3fb6f Mon Sep 17 00:00:00 2001 From: sammed-21 Date: Mon, 27 Apr 2026 16:06:46 +0530 Subject: [PATCH] test --- script/DeployOscillonHook.s.sol | 64 ++------ src/OscillonHook.sol | 187 ++++++++--------------- src/interface/IAggregatorV3Interface.sol | 21 +-- test/OscillonHook.t.sol | 36 +---- 4 files changed, 81 insertions(+), 227 deletions(-) diff --git a/script/DeployOscillonHook.s.sol b/script/DeployOscillonHook.s.sol index 7940932..826717a 100644 --- a/script/DeployOscillonHook.s.sol +++ b/script/DeployOscillonHook.s.sol @@ -23,12 +23,10 @@ import {OscillonHook} from "../src/OscillonHook.sol"; contract DeployOscillonHookScript is Script { // Foundry deterministic CREATE2 deployer proxy - address constant CREATE2_DEPLOYER = - 0x4e59b44847b379578588920cA78FbF26c0B4956C; + address constant CREATE2_DEPLOYER = 0x4e59b44847b379578588920cA78FbF26c0B4956C; // Arbitrum v4 PoolManager from Uniswap v4 deployments - address constant ARBITRUM_POOL_MANAGER = - 0x360E68faCcca8cA495c1B759Fd9EEe466db9FB32; + address constant ARBITRUM_POOL_MANAGER = 0x360E68faCcca8cA495c1B759Fd9EEe466db9FB32; // Stablecoins address constant USDC = 0xaf88d065e77c8cC2239327C5EDb3A432268e5831; @@ -43,10 +41,7 @@ contract DeployOscillonHookScript is Script { address constant CL_CRVUSD_USD = 0x0a32255dd4BB6177C994bAAc73E0606fDD568f66; function run() external { - address poolManager = vm.envOr( - "POOL_MANAGER", - ARBITRUM_POOL_MANAGER - ); + address poolManager = vm.envOr("POOL_MANAGER", ARBITRUM_POOL_MANAGER); uint256 deployerKey = vm.envUint("PRIVATE_KEY"); address deployer = vm.addr(deployerKey); @@ -55,60 +50,19 @@ contract DeployOscillonHookScript is Script { uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG); (address hookAddr, bytes32 salt) = HookMiner.find( - CREATE2_DEPLOYER, - flags, - type(OscillonHook).creationCode, - abi.encode(IPoolManager(poolManager)) + CREATE2_DEPLOYER, flags, type(OscillonHook).creationCode, abi.encode(IPoolManager(poolManager)) ); - OscillonHook hook = new OscillonHook{salt: salt}( - IPoolManager(poolManager) - ); + OscillonHook hook = new OscillonHook{salt: salt}(IPoolManager(poolManager)); require(address(hook) == hookAddr, "Hook address mismatch"); console2.log("OscillonHook deployed at:", address(hook)); console2.log("PoolManager:", poolManager); - _registerSortedPool( - hook, - USDC, - USDT, - CL_USDC_USD, - CL_USDT_USD, - 6, - 6, - "USDC/USDT" - ); - _registerSortedPool( - hook, - USDC, - DAI, - CL_USDC_USD, - CL_DAI_USD, - 6, - 18, - "USDC/DAI" - ); - _registerSortedPool( - hook, - USDT, - DAI, - CL_USDT_USD, - CL_DAI_USD, - 6, - 18, - "USDT/DAI" - ); - _registerSortedPool( - hook, - USDC, - CRVUSD, - CL_USDC_USD, - CL_CRVUSD_USD, - 6, - 18, - "USDC/crvUSD" - ); + _registerSortedPool(hook, USDC, USDT, CL_USDC_USD, CL_USDT_USD, 6, 6, "USDC/USDT"); + _registerSortedPool(hook, USDC, DAI, CL_USDC_USD, CL_DAI_USD, 6, 18, "USDC/DAI"); + _registerSortedPool(hook, USDT, DAI, CL_USDT_USD, CL_DAI_USD, 6, 18, "USDT/DAI"); + _registerSortedPool(hook, USDC, CRVUSD, CL_USDC_USD, CL_CRVUSD_USD, 6, 18, "USDC/crvUSD"); vm.stopBroadcast(); diff --git a/src/OscillonHook.sol b/src/OscillonHook.sol index 6d2f82b..177e304 100644 --- a/src/OscillonHook.sol +++ b/src/OscillonHook.sol @@ -7,10 +7,7 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; -import { - BeforeSwapDelta, - BeforeSwapDeltaLibrary -} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol"; import {IAggregatorV3Interface} from "./interface/IAggregatorV3Interface.sol"; @@ -33,30 +30,15 @@ contract OscillonHook is BaseHook { // ── Events ─────────────────────────────────────────────────────────────── /// @notice Emitted on every swap — useful for dashboard indexing. - event DepegDetected( - PoolId indexed poolId, - uint256 depegBps, - uint24 feeApplied, - uint256 swapSize, - bool isDrain - ); + event DepegDetected(PoolId indexed poolId, uint256 depegBps, uint24 feeApplied, uint256 swapSize, bool isDrain); /// @notice Emitted when owner registers a new stable pool. - event PoolRegistered( - PoolId indexed poolId, - address token0, - address token1, - address oracle0, - address oracle1 - ); + event PoolRegistered(PoolId indexed poolId, address token0, address token1, address oracle0, address oracle1); /// @notice Emitted when pool config is updated (oracle change etc). event PoolUpdated(PoolId indexed poolId); - event OwnershipTransferred( - address indexed oldOwner, - address indexed newOwner - ); + event OwnershipTransferred(address indexed oldOwner, address indexed newOwner); modifier onlyOwner() { if (msg.sender != owner) revert NotOwner(); @@ -78,7 +60,7 @@ contract OscillonHook is BaseHook { /// Severe depeg → fee capped here instead of freezing. /// Pool keeps running. LPs still protected by high cost of arb. uint24 public constant MAX_FEE_PIPS = 5000; // 50 bps — severe depeg cap - + // ── Depeg thresholds (bps) ──────────────────────────────────────────────── uint256 public constant SMALL_DEPEG_BPS = 7; @@ -146,29 +128,23 @@ contract OscillonHook is BaseHook { // ── Hook permissions ────────────────────────────────────────────────────── - function getHookPermissions() - public - pure - override - returns (Hooks.Permissions memory) - { - return - Hooks.Permissions({ - beforeInitialize: false, - afterInitialize: false, - beforeAddLiquidity: false, - afterAddLiquidity: false, - beforeRemoveLiquidity: false, - afterRemoveLiquidity: false, - beforeSwap: true, - afterSwap: false, - beforeDonate: false, - afterDonate: false, - beforeSwapReturnDelta: false, - afterSwapReturnDelta: false, - afterAddLiquidityReturnDelta: false, - afterRemoveLiquidityReturnDelta: false - }); + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + afterAddLiquidity: false, + beforeRemoveLiquidity: false, + afterRemoveLiquidity: false, + beforeSwap: true, + afterSwap: false, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); } // ── Pool registration ───────────────────────────────────────────────────── @@ -197,8 +173,9 @@ contract OscillonHook is BaseHook { uint8 stableDecimals0, uint8 stableDecimals1 ) external onlyOwner { - if (oracle0 == address(0) || oracle1 == address(0)) + if (oracle0 == address(0) || oracle1 == address(0)) { revert ZeroAddress(); + } address t0 = Currency.unwrap(key.currency0); address t1 = Currency.unwrap(key.currency1); @@ -219,10 +196,8 @@ contract OscillonHook is BaseHook { oracle1: oracle1, oracle0Decimals: dec0, oracle1Decimals: dec1, - maxDepegSwap0: MAX_DEPEG_SWAP_FACTOR * - (10 ** uint256(stableDecimals0)), - maxDepegSwap1: MAX_DEPEG_SWAP_FACTOR * - (10 ** uint256(stableDecimals1)), + maxDepegSwap0: MAX_DEPEG_SWAP_FACTOR * (10 ** uint256(stableDecimals0)), + maxDepegSwap1: MAX_DEPEG_SWAP_FACTOR * (10 ** uint256(stableDecimals1)), lastHighDepegAt: 0, surplusAccrued: 0 }); @@ -232,23 +207,14 @@ contract OscillonHook is BaseHook { /// @notice Update oracle addresses for a registered pool. /// Use if Chainlink deprecates a feed. - function updatePoolOracles( - PoolKey calldata key, - address newOracle0, - address newOracle1 - ) external onlyOwner { - require( - IAggregatorV3Interface(newOracle0).decimals() > 0, - "Invalid oracle" - ); - require( - IAggregatorV3Interface(newOracle1).decimals() > 0, - "Invalid oracle" - ); + function updatePoolOracles(PoolKey calldata key, address newOracle0, address newOracle1) external onlyOwner { + require(IAggregatorV3Interface(newOracle0).decimals() > 0, "Invalid oracle"); + require(IAggregatorV3Interface(newOracle1).decimals() > 0, "Invalid oracle"); PoolId id = key.toId(); if (!poolConfigs[id].registered) revert PoolNotRegistered(); - if (newOracle0 == address(0) || newOracle1 == address(0)) + if (newOracle0 == address(0) || newOracle1 == address(0)) { revert ZeroAddress(); + } PoolConfig storage cfg = poolConfigs[id]; @@ -276,12 +242,11 @@ contract OscillonHook is BaseHook { /// If _beforeSwap is called for a pool that isn't registered, /// we return BASE_FEE silently — the hook gracefully does nothing. /// This prevents reverts on any pool that happens to use this hook address. - function _beforeSwap( - address, - PoolKey calldata key, - SwapParams calldata params, - bytes calldata - ) internal override returns (bytes4, BeforeSwapDelta, uint24) { + function _beforeSwap(address, PoolKey calldata key, SwapParams calldata params, bytes calldata) + internal + override + returns (bytes4, BeforeSwapDelta, uint24) + { PoolId id = key.toId(); PoolConfig storage cfg = poolConfigs[id]; @@ -300,17 +265,14 @@ contract OscillonHook is BaseHook { emit DepegDetected(id, ctx.depegBps, fee, ctx.swapSize, ctx.isDrain); // ── 7. Return fee with OVERRIDE_FEE_FLAG - return ( - this.beforeSwap.selector, - BeforeSwapDeltaLibrary.ZERO_DELTA, - fee | LPFeeLibrary.OVERRIDE_FEE_FLAG - ); + return (this.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, fee | LPFeeLibrary.OVERRIDE_FEE_FLAG); } - function _buildSwapContext( - PoolConfig storage cfg, - SwapParams calldata params - ) internal view returns (SwapContext memory ctx) { + function _buildSwapContext(PoolConfig storage cfg, SwapParams calldata params) + internal + view + returns (SwapContext memory ctx) + { // zeroForOne = true -> tokenIn is token0 // zeroForOne = false -> tokenIn is token1 bool tokenInIsToken0 = params.zeroForOne; @@ -320,13 +282,10 @@ contract OscillonHook is BaseHook { } address oracleAddr = tokenInIsToken0 ? cfg.oracle0 : cfg.oracle1; - uint8 oracleDec = tokenInIsToken0 - ? cfg.oracle0Decimals - : cfg.oracle1Decimals; + uint8 oracleDec = tokenInIsToken0 ? cfg.oracle0Decimals : cfg.oracle1Decimals; (uint256 depegBps, bool pegBelow) = _readDepeg(oracleAddr, oracleDec); - uint256 swapSize = params.amountSpecified < 0 - ? uint256(-params.amountSpecified) - : uint256(params.amountSpecified); + uint256 swapSize = + params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified); ctx = SwapContext({ depegBps: depegBps, isDrain: pegBelow, @@ -359,13 +318,9 @@ contract OscillonHook is BaseHook { /// → additional swap size cap applied // test this each line - function _selectFee( - PoolConfig storage cfg, - SwapContext memory ctx - ) internal returns (uint24 fee) { + function _selectFee(PoolConfig storage cfg, SwapContext memory ctx) internal returns (uint24 fee) { // ── Restore window check ────────────────────────────────────────────── - bool inRestoreWindow = cfg.lastHighDepegAt != 0 && - (block.timestamp - cfg.lastHighDepegAt) <= RESTORE_WINDOW; + bool inRestoreWindow = cfg.lastHighDepegAt != 0 && (block.timestamp - cfg.lastHighDepegAt) <= RESTORE_WINDOW; // ── Healthy pool — no depeg ─────────────────────────────────────────── if (ctx.depegBps < SMALL_DEPEG_BPS) { @@ -407,17 +362,12 @@ contract OscillonHook is BaseHook { // ── Large swap cap during drain ─────────────────────────────────────── // Only applies to exact-in swaps above the size threshold. // Retail swaps (<$50k) never hit this. - uint256 maxSwap = ctx.tokenInIsToken0 - ? cfg.maxDepegSwap0 - : cfg.maxDepegSwap1; + uint256 maxSwap = ctx.tokenInIsToken0 ? cfg.maxDepegSwap0 : cfg.maxDepegSwap1; if (ctx.amountSpecified < 0 && ctx.swapSize > maxSwap) { // Cap the swap size — excess reverts // In production this uses BeforeSwapDelta to limit input // For MVP: simple require - require( - ctx.swapSize <= maxSwap, - "Oscillon: drain swap exceeds size limit" - ); + require(ctx.swapSize <= maxSwap, "Oscillon: drain swap exceeds size limit"); } // Accrue surplus for LP redistribution if (fee > BASE_FEE_PIPS) { @@ -434,45 +384,34 @@ contract OscillonHook is BaseHook { /// @notice Reads a Chainlink feed and returns deviation from $1 peg. /// @return depegBps Deviation in basis points /// @return pegBelow True if token is trading below $1 - function _readDepeg( - address oracleAddr, - uint8 oracleDec - ) internal view returns (uint256 depegBps, bool pegBelow) { - ( - uint80 roundId, - int256 answer, - , - uint256 updatedAt, - uint80 answeredInRound - ) = IAggregatorV3Interface(oracleAddr).latestRoundData(); + function _readDepeg(address oracleAddr, uint8 oracleDec) internal view returns (uint256 depegBps, bool pegBelow) { + (uint80 roundId, int256 answer,, uint256 updatedAt, uint80 answeredInRound) = + IAggregatorV3Interface(oracleAddr).latestRoundData(); if (answer <= 0) revert OracleAnswerInvalid(); - if (answeredInRound < roundId) + if (answeredInRound < roundId) { revert OracleRoundIncomplete(roundId, answeredInRound); - if (block.timestamp > updatedAt + MAX_ORACLE_AGE) + } + if (block.timestamp > updatedAt + MAX_ORACLE_AGE) { revert OracleStale(updatedAt, block.timestamp); + } // Normalise to 1e18. $1.0000 = 1e18. /** * 0.89 / 18 */ - uint256 price1e18 = (uint256(answer) * 1e18) / - (10 ** uint256(oracleDec)); + uint256 price1e18 = (uint256(answer) * 1e18) / (10 ** uint256(oracleDec)); pegBelow = price1e18 < 1e18; - depegBps = pegBelow - ? ((1e18 - price1e18) * 10_000) / 1e18 - : ((price1e18 - 1e18) * 10_000) / 1e18; + depegBps = pegBelow ? ((1e18 - price1e18) * 10_000) / 1e18 : ((price1e18 - 1e18) * 10_000) / 1e18; } // ── View helpers ────────────────────────────────────────────────────────── /// @notice Read the current state of any registered pool. /// Used by the Oscillon dashboard. - function getPoolState( - PoolKey calldata key - ) + function getPoolState(PoolKey calldata key) external view returns ( @@ -490,9 +429,7 @@ contract OscillonHook is BaseHook { registered = cfg.registered; surplusAccrued = cfg.surplusAccrued; - inRestoreWindow = - cfg.lastHighDepegAt != 0 && - (block.timestamp - cfg.lastHighDepegAt) <= RESTORE_WINDOW; + inRestoreWindow = cfg.lastHighDepegAt != 0 && (block.timestamp - cfg.lastHighDepegAt) <= RESTORE_WINDOW; if (!cfg.registered) return (false, 0, false, 0, false, false, 0); @@ -501,9 +438,7 @@ contract OscillonHook is BaseHook { } /// @notice Returns all config for a pool — useful for integrators. - function getPoolConfig( - PoolKey calldata key - ) external view returns (PoolConfig memory) { + function getPoolConfig(PoolKey calldata key) external view returns (PoolConfig memory) { return poolConfigs[key.toId()]; } diff --git a/src/interface/IAggregatorV3Interface.sol b/src/interface/IAggregatorV3Interface.sol index 8c76a32..a04d25a 100644 --- a/src/interface/IAggregatorV3Interface.sol +++ b/src/interface/IAggregatorV3Interface.sol @@ -1,29 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; + interface IAggregatorV3Interface { function decimals() external view returns (uint8); function description() external view returns (string memory); function version() external view returns (uint256); - function getRoundData( - uint80 _roundId - ) + function getRoundData(uint80 _roundId) external view - returns ( - uint80 roundId, - int256 answer, - uint256 startedAt, - uint256 updatedAt, - uint80 answeredInRound - ); + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); function latestRoundData() external view - returns ( - uint80 roundId, - int256 answer, - uint256 startedAt, - uint256 updatedAt, - uint80 answeredInRound - ); + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); } diff --git a/test/OscillonHook.t.sol b/test/OscillonHook.t.sol index ae4c303..de1c5a1 100644 --- a/test/OscillonHook.t.sol +++ b/test/OscillonHook.t.sol @@ -22,13 +22,7 @@ import {console} from "forge-std/console.sol"; contract OscillonHookBasicTest is Test, Deployers { using PoolIdLibrary for PoolKey; - event DepegDetected( - PoolId indexed poolId, - uint256 depegBps, - uint24 feeApplied, - uint256 swapSize, - bool isDrain - ); + event DepegDetected(PoolId indexed poolId, uint256 depegBps, uint24 feeApplied, uint256 swapSize, bool isDrain); uint256 constant AMOUNT_IN = 1e15; uint24 constant MAX_FEE_PIPS = 5000; // severe depeg fee cap in hook @@ -72,19 +66,9 @@ contract OscillonHookBasicTest is Test, Deployers { } console.log(Currency.unwrap(c0), Currency.unwrap(c1)); - (poolKey, ) = initPool( - c0, - c1, - IHooks(address(hook)), - LPFeeLibrary.DYNAMIC_FEE_FLAG, - SQRT_PRICE_1_1 - ); + (poolKey,) = initPool(c0, c1, IHooks(address(hook)), LPFeeLibrary.DYNAMIC_FEE_FLAG, SQRT_PRICE_1_1); - modifyLiquidityRouter.modifyLiquidity( - poolKey, - LIQUIDITY_PARAMS, - ZERO_BYTES - ); + modifyLiquidityRouter.modifyLiquidity(poolKey, LIQUIDITY_PARAMS, ZERO_BYTES); // Register pool using oracle order that matches currency0/currency1. address oracleForCurrency0; @@ -98,7 +82,7 @@ contract OscillonHookBasicTest is Test, Deployers { } // Use low-level call to avoid PoolKey type conflicts between remapped deps. - (bool ok, ) = address(hook).call( + (bool ok,) = address(hook).call( abi.encodeWithSignature( "registerPool((address,address,uint24,int24,address),address,address,uint8,uint8)", Currency.unwrap(poolKey.currency0), @@ -119,12 +103,9 @@ contract OscillonHookBasicTest is Test, Deployers { // Depeg stable1 from $1.00 -> $0.89 (11% depeg = 1100 bps). oracle1.updateAnswer(990000000000000000); - bool stable1IsCurrency0 = Currency.unwrap(poolKey.currency0) == - address(stable1); + bool stable1IsCurrency0 = Currency.unwrap(poolKey.currency0) == address(stable1); bool zeroForOne = stable1IsCurrency0; // sell stable1 into pool - uint160 sqrtPriceLimitX96 = zeroForOne - ? (TickMath.MIN_SQRT_PRICE + 1) - : (TickMath.MAX_SQRT_PRICE - 1); + uint160 sqrtPriceLimitX96 = zeroForOne ? (TickMath.MIN_SQRT_PRICE + 1) : (TickMath.MAX_SQRT_PRICE - 1); vm.expectEmit(true, false, false, true, address(hook)); emit DepegDetected(poolKey.toId(), 100, MAX_FEE_PIPS, AMOUNT_IN, true); @@ -136,10 +117,7 @@ contract OscillonHookBasicTest is Test, Deployers { amountSpecified: -int256(AMOUNT_IN), sqrtPriceLimitX96: sqrtPriceLimitX96 }), - PoolSwapTest.TestSettings({ - takeClaims: false, - settleUsingBurn: false - }), + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}), "" ); }