From 33c3d02f874536f15a70d82b670c3934492dde8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Thu, 11 Sep 2025 18:49:34 -0300 Subject: [PATCH 01/28] Lint --- .prettierrc.json | 2 +- .../hooks-quantamm/HyperSurgeHook.sol | 4 ++-- .../contracts/hooks-quantamm/LPNFT.sol | 18 ++++++------------ .../test/foundry/HyperSurgeMaxDeviation.t.sol | 1 - 4 files changed, 9 insertions(+), 16 deletions(-) diff --git a/.prettierrc.json b/.prettierrc.json index 452c7148..b474e5de 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -12,7 +12,7 @@ "options": { "singleQuote": false, "tabWidth": 4, - "compiler": "0.8.24" + "compiler": "0.8.26" } }, { diff --git a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol index b055adec..cab3d441 100644 --- a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol +++ b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol @@ -669,7 +669,7 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi // Pool-implied spot for j vs i: (Bj/wj) / (Bi/wi) locals.poolPx = _pairSpotFromBalancesWeights(locals.bj, locals.wj, locals.bi, locals.wi); - + if (locals.poolPx == 0) { continue; } @@ -677,7 +677,7 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi // External ratio j/i locals.extPx = locals.pxj.divDown(locals.pxi); locals.dev = _relAbsDiff(locals.poolPx, locals.extPx); - + if (locals.dev > locals.maxDev) { locals.maxDev = locals.dev; } diff --git a/pkg/pool-hooks/contracts/hooks-quantamm/LPNFT.sol b/pkg/pool-hooks/contracts/hooks-quantamm/LPNFT.sol index 43fcc74e..a1d4d2ed 100644 --- a/pkg/pool-hooks/contracts/hooks-quantamm/LPNFT.sol +++ b/pkg/pool-hooks/contracts/hooks-quantamm/LPNFT.sol @@ -4,16 +4,15 @@ pragma solidity >=0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "./UpliftOnlyExample.sol"; -/** +/** * @notice This contract is a simple ERC721 contract for LP NFTs. It overrides update which means * that the deposits can be transferred feely between users. * this does require the router to change the vault balances and deposit records as well */ -/// @title LPNFT contract for QuantAMM LP NFTs -/// @notice implements ERC721 for LP NFTs +/// @title LPNFT contract for QuantAMM LP NFTs +/// @notice implements ERC721 for LP NFTs contract LPNFT is ERC721 { - uint256 numMinted; /// @notice the address of the QuantAMM router this token is for @@ -21,16 +20,11 @@ contract LPNFT is ERC721 { /// @notice Modifier for only allowing the router to call certain functions modifier onlyUpliftOnlyRouter() { - require(msg.sender == address(router), "ROUTERONLY"); + require(msg.sender == address(router), "ROUTERONLY"); _; } - - constructor( - string memory _name, - string memory _symbol, - address _router - ) ERC721(_name, _symbol) { + constructor(string memory _name, string memory _symbol, address _router) ERC721(_name, _symbol) { router = UpliftOnlyExample(payable(_router)); } @@ -47,7 +41,7 @@ contract LPNFT is ERC721 { /// @inheritdoc ERC721 function _update(address to, uint256 tokenId, address auth) internal override returns (address previousOwner) { - previousOwner = super._update(to, tokenId, auth); + previousOwner = super._update(to, tokenId, auth); //_update is called during mint, burn and transfer. This functionality is only for transfer if (to != address(0) && previousOwner != address(0)) { //if transfering the record in the vault needs to be changed to reflect the change in ownership diff --git a/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol b/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol index f938c66d..8af458cc 100644 --- a/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol +++ b/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol @@ -162,7 +162,6 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { return fee; } - /// 1) Below threshold ⇒ the dynamic fee must equal the static (minimum) fee. function testFuzz_feeBelowThreshold_min(uint8 nSeed, uint256 wSeed, uint256 bSeed, uint256 dSeed) public view { uint8 n = uint8(bound(nSeed, 2, 8)); From 51a467da5414087ba076a3ce10dff361f0b04add Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Fri, 12 Sep 2025 10:43:51 -0300 Subject: [PATCH 02/28] Fix tests with forge using contracts compiled with hardhat --- pkg/pool-hooks/contracts/test/HardhatImports.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/pool-hooks/contracts/test/HardhatImports.sol b/pkg/pool-hooks/contracts/test/HardhatImports.sol index 88e30256..d71c0b9c 100644 --- a/pkg/pool-hooks/contracts/test/HardhatImports.sol +++ b/pkg/pool-hooks/contracts/test/HardhatImports.sol @@ -16,8 +16,9 @@ import { RateProviderMock } from "@balancer-labs/v3-vault/contracts/test/RatePro import { WETHTestToken } from "@balancer-labs/v3-solidity-utils/contracts/test/WETHTestToken.sol"; -import { WeightedPool } from "@balancer-labs/v3-pool-weighted/contracts/WeightedPool.sol"; import { WeightedPoolFactory } from "@balancer-labs/v3-pool-weighted/contracts/WeightedPoolFactory.sol"; +import { WeightedPoolMock } from "@balancer-labs/v3-pool-weighted/contracts/test/WeightedPoolMock.sol"; +import { WeightedPool } from "@balancer-labs/v3-pool-weighted/contracts/WeightedPool.sol"; import { StablePool } from "@balancer-labs/v3-pool-stable/contracts/StablePool.sol"; import { StablePoolFactory } from "@balancer-labs/v3-pool-stable/contracts/StablePoolFactory.sol"; From 90caadc41ea109ea47906341ae14190b713b0395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Fri, 12 Sep 2025 11:11:18 -0300 Subject: [PATCH 03/28] Fix percentage setters --- .../hooks-quantamm/HyperSurgeHook.sol | 89 ++++++++++--------- 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol index cab3d441..383bbcc6 100644 --- a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol +++ b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol @@ -79,6 +79,13 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi uint256 private immutable _defaultCapDeviationPercentage18; + modifier ensureValidPercentage(uint256 percentageValue) { + if (percentageValue < 1e9 || percentageValue > 1e18 || percentageValue % 1e9 != 0) { + revert InvalidPercentage(); + } + _; + } + constructor( IVault vault, uint256 defaultMaxSurgeFeePercentage18, @@ -108,22 +115,22 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi TokenConfig[] memory tokenCfgs, LiquidityManagement calldata ) public override onlyVault returns (bool) { - PoolDetails memory details; - if (tokenCfgs.length >= 2 && tokenCfgs.length <= 8) { - details.arbMaxSurgeFee9 = _safeConvertTo9Decimals(_defaultMaxSurgeFeePercentage18); - details.arbThresholdPercentage9 = _safeConvertTo9Decimals(_defaultThresholdPercentage18); - details.arbCapDeviationPercentage9 = _safeConvertTo9Decimals(_defaultCapDeviationPercentage18); - details.noiseMaxSurgeFee9 = _safeConvertTo9Decimals(_defaultMaxSurgeFeePercentage18); - details.noiseThresholdPercentage9 = _safeConvertTo9Decimals(_defaultThresholdPercentage18); - details.noiseCapDeviationPercentage9 = _safeConvertTo9Decimals(_defaultCapDeviationPercentage18); - - details.numTokens = uint8(tokenCfgs.length); - - _poolCfg[pool].details = details; - } else { + if (tokenCfgs.length < 2 && tokenCfgs.length > 8) { revert NumTokensOutOfRange(); } + PoolDetails memory details; + details.numTokens = uint8(tokenCfgs.length); + // Set the pool details, so we can use the setters and emit the proper events. + _poolCfg[pool].details = details; + + setMaxSurgeFeePercentage(pool, _defaultMaxSurgeFeePercentage18, TradeType.ARBITRAGE); + setMaxSurgeFeePercentage(pool, _defaultMaxSurgeFeePercentage18, TradeType.NOISE); + setSurgeThresholdPercentage(pool, _defaultThresholdPercentage18, TradeType.ARBITRAGE); + setSurgeThresholdPercentage(pool, _defaultThresholdPercentage18, TradeType.NOISE); + setCapDeviationPercentage(pool, _defaultCapDeviationPercentage18, TradeType.ARBITRAGE); + setCapDeviationPercentage(pool, _defaultCapDeviationPercentage18, TradeType.NOISE); + return true; } @@ -202,74 +209,68 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi ///@inheritdoc IHyperSurgeHook function setMaxSurgeFeePercentage( address pool, - uint256 pct18, + uint256 newMaxSurgeFeePercentageScaled18, TradeType tradeType - ) external override onlySwapFeeManagerOrGovernance(pool) { - _ensureValidPct(pct18); - + ) public override onlySwapFeeManagerOrGovernance(pool) ensureValidPercentage(newMaxSurgeFeePercentageScaled18) { if (tradeType == TradeType.ARBITRAGE) { - _poolCfg[pool].details.arbMaxSurgeFee9 = _safeConvertTo9Decimals(pct18); + _poolCfg[pool].details.arbMaxSurgeFee9 = _safeConvertTo9Decimals(newMaxSurgeFeePercentageScaled18); } else { - _poolCfg[pool].details.noiseMaxSurgeFee9 = _safeConvertTo9Decimals(pct18); + _poolCfg[pool].details.noiseMaxSurgeFee9 = _safeConvertTo9Decimals(newMaxSurgeFeePercentageScaled18); } - emit MaxSurgeFeePercentageChanged(msg.sender, pool, pct18, tradeType); + emit MaxSurgeFeePercentageChanged(msg.sender, pool, newMaxSurgeFeePercentageScaled18, tradeType); } ///@inheritdoc IHyperSurgeHook function setSurgeThresholdPercentage( address pool, - uint256 pct18, + uint256 newThresholdPercentageScaled18, TradeType tradeType - ) external override onlySwapFeeManagerOrGovernance(pool) { - _ensureValidPct(pct18); // keep a valid ramp span: threshold < capDev ≤ 1 - uint32 capDev; + ) public override onlySwapFeeManagerOrGovernance(pool) ensureValidPercentage(newThresholdPercentageScaled18) { + uint256 capDeviationPercentageScaled18; PoolDetails memory poolDetails = _poolCfg[pool].details; if (tradeType == TradeType.ARBITRAGE) { - poolDetails.arbThresholdPercentage9 = _safeConvertTo9Decimals(pct18); - capDev = poolDetails.arbCapDeviationPercentage9; + poolDetails.arbThresholdPercentage9 = _safeConvertTo9Decimals(newThresholdPercentageScaled18); + capDeviationPercentageScaled18 = _convertTo18Decimals(poolDetails.arbCapDeviationPercentage9); } else { - poolDetails.noiseThresholdPercentage9 = _safeConvertTo9Decimals(pct18); - capDev = poolDetails.noiseCapDeviationPercentage9; + poolDetails.noiseThresholdPercentage9 = _safeConvertTo9Decimals(newThresholdPercentageScaled18); + capDeviationPercentageScaled18 = _convertTo18Decimals(poolDetails.noiseCapDeviationPercentage9); } - uint256 capDev18 = _convertTo18Decimals(capDev); - //could be done before with two if/elses but more compact code this way - if (capDev18 != 0 && pct18 >= capDev18) { + // Keep a valid ramp span: threshold < capDev ≤ 1 + if (capDeviationPercentageScaled18 != 0 && newThresholdPercentageScaled18 >= capDeviationPercentageScaled18) { revert InvalidThresholdDeviation(); } _poolCfg[pool].details = poolDetails; - emit ThresholdPercentageChanged(msg.sender, pool, pct18, tradeType); + emit ThresholdPercentageChanged(msg.sender, pool, newThresholdPercentageScaled18, tradeType); } /// @inheritdoc IHyperSurgeHook function setCapDeviationPercentage( address pool, - uint256 capDevPct18, + uint256 newCapDeviationPercentageScaled18, TradeType tradeType - ) external override onlySwapFeeManagerOrGovernance(pool) { - _ensureValidPct(capDevPct18); - uint32 thr; + ) public override onlySwapFeeManagerOrGovernance(pool) ensureValidPercentage(newCapDeviationPercentageScaled18) { + uint256 thresholdPercentageScaled18; PoolDetails memory poolDetails = _poolCfg[pool].details; if (tradeType == TradeType.ARBITRAGE) { - poolDetails.arbCapDeviationPercentage9 = _safeConvertTo9Decimals(capDevPct18); - thr = poolDetails.arbThresholdPercentage9; + poolDetails.arbCapDeviationPercentage9 = _safeConvertTo9Decimals(newCapDeviationPercentageScaled18); + thresholdPercentageScaled18 = _convertTo18Decimals(poolDetails.arbThresholdPercentage9); } else { - poolDetails.noiseCapDeviationPercentage9 = _safeConvertTo9Decimals(capDevPct18); - thr = poolDetails.noiseThresholdPercentage9; + poolDetails.noiseCapDeviationPercentage9 = _safeConvertTo9Decimals(newCapDeviationPercentageScaled18); + thresholdPercentageScaled18 = _convertTo18Decimals(poolDetails.noiseThresholdPercentage9); } - uint256 thr18 = _convertTo18Decimals(thr); - - if (capDevPct18 <= thr18) { + // Keep a valid ramp span: threshold < capDev ≤ 1 + if (newCapDeviationPercentageScaled18 <= thresholdPercentageScaled18) { revert InvalidCapDeviationPercentage(); } _poolCfg[pool].details = poolDetails; - emit CapDeviationPercentageChanged(msg.sender, pool, capDevPct18, tradeType); + emit CapDeviationPercentageChanged(msg.sender, pool, newCapDeviationPercentageScaled18, tradeType); } struct AddLiquidityLocals { From 955a3dc944c4fa6dd2c770372f6b80f03e29b0dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Fri, 12 Sep 2025 11:45:07 -0300 Subject: [PATCH 04/28] Fix setters --- .../hooks-quantamm/HyperSurgeHook.sol | 295 ++++++++++-------- 1 file changed, 161 insertions(+), 134 deletions(-) diff --git a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol index 383bbcc6..44248879 100644 --- a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol +++ b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol @@ -3,32 +3,23 @@ pragma solidity ^0.8.26; import "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { IHyperSurgeHook } from "@balancer-labs/v3-interfaces/contracts/pool-hooks/IHyperSurgeHook.sol"; import { IHooks } from "@balancer-labs/v3-interfaces/contracts/vault/IHooks.sol"; import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; -import { IHyperSurgeHook } from "@balancer-labs/v3-interfaces/contracts/pool-hooks/IHyperSurgeHook.sol"; -import { - PoolSwapParams, - LiquidityManagement, - TokenConfig, - HookFlags, - SwapKind, - AddLiquidityKind, - RemoveLiquidityKind -} from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; -import { BaseHooks } from "@balancer-labs/v3-vault/contracts/BaseHooks.sol"; -import { VaultGuard } from "@balancer-labs/v3-vault/contracts/VaultGuard.sol"; import { SingletonAuthentication } from "@balancer-labs/v3-vault/contracts/SingletonAuthentication.sol"; -import { Version } from "@balancer-labs/v3-solidity-utils/contracts/helpers/Version.sol"; - import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; import { WeightedPool } from "@balancer-labs/v3-pool-weighted/contracts/WeightedPool.sol"; +import { Version } from "@balancer-labs/v3-solidity-utils/contracts/helpers/Version.sol"; import { HyperSpotPricePrecompile } from "@balancer-labs/v3-standalone-utils/contracts/utils/HyperSpotPricePrecompile.sol"; import { HyperTokenInfoPrecompile } from "@balancer-labs/v3-standalone-utils/contracts/utils/HyperTokenInfoPrecompile.sol"; +import { VaultGuard } from "@balancer-labs/v3-vault/contracts/VaultGuard.sol"; +import { BaseHooks } from "@balancer-labs/v3-vault/contracts/BaseHooks.sol"; /// ----------------------------------------------------------------------- /// Multitoken Hyper Surge Hook — struct-per-index configuration @@ -101,6 +92,10 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi _defaultCapDeviationPercentage18 = defaultCapDeviationPercentage18; } + /************************************************** + Hooks + **************************************************/ + ///@inheritdoc IHooks function getHookFlags() public pure override returns (HookFlags memory hookFlags) { hookFlags.shouldCallComputeDynamicSwapFee = true; @@ -115,7 +110,7 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi TokenConfig[] memory tokenCfgs, LiquidityManagement calldata ) public override onlyVault returns (bool) { - if (tokenCfgs.length < 2 && tokenCfgs.length > 8) { + if (tokenCfgs.length < 2 || tokenCfgs.length > 8) { revert NumTokensOutOfRange(); } @@ -124,16 +119,105 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi // Set the pool details, so we can use the setters and emit the proper events. _poolCfg[pool].details = details; - setMaxSurgeFeePercentage(pool, _defaultMaxSurgeFeePercentage18, TradeType.ARBITRAGE); - setMaxSurgeFeePercentage(pool, _defaultMaxSurgeFeePercentage18, TradeType.NOISE); - setSurgeThresholdPercentage(pool, _defaultThresholdPercentage18, TradeType.ARBITRAGE); - setSurgeThresholdPercentage(pool, _defaultThresholdPercentage18, TradeType.NOISE); - setCapDeviationPercentage(pool, _defaultCapDeviationPercentage18, TradeType.ARBITRAGE); - setCapDeviationPercentage(pool, _defaultCapDeviationPercentage18, TradeType.NOISE); + _setMaxSurgeFeePercentage(pool, _defaultMaxSurgeFeePercentage18, TradeType.ARBITRAGE); + _setMaxSurgeFeePercentage(pool, _defaultMaxSurgeFeePercentage18, TradeType.NOISE); + _setSurgeThresholdPercentage(pool, _defaultThresholdPercentage18, TradeType.ARBITRAGE); + _setSurgeThresholdPercentage(pool, _defaultThresholdPercentage18, TradeType.NOISE); + _setCapDeviationPercentage(pool, _defaultCapDeviationPercentage18, TradeType.ARBITRAGE); + _setCapDeviationPercentage(pool, _defaultCapDeviationPercentage18, TradeType.NOISE); return true; } + struct AddLiquidityLocals { + uint256[] oldBalances; + uint256 beforeDev; + uint256 afterDev; + uint256 threshold; + bool isWorseningSurge; + } + + /// @notice Allow proportional adds, but block non-proportional adds that worsen deviation and end above threshold. + function onAfterAddLiquidity( + address, + address pool, + AddLiquidityKind kind, + uint256[] memory amountsInScaled18, + uint256[] memory amountsInRaw, + uint256, // lpAmount (unused) + uint256[] memory balancesScaled18, + bytes memory // userData (unused) + ) public view override returns (bool success, uint256[] memory hookAdjustedAmountsInRaw) { + AddLiquidityLocals memory locals; + + // Proportional add is always allowed. + if (kind == AddLiquidityKind.PROPORTIONAL) { + return (true, amountsInRaw); + } + + locals.oldBalances = new uint256[](balancesScaled18.length); + for (uint256 i = 0; i < balancesScaled18.length; ++i) { + locals.oldBalances[i] = balancesScaled18[i] - amountsInScaled18[i]; + } + + uint256[] memory weights = WeightedPool(pool).getNormalizedWeights(); + locals.beforeDev = _computeOracleDeviationPct(pool, locals.oldBalances, weights); + locals.afterDev = _computeOracleDeviationPct(pool, balancesScaled18, weights); + locals.threshold = getSurgeThresholdPercentage(pool, TradeType.NOISE); + + // Block only if deviation worsens AND exceeds threshold after the change. + locals.isWorseningSurge = (locals.afterDev > locals.beforeDev) && (locals.afterDev > locals.threshold); + + return (!locals.isWorseningSurge, amountsInRaw); + } + + struct RemoveLiquidityLocals { + uint256 n; + uint256[] oldBalances; + uint256 beforeDev; + uint256 afterDev; + uint256 threshold; + bool isWorseningSurge; + } + + /// @notice Allow proportional removes, but block non-proportional removes that worsen deviation and end above threshold. + function onAfterRemoveLiquidity( + address, + address pool, + RemoveLiquidityKind kind, + uint256, // lpAmount (unused) + uint256[] memory amountsOutScaled18, + uint256[] memory amountsOutRaw, + uint256[] memory balancesScaled18, + bytes memory // userData (unused) + ) public view override returns (bool success, uint256[] memory hookAdjustedAmountsOutRaw) { + RemoveLiquidityLocals memory locals; + locals.n = balancesScaled18.length; + // Proportional remove is always allowed. should we check? + if (kind == RemoveLiquidityKind.PROPORTIONAL) { + return (true, amountsOutRaw); + } + + // Reconstruct pre-remove balances = post + out; if addition overflows, allow. + locals.oldBalances = new uint256[](locals.n); + for (uint256 i = 0; i < locals.n; ++i) { + locals.oldBalances[i] = balancesScaled18[i] + amountsOutScaled18[i]; + } + + uint256[] memory weights = WeightedPool(pool).getNormalizedWeights(); + locals.beforeDev = _computeOracleDeviationPct(pool, locals.oldBalances, weights); + locals.afterDev = _computeOracleDeviationPct(pool, balancesScaled18, weights); + locals.threshold = getSurgeThresholdPercentage(pool, TradeType.NOISE); + + locals.isWorseningSurge = (locals.afterDev > locals.beforeDev) && (locals.afterDev > locals.threshold); + + return (!locals.isWorseningSurge, amountsOutRaw); + } + + /************************************************** + Setters + **************************************************/ + /// @notice Configure a single token’s Hyperliquid mapping for a given pool by token index (0..7). /// @param pool The pool address to configure. /// @param tokenIndex The balancer index of the token to configure (0..7). @@ -149,6 +233,33 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi _setTokenPriceConfigIndex(pool, tokenIndex, hlPairIdx, hlTokenIdx, details); } + struct SetBatchConfigs { + TokenPriceCfg tempCfg; + uint256 i; + } + + /// @notice Batch version (indices). + /// @param pool the pool address + /// @param tokenIndices the indices of the token configs being changed + /// @param pairIdx the index of the pair being changed + function setTokenPriceConfigBatchIndex( + address pool, + uint8[] calldata tokenIndices, + uint32[] calldata pairIdx, + uint32[] calldata hlTokenIdx + ) external onlySwapFeeManagerOrGovernance(pool) { + PoolDetails storage detail = _poolCfg[pool].details; + SetBatchConfigs memory cfg; + + if (tokenIndices.length != pairIdx.length) { + revert InvalidArrayLengths(); + } + + for (cfg.i = 0; cfg.i < tokenIndices.length; ++cfg.i) { + _setTokenPriceConfigIndex(pool, tokenIndices[cfg.i], pairIdx[cfg.i], hlTokenIdx[cfg.i], detail); + } + } + function _setTokenPriceConfigIndex( address pool, uint8 tokenIndex, @@ -179,39 +290,20 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi emit TokenPriceConfiguredIndex(pool, tokenIndex, tempCfg.pairIndex, hlTokenIdx, tempCfg.sz); } - struct SetBatchConfigs { - TokenPriceCfg tempCfg; - uint256 i; - } - - /// @notice Batch version (indices). - /// @param pool the pool address - /// @param tokenIndices the indices of the token configs being changed - /// @param pairIdx the index of the pair being changed - function setTokenPriceConfigBatchIndex( + ///@inheritdoc IHyperSurgeHook + function setMaxSurgeFeePercentage( address pool, - uint8[] calldata tokenIndices, - uint32[] calldata pairIdx, - uint32[] calldata hlTokenIdx - ) external onlySwapFeeManagerOrGovernance(pool) { - PoolDetails storage detail = _poolCfg[pool].details; - SetBatchConfigs memory cfg; - - if (tokenIndices.length != pairIdx.length) { - revert InvalidArrayLengths(); - } - - for (cfg.i = 0; cfg.i < tokenIndices.length; ++cfg.i) { - _setTokenPriceConfigIndex(pool, tokenIndices[cfg.i], pairIdx[cfg.i], hlTokenIdx[cfg.i], detail); - } + uint256 newMaxSurgeFeePercentageScaled18, + TradeType tradeType + ) external override onlySwapFeeManagerOrGovernance(pool) { + _setMaxSurgeFeePercentage(pool, newMaxSurgeFeePercentageScaled18, tradeType); } - ///@inheritdoc IHyperSurgeHook - function setMaxSurgeFeePercentage( + function _setMaxSurgeFeePercentage( address pool, uint256 newMaxSurgeFeePercentageScaled18, TradeType tradeType - ) public override onlySwapFeeManagerOrGovernance(pool) ensureValidPercentage(newMaxSurgeFeePercentageScaled18) { + ) internal ensureValidPercentage(newMaxSurgeFeePercentageScaled18) { if (tradeType == TradeType.ARBITRAGE) { _poolCfg[pool].details.arbMaxSurgeFee9 = _safeConvertTo9Decimals(newMaxSurgeFeePercentageScaled18); } else { @@ -226,7 +318,15 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi address pool, uint256 newThresholdPercentageScaled18, TradeType tradeType - ) public override onlySwapFeeManagerOrGovernance(pool) ensureValidPercentage(newThresholdPercentageScaled18) { + ) external override onlySwapFeeManagerOrGovernance(pool) { + _setSurgeThresholdPercentage(pool, newThresholdPercentageScaled18, tradeType); + } + + function _setSurgeThresholdPercentage( + address pool, + uint256 newThresholdPercentageScaled18, + TradeType tradeType + ) internal ensureValidPercentage(newThresholdPercentageScaled18) { uint256 capDeviationPercentageScaled18; PoolDetails memory poolDetails = _poolCfg[pool].details; if (tradeType == TradeType.ARBITRAGE) { @@ -252,7 +352,15 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi address pool, uint256 newCapDeviationPercentageScaled18, TradeType tradeType - ) public override onlySwapFeeManagerOrGovernance(pool) ensureValidPercentage(newCapDeviationPercentageScaled18) { + ) external override onlySwapFeeManagerOrGovernance(pool) { + _setCapDeviationPercentage(pool, newCapDeviationPercentageScaled18, tradeType); + } + + function _setCapDeviationPercentage( + address pool, + uint256 newCapDeviationPercentageScaled18, + TradeType tradeType + ) internal ensureValidPercentage(newCapDeviationPercentageScaled18) { uint256 thresholdPercentageScaled18; PoolDetails memory poolDetails = _poolCfg[pool].details; if (tradeType == TradeType.ARBITRAGE) { @@ -273,90 +381,9 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi emit CapDeviationPercentageChanged(msg.sender, pool, newCapDeviationPercentageScaled18, tradeType); } - struct AddLiquidityLocals { - uint256[] oldBalances; - uint256 beforeDev; - uint256 afterDev; - uint256 threshold; - bool isWorseningSurge; - } - - /// @notice Allow proportional adds, but block non-proportional adds that worsen deviation and end above threshold. - function onAfterAddLiquidity( - address, - address pool, - AddLiquidityKind kind, - uint256[] memory amountsInScaled18, - uint256[] memory amountsInRaw, - uint256, // lpAmount (unused) - uint256[] memory balancesScaled18, - bytes memory // userData (unused) - ) public view override returns (bool success, uint256[] memory hookAdjustedAmountsInRaw) { - AddLiquidityLocals memory locals; - - // Proportional add is always allowed. - if (kind == AddLiquidityKind.PROPORTIONAL) { - return (true, amountsInRaw); - } - - locals.oldBalances = new uint256[](balancesScaled18.length); - for (uint256 i = 0; i < balancesScaled18.length; ++i) { - locals.oldBalances[i] = balancesScaled18[i] - amountsInScaled18[i]; - } - - uint256[] memory weights = WeightedPool(pool).getNormalizedWeights(); - locals.beforeDev = _computeOracleDeviationPct(pool, locals.oldBalances, weights); - locals.afterDev = _computeOracleDeviationPct(pool, balancesScaled18, weights); - locals.threshold = getSurgeThresholdPercentage(pool, TradeType.NOISE); - - // Block only if deviation worsens AND exceeds threshold after the change. - locals.isWorseningSurge = (locals.afterDev > locals.beforeDev) && (locals.afterDev > locals.threshold); - - return (!locals.isWorseningSurge, amountsInRaw); - } - - struct RemoveLiquidityLocals { - uint256 n; - uint256[] oldBalances; - uint256 beforeDev; - uint256 afterDev; - uint256 threshold; - bool isWorseningSurge; - } - - /// @notice Allow proportional removes, but block non-proportional removes that worsen deviation and end above threshold. - function onAfterRemoveLiquidity( - address, - address pool, - RemoveLiquidityKind kind, - uint256, // lpAmount (unused) - uint256[] memory amountsOutScaled18, - uint256[] memory amountsOutRaw, - uint256[] memory balancesScaled18, - bytes memory // userData (unused) - ) public view override returns (bool success, uint256[] memory hookAdjustedAmountsOutRaw) { - RemoveLiquidityLocals memory locals; - locals.n = balancesScaled18.length; - // Proportional remove is always allowed. should we check? - if (kind == RemoveLiquidityKind.PROPORTIONAL) { - return (true, amountsOutRaw); - } - - // Reconstruct pre-remove balances = post + out; if addition overflows, allow. - locals.oldBalances = new uint256[](locals.n); - for (uint256 i = 0; i < locals.n; ++i) { - locals.oldBalances[i] = balancesScaled18[i] + amountsOutScaled18[i]; - } - - uint256[] memory weights = WeightedPool(pool).getNormalizedWeights(); - locals.beforeDev = _computeOracleDeviationPct(pool, locals.oldBalances, weights); - locals.afterDev = _computeOracleDeviationPct(pool, balancesScaled18, weights); - locals.threshold = getSurgeThresholdPercentage(pool, TradeType.NOISE); - - locals.isWorseningSurge = (locals.afterDev > locals.beforeDev) && (locals.afterDev > locals.threshold); - - return (!locals.isWorseningSurge, amountsOutRaw); - } + /************************************************** + Getters + **************************************************/ /// @notice Getter to read the pool-specific surge threshold (1e18 = 100%). function getSurgeThresholdPercentage(address pool, TradeType tradeType) public view override returns (uint256) { From c1aeb7761a1d93f960a56068feb20a1f3cf6636d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Fri, 12 Sep 2025 11:54:55 -0300 Subject: [PATCH 05/28] Remove unnecessary structs for Add/Remove liquidity hooks --- .../hooks-quantamm/HyperSurgeHook.sol | 70 +++++++------------ 1 file changed, 26 insertions(+), 44 deletions(-) diff --git a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol index 44248879..fe98ed6c 100644 --- a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol +++ b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol @@ -129,15 +129,7 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi return true; } - struct AddLiquidityLocals { - uint256[] oldBalances; - uint256 beforeDev; - uint256 afterDev; - uint256 threshold; - bool isWorseningSurge; - } - - /// @notice Allow proportional adds, but block non-proportional adds that worsen deviation and end above threshold. + /// @inheritdoc IHooks function onAfterAddLiquidity( address, address pool, @@ -148,39 +140,22 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi uint256[] memory balancesScaled18, bytes memory // userData (unused) ) public view override returns (bool success, uint256[] memory hookAdjustedAmountsInRaw) { - AddLiquidityLocals memory locals; - - // Proportional add is always allowed. + // Allow proportional adds, but block non-proportional adds that worsen deviation and end above threshold. if (kind == AddLiquidityKind.PROPORTIONAL) { return (true, amountsInRaw); } - locals.oldBalances = new uint256[](balancesScaled18.length); + uint256[] memory oldBalancesScaled18 = new uint256[](balancesScaled18.length); for (uint256 i = 0; i < balancesScaled18.length; ++i) { - locals.oldBalances[i] = balancesScaled18[i] - amountsInScaled18[i]; + oldBalancesScaled18[i] = balancesScaled18[i] - amountsInScaled18[i]; } - uint256[] memory weights = WeightedPool(pool).getNormalizedWeights(); - locals.beforeDev = _computeOracleDeviationPct(pool, locals.oldBalances, weights); - locals.afterDev = _computeOracleDeviationPct(pool, balancesScaled18, weights); - locals.threshold = getSurgeThresholdPercentage(pool, TradeType.NOISE); - - // Block only if deviation worsens AND exceeds threshold after the change. - locals.isWorseningSurge = (locals.afterDev > locals.beforeDev) && (locals.afterDev > locals.threshold); - - return (!locals.isWorseningSurge, amountsInRaw); - } + bool isWorseningSurge = _isWorseningSurge(pool, oldBalancesScaled18, balancesScaled18); - struct RemoveLiquidityLocals { - uint256 n; - uint256[] oldBalances; - uint256 beforeDev; - uint256 afterDev; - uint256 threshold; - bool isWorseningSurge; + return (isWorseningSurge == false, amountsInRaw); } - /// @notice Allow proportional removes, but block non-proportional removes that worsen deviation and end above threshold. + /// @inheritdoc IHooks function onAfterRemoveLiquidity( address, address pool, @@ -191,27 +166,34 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi uint256[] memory balancesScaled18, bytes memory // userData (unused) ) public view override returns (bool success, uint256[] memory hookAdjustedAmountsOutRaw) { - RemoveLiquidityLocals memory locals; - locals.n = balancesScaled18.length; - // Proportional remove is always allowed. should we check? + // Allow proportional removes, but block non-proportional removes that worsen deviation and end above threshold. if (kind == RemoveLiquidityKind.PROPORTIONAL) { return (true, amountsOutRaw); } // Reconstruct pre-remove balances = post + out; if addition overflows, allow. - locals.oldBalances = new uint256[](locals.n); - for (uint256 i = 0; i < locals.n; ++i) { - locals.oldBalances[i] = balancesScaled18[i] + amountsOutScaled18[i]; + uint256[] memory oldBalancesScaled18 = new uint256[](balancesScaled18.length); + for (uint256 i = 0; i < balancesScaled18.length; ++i) { + oldBalancesScaled18[i] = balancesScaled18[i] + amountsOutScaled18[i]; } - uint256[] memory weights = WeightedPool(pool).getNormalizedWeights(); - locals.beforeDev = _computeOracleDeviationPct(pool, locals.oldBalances, weights); - locals.afterDev = _computeOracleDeviationPct(pool, balancesScaled18, weights); - locals.threshold = getSurgeThresholdPercentage(pool, TradeType.NOISE); + bool isWorseningSurge = _isWorseningSurge(pool, oldBalancesScaled18, balancesScaled18); + + return (isWorseningSurge == false, amountsOutRaw); + } - locals.isWorseningSurge = (locals.afterDev > locals.beforeDev) && (locals.afterDev > locals.threshold); + function _isWorseningSurge( + address pool, + uint256[] memory oldBalancesScaled18, + uint256[] memory newBalancesScaled18 + ) internal view returns (bool) { + uint256[] memory weights = WeightedPool(pool).getNormalizedWeights(); + uint256 oracleDeviationBefore = _computeOracleDeviationPct(pool, oldBalancesScaled18, weights); + uint256 oracleDeviationAfter = _computeOracleDeviationPct(pool, newBalancesScaled18, weights); + uint256 surgeThreshold = getSurgeThresholdPercentage(pool, TradeType.NOISE); - return (!locals.isWorseningSurge, amountsOutRaw); + // Block only if deviation worsens AND exceeds threshold after the change. + return (oracleDeviationAfter > oracleDeviationBefore) && (oracleDeviationAfter > surgeThreshold); } /************************************************** From dba482a5720171eb62ad66e416c232665699f302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Fri, 12 Sep 2025 12:20:17 -0300 Subject: [PATCH 06/28] Improve function names --- .../hooks-quantamm/HyperSurgeHook.sol | 184 +++++++++--------- .../contracts/test/HyperSurgeHookMock.sol | 2 +- 2 files changed, 97 insertions(+), 89 deletions(-) diff --git a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol index fe98ed6c..d827fc37 100644 --- a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol +++ b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol @@ -71,9 +71,8 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi uint256 private immutable _defaultCapDeviationPercentage18; modifier ensureValidPercentage(uint256 percentageValue) { - if (percentageValue < 1e9 || percentageValue > 1e18 || percentageValue % 1e9 != 0) { - revert InvalidPercentage(); - } + _ensureValidPercentage(percentageValue); + _; } @@ -84,9 +83,9 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi uint256 defaultCapDeviationPercentage18, string memory version ) SingletonAuthentication(vault) VaultGuard(vault) Version(version) { - _ensureValidPct(defaultMaxSurgeFeePercentage18); - _ensureValidPct(defaultThresholdPercentage18); - _ensureValidPct(defaultCapDeviationPercentage18); + _ensureValidPercentage(defaultMaxSurgeFeePercentage18); + _ensureValidPercentage(defaultThresholdPercentage18); + _ensureValidPercentage(defaultCapDeviationPercentage18); _defaultMaxSurgeFeePercentage18 = defaultMaxSurgeFeePercentage18; _defaultThresholdPercentage18 = defaultThresholdPercentage18; _defaultCapDeviationPercentage18 = defaultCapDeviationPercentage18; @@ -150,9 +149,9 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi oldBalancesScaled18[i] = balancesScaled18[i] - amountsInScaled18[i]; } - bool isWorseningSurge = _isWorseningSurge(pool, oldBalancesScaled18, balancesScaled18); + bool isPriceDeviationWorsening = _isPriceDeviationWorsening(pool, oldBalancesScaled18, balancesScaled18); - return (isWorseningSurge == false, amountsInRaw); + return (isPriceDeviationWorsening == false, amountsInRaw); } /// @inheritdoc IHooks @@ -177,23 +176,63 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi oldBalancesScaled18[i] = balancesScaled18[i] + amountsOutScaled18[i]; } - bool isWorseningSurge = _isWorseningSurge(pool, oldBalancesScaled18, balancesScaled18); + bool isPriceDeviationWorsening = _isPriceDeviationWorsening(pool, oldBalancesScaled18, balancesScaled18); - return (isWorseningSurge == false, amountsOutRaw); + return (isPriceDeviationWorsening == false, amountsOutRaw); } - function _isWorseningSurge( + struct ComputeSurgeFeeLocals { + uint256 calcAmountScaled18; + uint256 poolPxBefore; + uint256 poolPx; + uint256 pxIn; + uint256 pxOut; + uint256 extPx; + uint256 deviationBefore18; + uint256 deviation18; + uint256 threshold18; + uint256 maxPct18; + uint256 increment; + uint256 surgeFee18; + uint256 capDevPct18; + uint256 bIn; + uint256 bOut; + uint256 rawIn; + uint256 rawOut; + uint256 wIn; + uint256 wOut; + uint256 span; + uint256 norm; + PoolDetails poolDetails; + } + + /// @inheritdoc IHooks + function onComputeDynamicSwapFeePercentage( + PoolSwapParams calldata p, address pool, - uint256[] memory oldBalancesScaled18, - uint256[] memory newBalancesScaled18 - ) internal view returns (bool) { + uint256 staticSwapFee + ) public view override returns (bool, uint256) { + PoolCfg storage pc = _poolCfg[pool]; + ComputeSurgeFeeLocals memory locals; + locals.poolDetails = pc.details; + uint256[] memory weights = WeightedPool(pool).getNormalizedWeights(); - uint256 oracleDeviationBefore = _computeOracleDeviationPct(pool, oldBalancesScaled18, weights); - uint256 oracleDeviationAfter = _computeOracleDeviationPct(pool, newBalancesScaled18, weights); - uint256 surgeThreshold = getSurgeThresholdPercentage(pool, TradeType.NOISE); + locals.wIn = weights[p.indexIn]; + locals.wOut = weights[p.indexOut]; - // Block only if deviation worsens AND exceeds threshold after the change. - return (oracleDeviationAfter > oracleDeviationBefore) && (oracleDeviationAfter > surgeThreshold); + locals.calcAmountScaled18 = WeightedPool(pool).onSwap(p); + + TokenPriceCfg memory pInCfg = pc.tokenCfg[p.indexIn]; + TokenPriceCfg memory pOutCfg = pc.tokenCfg[p.indexOut]; + + locals.rawIn = HyperSpotPricePrecompile.spotPrice(pInCfg.pairIndex); + locals.rawOut = HyperSpotPricePrecompile.spotPrice(pOutCfg.pairIndex); + locals.pxIn = locals.rawIn.divDown(_divisorFromSz(pInCfg.sz)); + locals.pxOut = locals.rawOut.divDown(_divisorFromSz(pOutCfg.sz)); + locals.bIn = p.balancesScaled18[p.indexIn]; + locals.bOut = p.balancesScaled18[p.indexOut]; + + return _computeSurgeFee(locals, p, staticSwapFee); } /************************************************** @@ -439,60 +478,6 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi return _poolCfg[pool].details.numTokens; } - struct ComputeSurgeFeeLocals { - uint256 calcAmountScaled18; - uint256 poolPxBefore; - uint256 poolPx; - uint256 pxIn; - uint256 pxOut; - uint256 extPx; - uint256 deviationBefore18; - uint256 deviation18; - uint256 threshold18; - uint256 maxPct18; - uint256 increment; - uint256 surgeFee18; - uint256 capDevPct18; - uint256 bIn; - uint256 bOut; - uint256 rawIn; - uint256 rawOut; - uint256 wIn; - uint256 wOut; - uint256 span; - uint256 norm; - PoolDetails poolDetails; - } - - /// @inheritdoc IHooks - function onComputeDynamicSwapFeePercentage( - PoolSwapParams calldata p, - address pool, - uint256 staticSwapFee - ) public view override returns (bool, uint256) { - PoolCfg storage pc = _poolCfg[pool]; - ComputeSurgeFeeLocals memory locals; - locals.poolDetails = pc.details; - - uint256[] memory weights = WeightedPool(pool).getNormalizedWeights(); - locals.wIn = weights[p.indexIn]; - locals.wOut = weights[p.indexOut]; - - locals.calcAmountScaled18 = WeightedPool(pool).onSwap(p); - - TokenPriceCfg memory pInCfg = pc.tokenCfg[p.indexIn]; - TokenPriceCfg memory pOutCfg = pc.tokenCfg[p.indexOut]; - - locals.rawIn = HyperSpotPricePrecompile.spotPrice(pInCfg.pairIndex); - locals.rawOut = HyperSpotPricePrecompile.spotPrice(pOutCfg.pairIndex); - locals.pxIn = locals.rawIn.divDown(_divisorFromSz(pInCfg.sz)); - locals.pxOut = locals.rawOut.divDown(_divisorFromSz(pOutCfg.sz)); - locals.bIn = p.balancesScaled18[p.indexIn]; - locals.bOut = p.balancesScaled18[p.indexOut]; - - return _computeSurgeFee(locals, p, staticSwapFee); - } - /// @notice pure function to compute surge fee /// @param locals the locals struct containing all the necessary variables /// @param p swap parameters @@ -600,21 +585,6 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi return 1; } - function _ensureValidPct(uint256 pct) internal pure { - if (pct < 1e9 || pct > 1e18 || pct % 1e9 != 0) { - revert InvalidPercentage(); - } - } - - ///@notice Converts a 9 decimal places fixed point number to 18 decimal places. - function _convertTo18Decimals(uint32 setting9Dp) internal pure returns (uint256) { - return uint256(setting9Dp) * 1e9; - } - - function _safeConvertTo9Decimals(uint256 setting18Dp) internal pure returns (uint32) { - return (setting18Dp / 1e9).toUint32(); - } - struct ComputeOracleDeviationLocals { uint256[8] px; uint256 maxDev; @@ -696,4 +666,42 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi return locals.maxDev; } + + /** + * @notice Checks if the pool price deviation is worsening after a add/remove liquidity operation. + * @dev The pool price deviation is worsening if the deviation between oracle and pool price increased and the + * deviation is greater than the surge threshold. + * @param pool The pool address + * @param oldBalancesScaled18 The balances before the add/remove liquidity operation + * @param newBalancesScaled18 The balances after the add/remove liquidity operation + * @return True if the pool price deviation is worsening, false otherwise + */ + function _isPriceDeviationWorsening( + address pool, + uint256[] memory oldBalancesScaled18, + uint256[] memory newBalancesScaled18 + ) internal view returns (bool) { + uint256[] memory weights = WeightedPool(pool).getNormalizedWeights(); + uint256 priceDeviationBefore = _computeOracleDeviationPct(pool, oldBalancesScaled18, weights); + uint256 priceDeviationAfter = _computeOracleDeviationPct(pool, newBalancesScaled18, weights); + uint256 surgeThreshold = getSurgeThresholdPercentage(pool, TradeType.NOISE); + + return (priceDeviationAfter > priceDeviationBefore) && (priceDeviationAfter > surgeThreshold); + } + + ///@notice Converts a 9 decimal places fixed point number to 18 decimal places. + function _convertTo18Decimals(uint32 valueScaled9) internal pure returns (uint256) { + return uint256(valueScaled9) * 1e9; + } + + ///@notice Converts a 18 decimal places fixed point number to 9 decimal places. + function _safeConvertTo9Decimals(uint256 valueScaled18) internal pure returns (uint32) { + return (valueScaled18 / 1e9).toUint32(); + } + + function _ensureValidPercentage(uint256 percentageValue) internal pure { + if (percentageValue < 1e9 || percentageValue > 1e18 || percentageValue % 1e9 != 0) { + revert InvalidPercentage(); + } + } } diff --git a/pkg/pool-hooks/contracts/test/HyperSurgeHookMock.sol b/pkg/pool-hooks/contracts/test/HyperSurgeHookMock.sol index 130d63ac..c71dd7b6 100644 --- a/pkg/pool-hooks/contracts/test/HyperSurgeHookMock.sol +++ b/pkg/pool-hooks/contracts/test/HyperSurgeHookMock.sol @@ -51,7 +51,7 @@ contract HyperSurgeHookMock is HyperSurgeHook { } function EnsureValidPct(uint256 pct) external pure { - _ensureValidPct(pct); + _ensureValidPercentage(pct); } function ComputeSurgeFee( From 1a69991fb04026b6520cca866f8460b863c716ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Fri, 12 Sep 2025 17:21:39 -0300 Subject: [PATCH 07/28] Refactor computeSurgeFee to remove locals struct --- .../hooks-quantamm/HyperSurgeHook.sol | 211 +++++++++--------- .../contracts/test/HyperSurgeHookMock.sol | 19 +- 2 files changed, 122 insertions(+), 108 deletions(-) diff --git a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol index d827fc37..26682a1b 100644 --- a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol +++ b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol @@ -181,58 +181,17 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi return (isPriceDeviationWorsening == false, amountsOutRaw); } - struct ComputeSurgeFeeLocals { - uint256 calcAmountScaled18; - uint256 poolPxBefore; - uint256 poolPx; - uint256 pxIn; - uint256 pxOut; - uint256 extPx; - uint256 deviationBefore18; - uint256 deviation18; - uint256 threshold18; - uint256 maxPct18; - uint256 increment; - uint256 surgeFee18; - uint256 capDevPct18; - uint256 bIn; - uint256 bOut; - uint256 rawIn; - uint256 rawOut; - uint256 wIn; - uint256 wOut; - uint256 span; - uint256 norm; - PoolDetails poolDetails; - } - /// @inheritdoc IHooks function onComputeDynamicSwapFeePercentage( PoolSwapParams calldata p, address pool, uint256 staticSwapFee ) public view override returns (bool, uint256) { - PoolCfg storage pc = _poolCfg[pool]; - ComputeSurgeFeeLocals memory locals; - locals.poolDetails = pc.details; - + PoolCfg memory pc = _poolCfg[pool]; uint256[] memory weights = WeightedPool(pool).getNormalizedWeights(); - locals.wIn = weights[p.indexIn]; - locals.wOut = weights[p.indexOut]; - - locals.calcAmountScaled18 = WeightedPool(pool).onSwap(p); - - TokenPriceCfg memory pInCfg = pc.tokenCfg[p.indexIn]; - TokenPriceCfg memory pOutCfg = pc.tokenCfg[p.indexOut]; - - locals.rawIn = HyperSpotPricePrecompile.spotPrice(pInCfg.pairIndex); - locals.rawOut = HyperSpotPricePrecompile.spotPrice(pOutCfg.pairIndex); - locals.pxIn = locals.rawIn.divDown(_divisorFromSz(pInCfg.sz)); - locals.pxOut = locals.rawOut.divDown(_divisorFromSz(pOutCfg.sz)); - locals.bIn = p.balancesScaled18[p.indexIn]; - locals.bOut = p.balancesScaled18[p.indexOut]; - - return _computeSurgeFee(locals, p, staticSwapFee); + uint256 calculatedAmountScaled18 = WeightedPool(pool).onSwap(p); + uint256 oraclePrice = _computeExternalPrice(pc, p.indexIn, p.indexOut); + return _computeSurgeFee(p, pc.details, staticSwapFee, weights, calculatedAmountScaled18, oraclePrice); } /************************************************** @@ -478,81 +437,133 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi return _poolCfg[pool].details.numTokens; } - /// @notice pure function to compute surge fee - /// @param locals the locals struct containing all the necessary variables - /// @param p swap parameters - /// @param staticSwapFee the static swap fee from the pool function _computeSurgeFee( - ComputeSurgeFeeLocals memory locals, - PoolSwapParams calldata p, - uint256 staticSwapFee + PoolSwapParams calldata params, + PoolDetails memory poolDetails, + uint256 staticSwapFee, + uint256[] memory weights, + uint256 calculatedAmountScaled18, + uint256 oraclePrice ) internal pure returns (bool ok, uint256 surgeFee) { - locals.extPx = locals.pxOut.divDown(locals.pxIn); - - //Do not block if there is an issue with the hyperliquid price - if (locals.extPx == 0) { + // Do not block if there is an issue with the hyperliquid price + if (oraclePrice == 0) { return (true, staticSwapFee); } - locals.poolPxBefore = _pairSpotFromBalancesWeights(locals.bIn, locals.wIn, locals.bOut, locals.wOut); - locals.deviationBefore18 = _relAbsDiff(locals.poolPxBefore, locals.extPx); + ( + uint256 deviation18, + uint256 capDevPct18, + uint256 maxPct18, + uint256 threshold18 + ) = _computeDeviationAndSelectPoolDetails(params, weights, calculatedAmountScaled18, oraclePrice, poolDetails); - if (p.kind == SwapKind.EXACT_IN) { - locals.bIn += p.amountGivenScaled18; - locals.bOut -= locals.calcAmountScaled18; - } else { - locals.bIn += locals.calcAmountScaled18; - locals.bOut -= p.amountGivenScaled18; + if (deviation18 <= threshold18) { + return (true, staticSwapFee); } - // P_pool = (B_out/w_out) / (B_in/w_in) = (B_out * w_in) / (B_in * w_out) - locals.poolPx = _pairSpotFromBalancesWeights(locals.bIn, locals.wIn, locals.bOut, locals.wOut); - locals.deviation18 = _relAbsDiff(locals.poolPx, locals.extPx); // |pool - ext| / ext + uint256 span = capDevPct18 - threshold18; // > 0 by fallback above + uint256 norm = (deviation18 - threshold18).divDown(span); - if (locals.deviation18 > locals.deviationBefore18) { - locals.capDevPct18 = _convertTo18Decimals(locals.poolDetails.noiseCapDeviationPercentage9); - locals.maxPct18 = _convertTo18Decimals(locals.poolDetails.noiseMaxSurgeFee9); - locals.threshold18 = _convertTo18Decimals(locals.poolDetails.noiseThresholdPercentage9); - } else { - locals.capDevPct18 = _convertTo18Decimals(locals.poolDetails.arbCapDeviationPercentage9); - locals.maxPct18 = _convertTo18Decimals(locals.poolDetails.arbMaxSurgeFee9); - locals.threshold18 = _convertTo18Decimals(locals.poolDetails.arbThresholdPercentage9); - - //For the arbitrage direction we use the deviation before. - //Why this is the case is in the readme but in essence - //if a large noise deviation is being corrected the arbitrage pays more - //to take advantage of the larger arb opp and therefore greater profit - //as the fee decreases the closer you get to market price, another - //arb opportunity presents itself once the first arb is taken - //this means a large fee != a large no arb region and the pool stays close to market - locals.deviation18 = locals.deviationBefore18; + if (norm > FixedPoint.ONE) { + norm = FixedPoint.ONE; } - if (locals.deviation18 <= locals.threshold18) { - return (true, staticSwapFee); + uint256 increment = (maxPct18 - staticSwapFee).mulDown(norm); + uint256 surgeFee18 = staticSwapFee + increment; + + return (true, surgeFee18); + } + + function _computeDeviationAndSelectPoolDetails( + PoolSwapParams calldata params, + uint256[] memory weights, + uint256 calculatedAmountScaled18, + uint256 oraclePrice, + PoolDetails memory poolDetails + ) + internal + pure + returns ( + uint256 deviation18, + uint256 capDeviationScaled18, + uint256 maxSurgeFeeScaled18, + uint256 thresholdScaled18 + ) + { + uint256 poolPriceBefore = _pairSpotFromBalancesWeights( + params.balancesScaled18, + weights, + params.indexIn, + params.indexOut + ); + uint256 deviationBefore18 = _relAbsDiff(poolPriceBefore, oraclePrice); + + uint256[] memory newBalancesScaled18 = new uint256[](params.balancesScaled18.length); + for (uint256 i = 0; i < params.balancesScaled18.length; i++) { + newBalancesScaled18[i] = params.balancesScaled18[i]; } - locals.span = locals.capDevPct18 - locals.threshold18; // > 0 by fallback above - locals.norm = (locals.deviation18 - locals.threshold18).divDown(locals.span); + if (params.kind == SwapKind.EXACT_IN) { + newBalancesScaled18[params.indexIn] += params.amountGivenScaled18; + newBalancesScaled18[params.indexOut] -= calculatedAmountScaled18; + } else { + newBalancesScaled18[params.indexIn] += calculatedAmountScaled18; + newBalancesScaled18[params.indexOut] -= params.amountGivenScaled18; + } - if (locals.norm > FixedPoint.ONE) { - locals.norm = FixedPoint.ONE; + // P_pool = (B_out/w_out) / (B_in/w_in) = (B_out * w_in) / (B_in * w_out) + uint256 poolPriceAfter = _pairSpotFromBalancesWeights( + newBalancesScaled18, + weights, + params.indexIn, + params.indexOut + ); + deviation18 = _relAbsDiff(poolPriceAfter, oraclePrice); // |pool - ext| / ext + + // Check if the swap is a noise (deviation is worsening) or an arbitrage (deviation is improving). + if (deviation18 > deviationBefore18) { + // Deviation is worsening, use noise details. + capDeviationScaled18 = _convertTo18Decimals(poolDetails.noiseCapDeviationPercentage9); + maxSurgeFeeScaled18 = _convertTo18Decimals(poolDetails.noiseMaxSurgeFee9); + thresholdScaled18 = _convertTo18Decimals(poolDetails.noiseThresholdPercentage9); + } else { + // Deviation is improving, use arb details (informed swap). + capDeviationScaled18 = _convertTo18Decimals(poolDetails.arbCapDeviationPercentage9); + maxSurgeFeeScaled18 = _convertTo18Decimals(poolDetails.arbMaxSurgeFee9); + thresholdScaled18 = _convertTo18Decimals(poolDetails.arbThresholdPercentage9); + + // For the arbitrage direction we use the deviation before. If a large noise deviation is being corrected + // the arbitrage pays more to take advantage of the larger arb opp and therefore greater profit as the fee + // decreases the closer you get to market price, another arb opportunity presents itself once the first arb + // is taken. This means a large fee != a large no arb region and the pool stays close to market. For more + // information, check the HyperSurgeHook-README.md file. + deviation18 = deviationBefore18; } + } - locals.increment = (locals.maxPct18 - staticSwapFee).mulDown(locals.norm); - locals.surgeFee18 = staticSwapFee + locals.increment; + function _computeExternalPrice( + PoolCfg memory pc, + uint256 indexTokenIn, + uint256 indexTokenOut + ) internal view returns (uint256) { + TokenPriceCfg memory pInCfg = pc.tokenCfg[indexTokenIn]; + TokenPriceCfg memory pOutCfg = pc.tokenCfg[indexTokenOut]; - return (true, locals.surgeFee18); + uint256 rawPriceTokenIn = HyperSpotPricePrecompile.spotPrice(pInCfg.pairIndex); + uint256 rawPriceTokenOut = HyperSpotPricePrecompile.spotPrice(pOutCfg.pairIndex); + uint256 priceTokenInScaled18 = rawPriceTokenIn.divDown(_divisorFromSz(pInCfg.sz)); + uint256 priceTokenOutScaled18 = rawPriceTokenOut.divDown(_divisorFromSz(pOutCfg.sz)); + return priceTokenOutScaled18.divDown(priceTokenInScaled18); } function _pairSpotFromBalancesWeights( - uint256 bIn, - uint256 wIn, - uint256 bOut, - uint256 wOut + uint256[] memory balancesScaled18, + uint256[] memory weights, + uint256 indexTokenIn, + uint256 indexTokenOut ) internal pure returns (uint256) { - uint256 num = bOut.mulDown(wIn); - uint256 den = bIn.mulDown(wOut); + uint256 num = balancesScaled18[indexTokenOut].mulDown(weights[indexTokenIn]); + uint256 den = balancesScaled18[indexTokenIn].mulDown(weights[indexTokenOut]); //would be impossible given normal balances and weights but given //it is on the withdraw path keep the defensive check @@ -648,7 +659,7 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi locals.pxj = locals.px[locals.j]; // Pool-implied spot for j vs i: (Bj/wj) / (Bi/wi) - locals.poolPx = _pairSpotFromBalancesWeights(locals.bj, locals.wj, locals.bi, locals.wi); + locals.poolPx = _pairSpotFromBalancesWeights(balancesScaled18, w, locals.i, locals.j); if (locals.poolPx == 0) { continue; diff --git a/pkg/pool-hooks/contracts/test/HyperSurgeHookMock.sol b/pkg/pool-hooks/contracts/test/HyperSurgeHookMock.sol index c71dd7b6..c819aadd 100644 --- a/pkg/pool-hooks/contracts/test/HyperSurgeHookMock.sol +++ b/pkg/pool-hooks/contracts/test/HyperSurgeHookMock.sol @@ -34,12 +34,12 @@ contract HyperSurgeHookMock is HyperSurgeHook { } function PairSpotFromBalancesWeights( - uint256 bIn, - uint256 wIn, - uint256 bOut, - uint256 wOut + uint256[] memory balancesScaled18, + uint256[] memory weights, + uint256 indexTokenIn, + uint256 indexTokenOut ) external pure returns (uint256) { - return _pairSpotFromBalancesWeights(bIn, wIn, bOut, wOut); + return _pairSpotFromBalancesWeights(balancesScaled18, weights, indexTokenIn, indexTokenOut); } function RelAbsDiff(uint256 a, uint256 b) external pure returns (uint256) { @@ -55,10 +55,13 @@ contract HyperSurgeHookMock is HyperSurgeHook { } function ComputeSurgeFee( - ComputeSurgeFeeLocals memory locals, PoolSwapParams calldata p, - uint256 staticSwapFee + PoolDetails memory poolDetails, + uint256 staticSwapFee, + uint256[] memory weights, + uint256 calculatedAmountScaled18, + uint256 oraclePrice ) external pure returns (bool ok, uint256 surgeFee) { - return _computeSurgeFee(locals, p, staticSwapFee); + return _computeSurgeFee(p, poolDetails, staticSwapFee, weights, calculatedAmountScaled18, oraclePrice); } } From 2a7f72481300cf65c52bcaf5149dff4e648b77a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Fri, 12 Sep 2025 22:55:59 -0300 Subject: [PATCH 08/28] Fix stack-too-deep --- .../hooks-quantamm/HyperSurgeHook.sol | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol index 26682a1b..d06aa0b5 100644 --- a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol +++ b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol @@ -490,13 +490,16 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi uint256 thresholdScaled18 ) { - uint256 poolPriceBefore = _pairSpotFromBalancesWeights( - params.balancesScaled18, - weights, - params.indexIn, - params.indexOut - ); - uint256 deviationBefore18 = _relAbsDiff(poolPriceBefore, oraclePrice); + uint256 deviationBefore18; + { + uint256 poolPriceBefore = _pairSpotFromBalancesWeights( + params.balancesScaled18, + weights, + params.indexIn, + params.indexOut + ); + deviationBefore18 = _relAbsDiff(poolPriceBefore, oraclePrice); + } uint256[] memory newBalancesScaled18 = new uint256[](params.balancesScaled18.length); for (uint256 i = 0; i < params.balancesScaled18.length; i++) { @@ -512,13 +515,15 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi } // P_pool = (B_out/w_out) / (B_in/w_in) = (B_out * w_in) / (B_in * w_out) - uint256 poolPriceAfter = _pairSpotFromBalancesWeights( - newBalancesScaled18, - weights, - params.indexIn, - params.indexOut - ); - deviation18 = _relAbsDiff(poolPriceAfter, oraclePrice); // |pool - ext| / ext + { + uint256 poolPriceAfter = _pairSpotFromBalancesWeights( + newBalancesScaled18, + weights, + params.indexIn, + params.indexOut + ); + deviation18 = _relAbsDiff(poolPriceAfter, oraclePrice); // |pool - ext| / ext + } // Check if the swap is a noise (deviation is worsening) or an arbitrage (deviation is improving). if (deviation18 > deviationBefore18) { From 78b058dbe8ff2159e0550dfaae103b2cf8be94e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Fri, 12 Sep 2025 23:05:05 -0300 Subject: [PATCH 09/28] Fix tests - WIP --- .../test/foundry/HyperSurgeFee.t.sol | 4334 ++++++++--------- 1 file changed, 2139 insertions(+), 2195 deletions(-) diff --git a/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol b/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol index bd4a6ae7..3057347c 100644 --- a/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol +++ b/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol @@ -28,6 +28,7 @@ import { // Local deployer + mock import { HyperSurgeHookDeployer } from "./utils/HyperSurgeHookDeployer.sol"; import { HyperSurgeHookMock } from "../../contracts/test/HyperSurgeHookMock.sol"; +import { HyperSurgeHook } from "../../contracts/hooks-quantamm/HyperSurgeHook.sol"; import { HyperSpotPricePrecompile } from "@balancer-labs/v3-standalone-utils/contracts/utils/HyperSpotPricePrecompile.sol"; @@ -160,8 +161,9 @@ contract HLTokenInfoStub { * */ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoolContractsDeployer { - using ArrayHelpers for *; using CastingHelpers for address[]; + using FixedPoint for uint256; + using ArrayHelpers for *; uint256 constant ONE = 1e18; uint256 constant STATIC_SWAP_FEE = 1e16; // 1% (1e18 scale) @@ -535,9 +537,6 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo uint256[] b; uint8 i; uint8 j; - uint32 thrPPM9; - uint32 capPPM9; - uint32 maxPPM9; uint256 P; uint256 capDev; uint256 D; @@ -554,9 +553,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo uint256 wSeed, uint256 bSeed, uint256 dSeed, - uint32 thrPPM9, - uint32 capPPM9, - uint32 maxPPM9 + uint32[3] memory percentageSeeds ) public { FeeRampLocals memory locals; @@ -567,38 +564,33 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo locals.i = uint8(bound(uint256(keccak256(abi.encode(dSeed, 11))), 0, locals.n - 1)); locals.j = (locals.i + 1 + uint8(bound(uint256(keccak256(abi.encode(dSeed, 12))), 0, locals.n - 2))) % locals.n; - (locals.thrPPM9, locals.capPPM9, locals.maxPPM9) = fee_boundParams(thrPPM9, capPPM9, maxPPM9); - locals.P = fee_pairSpotFromBW(locals.b[locals.i], locals.w[locals.i], locals.b[locals.j], locals.w[locals.j]); vm.assume(locals.P > 0); - locals.capDev = fee_ppm9To1e18(locals.capPPM9); + PoolSwapParams memory p = _createPoolSwapParams(SwapKind.EXACT_IN, locals.b, locals.i, locals.j, 0); + HyperSurgeHook.PoolDetails memory poolDetails = _createPoolDetails(percentageSeeds, locals.n); + + locals.capDev = _convertTo18Decimals(poolDetails.arbCapDeviationPercentage9); locals.D = uint256(keccak256(abi.encode(dSeed))) % (locals.capDev + locals.capDev / 2 + 1); (locals.pxIn, locals.pxOut) = fee_localsForDeviation(locals.P, locals.D); HyperSurgeHookMock mock = new HyperSurgeHookMock( IVault(vault), - fee_ppm9To1e18(locals.maxPPM9), - fee_ppm9To1e18(locals.thrPPM9), - fee_ppm9To1e18(locals.capPPM9), + _convertTo18Decimals(poolDetails.arbMaxSurgeFee9), + _convertTo18Decimals(poolDetails.arbThresholdPercentage9), + _convertTo18Decimals(poolDetails.arbCapDeviationPercentage9), "fee-fuzz" ); - HyperSurgeHookMock.ComputeSurgeFeeLocals memory computeLocals = fee_makeLocals( - locals.b[locals.i], - locals.w[locals.i], - locals.b[locals.j], - locals.w[locals.j], - locals.pxIn, - locals.pxOut, - locals.thrPPM9, - locals.capPPM9, - locals.maxPPM9 - ); - PoolSwapParams memory p; - p.kind = SwapKind.EXACT_IN; - (locals.ok, locals.feeA) = mock.ComputeSurgeFee(computeLocals, p, STATIC_SWAP_FEE); + (locals.ok, locals.feeA) = mock.ComputeSurgeFee( + p, + poolDetails, + STATIC_SWAP_FEE, + locals.w, + 0, + locals.pxOut.divDown(locals.pxIn) + ); assertTrue(locals.ok, "compute must succeed"); locals.expected = fee_expectedFeeWithParams( @@ -606,9 +598,9 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo locals.pxIn, locals.pxOut, STATIC_SWAP_FEE, - locals.thrPPM9, - locals.capPPM9, - locals.maxPPM9 + poolDetails.arbThresholdPercentage9, + poolDetails.arbCapDeviationPercentage9, + poolDetails.arbMaxSurgeFee9 ); assertEq(locals.feeA, locals.expected, "mock engine must match expected ramp"); } @@ -619,9 +611,6 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo uint256[] b; uint8 i; uint8 j; - uint32 thrPPM9; - uint32 capPPM9; - uint32 maxPPM9; uint256 P; uint256 capDev; uint256 D1; @@ -634,16 +623,14 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo uint256 fee2; } - /// Monotonicity in deviation under arbitrary (valid) lane params. + // Monotonicity in deviation under arbitrary (valid) lane params. function testFuzz_internal_monotone_inDeviation( uint8 nSeed, uint256 wSeed, uint256 bSeed, uint256 d1, uint256 d2, - uint32 thrPPM9, - uint32 capPPM9, - uint32 maxPPM9 + uint32[3] memory percentageSeeds ) public { monotoneDeviationLocals memory locals; @@ -654,12 +641,13 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo locals.i = uint8(bound(uint256(keccak256(abi.encode(d1, 21))), 0, locals.n - 1)); locals.j = (locals.i + 1 + uint8(bound(uint256(keccak256(abi.encode(d1, 22))), 0, locals.n - 2))) % locals.n; - (locals.thrPPM9, locals.capPPM9, locals.maxPPM9) = fee_boundParams(thrPPM9, capPPM9, maxPPM9); + HyperSurgeHook.PoolDetails memory poolDetails = _createPoolDetails(percentageSeeds, locals.n); + PoolSwapParams memory p = _createPoolSwapParams(SwapKind.EXACT_IN, locals.b, locals.i, locals.j, 0); locals.P = fee_pairSpotFromBW(locals.b[locals.i], locals.w[locals.i], locals.b[locals.j], locals.w[locals.j]); vm.assume(locals.P > 0); - locals.capDev = fee_ppm9To1e18(locals.capPPM9); + locals.capDev = _convertTo18Decimals(poolDetails.arbCapDeviationPercentage9); locals.D1 = uint256(keccak256(abi.encode(d1))) % (locals.capDev + locals.capDev / 2 + 1); locals.D2 = uint256(keccak256(abi.encode(d2))) % (locals.capDev + locals.capDev / 2 + 1); @@ -670,43 +658,27 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo HyperSurgeHookMock mock = new HyperSurgeHookMock( IVault(vault), - fee_ppm9To1e18(locals.maxPPM9), - fee_ppm9To1e18(locals.thrPPM9), - fee_ppm9To1e18(locals.capPPM9), + _convertTo18Decimals(poolDetails.arbMaxSurgeFee9), + _convertTo18Decimals(poolDetails.arbThresholdPercentage9), + _convertTo18Decimals(poolDetails.arbCapDeviationPercentage9), "fee-mono" ); - PoolSwapParams memory p; - p.kind = SwapKind.EXACT_IN; (, locals.fee1) = mock.ComputeSurgeFee( - fee_makeLocals( - locals.b[locals.i], - locals.w[locals.i], - locals.b[locals.j], - locals.w[locals.j], - locals.pxIn1, - locals.pxOut1, - locals.thrPPM9, - locals.capPPM9, - locals.maxPPM9 - ), p, - STATIC_SWAP_FEE + poolDetails, + STATIC_SWAP_FEE, + locals.w, + 0, + locals.pxOut1.divDown(locals.pxIn1) ); (, locals.fee2) = mock.ComputeSurgeFee( - fee_makeLocals( - locals.b[locals.i], - locals.w[locals.i], - locals.b[locals.j], - locals.w[locals.j], - locals.pxIn2, - locals.pxOut2, - locals.thrPPM9, - locals.capPPM9, - locals.maxPPM9 - ), p, - STATIC_SWAP_FEE + poolDetails, + STATIC_SWAP_FEE, + locals.w, + 0, + locals.pxOut2.divDown(locals.pxIn2) ); assertLe(locals.fee1, locals.fee2, "fee must be non-decreasing in deviation"); @@ -718,9 +690,6 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo uint256[] b; uint8 i; uint8 j; - uint32 thrPPM9; - uint32 capPPM9; - uint32 maxPPM9; uint256 P; uint256 capDev; uint256 scaleSeed; @@ -739,9 +708,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo uint256 bSeed, uint256 dSeed, uint64 scaleSeed, - uint32 thrPPM9, - uint32 capPPM9, - uint32 maxPPM9 + uint32[3] memory percentageSeeds ) public { balanceScalingLocals memory locals; @@ -753,13 +720,13 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo locals.i = uint8(bound(uint256(keccak256(abi.encode(dSeed, 31))), 0, locals.n - 1)); locals.j = (locals.i + 1 + uint8(bound(uint256(keccak256(abi.encode(dSeed, 32))), 0, locals.n - 2))) % locals.n; - (locals.thrPPM9, locals.capPPM9, locals.maxPPM9) = fee_boundParams(thrPPM9, capPPM9, maxPPM9); + HyperSurgeHook.PoolDetails memory poolDetails = _createPoolDetails(percentageSeeds, locals.n); // Pool spot from balances/weights; ensure sane locals.P = fee_pairSpotFromBW(locals.b[locals.i], locals.w[locals.i], locals.b[locals.j], locals.w[locals.j]); vm.assume(locals.P > 0); - locals.capDev = fee_ppm9To1e18(locals.capPPM9); + locals.capDev = _convertTo18Decimals(poolDetails.arbCapDeviationPercentage9); // Choose a deviation up to 1.5 * cap to exercise both sides near edges locals.D = uint256(keccak256(abi.encode(dSeed))) % (locals.capDev + locals.capDev / 2 + 1); @@ -777,51 +744,48 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo HyperSurgeHookMock mock = new HyperSurgeHookMock( IVault(vault), - fee_ppm9To1e18(locals.maxPPM9), - fee_ppm9To1e18(locals.thrPPM9), - fee_ppm9To1e18(locals.capPPM9), + _convertTo18Decimals(poolDetails.arbMaxSurgeFee9), + _convertTo18Decimals(poolDetails.arbThresholdPercentage9), + _convertTo18Decimals(poolDetails.arbCapDeviationPercentage9), "fee-scale" ); // Unscaled trade - PoolSwapParams memory p1; - p1.kind = SwapKind.EXACT_IN; - p1.amountGivenScaled18 = locals.baseAmt; - - PoolSwapParams memory p2; - p2.kind = SwapKind.EXACT_IN; - p2.amountGivenScaled18 = locals.baseAmt * locals.scaleSeed; + PoolSwapParams memory p1 = _createPoolSwapParams( + SwapKind.EXACT_IN, + locals.b, + locals.i, + locals.j, + locals.baseAmt + ); + uint256[] memory bScaled = new uint256[](locals.n); + for (uint256 i = 0; i < locals.n; i++) { + bScaled[i] = locals.b[i] * locals.scaleSeed; + } + PoolSwapParams memory p2 = _createPoolSwapParams( + SwapKind.EXACT_IN, + bScaled, + locals.i, + locals.j, + locals.baseAmt * locals.scaleSeed + ); (, locals.fee1) = mock.ComputeSurgeFee( - fee_makeLocals( - locals.b[locals.i], - locals.w[locals.i], - locals.b[locals.j], - locals.w[locals.j], - locals.pxIn, - locals.pxOut, - locals.thrPPM9, - locals.capPPM9, - locals.maxPPM9 - ), p1, - STATIC_SWAP_FEE + poolDetails, + STATIC_SWAP_FEE, + locals.w, + 0, + locals.pxOut.divDown(locals.pxIn) ); (, locals.fee2) = mock.ComputeSurgeFee( - fee_makeLocals( - locals.b[locals.i] * locals.scaleSeed, - locals.w[locals.i], - locals.b[locals.j] * locals.scaleSeed, - locals.w[locals.j], - locals.pxIn, - locals.pxOut, - locals.thrPPM9, - locals.capPPM9, - locals.maxPPM9 - ), p2, - STATIC_SWAP_FEE + poolDetails, + STATIC_SWAP_FEE, + locals.w, + 0, + locals.pxOut.divDown(locals.pxIn) ); // --- Branch-aware assertion (inferred) --- @@ -852,10 +816,8 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo } struct ExactValuesBoundariesLocal { - uint256 w0; - uint256 w1; - uint256 b0; - uint256 b1; + uint256[] w; + uint256[] b; uint256 P; uint32 thr; uint32 cap; @@ -873,11 +835,9 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo ExactValuesBoundariesLocal memory locals; // 2 tokens, 50/50, equal balances - locals.w0 = 5e17; - locals.w1 = 5e17; - locals.b0 = 1e24; - locals.b1 = 1e24; - locals.P = fee_pairSpotFromBW(locals.b0, locals.w0, locals.b1, locals.w1); + locals.w = [uint256(5e17), uint256(5e17)].toMemoryArray(); + locals.b = [uint256(1e24), uint256(1e24)].toMemoryArray(); + locals.P = fee_pairSpotFromBW(locals.b[0], locals.w[0], locals.b[1], locals.w[1]); assertGt(locals.P, 0); locals.thr = 1_000_000; // 0.1% @@ -886,60 +846,53 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo HyperSurgeHookMock mock = new HyperSurgeHookMock( IVault(vault), - fee_ppm9To1e18(locals.maxp), - fee_ppm9To1e18(locals.thr), - fee_ppm9To1e18(locals.cap), + _convertTo18Decimals(locals.maxp), + _convertTo18Decimals(locals.thr), + _convertTo18Decimals(locals.cap), "fee-boundary" ); - HyperSurgeHookMock.ComputeSurgeFeeLocals memory computeLocals; - PoolSwapParams memory p; + + PoolSwapParams memory p = _createPoolSwapParams(SwapKind.EXACT_IN, locals.b, 0, 1, 0); p.kind = SwapKind.EXACT_IN; // Below threshold - locals.D = fee_ppm9To1e18(locals.thr) - 1; + locals.D = _convertTo18Decimals(locals.thr) - 1; (locals.pxIn, locals.pxOut) = fee_localsForDeviation(locals.P, locals.D); - computeLocals.bIn = locals.b0; - computeLocals.wIn = locals.w0; - computeLocals.bOut = locals.b1; - computeLocals.wOut = locals.w1; - computeLocals.pxIn = locals.pxIn; - computeLocals.pxOut = locals.pxOut; - computeLocals.calcAmountScaled18 = 0; + HyperSurgeHook.PoolDetails memory poolDetails; + poolDetails.numTokens = 2; // ARB lane = locals’ params (since deviation doesn’t increase with calcAmount=0) - computeLocals.poolDetails.arbThresholdPercentage9 = locals.thr; - computeLocals.poolDetails.arbCapDeviationPercentage9 = locals.cap; - computeLocals.poolDetails.arbMaxSurgeFee9 = locals.maxp; + poolDetails.arbThresholdPercentage9 = locals.thr; + poolDetails.arbCapDeviationPercentage9 = locals.cap; + poolDetails.arbMaxSurgeFee9 = locals.maxp; // Make NOISE lane different - computeLocals.poolDetails.noiseThresholdPercentage9 = locals.thr + 1; - computeLocals.poolDetails.noiseCapDeviationPercentage9 = locals.cap - 1; - computeLocals.poolDetails.noiseMaxSurgeFee9 = locals.maxp + 1; + poolDetails.noiseThresholdPercentage9 = locals.thr + 1; + poolDetails.noiseCapDeviationPercentage9 = locals.cap - 1; + poolDetails.noiseMaxSurgeFee9 = locals.maxp + 1; - (, locals.feeA) = mock.ComputeSurgeFee(computeLocals, p, STATIC_SWAP_FEE); + (, locals.feeA) = mock.ComputeSurgeFee( + p, + poolDetails, + STATIC_SWAP_FEE, + locals.w, + 0, + locals.pxOut.divDown(locals.pxIn) + ); assertEq(locals.feeA, STATIC_SWAP_FEE, "below threshold means static fee"); - locals.D = (fee_ppm9To1e18(locals.thr) + fee_ppm9To1e18(locals.cap)) / 2; + locals.D = (_convertTo18Decimals(locals.thr) + _convertTo18Decimals(locals.cap)) / 2; (locals.pxIn, locals.pxOut) = fee_localsForDeviation(locals.P, locals.D); - computeLocals.bIn = locals.b0; - computeLocals.wIn = locals.w0; - computeLocals.bOut = locals.b1; - computeLocals.wOut = locals.w1; - computeLocals.pxIn = locals.pxIn; - computeLocals.pxOut = locals.pxOut; - computeLocals.calcAmountScaled18 = 0; - - computeLocals.poolDetails.arbThresholdPercentage9 = locals.thr; - computeLocals.poolDetails.arbCapDeviationPercentage9 = locals.cap; - computeLocals.poolDetails.arbMaxSurgeFee9 = locals.maxp; - - computeLocals.poolDetails.noiseThresholdPercentage9 = locals.thr + 1; - computeLocals.poolDetails.noiseCapDeviationPercentage9 = locals.cap - 1; - computeLocals.poolDetails.noiseMaxSurgeFee9 = locals.maxp + 1; - - (, locals.feeB) = mock.ComputeSurgeFee(computeLocals, p, STATIC_SWAP_FEE); + (, locals.feeB) = mock.ComputeSurgeFee( + p, + poolDetails, + STATIC_SWAP_FEE, + locals.w, + 0, + locals.pxOut.divDown(locals.pxIn) + ); uint256 expected = fee_expectedFeeWithParams( locals.P, @@ -954,2000 +907,1984 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo // At cap and above cap - uint256 Dcap = fee_ppm9To1e18(locals.cap); + uint256 Dcap = _convertTo18Decimals(locals.cap); (locals.pxIn, locals.pxOut) = fee_localsForDeviation(locals.P, Dcap); - HyperSurgeHookMock.ComputeSurgeFeeLocals memory computeLocals1; - computeLocals1.bIn = locals.b0; - computeLocals1.wIn = locals.w0; - computeLocals1.bOut = locals.b1; - computeLocals1.wOut = locals.w1; - computeLocals1.pxIn = locals.pxIn; - computeLocals1.pxOut = locals.pxOut; - computeLocals1.calcAmountScaled18 = 0; - computeLocals1.poolDetails.arbThresholdPercentage9 = locals.thr; - computeLocals1.poolDetails.arbCapDeviationPercentage9 = locals.cap; - computeLocals1.poolDetails.arbMaxSurgeFee9 = locals.maxp; - computeLocals1.poolDetails.noiseThresholdPercentage9 = locals.thr + 1; - computeLocals1.poolDetails.noiseCapDeviationPercentage9 = locals.cap - 1; - computeLocals1.poolDetails.noiseMaxSurgeFee9 = locals.maxp + 1; - - (, locals.feeC) = mock.ComputeSurgeFee(computeLocals1, p, STATIC_SWAP_FEE); - assertEq(locals.feeC, fee_ppm9To1e18(locals.maxp), "at cap means max fee"); - - uint256 Dbeyond = Dcap + 1; - (locals.pxIn, locals.pxOut) = fee_localsForDeviation(locals.P, Dbeyond); - - HyperSurgeHookMock.ComputeSurgeFeeLocals memory computeLocals2; - computeLocals2.bIn = locals.b0; - computeLocals2.wIn = locals.w0; - computeLocals2.bOut = locals.b1; - computeLocals2.wOut = locals.w1; - computeLocals2.pxIn = locals.pxIn; - computeLocals2.pxOut = locals.pxOut; - computeLocals2.calcAmountScaled18 = 0; - computeLocals2.poolDetails.arbThresholdPercentage9 = locals.thr; - computeLocals2.poolDetails.arbCapDeviationPercentage9 = locals.cap; - computeLocals2.poolDetails.arbMaxSurgeFee9 = locals.maxp; - computeLocals2.poolDetails.noiseThresholdPercentage9 = locals.thr + 1; - computeLocals2.poolDetails.noiseCapDeviationPercentage9 = locals.cap - 1; - computeLocals2.poolDetails.noiseMaxSurgeFee9 = locals.maxp + 1; - - (, locals.feeD) = mock.ComputeSurgeFee(computeLocals2, p, STATIC_SWAP_FEE); - assertEq(locals.feeD, fee_ppm9To1e18(locals.maxp), "above cap means clamped to max fee"); - } - - struct ExactInEqualsExactOutLocals { - uint8 n; - uint256[] w; - uint256[] b; - uint8 i; - uint8 j; - uint32 thr; - uint32 cap; - uint32 maxp; - uint256 P; - uint256 capDev; - uint256 D; - uint256 pxIn; - uint256 pxOut; - uint256 feeIn; - uint256 feeOut; - } - - /// EXACT_IN vs EXACT_OUT: with identical lane params, the engine result must match. - /// Correction: keep the *effective* lane params for the chosen direction the same, - /// but make ARB and NOISE lanes different so a wrong-lane implementation would not hide here. - function testFuzz_internal_exactIn_equals_exactOut_whenParamsSame( - uint8 nSeed, - uint256 wSeed, - uint256 bSeed, - uint256 dSeed - ) public { - ExactInEqualsExactOutLocals memory locals; - - locals.n = uint8(bound(nSeed, 2, 8)); - locals.w = fee_normWeights(locals.n, wSeed); - locals.b = fee_balances(locals.n, bSeed); - - locals.i = uint8(bound(uint256(keccak256(abi.encode(dSeed, 41))), 0, locals.n - 1)); - locals.j = (locals.i + 1 + uint8(bound(uint256(keccak256(abi.encode(dSeed, 42))), 0, locals.n - 2))) % locals.n; - - locals.thr = 1_000_000; // 0.1% - locals.cap = 500_000_000; // 50% - locals.maxp = 50_000_000; // 5% - - locals.P = fee_pairSpotFromBW(locals.b[locals.i], locals.w[locals.i], locals.b[locals.j], locals.w[locals.j]); - vm.assume(locals.P > 0); - - locals.capDev = fee_ppm9To1e18(locals.cap); - locals.D = uint256(keccak256(abi.encode(dSeed))) % (locals.capDev + locals.capDev / 2 + 1); - (locals.pxIn, locals.pxOut) = fee_localsForDeviation(locals.P, locals.D); - - HyperSurgeHookMock mock = new HyperSurgeHookMock( - IVault(vault), - fee_ppm9To1e18(locals.maxp), - fee_ppm9To1e18(locals.thr), - fee_ppm9To1e18(locals.cap), - "fee-io" - ); - - // EXACT_IN - PoolSwapParams memory pIn; - pIn.kind = SwapKind.EXACT_IN; - - // Build locals with NOISE = (thr/cap/maxp) and ARB deliberately different - HyperSurgeHookMock.ComputeSurgeFeeLocals memory L1; - L1.bIn = locals.b[locals.i]; - L1.wIn = locals.w[locals.i]; - L1.bOut = locals.b[locals.j]; - L1.wOut = locals.w[locals.j]; - L1.pxIn = locals.pxIn; - L1.pxOut = locals.pxOut; - L1.calcAmountScaled18 = 0; - - // Effective (chosen) lane params - L1.poolDetails.noiseThresholdPercentage9 = locals.thr; - L1.poolDetails.noiseCapDeviationPercentage9 = locals.cap; - L1.poolDetails.noiseMaxSurgeFee9 = locals.maxp; - - // Different ARB lane params so wrong-lane usage wouldn’t accidentally match - L1.poolDetails.arbThresholdPercentage9 = locals.thr + 1; - L1.poolDetails.arbCapDeviationPercentage9 = locals.cap - 1; - L1.poolDetails.arbMaxSurgeFee9 = locals.maxp + 1; - - (, locals.feeIn) = mock.ComputeSurgeFee(L1, pIn, STATIC_SWAP_FEE); - - // EXACT_OUT - PoolSwapParams memory pOut; - pOut.kind = SwapKind.EXACT_OUT; - - HyperSurgeHookMock.ComputeSurgeFeeLocals memory L2; - L2.bIn = locals.b[locals.i]; - L2.wIn = locals.w[locals.i]; - L2.bOut = locals.b[locals.j]; - L2.wOut = locals.w[locals.j]; - L2.pxIn = locals.pxIn; - L2.pxOut = locals.pxOut; - L2.calcAmountScaled18 = 0; - - L2.poolDetails.noiseThresholdPercentage9 = locals.thr; - L2.poolDetails.noiseCapDeviationPercentage9 = locals.cap; - L2.poolDetails.noiseMaxSurgeFee9 = locals.maxp; - - L2.poolDetails.arbThresholdPercentage9 = locals.thr + 1; - L2.poolDetails.arbCapDeviationPercentage9 = locals.cap - 1; - L2.poolDetails.arbMaxSurgeFee9 = locals.maxp + 1; - - (, locals.feeOut) = mock.ComputeSurgeFee(L2, pOut, STATIC_SWAP_FEE); - - assertEq(locals.feeIn, locals.feeOut, "with equal lane params, kind should not change math result"); - } - - function testFuzz_view_missingPrices_reverts(uint8 nSeed, uint256 /* wSeed */, uint256 bSeed, uint8 iSeed) public { - // --- Register pool and adapt to its actual token count --- - uint8 nTarget = uint8(bound(nSeed, 2, 8)); - _registerBasePoolWithN(nTarget); - - uint256[] memory weights = WeightedPool(address(pool)).getNormalizedWeights(); - uint256 m = weights.length; - assertGe(m, 2, "pool must have at least 2 tokens"); - - // --- Random non-zero balances of exact pool length --- - uint256[] memory b = fee_balances(uint8(m), bSeed); - - // --- Pick a valid distinct pair (i != j) --- - uint256 i = uint256(bound(iSeed, 0, m - 1)); - uint256 j = (i + 1) % m; - - // --- Build base swap params template with those balances --- - PoolSwapParams memory p; - p.balancesScaled18 = new uint256[](m); - for (uint256 k = 0; k < m; ++k) { - p.balancesScaled18[k] = b[k]; - } - p.indexIn = i; - p.indexOut = j; - - uint256 bIn = b[i]; - uint256 bOut = b[j]; - - uint256 safeInAmt = bIn / 1e6; - if (safeInAmt == 0) safeInAmt = 1; - uint256 safeOutAmt = bOut / 1e6; - if (safeOutAmt == 0) safeOutAmt = 1; - - // Sanity: amounts are indeed tiny relative to balances to avoid accidental reverts - // (these checks also self-document the invariant we rely on) - assertLt(safeInAmt, bIn / 10, "safeInAmt too large vs balanceIn"); // < 10% (much stricter in practice) - assertLt(safeOutAmt, bOut / 10, "safeOutAmt too large vs balanceOut"); // < 10% - - p.kind = SwapKind.EXACT_IN; - p.amountGivenScaled18 = safeInAmt; - - vm.expectRevert(HyperSpotPricePrecompile.SpotPriceIsZero.selector); - hook.onComputeDynamicSwapFeePercentage(p, address(pool), STATIC_SWAP_FEE); - - p.kind = SwapKind.EXACT_OUT; - p.amountGivenScaled18 = safeOutAmt; - - vm.expectRevert(HyperSpotPricePrecompile.SpotPriceIsZero.selector); - hook.onComputeDynamicSwapFeePercentage(p, address(pool), STATIC_SWAP_FEE); - } - - function testFuzz_view_readsLaneParams_reverts_onSafePath(uint8 nSeed) public { - uint8 n = uint8(bound(nSeed, 2, 8)); - _registerBasePoolWithN(n); - - // Diverge NOISE and ARB lane params (authorized admin) - vm.startPrank(admin); - hook.setSurgeThresholdPercentage(address(pool), 5_000_000 * 1e9, IHyperSurgeHook.TradeType.NOISE); // 0.5% - hook.setCapDeviationPercentage(address(pool), 400_000_000 * 1e9, IHyperSurgeHook.TradeType.NOISE); // 40% - hook.setMaxSurgeFeePercentage(address(pool), 25_000_000 * 1e9, IHyperSurgeHook.TradeType.NOISE); // 2.5% - - hook.setSurgeThresholdPercentage(address(pool), 1_000_000 * 1e9, IHyperSurgeHook.TradeType.ARBITRAGE); // 0.1% - hook.setCapDeviationPercentage(address(pool), 300_000_000 * 1e9, IHyperSurgeHook.TradeType.ARBITRAGE); // 30% - hook.setMaxSurgeFeePercentage(address(pool), 50_000_000 * 1e9, IHyperSurgeHook.TradeType.ARBITRAGE); // 5% - vm.stopPrank(); - - // Adapt to the pool’s true size to avoid OOB / shape mismatches - uint256[] memory weights = WeightedPool(address(pool)).getNormalizedWeights(); - uint256 m = weights.length; - assertGe(m, 2, "pool must have at least 2 tokens"); - - // Build non-zero balances of correct length m - uint256[] memory balances = new uint256[](m); - for (uint256 k = 0; k < m; ++k) { - balances[k] = 1e24 + k; - } - - PoolSwapParams memory p; - p.amountGivenScaled18 = 1e18; // non-zero trade amount - p.balancesScaled18 = balances; - p.indexIn = 0; - p.indexOut = (m > 1) ? 1 : 0; - - // EXACT_IN: either revert or static fee (but never a computed dynamic fee) - p.kind = SwapKind.EXACT_IN; - vm.expectRevert(HyperSpotPricePrecompile.SpotPriceIsZero.selector); - hook.onComputeDynamicSwapFeePercentage(p, address(pool), STATIC_SWAP_FEE); - - // EXACT_OUT: same invariant - p.kind = SwapKind.EXACT_OUT; - vm.expectRevert(HyperSpotPricePrecompile.SpotPriceIsZero.selector); - hook.onComputeDynamicSwapFeePercentage(p, address(pool), STATIC_SWAP_FEE); - } - - struct DeviationEqualsThreshold { - uint256 staticFee; - uint256 maxFee; - uint32 thr9; - uint32 cap9; - uint32 max9; - uint256 E; - uint256 thr; - uint256 fee; - } - - /// 1) deviation == threshold => returns static fee (boundary counted as "inside") - function test_cfg_fee_static_at_threshold_usingMockWrapper() public view { - DeviationEqualsThreshold memory locals; - - locals.staticFee = 30e14; // 30 bps = 0.003 * 1e18 - locals.maxFee = 120e14; // 120 bps - - // 9 lane params (contract upscales to 18dp) - locals.thr9 = 100_000_000; // 10% - locals.cap9 = 500_000_000; // 50% - locals.max9 = uint32(locals.maxFee / 1e9); - - HyperSurgeHookMock.ComputeSurgeFeeLocals memory computeLocals; - computeLocals.pxIn = 1e18; - computeLocals.pxOut = 10e18; // external price E = 10 - - // set both lanes the same (lane choice irrelevant for this edge) - computeLocals.poolDetails.noiseThresholdPercentage9 = locals.thr9; - computeLocals.poolDetails.noiseCapDeviationPercentage9 = locals.cap9; - computeLocals.poolDetails.noiseMaxSurgeFee9 = locals.max9; - computeLocals.poolDetails.arbThresholdPercentage9 = locals.thr9; - computeLocals.poolDetails.arbCapDeviationPercentage9 = locals.cap9; - computeLocals.poolDetails.arbMaxSurgeFee9 = locals.max9; - - PoolSwapParams memory p; - p.kind = SwapKind.EXACT_IN; - p.amountGivenScaled18 = 0; - - locals.E = 10e18; - locals.thr = uint256(locals.thr9) * 1e9; // 18dp - - locals.fee = _feeAtDeviation(computeLocals, p, locals.staticFee, locals.E, locals.thr); - assertEq(locals.fee, locals.staticFee, "fee must equal static when deviation == threshold"); - } - - struct justAboveThreshold { - uint256 staticFee; - uint256 maxFee; - uint32 thr9; - uint32 cap9; - uint32 max9; - uint256 E; - uint256 thr; - uint256 cap; - uint256 dev; - uint256 span; - uint256 ramp; - uint256 expected; - } - - function test_cfg_fee_minimalRamp_just_above_threshold() public view { - justAboveThreshold memory locals; - - locals.staticFee = 30e14; // 30 bps - locals.maxFee = 120e14; // 120 bps - - locals.thr9 = 100_000_000; // 10% - locals.cap9 = 500_000_000; // 50% - locals.max9 = uint32(locals.maxFee / 1e9); - - HyperSurgeHookMock.ComputeSurgeFeeLocals memory computeLocals; - computeLocals.pxIn = 1e18; - computeLocals.pxOut = 10e18; - - computeLocals.poolDetails.noiseThresholdPercentage9 = locals.thr9; - computeLocals.poolDetails.noiseCapDeviationPercentage9 = locals.cap9; - computeLocals.poolDetails.noiseMaxSurgeFee9 = locals.max9; - computeLocals.poolDetails.arbThresholdPercentage9 = locals.thr9; - computeLocals.poolDetails.arbCapDeviationPercentage9 = locals.cap9; - computeLocals.poolDetails.arbMaxSurgeFee9 = locals.max9; - - PoolSwapParams memory p; - p.kind = SwapKind.EXACT_IN; - p.amountGivenScaled18 = 0; - - locals.E = 10e18; - locals.thr = uint256(locals.thr9) * 1e9; - locals.cap = uint256(locals.cap9) * 1e9; - locals.dev = (uint256(locals.thr9) + 1) * 1e9; // smallest 18dp step above threshold - - uint256 fee = _feeAtDeviation(computeLocals, p, locals.staticFee, locals.E, locals.dev); - - // Expected: static + (max - static) * (dev - thr) / (cap - thr) (div-down) - locals.span = locals.cap - locals.thr; - locals.ramp = ((locals.maxFee - locals.staticFee) * (locals.dev - locals.thr)) / locals.span; - locals.expected = locals.staticFee + locals.ramp; - - assertEq(fee, locals.expected, "minimal ramp just above threshold"); - assertGt(fee, locals.staticFee, "fee > static just above threshold"); - assertLt(fee, locals.maxFee, "fee < max when deviation < cap"); - } - - struct MaxEqualsStatic { - uint256 staticFee; - uint256 maxFee; - uint32 thr9; - uint32 cap9; - uint32 max9; - uint256 E; - uint256 thr; - uint256 cap; - uint256 devAtThr; - uint256 devMid; - uint256 devAtCap; - uint256 devBeyond; - } - - /// 3) degenerate: max == static => always static (even outside threshold) - function test_cfg_fee_degenerateRamp_max_equals_static() public view { - MaxEqualsStatic memory locals; - - locals.staticFee = 45e14; // 45 bps - locals.maxFee = locals.staticFee; - - locals.thr9 = 50_000_000; // 5% - locals.cap9 = 250_000_000; // 25% - locals.max9 = uint32(locals.maxFee / 1e9); - - HyperSurgeHookMock.ComputeSurgeFeeLocals memory computeLocals; - computeLocals.pxIn = 1e18; - computeLocals.pxOut = 10e18; - - computeLocals.poolDetails.noiseThresholdPercentage9 = locals.thr9; - computeLocals.poolDetails.noiseCapDeviationPercentage9 = locals.cap9; - computeLocals.poolDetails.noiseMaxSurgeFee9 = locals.max9; - computeLocals.poolDetails.arbThresholdPercentage9 = locals.thr9; - computeLocals.poolDetails.arbCapDeviationPercentage9 = locals.cap9; - computeLocals.poolDetails.arbMaxSurgeFee9 = locals.max9; - - PoolSwapParams memory p; - p.kind = SwapKind.EXACT_IN; - p.amountGivenScaled18 = 0; - - locals.E = 10e18; - locals.thr = uint256(locals.thr9) * 1e9; - locals.cap = uint256(locals.cap9) * 1e9; - - locals.devAtThr = locals.thr; - locals.devMid = locals.thr + (locals.cap - locals.thr) / 2; - locals.devAtCap = locals.cap; - locals.devBeyond = locals.cap + 12345; - - assertEq( - _feeAtDeviation(computeLocals, p, locals.staticFee, locals.E, locals.devAtThr), - locals.staticFee, - "at thr => static" - ); - assertEq( - _feeAtDeviation(computeLocals, p, locals.staticFee, locals.E, locals.devMid), - locals.staticFee, - "mid => static" - ); - assertEq( - _feeAtDeviation(computeLocals, p, locals.staticFee, locals.E, locals.devAtCap), - locals.staticFee, - "at cap => static" - ); - assertEq( - _feeAtDeviation(computeLocals, p, locals.staticFee, locals.E, locals.devBeyond), - locals.staticFee, - "beyond cap => static" - ); - } - - struct MaxBelowStatic { - uint256 staticFee; - uint256 maxFee; - uint32 thr9; - uint32 cap9; - uint32 max9; - uint256 E; - uint256 thr; - uint256 cap; - uint256 devMid; - uint256 feeMid; - uint256 span; - uint256 ramp; - uint256 expected; - } - - function test_fee_misconfig_maxBelowStatic_usingMockWrapper() public { - MaxBelowStatic memory locals; - - // Misconfig: max < static - locals.staticFee = 80e14; // 80 bps (1e18 scale) - locals.maxFee = 20e14; // 20 bps (1e18 scale) -> lower than static - locals.thr9 = 100_000_000; // 10% in 1e9 - locals.cap9 = 300_000_000; // 30% in 1e9 - locals.max9 = uint32(locals.maxFee / 1e9); - - // Local mock (don’t rely on global `hook`) - HyperSurgeHookMock mock = new HyperSurgeHookMock( - IVault(vault), - fee_ppm9To1e18(locals.max9), - fee_ppm9To1e18(locals.thr9), - fee_ppm9To1e18(locals.cap9), - "misconfig-maxBelowStatic" - ); - - // Base inputs used for both sub-tests - locals.E = 10e18; // external price - locals.thr = uint256(locals.thr9) * 1e9; // 18dp threshold - locals.cap = uint256(locals.cap9) * 1e9; // 18dp cap - - HyperSurgeHookMock.ComputeSurgeFeeLocals memory base; - base.pxIn = 1e18; - base.pxOut = locals.E; - - // Set BOTH lanes to the same (misconfigured) params so lane choice doesn't matter here. - base.poolDetails.noiseThresholdPercentage9 = locals.thr9; - base.poolDetails.noiseCapDeviationPercentage9 = locals.cap9; - base.poolDetails.noiseMaxSurgeFee9 = locals.max9; - base.poolDetails.arbThresholdPercentage9 = locals.thr9; - base.poolDetails.arbCapDeviationPercentage9 = locals.cap9; - base.poolDetails.arbMaxSurgeFee9 = locals.max9; - - PoolSwapParams memory p; - p.kind = SwapKind.EXACT_IN; - p.amountGivenScaled18 = 0; // keep balances-based price exact - p.balancesScaled18 = new uint256[](2); - p.balancesScaled18[0] = 1e18; - p.balancesScaled18[1] = locals.E; - - // Reused working struct - HyperSurgeHookMock.ComputeSurgeFeeLocals memory T; - - // ---------- (a) dev >= cap -> revert (underflow in mock ramp) ---------- - uint256 dev = locals.cap + 999; // strictly beyond cap - uint256 P = locals.E + (locals.E * dev) / 1e18; // P = E * (1 + dev) - T = base; - T.wIn = 1e18; - T.wOut = 1e18; - T.bIn = 1e18; - T.bOut = P; - T.calcAmountScaled18 = 0; - - vm.expectRevert(stdError.arithmeticError); - mock.ComputeSurgeFee(T, p, locals.staticFee); - - // ---------- (b) thr < dev < cap -> revert (underflow in mock ramp) ---------- - dev = locals.thr + (locals.cap - locals.thr) / 3; // strictly between thr & cap - P = locals.E + (locals.E * dev) / 1e18; - T = base; - T.wIn = 1e18; - T.wOut = 1e18; - T.bIn = 1e18; - T.bOut = P; - T.calcAmountScaled18 = 0; - - vm.expectRevert(stdError.arithmeticError); - mock.ComputeSurgeFee(T, p, locals.staticFee); - } - - struct OutsideDynamicAfterLocals { - uint256 E; - uint32 noiseThr9; - uint32 noiseCap9; - uint32 noiseMax9; - uint32 arbThr9; - uint32 arbCap9; - uint32 arbMax9; - uint256 thr; - uint256 cap; - uint256 deviationBefore; - uint256 price_before; - uint256 price_after; - HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - PoolSwapParams p; - uint256 expected; - uint256 dyn; - } - - /// 1) Noise: starts outside threshold, deviation worsens → NOISE lane, dynamic fee based on **after** deviation. - function testFuzz_logic_noise_worsens_outside_dynamic_after( - uint256 eSeed, - uint32 noiseThrSeed, - uint32 noiseCapSeed, - uint32 noiseMaxSeed, - uint64 amtSeed - ) public { - OutsideDynamicAfterLocals memory locals; - - // --- Fuzz + bounds --- - locals.E = bound(eSeed, 1e16, 1e24); // pxOut - locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); - locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); - locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); - // ARB lane (unused here, but keep distinct) - locals.arbThr9 = 1_000_000; - locals.arbCap9 = 300_000_000; - locals.arbMax9 = 50_000_000; - - locals.thr = uint256(locals.noiseThr9) * 1e9; - locals.cap = uint256(locals.noiseCap9) * 1e9; - locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; // strictly outside - - // Start BELOW E: price_before = E * (1 - deviationBefore) - locals.price_before = locals.E - (locals.E * locals.deviationBefore) / 1e18; - - // Build compute locals + swap that worsens deviation (EXACT_IN; calc=0 → P decreases further) - locals.comp.wIn = 1e18; - locals.comp.wOut = 1e18; - locals.comp.bIn = 1e18; - locals.comp.bOut = locals.price_before; - locals.comp.pxIn = 1e18; - locals.comp.pxOut = locals.E; - locals.comp.calcAmountScaled18 = 0; - locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; - locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; - locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; - - locals.p.kind = SwapKind.EXACT_IN; - // ensure deviation increases *measurably* in Q18 (avoid 1-wei changes) - locals.p.amountGivenScaled18 = bound(uint256(amtSeed), 1e9, 5e17); // [1e9, 0.5e18] - - // Expected (NOISE) uses AFTER deviation: price_after = price_before / (1 + x) - locals.price_after = (locals.price_before * 1e18) / (1e18 + locals.p.amountGivenScaled18); - locals.expected = fee_expectedFeeWithParams( - locals.price_after, - locals.comp.pxIn, - locals.comp.pxOut, - STATIC_SWAP_FEE, - locals.noiseThr9, - locals.noiseCap9, - locals.noiseMax9 - ); - - HyperSurgeHookMock mock = new HyperSurgeHookMock( - IVault(vault), - fee_ppm9To1e18(locals.arbMax9), - fee_ppm9To1e18(locals.arbThr9), - fee_ppm9To1e18(locals.arbCap9), - "logic-1" - ); - (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); - - assertEq(locals.dyn, locals.expected, "noise path must use AFTER deviation for dynamic fee"); - assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee >= static"); - } - - struct BetterStillOutsideLocals { - uint256 E; - uint32 arbThr9; - uint32 arbCap9; - uint32 arbMax9; - uint32 noiseThr9; - uint32 noiseCap9; - uint32 noiseMax9; - uint256 thr; - uint256 cap; - uint256 deviationBefore; - uint256 price_before; - uint256 price_after; - uint256 xMax; - HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - PoolSwapParams p; - uint256 expected; - uint256 dyn; - } - - function testFuzz_logic_arb_outside_improves_but_outside_dynamic_before( - uint256 eSeed, - uint32 arbThrSeed, - uint32 arbCapSeed, - uint32 arbMaxSeed, - uint64 amtSeed - ) public { - BetterStillOutsideLocals memory locals; - - // --- Fuzz + bounds --- - locals.E = bound(eSeed, 1e16, 1e24); - locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); - locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); - locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); - // NOISE lane different (unused in assertion) - locals.noiseThr9 = 5_000_000; - locals.noiseCap9 = 400_000_000; - locals.noiseMax9 = 25_000_000; - - locals.thr = uint256(locals.arbThr9) * 1e9; - locals.cap = uint256(locals.arbCap9) * 1e9; - locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; // strictly outside - - // Start ABOVE E - locals.price_before = locals.E + (locals.E * locals.deviationBefore) / 1e18; - - // Compute xMax to remain outside after: price_after >= E*(1 + thr) - // price_after = price_before / (1 + x) means x less than or equal to (price_before / (E*(1+thr)) - 1) * 1e18 - vm.assume(locals.E * (1e18 + locals.thr) != 0); // defensive - uint256 denom = (locals.E * (1e18 + locals.thr)) / 1e18; - vm.assume(denom != 0); - uint256 ratio = (locals.price_before * 1e18) / denom; - vm.assume(ratio > 1e18); // Ensure room to remain outside - locals.xMax = ratio - 1e18; - if (locals.xMax > 9e17) { - locals.xMax = 9e17; - } // clamp - - locals.comp.wIn = 1e18; - locals.comp.wOut = 1e18; - locals.comp.bIn = 1e18; - locals.comp.bOut = locals.price_before; - locals.comp.pxIn = 1e18; - locals.comp.pxOut = locals.E; - locals.comp.calcAmountScaled18 = 0; - locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; - locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; - locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; - - locals.p.kind = SwapKind.EXACT_IN; - locals.p.amountGivenScaled18 = bound(uint256(amtSeed), 1, locals.xMax == 0 ? 1 : locals.xMax); - - // Expected (ARB) uses BEFORE deviation - locals.expected = fee_expectedFeeWithParams( - locals.price_before, - locals.comp.pxIn, - locals.comp.pxOut, - STATIC_SWAP_FEE, - locals.arbThr9, - locals.arbCap9, - locals.arbMax9 - ); - - HyperSurgeHookMock mock = new HyperSurgeHookMock( - IVault(vault), - fee_ppm9To1e18(locals.arbMax9), - fee_ppm9To1e18(locals.arbThr9), - fee_ppm9To1e18(locals.arbCap9), - "logic-2" - ); - (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); - - // Still outside afterward (sanity) - locals.price_after = (locals.price_before * 1e18) / (1e18 + locals.p.amountGivenScaled18); - uint256 deviationAfter = (( - locals.price_after > locals.E ? (locals.price_after - locals.E) : (locals.E - locals.price_after) - ) * 1e18) / locals.E; - assertGt(deviationAfter, locals.thr, "should remain outside threshold after improving"); - - assertEq(locals.dyn, locals.expected, "arb path must use BEFORE deviation for dynamic fee"); - assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee >= static"); - } - - struct NoiseWorsensInsideButStaysInsideLocals { - uint256 E; - uint32 noiseThr9; - uint32 noiseCap9; - uint32 noiseMax9; - uint32 arbThr9; - uint32 arbCap9; - uint32 arbMax9; - uint256 thr; - uint256 deviationBefore; - uint256 price_before; - uint256 price_after; - uint256 xMax; - HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - PoolSwapParams p; - uint256 fee; - } - - /// 3) Noise: starts inside threshold, worsens but stays inside → NOISE lane, **base (static)** fee. - function testFuzz_logic_noise_inside_worse_but_inside_static( - uint256 eSeed, - uint32 noiseThrSeed, - uint32 noiseCapSeed, - uint32 noiseMaxSeed, - uint64 amtSeed - ) public { - NoiseWorsensInsideButStaysInsideLocals memory locals; - - locals.E = bound(eSeed, 1e16, 1e24); - locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 1_000_000_000 - 1)); // (0,1) - locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); - locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); - - locals.arbThr9 = 1_000_000; - locals.arbCap9 = 300_000_000; - locals.arbMax9 = 50_000_000; - locals.thr = uint256(locals.noiseThr9) * 1e9; - locals.deviationBefore = locals.thr / 4 + 1; - locals.price_before = locals.E - (locals.E * locals.deviationBefore) / 1e18; - - uint256 R1e18 = (locals.price_before * 1e18) / locals.E; - uint256 denom = 1e18 - locals.thr; - uint256 q = (R1e18 * 1e18) / denom; - locals.xMax = q > 1e18 ? (q - 1e18) : 0; - if (locals.xMax > 5e17) { - locals.xMax = 5e17; - } - - locals.comp.wIn = 1e18; - locals.comp.wOut = 1e18; - locals.comp.bIn = 1e18; - locals.comp.bOut = locals.price_before; - locals.comp.pxIn = 1e18; - locals.comp.pxOut = locals.E; - locals.comp.calcAmountScaled18 = 0; - locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; - locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; - locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; - - locals.p.kind = SwapKind.EXACT_IN; - - // Ensure a *measurable* worsening so NOISE is chosen: - // pick x with a lower floor (e.g., 1e9 wei) but never exceed xMax. - uint256 lo = 1e9; // 1e-9 in t; safely above Q18 rounding noise - uint256 hi = locals.xMax; - if (hi < lo) { - lo = 1; - } // if xMax < floor, fall back to [1, xMax] - if (hi < lo) { - hi = lo; - } // clamp - locals.p.amountGivenScaled18 = bound(uint256(amtSeed), lo, hi); - - HyperSurgeHookMock mock = new HyperSurgeHookMock( - IVault(vault), - fee_ppm9To1e18(locals.arbMax9), - fee_ppm9To1e18(locals.arbThr9), - fee_ppm9To1e18(locals.arbCap9), - "logic-3" - ); - (, locals.fee) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); - - // Sanity: still inside after worsening - locals.price_after = (locals.price_before * 1e18) / (1e18 + locals.p.amountGivenScaled18); - uint256 deviationAfter = (( - locals.price_after > locals.E ? (locals.price_after - locals.E) : (locals.E - locals.price_after) - ) * 1e18) / locals.E; - assertLe(deviationAfter, locals.thr, "must remain inside threshold"); - - // Inside-after on NOISE → static - assertEq(locals.fee, STATIC_SWAP_FEE, "inside threshold after worsening must still return static (noise path)"); - } - - struct NoiseCrossesPriceWorsensLocals { - uint256 E; - uint32 noiseThr9; - uint32 noiseCap9; - uint32 noiseMax9; - uint32 arbThr9; - uint32 arbCap9; - uint32 arbMax9; - uint256 thr; - uint256 cap; - uint256 deviationBefore; - uint256 price_before; - uint256 price_after; - uint256 tCross; - uint256 tWorse; - uint256 tMin; - uint256 x; - uint256 num; // numerator for tWorse calculation - uint256 den; // denominator for tWorse calculation - uint256 q; // intermediate value for tWorse calculation - uint256 epsT; // safety margin for tMin - uint256 span; // range for x selection - uint256 lo; // lower bound for x - uint256 hi; // upper bound for x - uint256 deviationAfter; // absolute deviation after - HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - PoolSwapParams p; - uint256 expected; - uint256 dyn; - } - - function testFuzz_logic_noise_outside_crosses_and_worsens_dynamic_after( - uint256 eSeed, - uint32 noiseThrSeed, - uint32 noiseCapSeed, - uint32 noiseMaxSeed, - uint64 amtSeed - ) public { - NoiseCrossesPriceWorsensLocals memory locals; - - // --- Fuzz + bounds --- - locals.E = bound(eSeed, 1e16, 1e24); - - // Keep thr < 1 so denominators stay positive and bands are non-degenerate - locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); // (0, 0.9) - locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); // (thr, 1] - locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); - - // ARB lane different (unused in assertion) - locals.arbThr9 = 1_000_000; - locals.arbCap9 = 300_000_000; - locals.arbMax9 = 50_000_000; - - locals.thr = uint256(locals.noiseThr9) * 1e9; - locals.cap = uint256(locals.noiseCap9) * 1e9; - - // Start ABOVE E with a deviation strictly outside the threshold: - locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 4; - locals.price_before = locals.E + (locals.E * locals.deviationBefore) / 1e18; - - // Build compute locals - locals.comp.wIn = 1e18; - locals.comp.wOut = 1e18; - locals.comp.bIn = 1e18; - locals.comp.bOut = locals.price_before; - locals.comp.pxIn = 1e18; - locals.comp.pxOut = locals.E; - locals.comp.calcAmountScaled18 = 0; - locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; - locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; - locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; - - locals.p.kind = SwapKind.EXACT_IN; - - // We need BOTH: - // (1) Cross: price_after < E means t > Db (R = 1 + Db) - // (2) Worsen: |after| > |before| when ending below: - // 1 - R/(1+t) > Db means (1 - Db)(1 + t) > 1 + Db means t > 2Db/(1 - Db) - locals.tCross = locals.deviationBefore; - // tWorse = ceil( (2*Db) / (1 - Db) ) in Q18 - locals.num = (2 * locals.deviationBefore) * 1e18; // Q36 - locals.den = 1e18 - locals.deviationBefore; - locals.q = (locals.num + locals.den - 1) / locals.den; // ceilDiv -> Q18 - locals.tWorse = locals.q; - - // Add a safety margin to overcome integer rounding in price_after and deviationAfter. - // Use 1e13 in Q18 (i.e., 1e-5) which is ample even for E as large as 1e24. - locals.epsT = 1e13; - locals.tMin = (locals.tWorse > locals.tCross ? locals.tWorse : locals.tCross) + locals.epsT; - - // Choose x = t*1e18 with t in [tMin, tMin + span] - locals.span = 5e17; // allow up to +0.5 in t - locals.lo = locals.tMin; - locals.hi = locals.tMin + locals.span; - if (locals.lo == 0) { - locals.lo = 1; - } // avoid x==0 - - if (locals.hi < locals.lo) { - locals.hi = locals.lo; - } // clamp on overflow - - locals.x = bound(uint256(amtSeed), locals.lo, locals.hi); - locals.p.amountGivenScaled18 = locals.x; - - // Expected uses NOISE with AFTER deviation - locals.price_after = (locals.price_before * 1e18) / (1e18 + locals.x); - - // Sanity: crossed and worsened absolute deviation - locals.deviationBefore = ((locals.price_before - locals.E) * 1e18) / locals.E; - locals.deviationAfter = ((locals.E - locals.price_after) * 1e18) / locals.E; - require(locals.price_after < locals.E, "must cross below E"); - require(locals.deviationAfter > locals.deviationBefore, "must worsen absolute deviation after crossing"); - - locals.expected = fee_expectedFeeWithParams( - locals.price_after, - locals.comp.pxIn, - locals.comp.pxOut, - STATIC_SWAP_FEE, - locals.noiseThr9, - locals.noiseCap9, - locals.noiseMax9 - ); - - HyperSurgeHookMock mock = new HyperSurgeHookMock( - IVault(vault), - fee_ppm9To1e18(locals.arbMax9), - fee_ppm9To1e18(locals.arbThr9), - fee_ppm9To1e18(locals.arbCap9), - "logic-4" - ); - (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); - - assertEq( - locals.dyn, - locals.expected, - "noise path must use AFTER deviation even when crossing the price (worsening)" - ); - assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee >= static"); - } - - struct OutsideToInsideDynamicBefore { - uint256 E; - uint32 arbThr9; - uint32 arbCap9; - uint32 arbMax9; - uint32 noiseThr9; - uint32 noiseCap9; - uint32 noiseMax9; - uint256 thr; - uint256 cap; - uint256 deviationBefore; - uint256 price_before; - uint256 price_after; - uint256 R1e18; // R in 1e18 scale: R = price_before / E - uint256 xLower; // min x to get price_after less than or equal to E*(1+thr) - uint256 xUpper; // max x to keep price_after greater than or equal to E*(1−thr) - uint256 x; // chosen amountGivenScaled18 inside [xLower, xUpper] - HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - PoolSwapParams p; - uint256 expected; - uint256 dyn; - } - - /// 5) Arb: starts outside, ends inside → ARB lane still uses **BEFORE** deviation (dynamic, not base). - function testFuzz_logic_arb_outside_to_inside_dynamic_before( - uint256 eSeed, - uint32 arbThrSeed, - uint32 arbCapSeed, - uint32 arbMaxSeed, - uint64 amtSeed - ) public { - OutsideToInsideDynamicBefore memory locals; - - // --- Fuzz + bounds --- - locals.E = bound(eSeed, 1e16, 1e24); - // Keep thr strictly < 1e9 so (1e18 - thr) > 0 - locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); - locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); - locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); - // NOISE lane can be anything different; not used by this assertion - locals.noiseThr9 = 5_000_000; - locals.noiseCap9 = 400_000_000; - locals.noiseMax9 = 25_000_000; - - locals.thr = uint256(locals.arbThr9) * 1e9; - locals.cap = uint256(locals.arbCap9) * 1e9; - - // Start ABOVE E with an outside deviation deviationBefore > thr - locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; // strictly outside - locals.price_before = locals.E + (locals.E * locals.deviationBefore) / 1e18; // price_before = E * (1 + deviationBefore) - locals.R1e18 = (locals.price_before * 1e18) / locals.E; // R = 1e18 + deviationBefore - - // Two-sided “inside” band: 1 − thr less than or equal to price_after/E less than or equal to 1 + thr, - // with price_after/E = R / (1 + t), t = x / 1e18. - - // Lower bound on t (bring down to less than or equal to 1+thr): - // t greater than or equal to R/(1+thr) − 1 means xLower = ceil( (R1e18 * 1e18) / (1e18 + thr) ) − 1e18 - uint256 denomPlus = 1e18 + locals.thr; - uint256 numPlus = locals.R1e18 * 1e18; // Q36 - uint256 qPlus = (numPlus + denomPlus - 1) / denomPlus; // ceilDiv to Q18 - locals.xLower = qPlus > 1e18 ? (qPlus - 1e18) : 0; - - // Upper bound on t (don’t overshoot below 1 − thr): - // t less than or equal to R/(1−thr) − 1 means xUpper = floor( (R1e18 * 1e18) / (1e18 − thr) ) − 1e18 - uint256 denomMinus = 1e18 - locals.thr; // > 0 by bound - uint256 numMinus = locals.R1e18 * 1e18; // Q36 - uint256 qMinus = numMinus / denomMinus; // floorDiv to Q18 - locals.xUpper = qMinus > 1e18 ? (qMinus - 1e18) : 0; - - // Choose x inside [xLower, xUpper] using bound (no vm.assume). Collapse if inverted. - uint256 lo = locals.xLower; - uint256 hi = locals.xUpper; - if (hi < lo) { - hi = lo; - } - // avoid degenerate zero (x == 0 keeps price_after == price_before and won’t end inside) - if (lo == 0) lo = 1; - if (hi < lo) hi = lo; - - locals.x = bound(uint256(amtSeed), lo, hi); - - // Build compute locals - locals.comp.wIn = 1e18; - locals.comp.wOut = 1e18; - locals.comp.bIn = 1e18; - locals.comp.bOut = locals.price_before; - locals.comp.pxIn = 1e18; - locals.comp.pxOut = locals.E; - locals.comp.calcAmountScaled18 = 0; - locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; - locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; - locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; - - locals.p.kind = SwapKind.EXACT_IN; - locals.p.amountGivenScaled18 = locals.x; - - // Expected (ARB) uses BEFORE deviation even though end is inside - locals.expected = fee_expectedFeeWithParams( - locals.price_before, - locals.comp.pxIn, - locals.comp.pxOut, - STATIC_SWAP_FEE, - locals.arbThr9, - locals.arbCap9, - locals.arbMax9 - ); - - HyperSurgeHookMock mock = new HyperSurgeHookMock( - IVault(vault), - fee_ppm9To1e18(locals.arbMax9), - fee_ppm9To1e18(locals.arbThr9), - fee_ppm9To1e18(locals.arbCap9), - "logic-5" - ); - (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); - - // Sanity: end is inside (two-sided) - locals.price_after = (locals.price_before * 1e18) / (1e18 + locals.p.amountGivenScaled18); - uint256 deviationAfter = (( - locals.price_after > locals.E ? (locals.price_after - locals.E) : (locals.E - locals.price_after) - ) * 1e18) / locals.E; - assertLe(deviationAfter, locals.thr, "end should be inside threshold"); - - assertEq( - locals.dyn, - locals.expected, - "arb path must use BEFORE deviation even if the end state is inside threshold" - ); - assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee >= static"); - } - - struct InsideToOutsideDynamicAfterLocals { - uint256 E; - uint32 noiseThr9; - uint32 noiseCap9; - uint32 noiseMax9; - uint32 arbThr9; - uint32 arbCap9; - uint32 arbMax9; - uint256 thr; - uint256 cap; - uint256 deviationBefore; - uint256 priceBefore; - uint256 priceAfter; - uint256 R1e18; // = priceBefore/E (Q18) - uint256 tLower; // min t to make priceAfter/E less than or equal to 1 - thr (Q18) - uint256 x; // = t * 1e18 (amount in) - uint256 num; // numerator for tLower calculation - uint256 den; // denominator for tLower calculation - uint256 q; // intermediate value for tLower calculation - uint256 eps; // epsilon for x calculation - uint256 lo; // lower bound for x - uint256 hi; // upper bound for x - HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - PoolSwapParams p; - uint256 expected; - uint256 dyn; - } - - /// [LANE] Inside → cross outside (NOISE, dynamic with AFTER) - function testFuzz_logic_noise_inside_to_outside_dynamic_after( - uint256 eSeed, - uint32 noiseThrSeed, - uint32 noiseCapSeed, - uint32 noiseMaxSeed, - uint64 amtSeed - ) public { - InsideToOutsideDynamicAfterLocals memory locals; - - // Lane params (NOISE fuzzed, ARB fixed and different) - locals.E = bound(eSeed, 1e16, 1e24); - locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); - locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); - locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); - locals.arbThr9 = 1_000_000; - locals.arbCap9 = 300_000_000; - locals.arbMax9 = 50_000_000; - - locals.thr = uint256(locals.noiseThr9) * 1e9; - locals.cap = uint256(locals.noiseCap9) * 1e9; - - // Start BELOW E but inside: deviationBefore ∈ [0, thr) - locals.deviationBefore = (locals.thr / 3) + 1; // safely inside - locals.priceBefore = locals.E - (locals.E * locals.deviationBefore) / 1e18; // P/E = 1 - deviationBefore - locals.R1e18 = (locals.priceBefore * 1e18) / locals.E; - - // Need priceAfter/E less than or equal to 1 - thr ⇒ t greater than or equal to R/(1 - thr) - 1 - - locals.num = locals.R1e18 * 1e18; // Q36 - locals.den = 1e18 - locals.thr; - locals.q = (locals.num + locals.den - 1) / locals.den; // ceilDiv → Q18 - locals.tLower = locals.q > 1e18 ? (locals.q - 1e18) : 0; - - // Pick x greater than or equal to tLower (plus small epsilon) to cross outside - locals.eps = 1e12; - locals.lo = locals.tLower + locals.eps; - if (locals.lo == 0) locals.lo = 1; - locals.hi = locals.lo + 5e17; // allow up to +0.5 in t - locals.x = bound(uint256(amtSeed), locals.lo, locals.hi); - - // Build locals - locals.comp.wIn = 1e18; - locals.comp.wOut = 1e18; - locals.comp.bIn = 1e18; - locals.comp.bOut = locals.priceBefore; - locals.comp.pxIn = 1e18; - locals.comp.pxOut = locals.E; - locals.comp.calcAmountScaled18 = 0; - locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; - locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; - locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; - - locals.p.kind = SwapKind.EXACT_IN; - locals.p.amountGivenScaled18 = locals.x; - - // Expected (NOISE) uses AFTER - locals.priceAfter = (locals.priceBefore * 1e18) / (1e18 + locals.x); - uint256 deviationAfter = (( - locals.priceAfter > locals.E ? (locals.priceAfter - locals.E) : (locals.E - locals.priceAfter) - ) * 1e18) / locals.E; - assertGt(deviationAfter, locals.thr, "must end outside threshold (worsened)"); - locals.expected = fee_expectedFeeWithParams( - locals.priceAfter, - locals.comp.pxIn, - locals.comp.pxOut, - STATIC_SWAP_FEE, - locals.noiseThr9, - locals.noiseCap9, - locals.noiseMax9 - ); - - HyperSurgeHookMock mock = new HyperSurgeHookMock( - IVault(vault), - fee_ppm9To1e18(locals.arbMax9), - fee_ppm9To1e18(locals.arbThr9), - fee_ppm9To1e18(locals.arbCap9), - "lane-inside2outside" - ); - (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); - - assertEq(locals.dyn, locals.expected, "noise/after: dynamic fee must match expected"); - assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee greater than or equal to static"); - } - - struct OutsideToThresholdDynamicBeforeLocals { - uint256 E; - uint32 arbThr9; - uint32 arbCap9; - uint32 arbMax9; - uint32 noiseThr9; - uint32 noiseCap9; - uint32 noiseMax9; - uint256 thr; - uint256 cap; - uint256 deviationBefore; - uint256 priceBefore; - uint256 priceAfter; - uint256 R1e18; - uint256 tLower; - uint256 tUpper; - uint256 x; - uint256 epsT; - uint256 lo; - uint256 hi; - HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - PoolSwapParams p; - uint256 expected; - uint256 dyn; - } - - /// [LANE] Outside → to (or just inside) threshold (ARB, dynamic with BEFORE) - function testFuzz_logic_arb_outside_to_threshold_dynamic_before( - uint256 eSeed, - uint32 arbThrSeed, - uint32 arbCapSeed, - uint32 arbMaxSeed, - uint64 amtSeed - ) public { - OutsideToThresholdDynamicBeforeLocals memory locals; - - locals.E = bound(eSeed, 1e16, 1e24); - locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); - locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); - locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); - // Distinct NOISE lane (unused in expected but kept different) - locals.noiseThr9 = 5_000_000; - locals.noiseCap9 = 400_000_000; - locals.noiseMax9 = 25_000_000; - - locals.thr = uint256(locals.arbThr9) * 1e9; - locals.cap = uint256(locals.arbCap9) * 1e9; - - // Start ABOVE, outside - locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; // strictly outside - locals.priceBefore = locals.E + (locals.E * locals.deviationBefore) / 1e18; - - // R = priceBefore / E in Q18; compute both ceil and floor variants to bound tightly - // R_up = ceil( (priceBefore * 1e18) / E ) - // R_down = floor( (priceBefore * 1e18) / E ) - uint256 numR = locals.priceBefore * 1e18; - locals.R1e18 = (numR + locals.E - 1) / locals.E; - - // We need 1 - thr less than or equal to priceAfter/E less than or equal to 1 + thr, and priceAfter/E = R / (1 + t), with t = x/1e18 (Q18). - // Lower bound on t (to get under the upper edge 1 + thr): - // t ≥ R/(1 + thr) − 1 - // Use R_up and ceil-div to be conservative, then subtract 1e18. - uint256 denomPlus = 1e18 + locals.thr; - uint256 numPlus = locals.R1e18 * 1e18; // Q36 - uint256 qPlus = (numPlus + denomPlus - 1) / denomPlus; // ceilDiv → Q18 - locals.tLower = qPlus > 1e18 ? (qPlus - 1e18) : 0; - - // Upper bound on t (don’t drop below the lower edge 1 − thr): - // t less than or equal to R/(1 − thr) − 1 - // Use R_down and floor-div to be conservative, then subtract 1e18. - uint256 denomMinus = 1e18 - locals.thr; - uint256 numMinus = locals.R1e18 * 1e18; // Q36 - uint256 qMinus = numMinus / denomMinus; // floorDiv → Q18 - locals.tUpper = qMinus > 1e18 ? (qMinus - 1e18) : 0; - - // Choose t inside [tLower + eps, tUpper − eps] and map amtSeed with bound(...). - // eps helps avoid equality-edge flips due to integer rounding. - locals.epsT = 1; // one Q18 unit (~1e-18) is ample given we used ceil/floor conservatively - locals.lo = locals.tLower + locals.epsT; - locals.hi = (locals.tUpper > locals.epsT) ? (locals.tUpper - locals.epsT) : locals.tUpper; - - // If interval collapses or inverted (can happen with extreme tiny thr), clamp to a point and proceed. - if (locals.hi < locals.lo) { - locals.hi = locals.lo; - } - if (locals.lo == 0) { - locals.lo = 1; - if (locals.hi < locals.lo) locals.hi = locals.lo; - } - - locals.x = bound(uint256(amtSeed), locals.lo, locals.hi); - - // Build locals - locals.comp.wIn = 1e18; - locals.comp.wOut = 1e18; - locals.comp.bIn = 1e18; - locals.comp.bOut = locals.priceBefore; - locals.comp.pxIn = 1e18; - locals.comp.pxOut = locals.E; - locals.comp.calcAmountScaled18 = 0; - locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; - locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; - locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; - - locals.p.kind = SwapKind.EXACT_IN; - locals.p.amountGivenScaled18 = locals.x; - - // Sanity: end is inside (two-sided) - locals.priceAfter = (locals.priceBefore * 1e18) / (1e18 + locals.p.amountGivenScaled18); - uint256 dAfter = (( - locals.priceAfter > locals.E ? (locals.priceAfter - locals.E) : (locals.E - locals.priceAfter) - ) * 1e18) / locals.E; - assertLe(dAfter, locals.thr, "end should be at/inside threshold"); - - // Expected (ARB) uses BEFORE even if end is at/inside threshold - locals.expected = fee_expectedFeeWithParams( - locals.priceBefore, - locals.comp.pxIn, - locals.comp.pxOut, - STATIC_SWAP_FEE, - locals.arbThr9, - locals.arbCap9, - locals.arbMax9 - ); - - HyperSurgeHookMock mock = new HyperSurgeHookMock( - IVault(vault), - fee_ppm9To1e18(locals.arbMax9), - fee_ppm9To1e18(locals.arbThr9), - fee_ppm9To1e18(locals.arbCap9), - "lane-out2thr" - ); - (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); - - assertEq( - locals.dyn, - locals.expected, - "arb/before: dynamic fee must use BEFORE deviation even at threshold end" - ); - assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee greater than or equal to static"); - } - - struct ArbNoMoveOutsideDynamicLocals { - uint256 E; - uint32 arbThr9; - uint32 arbCap9; - uint32 arbMax9; - uint32 noiseThr9; - uint32 noiseCap9; - uint32 noiseMax9; - uint256 thr; - uint256 cap; - uint256 deviationBefore; - uint256 priceBefore; - HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - PoolSwapParams p; - uint256 expected; - uint256 dyn; - } - - function test_logic_arb_outside_nochange_dynamic_before( - uint256 eSeed, - uint32 arbThrSeed, - uint32 arbCapSeed, - uint32 arbMaxSeed - ) public { - ArbNoMoveOutsideDynamicLocals memory locals; - - locals.E = bound(eSeed, 1e16, 1e24); - locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); - locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); - locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); - // NOISE lane different (unused) - locals.noiseThr9 = 5_000_000; - locals.noiseCap9 = 400_000_000; - locals.noiseMax9 = 25_000_000; - - locals.thr = uint256(locals.arbThr9) * 1e9; - locals.cap = uint256(locals.arbCap9) * 1e9; - - // Start ABOVE, outside - locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; - locals.priceBefore = locals.E + (locals.E * locals.deviationBefore) / 1e18; - - // No movement: amount = 0, so deviationAfter == deviationBefore → ARB path - locals.comp.wIn = 1e18; - locals.comp.wOut = 1e18; - locals.comp.bIn = 1e18; - locals.comp.bOut = locals.priceBefore; - locals.comp.pxIn = 1e18; - locals.comp.pxOut = locals.E; - locals.comp.calcAmountScaled18 = 0; - locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; - locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; - locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; - - locals.p.kind = SwapKind.EXACT_IN; - locals.p.amountGivenScaled18 = 0; - - locals.expected = fee_expectedFeeWithParams( - locals.priceBefore, - locals.comp.pxIn, - locals.comp.pxOut, - STATIC_SWAP_FEE, - locals.arbThr9, - locals.arbCap9, - locals.arbMax9 - ); - - HyperSurgeHookMock mock = new HyperSurgeHookMock( - IVault(vault), - fee_ppm9To1e18(locals.arbMax9), - fee_ppm9To1e18(locals.arbThr9), - fee_ppm9To1e18(locals.arbCap9), - "lane-nomove-outside" - ); - (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); - - assertEq(locals.dyn, locals.expected, "no-move/outside must be ARB, dynamic from BEFORE"); - assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee greater than or equal to static"); - } - - struct ArbNoMoveInsideLocals { - uint256 E; - uint32 arbThr9; - uint32 arbCap9; - uint32 arbMax9; - uint32 noiseThr9; - uint32 noiseCap9; - uint32 noiseMax9; - uint256 thr; - uint256 deviationBefore; - uint256 priceBefore; - uint256 fee; - HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - PoolSwapParams p; - } - - /// [LANE] No movement, inside: ARB path, but STATIC fee (since BEFORE less than or equal to thr) - function test_logic_arb_inside_nochange_static( - uint256 eSeed, - uint32 arbThrSeed, - uint32 arbCapSeed, - uint32 arbMaxSeed - ) public { - ArbNoMoveInsideLocals memory locals; - - locals.E = bound(eSeed, 1e16, 1e24); - locals.arbThr9 = uint32(bound(arbThrSeed, 1, 1_000_000_000 - 1)); - locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); - locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); - locals.noiseThr9 = 5_000_000; - locals.noiseCap9 = 400_000_000; - locals.noiseMax9 = 25_000_000; - - locals.thr = uint256(locals.arbThr9) * 1e9; - - // Start BELOW, inside - locals.deviationBefore = (locals.thr / 3) + 1; // strictly inside - locals.priceBefore = locals.E - (locals.E * locals.deviationBefore) / 1e18; - - // No movement: deviationAfter == deviationBefore → ARB branch, but less than or equal to thr ⇒ static - locals.comp.wIn = 1e18; - locals.comp.wOut = 1e18; - locals.comp.bIn = 1e18; - locals.comp.bOut = locals.priceBefore; - locals.comp.pxIn = 1e18; - locals.comp.pxOut = locals.E; - locals.comp.calcAmountScaled18 = 0; - locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - locals.comp.poolDetails.arbCapDeviationPercentage9 = uint32(locals.arbCap9); - locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; - locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; - - locals.p.kind = SwapKind.EXACT_IN; - locals.p.amountGivenScaled18 = 0; - - HyperSurgeHookMock mock = new HyperSurgeHookMock( - IVault(vault), - fee_ppm9To1e18(locals.arbMax9), - fee_ppm9To1e18(locals.arbThr9), - fee_ppm9To1e18(locals.arbCap9), - "lane-nomove-inside" - ); - (, locals.fee) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); - - assertEq( - locals.fee, - STATIC_SWAP_FEE, - "no-move/inside must return static (ARB branch, but less than or equal to thr)" - ); - } - - struct NoiseCrossesPriceWorsensDymanicLocals { - uint256 E; - uint32 noiseThr9; - uint32 noiseCap9; - uint32 noiseMax9; - uint32 arbThr9; - uint32 arbCap9; - uint32 arbMax9; - uint256 thr; - uint256 cap; - uint256 deviationBefore; - uint256 priceBefore; - uint256 priceAfter; - uint256 tCross; - uint256 tWorse; - uint256 tMin; - uint256 x; - uint256 num; - uint256 den; - uint256 q; - uint256 epsT; - uint256 lo; - uint256 hi; - uint256 deviationAfter; - HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - PoolSwapParams p; - uint256 expected; - uint256 dyn; - } - - /// [LANE] Symmetric “below” case: start outside BELOW, worsen further BELOW (no cross) → NOISE uses AFTER - /// Note: With calc=0 and this simplified price update, EXACT_IN can only decrease P, - /// so a true below→above cross is not representable without changing the price update model. - /// This test locks the symmetric NOISE/AFTER behavior from the “below” side. - function testFuzz_logic_noise_outside_below_worsens_dynamic_after( - uint256 eSeed, - uint32 noiseThrSeed, - uint32 noiseCapSeed, - uint32 noiseMaxSeed, - uint64 amtSeed - ) public { - NoiseCrossesPriceWorsensDymanicLocals memory locals; - - // External price (pxOut/pxIn -> E); keep as in all other tests - locals.E = bound(eSeed, 1e16, 1e24); - - // Distinct NOISE lane params (fuzzed) and different ARB params (unused in expected but distinct to catch wrong-lane) - locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); - locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); - locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); - locals.arbThr9 = 1_000_000; - locals.arbCap9 = 300_000_000; - locals.arbMax9 = 50_000_000; - - locals.thr = uint256(locals.noiseThr9) * 1e9; - locals.cap = uint256(locals.noiseCap9) * 1e9; - - // Start OUTSIDE BELOW price: priceBefore = E * (1 - D_before), with D_before in (thr, cap) - locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; - locals.priceBefore = locals.E - (locals.E * locals.deviationBefore) / 1e18; - - // Build compute locals with the standard orientation (pxIn=1e18, pxOut=E) - locals.comp.wIn = 1e18; - locals.comp.wOut = 1e18; - locals.comp.bIn = 1e18; - locals.comp.bOut = locals.priceBefore; - locals.comp.pxIn = 1e18; // keep the usual frame - locals.comp.pxOut = locals.E; - locals.comp.calcAmountScaled18 = 0; - locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; - locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; - locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; - - // EXACT_IN reduces P further → deviation worsens from the BELOW side (NOISE lane) - locals.p.kind = SwapKind.EXACT_IN; - // ensure a measurable worsening but no overflow; avoid 1-wei knife edges - uint256 lo = 1e9; - uint256 hi = 5e17; - locals.p.amountGivenScaled18 = bound(uint256(amtSeed), lo, hi); - - // AFTER price for expected (NOISE uses AFTER) - locals.priceAfter = (locals.priceBefore * 1e18) / (1e18 + locals.p.amountGivenScaled18); - - // Sanity: still BELOW E and deviation increased - uint256 dBefore = ((locals.E - locals.priceBefore) * 1e18) / locals.E; - uint256 dAfter = ((locals.E - locals.priceAfter) * 1e18) / locals.E; - assertGt(dAfter, dBefore, "deviation must worsen from the below side"); - - // Expected NOISE fee from AFTER deviation - locals.expected = fee_expectedFeeWithParams( - locals.priceAfter, - locals.comp.pxIn, - locals.comp.pxOut, + (, locals.feeC) = mock.ComputeSurgeFee( + p, + poolDetails, STATIC_SWAP_FEE, - locals.noiseThr9, - locals.noiseCap9, - locals.noiseMax9 - ); - - HyperSurgeHookMock mock = new HyperSurgeHookMock( - IVault(vault), - fee_ppm9To1e18(locals.arbMax9), - fee_ppm9To1e18(locals.arbThr9), - fee_ppm9To1e18(locals.arbCap9), - "lane-below-worsen" + locals.w, + 0, + locals.pxOut.divDown(locals.pxIn) ); - (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); - - assertEq(locals.dyn, locals.expected, "noise/after (below side): dynamic fee must match expected"); - assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee greater than or equal to static"); - } - - struct BoundArbBeforeClampToMaxLocals { - uint256 E; - uint32 arbThr9; - uint32 arbCap9; - uint32 arbMax9; - uint32 noiseThr9; - uint32 noiseCap9; - uint32 noiseMax9; - uint256 thr; - uint256 cap; - uint256 Db; - uint256 priceBefore; - uint256 priceAfter; - uint256 tLower; - uint256 tUpperNoCross; - uint256 x; - HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - PoolSwapParams p; - uint256 fee; - uint256 expected; - } - - /// [BOUND] ARB with BEFORE > cap, AFTER < cap: ARB clamps to maxArb (basis = BEFORE) - /// Start ABOVE with BEFORE deviation > cap, improve so AFTER less than or equal to cap (stay above; no cross). - /// Assert: ARB lane; fee == arbMax (clamped by BEFORE). - function testFuzz_bound_arb_before_gt_cap_clamps_to_max_before( - uint256 eSeed, - uint32 arbThrSeed, - uint32 arbCapSeed, - uint32 arbMaxSeed, - uint64 amtSeed - ) public { - BoundArbBeforeClampToMaxLocals memory locals; - - // External price - locals.E = bound(eSeed, 1e16, 1e24); - - // ARB lane params (ensure thr < cap < 1.0) - locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); // (0, 0.9) - locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000 - 1)); // (thr, 1) - locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9) + 1, 1_000_000_000)); - - // Distinct NOISE params (unused in expected but kept different to catch wrong-lane) - locals.noiseThr9 = 5_000_000; - locals.noiseCap9 = 400_000_000; - locals.noiseMax9 = 25_000_000; - - locals.thr = uint256(locals.arbThr9) * 1e9; - locals.cap = uint256(locals.arbCap9) * 1e9; - assertLt(locals.cap, 1e18, "cap must be < 100%"); - - // BEFORE deviation strictly above cap but < 1, with safe margin - // margin = max(1, (1e18 - cap)/16) keeps Db < 1 while staying comfortably > cap - uint256 margin = (1e18 - locals.cap) / 16; - if (margin == 0) { - margin = 1; - } - locals.Db = locals.cap + margin; - if (locals.Db >= 1e18) { - locals.Db = 1e18 - 1; - } - - // Sanity: BEFORE > cap - assertGt(locals.Db, locals.cap, "setup must have BEFORE > cap"); - - // Price ABOVE E with BEFORE deviation Db - locals.priceBefore = locals.E + (locals.E * locals.Db) / 1e18; - - // ABOVE side with EXACT_IN: - // D_after_pos (no-cross) = (Db - t)/(1 + t). Want AFTER less than or equal to cap ⇒ t ≥ (Db - cap)/(1 + cap). - - uint256 num = (locals.Db - locals.cap) * 1e18; // Q36 (Db > cap guaranteed) - uint256 den = 1e18 + locals.cap; - uint256 q = (num + den - 1) / den; // ceilDiv → Q18 - locals.tLower = q; - - // Avoid crossing E: need t < Db. Use tiny epsilon below Db to stay strictly above E. - uint256 epsCross = 1; // one Q18 unit - locals.tUpperNoCross = (locals.Db > epsCross) ? (locals.Db - epsCross) : 0; - - uint256 lo = (locals.tLower == 0 ? 1 : locals.tLower); - uint256 hi = locals.tUpperNoCross; + assertEq(locals.feeC, _convertTo18Decimals(locals.maxp), "at cap means max fee"); - if (hi < lo) { - hi = lo; - } - locals.x = bound(uint256(amtSeed), lo, hi); - - locals.comp.wIn = 1e18; - locals.comp.wOut = 1e18; - locals.comp.bIn = 1e18; - locals.comp.bOut = locals.priceBefore; - locals.comp.pxIn = 1e18; - locals.comp.pxOut = locals.E; - locals.comp.calcAmountScaled18 = 0; - locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; - locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; - locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; - - locals.p.kind = SwapKind.EXACT_IN; - locals.p.amountGivenScaled18 = locals.x; - - // AFTER should be less than or equal to cap (improved) and we shouldn’t have crossed E. - locals.priceAfter = (locals.priceBefore * 1e18) / (1e18 + locals.x); - uint256 dAfter = (( - locals.priceAfter > locals.E ? (locals.priceAfter - locals.E) : (locals.E - locals.priceAfter) - ) * 1e18) / locals.E; - assertLe(dAfter, locals.cap, "AFTER should be less than or equal to cap (improved)"); - - // ARB uses BEFORE and must clamp to maxArb - (, locals.fee) = new HyperSurgeHookMock( - IVault(vault), - fee_ppm9To1e18(locals.arbMax9), - fee_ppm9To1e18(locals.arbThr9), - fee_ppm9To1e18(locals.arbCap9), - "arb-before-cap" - ).ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); + uint256 Dbeyond = Dcap + 1; + (locals.pxIn, locals.pxOut) = fee_localsForDeviation(locals.P, Dbeyond); - locals.expected = fee_expectedFeeWithParams( - locals.priceBefore, - locals.comp.pxIn, - locals.comp.pxOut, + (, locals.feeD) = mock.ComputeSurgeFee( + p, + poolDetails, STATIC_SWAP_FEE, - locals.arbThr9, - locals.arbCap9, - locals.arbMax9 + locals.w, + 0, + locals.pxOut.divDown(locals.pxIn) ); - assertEq(locals.fee, locals.expected, "ARB should compute from BEFORE and clamp at cap->max"); - assertEq(locals.fee, fee_ppm9To1e18(locals.arbMax9), "ARB fee must equal arbMax"); - } - - struct BoundNoiseExactThresholdLocals { - uint256 E; - uint32 noiseThr9; - uint32 noiseCap9; - uint32 noiseMax9; - uint32 arbThr9; - uint32 arbCap9; - uint32 arbMax9; - uint256 thr; - uint256 Db; - uint256 priceBefore; - uint256 priceAfter; - uint256 tEdge; - uint256 x; - uint256 fee; - HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - PoolSwapParams p; - } - - function testFuzz_bound_noise_after_at_threshold_static( - uint256 eSeed, - uint32 noiseThrSeed, - uint32 noiseCapSeed, - uint32 noiseMaxSeed, - uint64 amtSeed - ) public { - BoundNoiseExactThresholdLocals memory locals; - - locals.E = bound(eSeed, 1e16, 1e24); - locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); - locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); - locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); - locals.arbThr9 = 1_000_000; - locals.arbCap9 = 300_000_000; - locals.arbMax9 = 50_000_000; - - locals.thr = uint256(locals.noiseThr9) * 1e9; - - locals.Db = locals.thr / 4 + 1; - locals.priceBefore = locals.E - (locals.E * locals.Db) / 1e18; - - uint256 num = (locals.thr - locals.Db) * 1e18; - uint256 den = 1e18 - locals.thr; - locals.tEdge = den == 0 ? 0 : (num / den); - - uint256 epsT = 1e6; - uint256 lo = (locals.tEdge > epsT) ? (locals.tEdge - epsT) : 1; - uint256 hi = locals.tEdge; - if (hi < lo) { - hi = lo; - } - - locals.x = bound(uint256(amtSeed), lo, hi); - locals.comp.wIn = 1e18; - locals.comp.wOut = 1e18; - locals.comp.bIn = 1e18; - locals.comp.bOut = locals.priceBefore; - locals.comp.pxIn = 1e18; - locals.comp.pxOut = locals.E; - locals.comp.calcAmountScaled18 = 0; - locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; - locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; - locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; - locals.p.kind = SwapKind.EXACT_IN; - locals.p.amountGivenScaled18 = locals.x; - locals.priceAfter = (locals.priceBefore * 1e18) / (1e18 + locals.p.amountGivenScaled18); - - uint256 dBefore = ((locals.E - locals.priceBefore) * 1e18) / locals.E; - uint256 dAfter = ((locals.E - locals.priceAfter) * 1e18) / locals.E; - assertLe(dAfter, locals.thr, "AFTER should be less than or equal to threshold (at-or-just-inside)"); - assertGt(dAfter, dBefore, "deviation must worsen (positive t)"); - - (, locals.fee) = new HyperSurgeHookMock( - IVault(vault), - fee_ppm9To1e18(locals.arbMax9), - fee_ppm9To1e18(locals.arbThr9), - fee_ppm9To1e18(locals.arbCap9), - "noise-exact-thr" - ).ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); - - assertEq(locals.fee, STATIC_SWAP_FEE, "At threshold end-state: NOISE must return static (no ramp)"); - } - - uint32 constant HL_IDX_SZ_0 = 100; - uint32 constant HL_IDX_SZ_8 = 108; - - bytes4 constant _SEL_SPOT_PRICE = bytes4(keccak256("spotPrice(uint32)")); - address constant _HYPER_SPOT_PRICE_PRECOMPILE = 0x0000000000000000000000000000000000000808; - - function _mockHyperSpotPrice(uint32 pairIndex, uint64 raw) internal { - vm.mockCall( - _HYPER_SPOT_PRICE_PRECOMPILE, - abi.encode(pairIndex), // <- no selector - abi.encode(raw) // 32-byte padded uint64 - ); - } - - function testFuzz_Fee_FallbacksToStatic_When_ExtPxZero(bool givenIn, uint64 rawInHuge) public { - TokenConfig[] memory cfg = new TokenConfig[](2); - LiquidityManagement memory lm; - vm.prank(address(vault)); - hook.onRegister(poolFactory, address(pool), cfg, lm); - - // 2) Use the same HL token index you used (108 -> sz=0 -> divisor=1e8) on BOTH tokens - uint32 pairIn = 8001; - uint32 pairOut = 8002; - - vm.startPrank(admin); - hook.setTokenPriceConfigIndex(address(pool), 0, pairIn, 108); // div=1e8 - hook.setTokenPriceConfigIndex(address(pool), 1, pairOut, 108); // div=1e8 - vm.stopPrank(); - - // 3) Force extPx == 0 with NON-ZERO raws: - // extPx = floor((pxOut*1e18)/pxIn) = floor((rawOut*1e18)/rawIn) - // => choose rawOut=1 and rawIn > 1e18 (fits in uint64), so extPx == 0 - rawInHuge = uint64(bound(uint256(rawInHuge), 1e18 + 1, type(uint64).max)); - - // (optional) prove we hit the correct precompile and calldata (no selector) - vm.expectCall(_HYPER_SPOT_PRICE_PRECOMPILE, abi.encode(pairIn)); - vm.expectCall(_HYPER_SPOT_PRICE_PRECOMPILE, abi.encode(pairOut)); - - // Mock the spot prices with the correct calldata (NO selector) - _mockHyperSpotPrice(pairIn, rawInHuge); // pxIn = rawInHuge * 1e10 - _mockHyperSpotPrice(pairOut, 1); // pxOut = 1 * 1e10 - - // 4) Build params (all 7 fields) - uint256[] memory balances = new uint256[](2); - balances[0] = 1e18; - balances[1] = 1e18; - - SwapKind kind = givenIn ? SwapKind.EXACT_IN : SwapKind.EXACT_OUT; - - PoolSwapParams memory p = PoolSwapParams({ - kind: kind, - amountGivenScaled18: 5e15, - balancesScaled18: balances, - indexIn: 0, - indexOut: 1, - router: address(0), - userData: "" - }); - - // 5) Expect: NO revert; the hook falls back to pool static fee because extPx == 0 - uint256 staticFee = WeightedPool(address(pool)).getStaticSwapFeePercentage(); - (bool ok, uint256 dynFee) = hook.onComputeDynamicSwapFeePercentage(p, address(pool), staticFee); - - assertTrue(ok, "extPx==0 must not block"); - assertEq(dynFee, staticFee, "extPx==0 must return static fee"); - } - - function testFuzz_Fee_ClampsToMax_When_DeviationBeyondCap(bool givenIn, uint64 rawOutHuge) public { - uint256 idxIn = 0; - uint256 idxOut = 1; - uint256 amountGiven = 5e15; - - uint256[] memory balances = new uint256[](2); - balances[0] = 1e18; - balances[1] = 1e18; - - TokenConfig[] memory cfg = new TokenConfig[](2); - LiquidityManagement memory lm; - vm.prank(address(vault)); - hook.onRegister(poolFactory, address(pool), cfg, lm); - - uint32 pairIn = 91001; - uint32 pairOut = 91002; - vm.startPrank(admin); - hook.setTokenPriceConfigIndex(address(pool), uint8(idxIn), pairIn, HL_IDX_SZ_8); - hook.setTokenPriceConfigIndex(address(pool), uint8(idxOut), pairOut, HL_IDX_SZ_8); - - uint256 thr = 1e16; // 1% - uint256 cap = 2e16; // 2% - uint256 max = 15e15; // 1.5% - - hook.setSurgeThresholdPercentage(address(pool), thr, IHyperSurgeHook.TradeType.ARBITRAGE); - hook.setSurgeThresholdPercentage(address(pool), thr, IHyperSurgeHook.TradeType.NOISE); - hook.setCapDeviationPercentage(address(pool), cap, IHyperSurgeHook.TradeType.ARBITRAGE); - hook.setCapDeviationPercentage(address(pool), cap, IHyperSurgeHook.TradeType.NOISE); - hook.setMaxSurgeFeePercentage(address(pool), max, IHyperSurgeHook.TradeType.ARBITRAGE); - hook.setMaxSurgeFeePercentage(address(pool), max, IHyperSurgeHook.TradeType.NOISE); - vm.stopPrank(); - - // External price >> 1.0: - // extPx = (pxOut / pxIn) with same divisor. Set pxOut very large, pxIn = 1 unit. - // Use HL_IDX_SZ_8 (divisor 1e8) so raw numbers are easy: rawIn=1e8, rawOut in [5e9, max]. - rawOutHuge = uint64(bound(uint256(rawOutHuge), 5e9, type(uint64).max)); - _mockHyperSpotPrice(pairIn, uint64(1e8)); - _mockHyperSpotPrice(pairOut, rawOutHuge); - - SwapKind kind = givenIn ? SwapKind.EXACT_IN : SwapKind.EXACT_OUT; - PoolSwapParams memory p = _makeParams(idxIn, idxOut, kind, amountGiven, balances); - - uint256 staticFee = WeightedPool(address(pool)).getStaticSwapFeePercentage(); - (bool ok, uint256 fee) = hook.onComputeDynamicSwapFeePercentage(p, address(pool), staticFee); - - assertTrue(ok, "fee path must not block"); - assertEq(fee, max, "fee must clamp at configured maxPct"); - } - - function testFuzz_Fee_ReturnsStatic_When_DeviationBelowThreshold(bool givenIn, uint64 rawBase) public { - uint256 idxIn = 0; - uint256 idxOut = 1; - uint256 amountGiven = 5e15; - - uint256[] memory balances = new uint256[](2); - balances[0] = 1e18; - balances[1] = 1e18; - - TokenConfig[] memory cfg = new TokenConfig[](2); - LiquidityManagement memory lm; - vm.prank(address(vault)); - hook.onRegister(poolFactory, address(pool), cfg, lm); - - uint32 pairIn = 92001; - uint32 pairOut = 92002; - vm.startPrank(admin); - hook.setTokenPriceConfigIndex(address(pool), uint8(idxIn), pairIn, HL_IDX_SZ_8); - hook.setTokenPriceConfigIndex(address(pool), uint8(idxOut), pairOut, HL_IDX_SZ_8); - - // Set a relatively generous threshold (5%) and a higher cap so we stay in "below threshold" - uint256 thr = 5e16; // 5% - uint256 cap = 20e16; // 20% (arbitrary > thr) - uint256 max = 50e16; // 50% (irrelevant here) - - hook.setSurgeThresholdPercentage(address(pool), thr, IHyperSurgeHook.TradeType.ARBITRAGE); - hook.setSurgeThresholdPercentage(address(pool), thr, IHyperSurgeHook.TradeType.NOISE); - hook.setCapDeviationPercentage(address(pool), cap, IHyperSurgeHook.TradeType.ARBITRAGE); - hook.setCapDeviationPercentage(address(pool), cap, IHyperSurgeHook.TradeType.NOISE); - hook.setMaxSurgeFeePercentage(address(pool), max, IHyperSurgeHook.TradeType.ARBITRAGE); - hook.setMaxSurgeFeePercentage(address(pool), max, IHyperSurgeHook.TradeType.NOISE); - vm.stopPrank(); - - // Make extPx ≈ 1.0 within ~1e-8 relative drift, far below the 5% threshold. - // Same divisor (1e8): extPx = (rawOut/rawIn). Pick rawOut = rawBase + 1, rawIn = rawBase. - rawBase = uint64(bound(uint256(rawBase), 1e8, 5e9)); // ensure > 0 and leaves headroom for +1 - _mockHyperSpotPrice(pairIn, rawBase); - _mockHyperSpotPrice(pairOut, rawBase + 1); - - SwapKind kind = givenIn ? SwapKind.EXACT_IN : SwapKind.EXACT_OUT; - PoolSwapParams memory p = _makeParams(idxIn, idxOut, kind, amountGiven, balances); - - uint256 staticFee = WeightedPool(address(pool)).getStaticSwapFeePercentage(); - (bool ok, uint256 fee) = hook.onComputeDynamicSwapFeePercentage(p, address(pool), staticFee); - - assertTrue(ok, "below-threshold path must not block"); - assertEq(fee, staticFee, "below-threshold deviation must return static fee"); - } + assertEq(locals.feeD, _convertTo18Decimals(locals.maxp), "above cap means clamped to max fee"); + } + + // struct ExactInEqualsExactOutLocals { + // uint8 n; + // uint256[] w; + // uint256[] b; + // uint8 i; + // uint8 j; + // uint32 thr; + // uint32 cap; + // uint32 maxp; + // uint256 P; + // uint256 capDev; + // uint256 D; + // uint256 pxIn; + // uint256 pxOut; + // uint256 feeIn; + // uint256 feeOut; + // } + + // /// EXACT_IN vs EXACT_OUT: with identical lane params, the engine result must match. + // /// Correction: keep the *effective* lane params for the chosen direction the same, + // /// but make ARB and NOISE lanes different so a wrong-lane implementation would not hide here. + // // function testFuzz_internal_exactIn_equals_exactOut_whenParamsSame( + // // uint8 nSeed, + // // uint256 wSeed, + // // uint256 bSeed, + // // uint256 dSeed + // // ) public { + // // ExactInEqualsExactOutLocals memory locals; + + // // locals.n = uint8(bound(nSeed, 2, 8)); + // // locals.w = fee_normWeights(locals.n, wSeed); + // // locals.b = fee_balances(locals.n, bSeed); + + // // locals.i = uint8(bound(uint256(keccak256(abi.encode(dSeed, 41))), 0, locals.n - 1)); + // // locals.j = (locals.i + 1 + uint8(bound(uint256(keccak256(abi.encode(dSeed, 42))), 0, locals.n - 2))) % locals.n; + + // // locals.thr = 1_000_000; // 0.1% + // // locals.cap = 500_000_000; // 50% + // // locals.maxp = 50_000_000; // 5% + + // // locals.P = fee_pairSpotFromBW(locals.b[locals.i], locals.w[locals.i], locals.b[locals.j], locals.w[locals.j]); + // // vm.assume(locals.P > 0); + + // // locals.capDev = _convertTo18Decimals(locals.cap); + // // locals.D = uint256(keccak256(abi.encode(dSeed))) % (locals.capDev + locals.capDev / 2 + 1); + // // (locals.pxIn, locals.pxOut) = fee_localsForDeviation(locals.P, locals.D); + + // // HyperSurgeHookMock mock = new HyperSurgeHookMock( + // // IVault(vault), + // // _convertTo18Decimals(locals.maxp), + // // _convertTo18Decimals(locals.thr), + // // _convertTo18Decimals(locals.cap), + // // "fee-io" + // // ); + + // // // EXACT_IN + // // PoolSwapParams memory pIn; + // // pIn.kind = SwapKind.EXACT_IN; + + // // // Build locals with NOISE = (thr/cap/maxp) and ARB deliberately different + // // HyperSurgeHookMock.ComputeSurgeFeeLocals memory L1; + // // L1.bIn = locals.b[locals.i]; + // // L1.wIn = locals.w[locals.i]; + // // L1.bOut = locals.b[locals.j]; + // // L1.wOut = locals.w[locals.j]; + // // L1.pxIn = locals.pxIn; + // // L1.pxOut = locals.pxOut; + // // L1.calcAmountScaled18 = 0; + + // // // Effective (chosen) lane params + // // L1.poolDetails.noiseThresholdPercentage9 = locals.thr; + // // L1.poolDetails.noiseCapDeviationPercentage9 = locals.cap; + // // L1.poolDetails.noiseMaxSurgeFee9 = locals.maxp; + + // // // Different ARB lane params so wrong-lane usage wouldn’t accidentally match + // // L1.poolDetails.arbThresholdPercentage9 = locals.thr + 1; + // // L1.poolDetails.arbCapDeviationPercentage9 = locals.cap - 1; + // // L1.poolDetails.arbMaxSurgeFee9 = locals.maxp + 1; + + // // (, locals.feeIn) = mock.ComputeSurgeFee(L1, pIn, STATIC_SWAP_FEE); + + // // // EXACT_OUT + // // PoolSwapParams memory pOut; + // // pOut.kind = SwapKind.EXACT_OUT; + + // // HyperSurgeHookMock.ComputeSurgeFeeLocals memory L2; + // // L2.bIn = locals.b[locals.i]; + // // L2.wIn = locals.w[locals.i]; + // // L2.bOut = locals.b[locals.j]; + // // L2.wOut = locals.w[locals.j]; + // // L2.pxIn = locals.pxIn; + // // L2.pxOut = locals.pxOut; + // // L2.calcAmountScaled18 = 0; + + // // L2.poolDetails.noiseThresholdPercentage9 = locals.thr; + // // L2.poolDetails.noiseCapDeviationPercentage9 = locals.cap; + // // L2.poolDetails.noiseMaxSurgeFee9 = locals.maxp; + + // // L2.poolDetails.arbThresholdPercentage9 = locals.thr + 1; + // // L2.poolDetails.arbCapDeviationPercentage9 = locals.cap - 1; + // // L2.poolDetails.arbMaxSurgeFee9 = locals.maxp + 1; + + // // (, locals.feeOut) = mock.ComputeSurgeFee(L2, pOut, STATIC_SWAP_FEE); + + // // assertEq(locals.feeIn, locals.feeOut, "with equal lane params, kind should not change math result"); + // // } + + // function testFuzz_view_missingPrices_reverts(uint8 nSeed, uint256 /* wSeed */, uint256 bSeed, uint8 iSeed) public { + // // --- Register pool and adapt to its actual token count --- + // uint8 nTarget = uint8(bound(nSeed, 2, 8)); + // _registerBasePoolWithN(nTarget); + + // uint256[] memory weights = WeightedPool(address(pool)).getNormalizedWeights(); + // uint256 m = weights.length; + // assertGe(m, 2, "pool must have at least 2 tokens"); + + // // --- Random non-zero balances of exact pool length --- + // uint256[] memory b = fee_balances(uint8(m), bSeed); + + // // --- Pick a valid distinct pair (i != j) --- + // uint256 i = uint256(bound(iSeed, 0, m - 1)); + // uint256 j = (i + 1) % m; + + // // --- Build base swap params template with those balances --- + // PoolSwapParams memory p; + // p.balancesScaled18 = new uint256[](m); + // for (uint256 k = 0; k < m; ++k) { + // p.balancesScaled18[k] = b[k]; + // } + // p.indexIn = i; + // p.indexOut = j; + + // uint256 bIn = b[i]; + // uint256 bOut = b[j]; + + // uint256 safeInAmt = bIn / 1e6; + // if (safeInAmt == 0) safeInAmt = 1; + // uint256 safeOutAmt = bOut / 1e6; + // if (safeOutAmt == 0) safeOutAmt = 1; + + // // Sanity: amounts are indeed tiny relative to balances to avoid accidental reverts + // // (these checks also self-document the invariant we rely on) + // assertLt(safeInAmt, bIn / 10, "safeInAmt too large vs balanceIn"); // < 10% (much stricter in practice) + // assertLt(safeOutAmt, bOut / 10, "safeOutAmt too large vs balanceOut"); // < 10% + + // p.kind = SwapKind.EXACT_IN; + // p.amountGivenScaled18 = safeInAmt; + + // vm.expectRevert(HyperSpotPricePrecompile.SpotPriceIsZero.selector); + // hook.onComputeDynamicSwapFeePercentage(p, address(pool), STATIC_SWAP_FEE); + + // p.kind = SwapKind.EXACT_OUT; + // p.amountGivenScaled18 = safeOutAmt; + + // vm.expectRevert(HyperSpotPricePrecompile.SpotPriceIsZero.selector); + // hook.onComputeDynamicSwapFeePercentage(p, address(pool), STATIC_SWAP_FEE); + // } + + // function testFuzz_view_readsLaneParams_reverts_onSafePath(uint8 nSeed) public { + // uint8 n = uint8(bound(nSeed, 2, 8)); + // _registerBasePoolWithN(n); + + // // Diverge NOISE and ARB lane params (authorized admin) + // vm.startPrank(admin); + // hook.setSurgeThresholdPercentage(address(pool), 5_000_000 * 1e9, IHyperSurgeHook.TradeType.NOISE); // 0.5% + // hook.setCapDeviationPercentage(address(pool), 400_000_000 * 1e9, IHyperSurgeHook.TradeType.NOISE); // 40% + // hook.setMaxSurgeFeePercentage(address(pool), 25_000_000 * 1e9, IHyperSurgeHook.TradeType.NOISE); // 2.5% + + // hook.setSurgeThresholdPercentage(address(pool), 1_000_000 * 1e9, IHyperSurgeHook.TradeType.ARBITRAGE); // 0.1% + // hook.setCapDeviationPercentage(address(pool), 300_000_000 * 1e9, IHyperSurgeHook.TradeType.ARBITRAGE); // 30% + // hook.setMaxSurgeFeePercentage(address(pool), 50_000_000 * 1e9, IHyperSurgeHook.TradeType.ARBITRAGE); // 5% + // vm.stopPrank(); + + // // Adapt to the pool’s true size to avoid OOB / shape mismatches + // uint256[] memory weights = WeightedPool(address(pool)).getNormalizedWeights(); + // uint256 m = weights.length; + // assertGe(m, 2, "pool must have at least 2 tokens"); + + // // Build non-zero balances of correct length m + // uint256[] memory balances = new uint256[](m); + // for (uint256 k = 0; k < m; ++k) { + // balances[k] = 1e24 + k; + // } + + // PoolSwapParams memory p; + // p.amountGivenScaled18 = 1e18; // non-zero trade amount + // p.balancesScaled18 = balances; + // p.indexIn = 0; + // p.indexOut = (m > 1) ? 1 : 0; + + // // EXACT_IN: either revert or static fee (but never a computed dynamic fee) + // p.kind = SwapKind.EXACT_IN; + // vm.expectRevert(HyperSpotPricePrecompile.SpotPriceIsZero.selector); + // hook.onComputeDynamicSwapFeePercentage(p, address(pool), STATIC_SWAP_FEE); + + // // EXACT_OUT: same invariant + // p.kind = SwapKind.EXACT_OUT; + // vm.expectRevert(HyperSpotPricePrecompile.SpotPriceIsZero.selector); + // hook.onComputeDynamicSwapFeePercentage(p, address(pool), STATIC_SWAP_FEE); + // } + + // struct DeviationEqualsThreshold { + // uint256 staticFee; + // uint256 maxFee; + // uint32 thr9; + // uint32 cap9; + // uint32 max9; + // uint256 E; + // uint256 thr; + // uint256 fee; + // } + + // /// 1) deviation == threshold => returns static fee (boundary counted as "inside") + // // function test_cfg_fee_static_at_threshold_usingMockWrapper() public view { + // // DeviationEqualsThreshold memory locals; + + // // locals.staticFee = 30e14; // 30 bps = 0.003 * 1e18 + // // locals.maxFee = 120e14; // 120 bps + + // // // 9 lane params (contract upscales to 18dp) + // // locals.thr9 = 100_000_000; // 10% + // // locals.cap9 = 500_000_000; // 50% + // // locals.max9 = uint32(locals.maxFee / 1e9); + + // // HyperSurgeHookMock.ComputeSurgeFeeLocals memory computeLocals; + // // computeLocals.pxIn = 1e18; + // // computeLocals.pxOut = 10e18; // external price E = 10 + + // // // set both lanes the same (lane choice irrelevant for this edge) + // // computeLocals.poolDetails.noiseThresholdPercentage9 = locals.thr9; + // // computeLocals.poolDetails.noiseCapDeviationPercentage9 = locals.cap9; + // // computeLocals.poolDetails.noiseMaxSurgeFee9 = locals.max9; + // // computeLocals.poolDetails.arbThresholdPercentage9 = locals.thr9; + // // computeLocals.poolDetails.arbCapDeviationPercentage9 = locals.cap9; + // // computeLocals.poolDetails.arbMaxSurgeFee9 = locals.max9; + + // // PoolSwapParams memory p; + // // p.kind = SwapKind.EXACT_IN; + // // p.amountGivenScaled18 = 0; + + // // locals.E = 10e18; + // // locals.thr = uint256(locals.thr9) * 1e9; // 18dp + + // // locals.fee = _feeAtDeviation(computeLocals, p, locals.staticFee, locals.E, locals.thr); + // // assertEq(locals.fee, locals.staticFee, "fee must equal static when deviation == threshold"); + // // } + + // struct justAboveThreshold { + // uint256 staticFee; + // uint256 maxFee; + // uint32 thr9; + // uint32 cap9; + // uint32 max9; + // uint256 E; + // uint256 thr; + // uint256 cap; + // uint256 dev; + // uint256 span; + // uint256 ramp; + // uint256 expected; + // } + + // // function test_cfg_fee_minimalRamp_just_above_threshold() public view { + // // justAboveThreshold memory locals; + + // // locals.staticFee = 30e14; // 30 bps + // // locals.maxFee = 120e14; // 120 bps + + // // locals.thr9 = 100_000_000; // 10% + // // locals.cap9 = 500_000_000; // 50% + // // locals.max9 = uint32(locals.maxFee / 1e9); + + // // HyperSurgeHookMock.ComputeSurgeFeeLocals memory computeLocals; + // // computeLocals.pxIn = 1e18; + // // computeLocals.pxOut = 10e18; + + // // computeLocals.poolDetails.noiseThresholdPercentage9 = locals.thr9; + // // computeLocals.poolDetails.noiseCapDeviationPercentage9 = locals.cap9; + // // computeLocals.poolDetails.noiseMaxSurgeFee9 = locals.max9; + // // computeLocals.poolDetails.arbThresholdPercentage9 = locals.thr9; + // // computeLocals.poolDetails.arbCapDeviationPercentage9 = locals.cap9; + // // computeLocals.poolDetails.arbMaxSurgeFee9 = locals.max9; + + // // PoolSwapParams memory p; + // // p.kind = SwapKind.EXACT_IN; + // // p.amountGivenScaled18 = 0; + + // // locals.E = 10e18; + // // locals.thr = uint256(locals.thr9) * 1e9; + // // locals.cap = uint256(locals.cap9) * 1e9; + // // locals.dev = (uint256(locals.thr9) + 1) * 1e9; // smallest 18dp step above threshold + + // // uint256 fee = _feeAtDeviation(computeLocals, p, locals.staticFee, locals.E, locals.dev); + + // // // Expected: static + (max - static) * (dev - thr) / (cap - thr) (div-down) + // // locals.span = locals.cap - locals.thr; + // // locals.ramp = ((locals.maxFee - locals.staticFee) * (locals.dev - locals.thr)) / locals.span; + // // locals.expected = locals.staticFee + locals.ramp; + + // // assertEq(fee, locals.expected, "minimal ramp just above threshold"); + // // assertGt(fee, locals.staticFee, "fee > static just above threshold"); + // // assertLt(fee, locals.maxFee, "fee < max when deviation < cap"); + // // } + + // struct MaxEqualsStatic { + // uint256 staticFee; + // uint256 maxFee; + // uint32 thr9; + // uint32 cap9; + // uint32 max9; + // uint256 E; + // uint256 thr; + // uint256 cap; + // uint256 devAtThr; + // uint256 devMid; + // uint256 devAtCap; + // uint256 devBeyond; + // } + + // /// 3) degenerate: max == static => always static (even outside threshold) + // function test_cfg_fee_degenerateRamp_max_equals_static() public view { + // MaxEqualsStatic memory locals; + + // locals.staticFee = 45e14; // 45 bps + // locals.maxFee = locals.staticFee; + + // locals.thr9 = 50_000_000; // 5% + // locals.cap9 = 250_000_000; // 25% + // locals.max9 = uint32(locals.maxFee / 1e9); + + // HyperSurgeHookMock.ComputeSurgeFeeLocals memory computeLocals; + // computeLocals.pxIn = 1e18; + // computeLocals.pxOut = 10e18; + + // computeLocals.poolDetails.noiseThresholdPercentage9 = locals.thr9; + // computeLocals.poolDetails.noiseCapDeviationPercentage9 = locals.cap9; + // computeLocals.poolDetails.noiseMaxSurgeFee9 = locals.max9; + // computeLocals.poolDetails.arbThresholdPercentage9 = locals.thr9; + // computeLocals.poolDetails.arbCapDeviationPercentage9 = locals.cap9; + // computeLocals.poolDetails.arbMaxSurgeFee9 = locals.max9; + + // PoolSwapParams memory p; + // p.kind = SwapKind.EXACT_IN; + // p.amountGivenScaled18 = 0; + + // locals.E = 10e18; + // locals.thr = uint256(locals.thr9) * 1e9; + // locals.cap = uint256(locals.cap9) * 1e9; + + // locals.devAtThr = locals.thr; + // locals.devMid = locals.thr + (locals.cap - locals.thr) / 2; + // locals.devAtCap = locals.cap; + // locals.devBeyond = locals.cap + 12345; + + // assertEq( + // _feeAtDeviation(computeLocals, p, locals.staticFee, locals.E, locals.devAtThr), + // locals.staticFee, + // "at thr => static" + // ); + // assertEq( + // _feeAtDeviation(computeLocals, p, locals.staticFee, locals.E, locals.devMid), + // locals.staticFee, + // "mid => static" + // ); + // assertEq( + // _feeAtDeviation(computeLocals, p, locals.staticFee, locals.E, locals.devAtCap), + // locals.staticFee, + // "at cap => static" + // ); + // assertEq( + // _feeAtDeviation(computeLocals, p, locals.staticFee, locals.E, locals.devBeyond), + // locals.staticFee, + // "beyond cap => static" + // ); + // } + + // struct MaxBelowStatic { + // uint256 staticFee; + // uint256 maxFee; + // uint32 thr9; + // uint32 cap9; + // uint32 max9; + // uint256 E; + // uint256 thr; + // uint256 cap; + // uint256 devMid; + // uint256 feeMid; + // uint256 span; + // uint256 ramp; + // uint256 expected; + // } + + // function test_fee_misconfig_maxBelowStatic_usingMockWrapper() public { + // MaxBelowStatic memory locals; + + // // Misconfig: max < static + // locals.staticFee = 80e14; // 80 bps (1e18 scale) + // locals.maxFee = 20e14; // 20 bps (1e18 scale) -> lower than static + // locals.thr9 = 100_000_000; // 10% in 1e9 + // locals.cap9 = 300_000_000; // 30% in 1e9 + // locals.max9 = uint32(locals.maxFee / 1e9); + + // // Local mock (don’t rely on global `hook`) + // HyperSurgeHookMock mock = new HyperSurgeHookMock( + // IVault(vault), + // _convertTo18Decimals(locals.max9), + // _convertTo18Decimals(locals.thr9), + // _convertTo18Decimals(locals.cap9), + // "misconfig-maxBelowStatic" + // ); + + // // Base inputs used for both sub-tests + // locals.E = 10e18; // external price + // locals.thr = uint256(locals.thr9) * 1e9; // 18dp threshold + // locals.cap = uint256(locals.cap9) * 1e9; // 18dp cap + + // HyperSurgeHookMock.ComputeSurgeFeeLocals memory base; + // base.pxIn = 1e18; + // base.pxOut = locals.E; + + // // Set BOTH lanes to the same (misconfigured) params so lane choice doesn't matter here. + // base.poolDetails.noiseThresholdPercentage9 = locals.thr9; + // base.poolDetails.noiseCapDeviationPercentage9 = locals.cap9; + // base.poolDetails.noiseMaxSurgeFee9 = locals.max9; + // base.poolDetails.arbThresholdPercentage9 = locals.thr9; + // base.poolDetails.arbCapDeviationPercentage9 = locals.cap9; + // base.poolDetails.arbMaxSurgeFee9 = locals.max9; + + // PoolSwapParams memory p; + // p.kind = SwapKind.EXACT_IN; + // p.amountGivenScaled18 = 0; // keep balances-based price exact + // p.balancesScaled18 = new uint256[](2); + // p.balancesScaled18[0] = 1e18; + // p.balancesScaled18[1] = locals.E; + + // // Reused working struct + // HyperSurgeHookMock.ComputeSurgeFeeLocals memory T; + + // // ---------- (a) dev >= cap -> revert (underflow in mock ramp) ---------- + // uint256 dev = locals.cap + 999; // strictly beyond cap + // uint256 P = locals.E + (locals.E * dev) / 1e18; // P = E * (1 + dev) + // T = base; + // T.wIn = 1e18; + // T.wOut = 1e18; + // T.bIn = 1e18; + // T.bOut = P; + // T.calcAmountScaled18 = 0; + + // vm.expectRevert(stdError.arithmeticError); + // mock.ComputeSurgeFee(T, p, locals.staticFee); + + // // ---------- (b) thr < dev < cap -> revert (underflow in mock ramp) ---------- + // dev = locals.thr + (locals.cap - locals.thr) / 3; // strictly between thr & cap + // P = locals.E + (locals.E * dev) / 1e18; + // T = base; + // T.wIn = 1e18; + // T.wOut = 1e18; + // T.bIn = 1e18; + // T.bOut = P; + // T.calcAmountScaled18 = 0; + + // vm.expectRevert(stdError.arithmeticError); + // mock.ComputeSurgeFee(T, p, locals.staticFee); + // } + + // struct OutsideDynamicAfterLocals { + // uint256 E; + // uint32 noiseThr9; + // uint32 noiseCap9; + // uint32 noiseMax9; + // uint32 arbThr9; + // uint32 arbCap9; + // uint32 arbMax9; + // uint256 thr; + // uint256 cap; + // uint256 deviationBefore; + // uint256 price_before; + // uint256 price_after; + // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; + // PoolSwapParams p; + // uint256 expected; + // uint256 dyn; + // } + + // /// 1) Noise: starts outside threshold, deviation worsens → NOISE lane, dynamic fee based on **after** deviation. + // function testFuzz_logic_noise_worsens_outside_dynamic_after( + // uint256 eSeed, + // uint32 noiseThrSeed, + // uint32 noiseCapSeed, + // uint32 noiseMaxSeed, + // uint64 amtSeed + // ) public { + // OutsideDynamicAfterLocals memory locals; + + // // --- Fuzz + bounds --- + // locals.E = bound(eSeed, 1e16, 1e24); // pxOut + // locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); + // locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); + // locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + // // ARB lane (unused here, but keep distinct) + // locals.arbThr9 = 1_000_000; + // locals.arbCap9 = 300_000_000; + // locals.arbMax9 = 50_000_000; + + // locals.thr = uint256(locals.noiseThr9) * 1e9; + // locals.cap = uint256(locals.noiseCap9) * 1e9; + // locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; // strictly outside + + // // Start BELOW E: price_before = E * (1 - deviationBefore) + // locals.price_before = locals.E - (locals.E * locals.deviationBefore) / 1e18; + + // // Build compute locals + swap that worsens deviation (EXACT_IN; calc=0 → P decreases further) + // locals.comp.wIn = 1e18; + // locals.comp.wOut = 1e18; + // locals.comp.bIn = 1e18; + // locals.comp.bOut = locals.price_before; + // locals.comp.pxIn = 1e18; + // locals.comp.pxOut = locals.E; + // locals.comp.calcAmountScaled18 = 0; + // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; + // locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; + // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + + // locals.p.kind = SwapKind.EXACT_IN; + // // ensure deviation increases *measurably* in Q18 (avoid 1-wei changes) + // locals.p.amountGivenScaled18 = bound(uint256(amtSeed), 1e9, 5e17); // [1e9, 0.5e18] + + // // Expected (NOISE) uses AFTER deviation: price_after = price_before / (1 + x) + // locals.price_after = (locals.price_before * 1e18) / (1e18 + locals.p.amountGivenScaled18); + // locals.expected = fee_expectedFeeWithParams( + // locals.price_after, + // locals.comp.pxIn, + // locals.comp.pxOut, + // STATIC_SWAP_FEE, + // locals.noiseThr9, + // locals.noiseCap9, + // locals.noiseMax9 + // ); + + // HyperSurgeHookMock mock = new HyperSurgeHookMock( + // IVault(vault), + // _convertTo18Decimals(locals.arbMax9), + // _convertTo18Decimals(locals.arbThr9), + // _convertTo18Decimals(locals.arbCap9), + // "logic-1" + // ); + // (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); + + // assertEq(locals.dyn, locals.expected, "noise path must use AFTER deviation for dynamic fee"); + // assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee >= static"); + // } + + // struct BetterStillOutsideLocals { + // uint256 E; + // uint32 arbThr9; + // uint32 arbCap9; + // uint32 arbMax9; + // uint32 noiseThr9; + // uint32 noiseCap9; + // uint32 noiseMax9; + // uint256 thr; + // uint256 cap; + // uint256 deviationBefore; + // uint256 price_before; + // uint256 price_after; + // uint256 xMax; + // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; + // PoolSwapParams p; + // uint256 expected; + // uint256 dyn; + // } + + // function testFuzz_logic_arb_outside_improves_but_outside_dynamic_before( + // uint256 eSeed, + // uint32 arbThrSeed, + // uint32 arbCapSeed, + // uint32 arbMaxSeed, + // uint64 amtSeed + // ) public { + // BetterStillOutsideLocals memory locals; + + // // --- Fuzz + bounds --- + // locals.E = bound(eSeed, 1e16, 1e24); + // locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); + // locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); + // locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + // // NOISE lane different (unused in assertion) + // locals.noiseThr9 = 5_000_000; + // locals.noiseCap9 = 400_000_000; + // locals.noiseMax9 = 25_000_000; + + // locals.thr = uint256(locals.arbThr9) * 1e9; + // locals.cap = uint256(locals.arbCap9) * 1e9; + // locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; // strictly outside + + // // Start ABOVE E + // locals.price_before = locals.E + (locals.E * locals.deviationBefore) / 1e18; + + // // Compute xMax to remain outside after: price_after >= E*(1 + thr) + // // price_after = price_before / (1 + x) means x less than or equal to (price_before / (E*(1+thr)) - 1) * 1e18 + // vm.assume(locals.E * (1e18 + locals.thr) != 0); // defensive + // uint256 denom = (locals.E * (1e18 + locals.thr)) / 1e18; + // vm.assume(denom != 0); + // uint256 ratio = (locals.price_before * 1e18) / denom; + // vm.assume(ratio > 1e18); // Ensure room to remain outside + // locals.xMax = ratio - 1e18; + // if (locals.xMax > 9e17) { + // locals.xMax = 9e17; + // } // clamp + + // locals.comp.wIn = 1e18; + // locals.comp.wOut = 1e18; + // locals.comp.bIn = 1e18; + // locals.comp.bOut = locals.price_before; + // locals.comp.pxIn = 1e18; + // locals.comp.pxOut = locals.E; + // locals.comp.calcAmountScaled18 = 0; + // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; + // locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; + // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + + // locals.p.kind = SwapKind.EXACT_IN; + // locals.p.amountGivenScaled18 = bound(uint256(amtSeed), 1, locals.xMax == 0 ? 1 : locals.xMax); + + // // Expected (ARB) uses BEFORE deviation + // locals.expected = fee_expectedFeeWithParams( + // locals.price_before, + // locals.comp.pxIn, + // locals.comp.pxOut, + // STATIC_SWAP_FEE, + // locals.arbThr9, + // locals.arbCap9, + // locals.arbMax9 + // ); + + // HyperSurgeHookMock mock = new HyperSurgeHookMock( + // IVault(vault), + // _convertTo18Decimals(locals.arbMax9), + // _convertTo18Decimals(locals.arbThr9), + // _convertTo18Decimals(locals.arbCap9), + // "logic-2" + // ); + // (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); + + // // Still outside afterward (sanity) + // locals.price_after = (locals.price_before * 1e18) / (1e18 + locals.p.amountGivenScaled18); + // uint256 deviationAfter = (( + // locals.price_after > locals.E ? (locals.price_after - locals.E) : (locals.E - locals.price_after) + // ) * 1e18) / locals.E; + // assertGt(deviationAfter, locals.thr, "should remain outside threshold after improving"); + + // assertEq(locals.dyn, locals.expected, "arb path must use BEFORE deviation for dynamic fee"); + // assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee >= static"); + // } + + // struct NoiseWorsensInsideButStaysInsideLocals { + // uint256 E; + // uint32 noiseThr9; + // uint32 noiseCap9; + // uint32 noiseMax9; + // uint32 arbThr9; + // uint32 arbCap9; + // uint32 arbMax9; + // uint256 thr; + // uint256 deviationBefore; + // uint256 price_before; + // uint256 price_after; + // uint256 xMax; + // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; + // PoolSwapParams p; + // uint256 fee; + // } + + // /// 3) Noise: starts inside threshold, worsens but stays inside → NOISE lane, **base (static)** fee. + // function testFuzz_logic_noise_inside_worse_but_inside_static( + // uint256 eSeed, + // uint32 noiseThrSeed, + // uint32 noiseCapSeed, + // uint32 noiseMaxSeed, + // uint64 amtSeed + // ) public { + // NoiseWorsensInsideButStaysInsideLocals memory locals; + + // locals.E = bound(eSeed, 1e16, 1e24); + // locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 1_000_000_000 - 1)); // (0,1) + // locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); + // locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + + // locals.arbThr9 = 1_000_000; + // locals.arbCap9 = 300_000_000; + // locals.arbMax9 = 50_000_000; + // locals.thr = uint256(locals.noiseThr9) * 1e9; + // locals.deviationBefore = locals.thr / 4 + 1; + // locals.price_before = locals.E - (locals.E * locals.deviationBefore) / 1e18; + + // uint256 R1e18 = (locals.price_before * 1e18) / locals.E; + // uint256 denom = 1e18 - locals.thr; + // uint256 q = (R1e18 * 1e18) / denom; + // locals.xMax = q > 1e18 ? (q - 1e18) : 0; + // if (locals.xMax > 5e17) { + // locals.xMax = 5e17; + // } + + // locals.comp.wIn = 1e18; + // locals.comp.wOut = 1e18; + // locals.comp.bIn = 1e18; + // locals.comp.bOut = locals.price_before; + // locals.comp.pxIn = 1e18; + // locals.comp.pxOut = locals.E; + // locals.comp.calcAmountScaled18 = 0; + // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; + // locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; + // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + + // locals.p.kind = SwapKind.EXACT_IN; + + // // Ensure a *measurable* worsening so NOISE is chosen: + // // pick x with a lower floor (e.g., 1e9 wei) but never exceed xMax. + // uint256 lo = 1e9; // 1e-9 in t; safely above Q18 rounding noise + // uint256 hi = locals.xMax; + // if (hi < lo) { + // lo = 1; + // } // if xMax < floor, fall back to [1, xMax] + // if (hi < lo) { + // hi = lo; + // } // clamp + // locals.p.amountGivenScaled18 = bound(uint256(amtSeed), lo, hi); + + // HyperSurgeHookMock mock = new HyperSurgeHookMock( + // IVault(vault), + // _convertTo18Decimals(locals.arbMax9), + // _convertTo18Decimals(locals.arbThr9), + // _convertTo18Decimals(locals.arbCap9), + // "logic-3" + // ); + // (, locals.fee) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); + + // // Sanity: still inside after worsening + // locals.price_after = (locals.price_before * 1e18) / (1e18 + locals.p.amountGivenScaled18); + // uint256 deviationAfter = (( + // locals.price_after > locals.E ? (locals.price_after - locals.E) : (locals.E - locals.price_after) + // ) * 1e18) / locals.E; + // assertLe(deviationAfter, locals.thr, "must remain inside threshold"); + + // // Inside-after on NOISE → static + // assertEq(locals.fee, STATIC_SWAP_FEE, "inside threshold after worsening must still return static (noise path)"); + // } + + // struct NoiseCrossesPriceWorsensLocals { + // uint256 E; + // uint32 noiseThr9; + // uint32 noiseCap9; + // uint32 noiseMax9; + // uint32 arbThr9; + // uint32 arbCap9; + // uint32 arbMax9; + // uint256 thr; + // uint256 cap; + // uint256 deviationBefore; + // uint256 price_before; + // uint256 price_after; + // uint256 tCross; + // uint256 tWorse; + // uint256 tMin; + // uint256 x; + // uint256 num; // numerator for tWorse calculation + // uint256 den; // denominator for tWorse calculation + // uint256 q; // intermediate value for tWorse calculation + // uint256 epsT; // safety margin for tMin + // uint256 span; // range for x selection + // uint256 lo; // lower bound for x + // uint256 hi; // upper bound for x + // uint256 deviationAfter; // absolute deviation after + // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; + // PoolSwapParams p; + // uint256 expected; + // uint256 dyn; + // } + + // function testFuzz_logic_noise_outside_crosses_and_worsens_dynamic_after( + // uint256 eSeed, + // uint32 noiseThrSeed, + // uint32 noiseCapSeed, + // uint32 noiseMaxSeed, + // uint64 amtSeed + // ) public { + // NoiseCrossesPriceWorsensLocals memory locals; + + // // --- Fuzz + bounds --- + // locals.E = bound(eSeed, 1e16, 1e24); + + // // Keep thr < 1 so denominators stay positive and bands are non-degenerate + // locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); // (0, 0.9) + // locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); // (thr, 1] + // locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + + // // ARB lane different (unused in assertion) + // locals.arbThr9 = 1_000_000; + // locals.arbCap9 = 300_000_000; + // locals.arbMax9 = 50_000_000; + + // locals.thr = uint256(locals.noiseThr9) * 1e9; + // locals.cap = uint256(locals.noiseCap9) * 1e9; + + // // Start ABOVE E with a deviation strictly outside the threshold: + // locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 4; + // locals.price_before = locals.E + (locals.E * locals.deviationBefore) / 1e18; + + // // Build compute locals + // locals.comp.wIn = 1e18; + // locals.comp.wOut = 1e18; + // locals.comp.bIn = 1e18; + // locals.comp.bOut = locals.price_before; + // locals.comp.pxIn = 1e18; + // locals.comp.pxOut = locals.E; + // locals.comp.calcAmountScaled18 = 0; + // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; + // locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; + // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + + // locals.p.kind = SwapKind.EXACT_IN; + + // // We need BOTH: + // // (1) Cross: price_after < E means t > Db (R = 1 + Db) + // // (2) Worsen: |after| > |before| when ending below: + // // 1 - R/(1+t) > Db means (1 - Db)(1 + t) > 1 + Db means t > 2Db/(1 - Db) + // locals.tCross = locals.deviationBefore; + // // tWorse = ceil( (2*Db) / (1 - Db) ) in Q18 + // locals.num = (2 * locals.deviationBefore) * 1e18; // Q36 + // locals.den = 1e18 - locals.deviationBefore; + // locals.q = (locals.num + locals.den - 1) / locals.den; // ceilDiv -> Q18 + // locals.tWorse = locals.q; + + // // Add a safety margin to overcome integer rounding in price_after and deviationAfter. + // // Use 1e13 in Q18 (i.e., 1e-5) which is ample even for E as large as 1e24. + // locals.epsT = 1e13; + // locals.tMin = (locals.tWorse > locals.tCross ? locals.tWorse : locals.tCross) + locals.epsT; + + // // Choose x = t*1e18 with t in [tMin, tMin + span] + // locals.span = 5e17; // allow up to +0.5 in t + // locals.lo = locals.tMin; + // locals.hi = locals.tMin + locals.span; + // if (locals.lo == 0) { + // locals.lo = 1; + // } // avoid x==0 + + // if (locals.hi < locals.lo) { + // locals.hi = locals.lo; + // } // clamp on overflow + + // locals.x = bound(uint256(amtSeed), locals.lo, locals.hi); + // locals.p.amountGivenScaled18 = locals.x; + + // // Expected uses NOISE with AFTER deviation + // locals.price_after = (locals.price_before * 1e18) / (1e18 + locals.x); + + // // Sanity: crossed and worsened absolute deviation + // locals.deviationBefore = ((locals.price_before - locals.E) * 1e18) / locals.E; + // locals.deviationAfter = ((locals.E - locals.price_after) * 1e18) / locals.E; + // require(locals.price_after < locals.E, "must cross below E"); + // require(locals.deviationAfter > locals.deviationBefore, "must worsen absolute deviation after crossing"); + + // locals.expected = fee_expectedFeeWithParams( + // locals.price_after, + // locals.comp.pxIn, + // locals.comp.pxOut, + // STATIC_SWAP_FEE, + // locals.noiseThr9, + // locals.noiseCap9, + // locals.noiseMax9 + // ); + + // HyperSurgeHookMock mock = new HyperSurgeHookMock( + // IVault(vault), + // _convertTo18Decimals(locals.arbMax9), + // _convertTo18Decimals(locals.arbThr9), + // _convertTo18Decimals(locals.arbCap9), + // "logic-4" + // ); + // (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); + + // assertEq( + // locals.dyn, + // locals.expected, + // "noise path must use AFTER deviation even when crossing the price (worsening)" + // ); + // assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee >= static"); + // } + + // struct OutsideToInsideDynamicBefore { + // uint256 E; + // uint32 arbThr9; + // uint32 arbCap9; + // uint32 arbMax9; + // uint32 noiseThr9; + // uint32 noiseCap9; + // uint32 noiseMax9; + // uint256 thr; + // uint256 cap; + // uint256 deviationBefore; + // uint256 price_before; + // uint256 price_after; + // uint256 R1e18; // R in 1e18 scale: R = price_before / E + // uint256 xLower; // min x to get price_after less than or equal to E*(1+thr) + // uint256 xUpper; // max x to keep price_after greater than or equal to E*(1−thr) + // uint256 x; // chosen amountGivenScaled18 inside [xLower, xUpper] + // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; + // PoolSwapParams p; + // uint256 expected; + // uint256 dyn; + // } + + // /// 5) Arb: starts outside, ends inside → ARB lane still uses **BEFORE** deviation (dynamic, not base). + // function testFuzz_logic_arb_outside_to_inside_dynamic_before( + // uint256 eSeed, + // uint32 arbThrSeed, + // uint32 arbCapSeed, + // uint32 arbMaxSeed, + // uint64 amtSeed + // ) public { + // OutsideToInsideDynamicBefore memory locals; + + // // --- Fuzz + bounds --- + // locals.E = bound(eSeed, 1e16, 1e24); + // // Keep thr strictly < 1e9 so (1e18 - thr) > 0 + // locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); + // locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); + // locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + // // NOISE lane can be anything different; not used by this assertion + // locals.noiseThr9 = 5_000_000; + // locals.noiseCap9 = 400_000_000; + // locals.noiseMax9 = 25_000_000; + + // locals.thr = uint256(locals.arbThr9) * 1e9; + // locals.cap = uint256(locals.arbCap9) * 1e9; + + // // Start ABOVE E with an outside deviation deviationBefore > thr + // locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; // strictly outside + // locals.price_before = locals.E + (locals.E * locals.deviationBefore) / 1e18; // price_before = E * (1 + deviationBefore) + // locals.R1e18 = (locals.price_before * 1e18) / locals.E; // R = 1e18 + deviationBefore + + // // Two-sided “inside” band: 1 − thr less than or equal to price_after/E less than or equal to 1 + thr, + // // with price_after/E = R / (1 + t), t = x / 1e18. + + // // Lower bound on t (bring down to less than or equal to 1+thr): + // // t greater than or equal to R/(1+thr) − 1 means xLower = ceil( (R1e18 * 1e18) / (1e18 + thr) ) − 1e18 + // uint256 denomPlus = 1e18 + locals.thr; + // uint256 numPlus = locals.R1e18 * 1e18; // Q36 + // uint256 qPlus = (numPlus + denomPlus - 1) / denomPlus; // ceilDiv to Q18 + // locals.xLower = qPlus > 1e18 ? (qPlus - 1e18) : 0; + + // // Upper bound on t (don’t overshoot below 1 − thr): + // // t less than or equal to R/(1−thr) − 1 means xUpper = floor( (R1e18 * 1e18) / (1e18 − thr) ) − 1e18 + // uint256 denomMinus = 1e18 - locals.thr; // > 0 by bound + // uint256 numMinus = locals.R1e18 * 1e18; // Q36 + // uint256 qMinus = numMinus / denomMinus; // floorDiv to Q18 + // locals.xUpper = qMinus > 1e18 ? (qMinus - 1e18) : 0; + + // // Choose x inside [xLower, xUpper] using bound (no vm.assume). Collapse if inverted. + // uint256 lo = locals.xLower; + // uint256 hi = locals.xUpper; + // if (hi < lo) { + // hi = lo; + // } + // // avoid degenerate zero (x == 0 keeps price_after == price_before and won’t end inside) + // if (lo == 0) lo = 1; + // if (hi < lo) hi = lo; + + // locals.x = bound(uint256(amtSeed), lo, hi); + + // // Build compute locals + // locals.comp.wIn = 1e18; + // locals.comp.wOut = 1e18; + // locals.comp.bIn = 1e18; + // locals.comp.bOut = locals.price_before; + // locals.comp.pxIn = 1e18; + // locals.comp.pxOut = locals.E; + // locals.comp.calcAmountScaled18 = 0; + // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; + // locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; + // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + + // locals.p.kind = SwapKind.EXACT_IN; + // locals.p.amountGivenScaled18 = locals.x; + + // // Expected (ARB) uses BEFORE deviation even though end is inside + // locals.expected = fee_expectedFeeWithParams( + // locals.price_before, + // locals.comp.pxIn, + // locals.comp.pxOut, + // STATIC_SWAP_FEE, + // locals.arbThr9, + // locals.arbCap9, + // locals.arbMax9 + // ); + + // HyperSurgeHookMock mock = new HyperSurgeHookMock( + // IVault(vault), + // _convertTo18Decimals(locals.arbMax9), + // _convertTo18Decimals(locals.arbThr9), + // _convertTo18Decimals(locals.arbCap9), + // "logic-5" + // ); + // (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); + + // // Sanity: end is inside (two-sided) + // locals.price_after = (locals.price_before * 1e18) / (1e18 + locals.p.amountGivenScaled18); + // uint256 deviationAfter = (( + // locals.price_after > locals.E ? (locals.price_after - locals.E) : (locals.E - locals.price_after) + // ) * 1e18) / locals.E; + // assertLe(deviationAfter, locals.thr, "end should be inside threshold"); + + // assertEq( + // locals.dyn, + // locals.expected, + // "arb path must use BEFORE deviation even if the end state is inside threshold" + // ); + // assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee >= static"); + // } + + // struct InsideToOutsideDynamicAfterLocals { + // uint256 E; + // uint32 noiseThr9; + // uint32 noiseCap9; + // uint32 noiseMax9; + // uint32 arbThr9; + // uint32 arbCap9; + // uint32 arbMax9; + // uint256 thr; + // uint256 cap; + // uint256 deviationBefore; + // uint256 priceBefore; + // uint256 priceAfter; + // uint256 R1e18; // = priceBefore/E (Q18) + // uint256 tLower; // min t to make priceAfter/E less than or equal to 1 - thr (Q18) + // uint256 x; // = t * 1e18 (amount in) + // uint256 num; // numerator for tLower calculation + // uint256 den; // denominator for tLower calculation + // uint256 q; // intermediate value for tLower calculation + // uint256 eps; // epsilon for x calculation + // uint256 lo; // lower bound for x + // uint256 hi; // upper bound for x + // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; + // PoolSwapParams p; + // uint256 expected; + // uint256 dyn; + // } + + // /// [LANE] Inside → cross outside (NOISE, dynamic with AFTER) + // function testFuzz_logic_noise_inside_to_outside_dynamic_after( + // uint256 eSeed, + // uint32 noiseThrSeed, + // uint32 noiseCapSeed, + // uint32 noiseMaxSeed, + // uint64 amtSeed + // ) public { + // InsideToOutsideDynamicAfterLocals memory locals; + + // // Lane params (NOISE fuzzed, ARB fixed and different) + // locals.E = bound(eSeed, 1e16, 1e24); + // locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); + // locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); + // locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + // locals.arbThr9 = 1_000_000; + // locals.arbCap9 = 300_000_000; + // locals.arbMax9 = 50_000_000; + + // locals.thr = uint256(locals.noiseThr9) * 1e9; + // locals.cap = uint256(locals.noiseCap9) * 1e9; + + // // Start BELOW E but inside: deviationBefore ∈ [0, thr) + // locals.deviationBefore = (locals.thr / 3) + 1; // safely inside + // locals.priceBefore = locals.E - (locals.E * locals.deviationBefore) / 1e18; // P/E = 1 - deviationBefore + // locals.R1e18 = (locals.priceBefore * 1e18) / locals.E; + + // // Need priceAfter/E less than or equal to 1 - thr ⇒ t greater than or equal to R/(1 - thr) - 1 + + // locals.num = locals.R1e18 * 1e18; // Q36 + // locals.den = 1e18 - locals.thr; + // locals.q = (locals.num + locals.den - 1) / locals.den; // ceilDiv → Q18 + // locals.tLower = locals.q > 1e18 ? (locals.q - 1e18) : 0; + + // // Pick x greater than or equal to tLower (plus small epsilon) to cross outside + // locals.eps = 1e12; + // locals.lo = locals.tLower + locals.eps; + // if (locals.lo == 0) locals.lo = 1; + // locals.hi = locals.lo + 5e17; // allow up to +0.5 in t + // locals.x = bound(uint256(amtSeed), locals.lo, locals.hi); + + // // Build locals + // locals.comp.wIn = 1e18; + // locals.comp.wOut = 1e18; + // locals.comp.bIn = 1e18; + // locals.comp.bOut = locals.priceBefore; + // locals.comp.pxIn = 1e18; + // locals.comp.pxOut = locals.E; + // locals.comp.calcAmountScaled18 = 0; + // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; + // locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; + // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + + // locals.p.kind = SwapKind.EXACT_IN; + // locals.p.amountGivenScaled18 = locals.x; + + // // Expected (NOISE) uses AFTER + // locals.priceAfter = (locals.priceBefore * 1e18) / (1e18 + locals.x); + // uint256 deviationAfter = (( + // locals.priceAfter > locals.E ? (locals.priceAfter - locals.E) : (locals.E - locals.priceAfter) + // ) * 1e18) / locals.E; + // assertGt(deviationAfter, locals.thr, "must end outside threshold (worsened)"); + // locals.expected = fee_expectedFeeWithParams( + // locals.priceAfter, + // locals.comp.pxIn, + // locals.comp.pxOut, + // STATIC_SWAP_FEE, + // locals.noiseThr9, + // locals.noiseCap9, + // locals.noiseMax9 + // ); + + // HyperSurgeHookMock mock = new HyperSurgeHookMock( + // IVault(vault), + // _convertTo18Decimals(locals.arbMax9), + // _convertTo18Decimals(locals.arbThr9), + // _convertTo18Decimals(locals.arbCap9), + // "lane-inside2outside" + // ); + // (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); + + // assertEq(locals.dyn, locals.expected, "noise/after: dynamic fee must match expected"); + // assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee greater than or equal to static"); + // } + + // struct OutsideToThresholdDynamicBeforeLocals { + // uint256 E; + // uint32 arbThr9; + // uint32 arbCap9; + // uint32 arbMax9; + // uint32 noiseThr9; + // uint32 noiseCap9; + // uint32 noiseMax9; + // uint256 thr; + // uint256 cap; + // uint256 deviationBefore; + // uint256 priceBefore; + // uint256 priceAfter; + // uint256 R1e18; + // uint256 tLower; + // uint256 tUpper; + // uint256 x; + // uint256 epsT; + // uint256 lo; + // uint256 hi; + // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; + // PoolSwapParams p; + // uint256 expected; + // uint256 dyn; + // } + + // /// [LANE] Outside → to (or just inside) threshold (ARB, dynamic with BEFORE) + // function testFuzz_logic_arb_outside_to_threshold_dynamic_before( + // uint256 eSeed, + // uint32 arbThrSeed, + // uint32 arbCapSeed, + // uint32 arbMaxSeed, + // uint64 amtSeed + // ) public { + // OutsideToThresholdDynamicBeforeLocals memory locals; + + // locals.E = bound(eSeed, 1e16, 1e24); + // locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); + // locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); + // locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + // // Distinct NOISE lane (unused in expected but kept different) + // locals.noiseThr9 = 5_000_000; + // locals.noiseCap9 = 400_000_000; + // locals.noiseMax9 = 25_000_000; + + // locals.thr = uint256(locals.arbThr9) * 1e9; + // locals.cap = uint256(locals.arbCap9) * 1e9; + + // // Start ABOVE, outside + // locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; // strictly outside + // locals.priceBefore = locals.E + (locals.E * locals.deviationBefore) / 1e18; + + // // R = priceBefore / E in Q18; compute both ceil and floor variants to bound tightly + // // R_up = ceil( (priceBefore * 1e18) / E ) + // // R_down = floor( (priceBefore * 1e18) / E ) + // uint256 numR = locals.priceBefore * 1e18; + // locals.R1e18 = (numR + locals.E - 1) / locals.E; + + // // We need 1 - thr less than or equal to priceAfter/E less than or equal to 1 + thr, and priceAfter/E = R / (1 + t), with t = x/1e18 (Q18). + // // Lower bound on t (to get under the upper edge 1 + thr): + // // t ≥ R/(1 + thr) − 1 + // // Use R_up and ceil-div to be conservative, then subtract 1e18. + // uint256 denomPlus = 1e18 + locals.thr; + // uint256 numPlus = locals.R1e18 * 1e18; // Q36 + // uint256 qPlus = (numPlus + denomPlus - 1) / denomPlus; // ceilDiv → Q18 + // locals.tLower = qPlus > 1e18 ? (qPlus - 1e18) : 0; + + // // Upper bound on t (don’t drop below the lower edge 1 − thr): + // // t less than or equal to R/(1 − thr) − 1 + // // Use R_down and floor-div to be conservative, then subtract 1e18. + // uint256 denomMinus = 1e18 - locals.thr; + // uint256 numMinus = locals.R1e18 * 1e18; // Q36 + // uint256 qMinus = numMinus / denomMinus; // floorDiv → Q18 + // locals.tUpper = qMinus > 1e18 ? (qMinus - 1e18) : 0; + + // // Choose t inside [tLower + eps, tUpper − eps] and map amtSeed with bound(...). + // // eps helps avoid equality-edge flips due to integer rounding. + // locals.epsT = 1; // one Q18 unit (~1e-18) is ample given we used ceil/floor conservatively + // locals.lo = locals.tLower + locals.epsT; + // locals.hi = (locals.tUpper > locals.epsT) ? (locals.tUpper - locals.epsT) : locals.tUpper; + + // // If interval collapses or inverted (can happen with extreme tiny thr), clamp to a point and proceed. + // if (locals.hi < locals.lo) { + // locals.hi = locals.lo; + // } + // if (locals.lo == 0) { + // locals.lo = 1; + // if (locals.hi < locals.lo) locals.hi = locals.lo; + // } + + // locals.x = bound(uint256(amtSeed), locals.lo, locals.hi); + + // // Build locals + // locals.comp.wIn = 1e18; + // locals.comp.wOut = 1e18; + // locals.comp.bIn = 1e18; + // locals.comp.bOut = locals.priceBefore; + // locals.comp.pxIn = 1e18; + // locals.comp.pxOut = locals.E; + // locals.comp.calcAmountScaled18 = 0; + // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; + // locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; + // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + + // locals.p.kind = SwapKind.EXACT_IN; + // locals.p.amountGivenScaled18 = locals.x; + + // // Sanity: end is inside (two-sided) + // locals.priceAfter = (locals.priceBefore * 1e18) / (1e18 + locals.p.amountGivenScaled18); + // uint256 dAfter = (( + // locals.priceAfter > locals.E ? (locals.priceAfter - locals.E) : (locals.E - locals.priceAfter) + // ) * 1e18) / locals.E; + // assertLe(dAfter, locals.thr, "end should be at/inside threshold"); + + // // Expected (ARB) uses BEFORE even if end is at/inside threshold + // locals.expected = fee_expectedFeeWithParams( + // locals.priceBefore, + // locals.comp.pxIn, + // locals.comp.pxOut, + // STATIC_SWAP_FEE, + // locals.arbThr9, + // locals.arbCap9, + // locals.arbMax9 + // ); + + // HyperSurgeHookMock mock = new HyperSurgeHookMock( + // IVault(vault), + // _convertTo18Decimals(locals.arbMax9), + // _convertTo18Decimals(locals.arbThr9), + // _convertTo18Decimals(locals.arbCap9), + // "lane-out2thr" + // ); + // (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); + + // assertEq( + // locals.dyn, + // locals.expected, + // "arb/before: dynamic fee must use BEFORE deviation even at threshold end" + // ); + // assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee greater than or equal to static"); + // } + + // struct ArbNoMoveOutsideDynamicLocals { + // uint256 E; + // uint32 arbThr9; + // uint32 arbCap9; + // uint32 arbMax9; + // uint32 noiseThr9; + // uint32 noiseCap9; + // uint32 noiseMax9; + // uint256 thr; + // uint256 cap; + // uint256 deviationBefore; + // uint256 priceBefore; + // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; + // PoolSwapParams p; + // uint256 expected; + // uint256 dyn; + // } + + // function test_logic_arb_outside_nochange_dynamic_before( + // uint256 eSeed, + // uint32 arbThrSeed, + // uint32 arbCapSeed, + // uint32 arbMaxSeed + // ) public { + // ArbNoMoveOutsideDynamicLocals memory locals; + + // locals.E = bound(eSeed, 1e16, 1e24); + // locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); + // locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); + // locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + // // NOISE lane different (unused) + // locals.noiseThr9 = 5_000_000; + // locals.noiseCap9 = 400_000_000; + // locals.noiseMax9 = 25_000_000; + + // locals.thr = uint256(locals.arbThr9) * 1e9; + // locals.cap = uint256(locals.arbCap9) * 1e9; + + // // Start ABOVE, outside + // locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; + // locals.priceBefore = locals.E + (locals.E * locals.deviationBefore) / 1e18; + + // // No movement: amount = 0, so deviationAfter == deviationBefore → ARB path + // locals.comp.wIn = 1e18; + // locals.comp.wOut = 1e18; + // locals.comp.bIn = 1e18; + // locals.comp.bOut = locals.priceBefore; + // locals.comp.pxIn = 1e18; + // locals.comp.pxOut = locals.E; + // locals.comp.calcAmountScaled18 = 0; + // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; + // locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; + // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + + // locals.p.kind = SwapKind.EXACT_IN; + // locals.p.amountGivenScaled18 = 0; + + // locals.expected = fee_expectedFeeWithParams( + // locals.priceBefore, + // locals.comp.pxIn, + // locals.comp.pxOut, + // STATIC_SWAP_FEE, + // locals.arbThr9, + // locals.arbCap9, + // locals.arbMax9 + // ); + + // HyperSurgeHookMock mock = new HyperSurgeHookMock( + // IVault(vault), + // _convertTo18Decimals(locals.arbMax9), + // _convertTo18Decimals(locals.arbThr9), + // _convertTo18Decimals(locals.arbCap9), + // "lane-nomove-outside" + // ); + // (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); + + // assertEq(locals.dyn, locals.expected, "no-move/outside must be ARB, dynamic from BEFORE"); + // assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee greater than or equal to static"); + // } + + // struct ArbNoMoveInsideLocals { + // uint256 E; + // uint32 arbThr9; + // uint32 arbCap9; + // uint32 arbMax9; + // uint32 noiseThr9; + // uint32 noiseCap9; + // uint32 noiseMax9; + // uint256 thr; + // uint256 deviationBefore; + // uint256 priceBefore; + // uint256 fee; + // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; + // PoolSwapParams p; + // } + + // /// [LANE] No movement, inside: ARB path, but STATIC fee (since BEFORE less than or equal to thr) + // function test_logic_arb_inside_nochange_static( + // uint256 eSeed, + // uint32 arbThrSeed, + // uint32 arbCapSeed, + // uint32 arbMaxSeed + // ) public { + // ArbNoMoveInsideLocals memory locals; + + // locals.E = bound(eSeed, 1e16, 1e24); + // locals.arbThr9 = uint32(bound(arbThrSeed, 1, 1_000_000_000 - 1)); + // locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); + // locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + // locals.noiseThr9 = 5_000_000; + // locals.noiseCap9 = 400_000_000; + // locals.noiseMax9 = 25_000_000; + + // locals.thr = uint256(locals.arbThr9) * 1e9; + + // // Start BELOW, inside + // locals.deviationBefore = (locals.thr / 3) + 1; // strictly inside + // locals.priceBefore = locals.E - (locals.E * locals.deviationBefore) / 1e18; + + // // No movement: deviationAfter == deviationBefore → ARB branch, but less than or equal to thr ⇒ static + // locals.comp.wIn = 1e18; + // locals.comp.wOut = 1e18; + // locals.comp.bIn = 1e18; + // locals.comp.bOut = locals.priceBefore; + // locals.comp.pxIn = 1e18; + // locals.comp.pxOut = locals.E; + // locals.comp.calcAmountScaled18 = 0; + // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; + // locals.comp.poolDetails.arbCapDeviationPercentage9 = uint32(locals.arbCap9); + // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + + // locals.p.kind = SwapKind.EXACT_IN; + // locals.p.amountGivenScaled18 = 0; + + // HyperSurgeHookMock mock = new HyperSurgeHookMock( + // IVault(vault), + // _convertTo18Decimals(locals.arbMax9), + // _convertTo18Decimals(locals.arbThr9), + // _convertTo18Decimals(locals.arbCap9), + // "lane-nomove-inside" + // ); + // (, locals.fee) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); + + // assertEq( + // locals.fee, + // STATIC_SWAP_FEE, + // "no-move/inside must return static (ARB branch, but less than or equal to thr)" + // ); + // } + + // struct NoiseCrossesPriceWorsensDymanicLocals { + // uint256 E; + // uint32 noiseThr9; + // uint32 noiseCap9; + // uint32 noiseMax9; + // uint32 arbThr9; + // uint32 arbCap9; + // uint32 arbMax9; + // uint256 thr; + // uint256 cap; + // uint256 deviationBefore; + // uint256 priceBefore; + // uint256 priceAfter; + // uint256 tCross; + // uint256 tWorse; + // uint256 tMin; + // uint256 x; + // uint256 num; + // uint256 den; + // uint256 q; + // uint256 epsT; + // uint256 lo; + // uint256 hi; + // uint256 deviationAfter; + // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; + // PoolSwapParams p; + // uint256 expected; + // uint256 dyn; + // } + + // /// [LANE] Symmetric “below” case: start outside BELOW, worsen further BELOW (no cross) → NOISE uses AFTER + // /// Note: With calc=0 and this simplified price update, EXACT_IN can only decrease P, + // /// so a true below→above cross is not representable without changing the price update model. + // /// This test locks the symmetric NOISE/AFTER behavior from the “below” side. + // function testFuzz_logic_noise_outside_below_worsens_dynamic_after( + // uint256 eSeed, + // uint32 noiseThrSeed, + // uint32 noiseCapSeed, + // uint32 noiseMaxSeed, + // uint64 amtSeed + // ) public { + // NoiseCrossesPriceWorsensDymanicLocals memory locals; + + // // External price (pxOut/pxIn -> E); keep as in all other tests + // locals.E = bound(eSeed, 1e16, 1e24); + + // // Distinct NOISE lane params (fuzzed) and different ARB params (unused in expected but distinct to catch wrong-lane) + // locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); + // locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); + // locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + // locals.arbThr9 = 1_000_000; + // locals.arbCap9 = 300_000_000; + // locals.arbMax9 = 50_000_000; + + // locals.thr = uint256(locals.noiseThr9) * 1e9; + // locals.cap = uint256(locals.noiseCap9) * 1e9; + + // // Start OUTSIDE BELOW price: priceBefore = E * (1 - D_before), with D_before in (thr, cap) + // locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; + // locals.priceBefore = locals.E - (locals.E * locals.deviationBefore) / 1e18; + + // // Build compute locals with the standard orientation (pxIn=1e18, pxOut=E) + // locals.comp.wIn = 1e18; + // locals.comp.wOut = 1e18; + // locals.comp.bIn = 1e18; + // locals.comp.bOut = locals.priceBefore; + // locals.comp.pxIn = 1e18; // keep the usual frame + // locals.comp.pxOut = locals.E; + // locals.comp.calcAmountScaled18 = 0; + // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; + // locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; + // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + + // // EXACT_IN reduces P further → deviation worsens from the BELOW side (NOISE lane) + // locals.p.kind = SwapKind.EXACT_IN; + // // ensure a measurable worsening but no overflow; avoid 1-wei knife edges + // uint256 lo = 1e9; + // uint256 hi = 5e17; + // locals.p.amountGivenScaled18 = bound(uint256(amtSeed), lo, hi); + + // // AFTER price for expected (NOISE uses AFTER) + // locals.priceAfter = (locals.priceBefore * 1e18) / (1e18 + locals.p.amountGivenScaled18); + + // // Sanity: still BELOW E and deviation increased + // uint256 dBefore = ((locals.E - locals.priceBefore) * 1e18) / locals.E; + // uint256 dAfter = ((locals.E - locals.priceAfter) * 1e18) / locals.E; + // assertGt(dAfter, dBefore, "deviation must worsen from the below side"); + + // // Expected NOISE fee from AFTER deviation + // locals.expected = fee_expectedFeeWithParams( + // locals.priceAfter, + // locals.comp.pxIn, + // locals.comp.pxOut, + // STATIC_SWAP_FEE, + // locals.noiseThr9, + // locals.noiseCap9, + // locals.noiseMax9 + // ); + + // HyperSurgeHookMock mock = new HyperSurgeHookMock( + // IVault(vault), + // _convertTo18Decimals(locals.arbMax9), + // _convertTo18Decimals(locals.arbThr9), + // _convertTo18Decimals(locals.arbCap9), + // "lane-below-worsen" + // ); + // (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); + + // assertEq(locals.dyn, locals.expected, "noise/after (below side): dynamic fee must match expected"); + // assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee greater than or equal to static"); + // } + + // struct BoundArbBeforeClampToMaxLocals { + // uint256 E; + // uint32 arbThr9; + // uint32 arbCap9; + // uint32 arbMax9; + // uint32 noiseThr9; + // uint32 noiseCap9; + // uint32 noiseMax9; + // uint256 thr; + // uint256 cap; + // uint256 Db; + // uint256 priceBefore; + // uint256 priceAfter; + // uint256 tLower; + // uint256 tUpperNoCross; + // uint256 x; + // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; + // PoolSwapParams p; + // uint256 fee; + // uint256 expected; + // } + + // /// [BOUND] ARB with BEFORE > cap, AFTER < cap: ARB clamps to maxArb (basis = BEFORE) + // /// Start ABOVE with BEFORE deviation > cap, improve so AFTER less than or equal to cap (stay above; no cross). + // /// Assert: ARB lane; fee == arbMax (clamped by BEFORE). + // function testFuzz_bound_arb_before_gt_cap_clamps_to_max_before( + // uint256 eSeed, + // uint32 arbThrSeed, + // uint32 arbCapSeed, + // uint32 arbMaxSeed, + // uint64 amtSeed + // ) public { + // BoundArbBeforeClampToMaxLocals memory locals; + + // // External price + // locals.E = bound(eSeed, 1e16, 1e24); + + // // ARB lane params (ensure thr < cap < 1.0) + // locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); // (0, 0.9) + // locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000 - 1)); // (thr, 1) + // locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9) + 1, 1_000_000_000)); + + // // Distinct NOISE params (unused in expected but kept different to catch wrong-lane) + // locals.noiseThr9 = 5_000_000; + // locals.noiseCap9 = 400_000_000; + // locals.noiseMax9 = 25_000_000; + + // locals.thr = uint256(locals.arbThr9) * 1e9; + // locals.cap = uint256(locals.arbCap9) * 1e9; + // assertLt(locals.cap, 1e18, "cap must be < 100%"); + + // // BEFORE deviation strictly above cap but < 1, with safe margin + // // margin = max(1, (1e18 - cap)/16) keeps Db < 1 while staying comfortably > cap + // uint256 margin = (1e18 - locals.cap) / 16; + // if (margin == 0) { + // margin = 1; + // } + // locals.Db = locals.cap + margin; + // if (locals.Db >= 1e18) { + // locals.Db = 1e18 - 1; + // } + + // // Sanity: BEFORE > cap + // assertGt(locals.Db, locals.cap, "setup must have BEFORE > cap"); + + // // Price ABOVE E with BEFORE deviation Db + // locals.priceBefore = locals.E + (locals.E * locals.Db) / 1e18; + + // // ABOVE side with EXACT_IN: + // // D_after_pos (no-cross) = (Db - t)/(1 + t). Want AFTER less than or equal to cap ⇒ t ≥ (Db - cap)/(1 + cap). + + // uint256 num = (locals.Db - locals.cap) * 1e18; // Q36 (Db > cap guaranteed) + // uint256 den = 1e18 + locals.cap; + // uint256 q = (num + den - 1) / den; // ceilDiv → Q18 + // locals.tLower = q; + + // // Avoid crossing E: need t < Db. Use tiny epsilon below Db to stay strictly above E. + // uint256 epsCross = 1; // one Q18 unit + // locals.tUpperNoCross = (locals.Db > epsCross) ? (locals.Db - epsCross) : 0; + + // uint256 lo = (locals.tLower == 0 ? 1 : locals.tLower); + // uint256 hi = locals.tUpperNoCross; + + // if (hi < lo) { + // hi = lo; + // } + // locals.x = bound(uint256(amtSeed), lo, hi); + + // locals.comp.wIn = 1e18; + // locals.comp.wOut = 1e18; + // locals.comp.bIn = 1e18; + // locals.comp.bOut = locals.priceBefore; + // locals.comp.pxIn = 1e18; + // locals.comp.pxOut = locals.E; + // locals.comp.calcAmountScaled18 = 0; + // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; + // locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; + // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + + // locals.p.kind = SwapKind.EXACT_IN; + // locals.p.amountGivenScaled18 = locals.x; + + // // AFTER should be less than or equal to cap (improved) and we shouldn’t have crossed E. + // locals.priceAfter = (locals.priceBefore * 1e18) / (1e18 + locals.x); + // uint256 dAfter = (( + // locals.priceAfter > locals.E ? (locals.priceAfter - locals.E) : (locals.E - locals.priceAfter) + // ) * 1e18) / locals.E; + // assertLe(dAfter, locals.cap, "AFTER should be less than or equal to cap (improved)"); + + // // ARB uses BEFORE and must clamp to maxArb + // (, locals.fee) = new HyperSurgeHookMock( + // IVault(vault), + // _convertTo18Decimals(locals.arbMax9), + // _convertTo18Decimals(locals.arbThr9), + // _convertTo18Decimals(locals.arbCap9), + // "arb-before-cap" + // ).ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); + + // locals.expected = fee_expectedFeeWithParams( + // locals.priceBefore, + // locals.comp.pxIn, + // locals.comp.pxOut, + // STATIC_SWAP_FEE, + // locals.arbThr9, + // locals.arbCap9, + // locals.arbMax9 + // ); + // assertEq(locals.fee, locals.expected, "ARB should compute from BEFORE and clamp at cap->max"); + // assertEq(locals.fee, _convertTo18Decimals(locals.arbMax9), "ARB fee must equal arbMax"); + // } + + // struct BoundNoiseExactThresholdLocals { + // uint256 E; + // uint32 noiseThr9; + // uint32 noiseCap9; + // uint32 noiseMax9; + // uint32 arbThr9; + // uint32 arbCap9; + // uint32 arbMax9; + // uint256 thr; + // uint256 Db; + // uint256 priceBefore; + // uint256 priceAfter; + // uint256 tEdge; + // uint256 x; + // uint256 fee; + // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; + // PoolSwapParams p; + // } + + // function testFuzz_bound_noise_after_at_threshold_static( + // uint256 eSeed, + // uint32 noiseThrSeed, + // uint32 noiseCapSeed, + // uint32 noiseMaxSeed, + // uint64 amtSeed + // ) public { + // BoundNoiseExactThresholdLocals memory locals; + + // locals.E = bound(eSeed, 1e16, 1e24); + // locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); + // locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); + // locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + // locals.arbThr9 = 1_000_000; + // locals.arbCap9 = 300_000_000; + // locals.arbMax9 = 50_000_000; + + // locals.thr = uint256(locals.noiseThr9) * 1e9; + + // locals.Db = locals.thr / 4 + 1; + // locals.priceBefore = locals.E - (locals.E * locals.Db) / 1e18; + + // uint256 num = (locals.thr - locals.Db) * 1e18; + // uint256 den = 1e18 - locals.thr; + // locals.tEdge = den == 0 ? 0 : (num / den); + + // uint256 epsT = 1e6; + // uint256 lo = (locals.tEdge > epsT) ? (locals.tEdge - epsT) : 1; + // uint256 hi = locals.tEdge; + // if (hi < lo) { + // hi = lo; + // } + + // locals.x = bound(uint256(amtSeed), lo, hi); + // locals.comp.wIn = 1e18; + // locals.comp.wOut = 1e18; + // locals.comp.bIn = 1e18; + // locals.comp.bOut = locals.priceBefore; + // locals.comp.pxIn = 1e18; + // locals.comp.pxOut = locals.E; + // locals.comp.calcAmountScaled18 = 0; + // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; + // locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; + // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + // locals.p.kind = SwapKind.EXACT_IN; + // locals.p.amountGivenScaled18 = locals.x; + // locals.priceAfter = (locals.priceBefore * 1e18) / (1e18 + locals.p.amountGivenScaled18); + + // uint256 dBefore = ((locals.E - locals.priceBefore) * 1e18) / locals.E; + // uint256 dAfter = ((locals.E - locals.priceAfter) * 1e18) / locals.E; + // assertLe(dAfter, locals.thr, "AFTER should be less than or equal to threshold (at-or-just-inside)"); + // assertGt(dAfter, dBefore, "deviation must worsen (positive t)"); + + // (, locals.fee) = new HyperSurgeHookMock( + // IVault(vault), + // _convertTo18Decimals(locals.arbMax9), + // _convertTo18Decimals(locals.arbThr9), + // _convertTo18Decimals(locals.arbCap9), + // "noise-exact-thr" + // ).ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); + + // assertEq(locals.fee, STATIC_SWAP_FEE, "At threshold end-state: NOISE must return static (no ramp)"); + // } + + // uint32 constant HL_IDX_SZ_0 = 100; + // uint32 constant HL_IDX_SZ_8 = 108; + + // bytes4 constant _SEL_SPOT_PRICE = bytes4(keccak256("spotPrice(uint32)")); + // address constant _HYPER_SPOT_PRICE_PRECOMPILE = 0x0000000000000000000000000000000000000808; + + // function _mockHyperSpotPrice(uint32 pairIndex, uint64 raw) internal { + // vm.mockCall( + // _HYPER_SPOT_PRICE_PRECOMPILE, + // abi.encode(pairIndex), // <- no selector + // abi.encode(raw) // 32-byte padded uint64 + // ); + // } + + // function testFuzz_Fee_FallbacksToStatic_When_ExtPxZero(bool givenIn, uint64 rawInHuge) public { + // TokenConfig[] memory cfg = new TokenConfig[](2); + // LiquidityManagement memory lm; + // vm.prank(address(vault)); + // hook.onRegister(poolFactory, address(pool), cfg, lm); + + // // 2) Use the same HL token index you used (108 -> sz=0 -> divisor=1e8) on BOTH tokens + // uint32 pairIn = 8001; + // uint32 pairOut = 8002; + + // vm.startPrank(admin); + // hook.setTokenPriceConfigIndex(address(pool), 0, pairIn, 108); // div=1e8 + // hook.setTokenPriceConfigIndex(address(pool), 1, pairOut, 108); // div=1e8 + // vm.stopPrank(); + + // // 3) Force extPx == 0 with NON-ZERO raws: + // // extPx = floor((pxOut*1e18)/pxIn) = floor((rawOut*1e18)/rawIn) + // // => choose rawOut=1 and rawIn > 1e18 (fits in uint64), so extPx == 0 + // rawInHuge = uint64(bound(uint256(rawInHuge), 1e18 + 1, type(uint64).max)); + + // // (optional) prove we hit the correct precompile and calldata (no selector) + // vm.expectCall(_HYPER_SPOT_PRICE_PRECOMPILE, abi.encode(pairIn)); + // vm.expectCall(_HYPER_SPOT_PRICE_PRECOMPILE, abi.encode(pairOut)); + + // // Mock the spot prices with the correct calldata (NO selector) + // _mockHyperSpotPrice(pairIn, rawInHuge); // pxIn = rawInHuge * 1e10 + // _mockHyperSpotPrice(pairOut, 1); // pxOut = 1 * 1e10 + + // // 4) Build params (all 7 fields) + // uint256[] memory balances = new uint256[](2); + // balances[0] = 1e18; + // balances[1] = 1e18; + + // SwapKind kind = givenIn ? SwapKind.EXACT_IN : SwapKind.EXACT_OUT; + + // PoolSwapParams memory p = PoolSwapParams({ + // kind: kind, + // amountGivenScaled18: 5e15, + // balancesScaled18: balances, + // indexIn: 0, + // indexOut: 1, + // router: address(0), + // userData: "" + // }); + + // // 5) Expect: NO revert; the hook falls back to pool static fee because extPx == 0 + // uint256 staticFee = WeightedPool(address(pool)).getStaticSwapFeePercentage(); + // (bool ok, uint256 dynFee) = hook.onComputeDynamicSwapFeePercentage(p, address(pool), staticFee); + + // assertTrue(ok, "extPx==0 must not block"); + // assertEq(dynFee, staticFee, "extPx==0 must return static fee"); + // } + + // function testFuzz_Fee_ClampsToMax_When_DeviationBeyondCap(bool givenIn, uint64 rawOutHuge) public { + // uint256 idxIn = 0; + // uint256 idxOut = 1; + // uint256 amountGiven = 5e15; + + // uint256[] memory balances = new uint256[](2); + // balances[0] = 1e18; + // balances[1] = 1e18; + + // TokenConfig[] memory cfg = new TokenConfig[](2); + // LiquidityManagement memory lm; + // vm.prank(address(vault)); + // hook.onRegister(poolFactory, address(pool), cfg, lm); + + // uint32 pairIn = 91001; + // uint32 pairOut = 91002; + // vm.startPrank(admin); + // hook.setTokenPriceConfigIndex(address(pool), uint8(idxIn), pairIn, HL_IDX_SZ_8); + // hook.setTokenPriceConfigIndex(address(pool), uint8(idxOut), pairOut, HL_IDX_SZ_8); + + // uint256 thr = 1e16; // 1% + // uint256 cap = 2e16; // 2% + // uint256 max = 15e15; // 1.5% + + // hook.setSurgeThresholdPercentage(address(pool), thr, IHyperSurgeHook.TradeType.ARBITRAGE); + // hook.setSurgeThresholdPercentage(address(pool), thr, IHyperSurgeHook.TradeType.NOISE); + // hook.setCapDeviationPercentage(address(pool), cap, IHyperSurgeHook.TradeType.ARBITRAGE); + // hook.setCapDeviationPercentage(address(pool), cap, IHyperSurgeHook.TradeType.NOISE); + // hook.setMaxSurgeFeePercentage(address(pool), max, IHyperSurgeHook.TradeType.ARBITRAGE); + // hook.setMaxSurgeFeePercentage(address(pool), max, IHyperSurgeHook.TradeType.NOISE); + // vm.stopPrank(); + + // // External price >> 1.0: + // // extPx = (pxOut / pxIn) with same divisor. Set pxOut very large, pxIn = 1 unit. + // // Use HL_IDX_SZ_8 (divisor 1e8) so raw numbers are easy: rawIn=1e8, rawOut in [5e9, max]. + // rawOutHuge = uint64(bound(uint256(rawOutHuge), 5e9, type(uint64).max)); + // _mockHyperSpotPrice(pairIn, uint64(1e8)); + // _mockHyperSpotPrice(pairOut, rawOutHuge); + + // SwapKind kind = givenIn ? SwapKind.EXACT_IN : SwapKind.EXACT_OUT; + // PoolSwapParams memory p = _makeParams(idxIn, idxOut, kind, amountGiven, balances); + + // uint256 staticFee = WeightedPool(address(pool)).getStaticSwapFeePercentage(); + // (bool ok, uint256 fee) = hook.onComputeDynamicSwapFeePercentage(p, address(pool), staticFee); + + // assertTrue(ok, "fee path must not block"); + // assertEq(fee, max, "fee must clamp at configured maxPct"); + // } + + // function testFuzz_Fee_ReturnsStatic_When_DeviationBelowThreshold(bool givenIn, uint64 rawBase) public { + // uint256 idxIn = 0; + // uint256 idxOut = 1; + // uint256 amountGiven = 5e15; + + // uint256[] memory balances = new uint256[](2); + // balances[0] = 1e18; + // balances[1] = 1e18; + + // TokenConfig[] memory cfg = new TokenConfig[](2); + // LiquidityManagement memory lm; + // vm.prank(address(vault)); + // hook.onRegister(poolFactory, address(pool), cfg, lm); + + // uint32 pairIn = 92001; + // uint32 pairOut = 92002; + // vm.startPrank(admin); + // hook.setTokenPriceConfigIndex(address(pool), uint8(idxIn), pairIn, HL_IDX_SZ_8); + // hook.setTokenPriceConfigIndex(address(pool), uint8(idxOut), pairOut, HL_IDX_SZ_8); + + // // Set a relatively generous threshold (5%) and a higher cap so we stay in "below threshold" + // uint256 thr = 5e16; // 5% + // uint256 cap = 20e16; // 20% (arbitrary > thr) + // uint256 max = 50e16; // 50% (irrelevant here) + + // hook.setSurgeThresholdPercentage(address(pool), thr, IHyperSurgeHook.TradeType.ARBITRAGE); + // hook.setSurgeThresholdPercentage(address(pool), thr, IHyperSurgeHook.TradeType.NOISE); + // hook.setCapDeviationPercentage(address(pool), cap, IHyperSurgeHook.TradeType.ARBITRAGE); + // hook.setCapDeviationPercentage(address(pool), cap, IHyperSurgeHook.TradeType.NOISE); + // hook.setMaxSurgeFeePercentage(address(pool), max, IHyperSurgeHook.TradeType.ARBITRAGE); + // hook.setMaxSurgeFeePercentage(address(pool), max, IHyperSurgeHook.TradeType.NOISE); + // vm.stopPrank(); + + // // Make extPx ≈ 1.0 within ~1e-8 relative drift, far below the 5% threshold. + // // Same divisor (1e8): extPx = (rawOut/rawIn). Pick rawOut = rawBase + 1, rawIn = rawBase. + // rawBase = uint64(bound(uint256(rawBase), 1e8, 5e9)); // ensure > 0 and leaves headroom for +1 + // _mockHyperSpotPrice(pairIn, rawBase); + // _mockHyperSpotPrice(pairOut, rawBase + 1); + + // SwapKind kind = givenIn ? SwapKind.EXACT_IN : SwapKind.EXACT_OUT; + // PoolSwapParams memory p = _makeParams(idxIn, idxOut, kind, amountGiven, balances); + + // uint256 staticFee = WeightedPool(address(pool)).getStaticSwapFeePercentage(); + // (bool ok, uint256 fee) = hook.onComputeDynamicSwapFeePercentage(p, address(pool), staticFee); + + // assertTrue(ok, "below-threshold path must not block"); + // assertEq(fee, staticFee, "below-threshold deviation must return static fee"); + // } function _makeParams( uint256 indexIn, @@ -3044,30 +2981,30 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo vm.store(HyperTokenInfoPrecompile.TOKEN_INFO_PRECOMPILE_ADDRESS, slot, bytes32(uint256(sz))); } - function _feeAtDeviation( - HyperSurgeHookMock.ComputeSurgeFeeLocals memory computeLocals, - PoolSwapParams memory p, - uint256 staticFee, - uint256 extPxE18, - uint256 deviation18 - ) internal view returns (uint256) { - // pool price P = E * (1 + deviation) - uint256 P = extPxE18 + (extPxE18 * deviation18) / 1e18; - - // Make poolPx = P using simple weights/balances: - // poolPx = (bOut * wIn) / (bIn * wOut) - computeLocals.wIn = 1e18; - computeLocals.wOut = 1e18; - computeLocals.bIn = 1e18; - computeLocals.bOut = P; - - // Keep deltas zero so poolPx == poolPxBefore (no lane flip due to swap) - computeLocals.calcAmountScaled18 = 0; - - (bool ok, uint256 fee) = hook.ComputeSurgeFee(computeLocals, p, staticFee); - assertTrue(ok, "compute ok"); - return fee; - } + // function _feeAtDeviation( + // HyperSurgeHookMock.ComputeSurgeFeeLocals memory computeLocals, + // PoolSwapParams memory p, + // uint256 staticFee, + // uint256 extPxE18, + // uint256 deviation18 + // ) internal view returns (uint256) { + // // pool price P = E * (1 + deviation) + // uint256 P = extPxE18 + (extPxE18 * deviation18) / 1e18; + + // // Make poolPx = P using simple weights/balances: + // // poolPx = (bOut * wIn) / (bIn * wOut) + // computeLocals.wIn = 1e18; + // computeLocals.wOut = 1e18; + // computeLocals.bIn = 1e18; + // computeLocals.bOut = P; + + // // Keep deltas zero so poolPx == poolPxBefore (no lane flip due to swap) + // computeLocals.calcAmountScaled18 = 0; + + // (bool ok, uint256 fee) = hook.ComputeSurgeFee(computeLocals, p, staticFee); + // assertTrue(ok, "compute ok"); + // return fee; + // } function fee_mulDown(uint256 a, uint256 b) internal pure returns (uint256) { return (a * b) / FEE_ONE; @@ -3137,8 +3074,8 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo pxOut = fee_divDown(P, FEE_ONE + D); } - function fee_ppm9To1e18(uint32 v) internal pure returns (uint256) { - return uint256(v) * 1e9; + function _convertTo18Decimals(uint32 valueScaled9) internal pure returns (uint256) { + return uint256(valueScaled9) * 1e9; } // Expected fee (exact same rounding & clamping as the hook) @@ -3154,9 +3091,9 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo uint256 extPx = fee_divDown(pxOut, pxIn); uint256 deviation = fee_relAbsDiff(poolPx, extPx); - uint256 threshold = fee_ppm9To1e18(thresholdPPM9); - uint256 capDev = fee_ppm9To1e18(capDevPPM9); - uint256 maxPct = fee_ppm9To1e18(maxFeePPM9); + uint256 threshold = _convertTo18Decimals(thresholdPPM9); + uint256 capDev = _convertTo18Decimals(capDevPPM9); + uint256 maxPct = _convertTo18Decimals(maxFeePPM9); if (deviation <= threshold) { return staticSwapFee; @@ -3176,29 +3113,36 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo return fee; } - function fee_makeLocals( - uint256 bIn, - uint256 wIn, - uint256 bOut, - uint256 wOut, - uint256 pxIn, - uint256 pxOut, - uint32 thrPPM9, - uint32 capPPM9, - uint32 maxPPM9 - ) internal pure returns (HyperSurgeHookMock.ComputeSurgeFeeLocals memory computeLocals) { - computeLocals.bIn = bIn; - computeLocals.wIn = wIn; - computeLocals.bOut = bOut; - computeLocals.wOut = wOut; - computeLocals.pxIn = pxIn; - computeLocals.pxOut = pxOut; - computeLocals.poolDetails.noiseThresholdPercentage9 = thrPPM9; - computeLocals.poolDetails.noiseCapDeviationPercentage9 = capPPM9; - computeLocals.poolDetails.noiseMaxSurgeFee9 = maxPPM9; - computeLocals.poolDetails.arbThresholdPercentage9 = thrPPM9; - computeLocals.poolDetails.arbCapDeviationPercentage9 = capPPM9; - computeLocals.poolDetails.arbMaxSurgeFee9 = maxPPM9; + function _createPoolSwapParams( + SwapKind kind, + uint256[] memory balancesScaled18, + uint8 indexIn, + uint8 indexOut, + uint256 amountGivenScaled18 + ) internal pure returns (PoolSwapParams memory p) { + p.kind = kind; + p.balancesScaled18 = balancesScaled18; + p.indexIn = indexIn; + p.indexOut = indexOut; + p.amountGivenScaled18 = amountGivenScaled18; + } + + function _createPoolDetails( + uint32[3] memory percentageSeeds, + uint8 n + ) internal pure returns (HyperSurgeHook.PoolDetails memory poolDetails) { + (uint32 thrPPM9, uint32 capPPM9, uint32 maxPPM9) = fee_boundParams( + percentageSeeds[0], + percentageSeeds[1], + percentageSeeds[2] + ); + poolDetails.noiseThresholdPercentage9 = thrPPM9; + poolDetails.noiseCapDeviationPercentage9 = capPPM9; + poolDetails.noiseMaxSurgeFee9 = maxPPM9; + poolDetails.arbThresholdPercentage9 = thrPPM9; + poolDetails.arbCapDeviationPercentage9 = capPPM9; + poolDetails.arbMaxSurgeFee9 = maxPPM9; + poolDetails.numTokens = n; } function fee_boundParams( From d149b2ccc283bafc62eabe9e0e034044f778090d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Fri, 12 Sep 2025 23:16:33 -0300 Subject: [PATCH 10/28] Fix tests - WIP --- .../test/foundry/HyperSurgeFee.t.sol | 268 ++++++++---------- 1 file changed, 124 insertions(+), 144 deletions(-) diff --git a/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol b/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol index 3057347c..01ba5188 100644 --- a/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol +++ b/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol @@ -934,163 +934,143 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo assertEq(locals.feeD, _convertTo18Decimals(locals.maxp), "above cap means clamped to max fee"); } - // struct ExactInEqualsExactOutLocals { - // uint8 n; - // uint256[] w; - // uint256[] b; - // uint8 i; - // uint8 j; - // uint32 thr; - // uint32 cap; - // uint32 maxp; - // uint256 P; - // uint256 capDev; - // uint256 D; - // uint256 pxIn; - // uint256 pxOut; - // uint256 feeIn; - // uint256 feeOut; - // } + struct ExactInEqualsExactOutLocals { + uint8 n; + uint256[] w; + uint256[] b; + uint8 i; + uint8 j; + uint32 thr; + uint32 cap; + uint32 maxp; + uint256 P; + uint256 capDev; + uint256 D; + uint256 pxIn; + uint256 pxOut; + uint256 feeIn; + uint256 feeOut; + } - // /// EXACT_IN vs EXACT_OUT: with identical lane params, the engine result must match. - // /// Correction: keep the *effective* lane params for the chosen direction the same, - // /// but make ARB and NOISE lanes different so a wrong-lane implementation would not hide here. - // // function testFuzz_internal_exactIn_equals_exactOut_whenParamsSame( - // // uint8 nSeed, - // // uint256 wSeed, - // // uint256 bSeed, - // // uint256 dSeed - // // ) public { - // // ExactInEqualsExactOutLocals memory locals; - - // // locals.n = uint8(bound(nSeed, 2, 8)); - // // locals.w = fee_normWeights(locals.n, wSeed); - // // locals.b = fee_balances(locals.n, bSeed); - - // // locals.i = uint8(bound(uint256(keccak256(abi.encode(dSeed, 41))), 0, locals.n - 1)); - // // locals.j = (locals.i + 1 + uint8(bound(uint256(keccak256(abi.encode(dSeed, 42))), 0, locals.n - 2))) % locals.n; - - // // locals.thr = 1_000_000; // 0.1% - // // locals.cap = 500_000_000; // 50% - // // locals.maxp = 50_000_000; // 5% - - // // locals.P = fee_pairSpotFromBW(locals.b[locals.i], locals.w[locals.i], locals.b[locals.j], locals.w[locals.j]); - // // vm.assume(locals.P > 0); - - // // locals.capDev = _convertTo18Decimals(locals.cap); - // // locals.D = uint256(keccak256(abi.encode(dSeed))) % (locals.capDev + locals.capDev / 2 + 1); - // // (locals.pxIn, locals.pxOut) = fee_localsForDeviation(locals.P, locals.D); - - // // HyperSurgeHookMock mock = new HyperSurgeHookMock( - // // IVault(vault), - // // _convertTo18Decimals(locals.maxp), - // // _convertTo18Decimals(locals.thr), - // // _convertTo18Decimals(locals.cap), - // // "fee-io" - // // ); - - // // // EXACT_IN - // // PoolSwapParams memory pIn; - // // pIn.kind = SwapKind.EXACT_IN; - - // // // Build locals with NOISE = (thr/cap/maxp) and ARB deliberately different - // // HyperSurgeHookMock.ComputeSurgeFeeLocals memory L1; - // // L1.bIn = locals.b[locals.i]; - // // L1.wIn = locals.w[locals.i]; - // // L1.bOut = locals.b[locals.j]; - // // L1.wOut = locals.w[locals.j]; - // // L1.pxIn = locals.pxIn; - // // L1.pxOut = locals.pxOut; - // // L1.calcAmountScaled18 = 0; - - // // // Effective (chosen) lane params - // // L1.poolDetails.noiseThresholdPercentage9 = locals.thr; - // // L1.poolDetails.noiseCapDeviationPercentage9 = locals.cap; - // // L1.poolDetails.noiseMaxSurgeFee9 = locals.maxp; - - // // // Different ARB lane params so wrong-lane usage wouldn’t accidentally match - // // L1.poolDetails.arbThresholdPercentage9 = locals.thr + 1; - // // L1.poolDetails.arbCapDeviationPercentage9 = locals.cap - 1; - // // L1.poolDetails.arbMaxSurgeFee9 = locals.maxp + 1; - - // // (, locals.feeIn) = mock.ComputeSurgeFee(L1, pIn, STATIC_SWAP_FEE); - - // // // EXACT_OUT - // // PoolSwapParams memory pOut; - // // pOut.kind = SwapKind.EXACT_OUT; - - // // HyperSurgeHookMock.ComputeSurgeFeeLocals memory L2; - // // L2.bIn = locals.b[locals.i]; - // // L2.wIn = locals.w[locals.i]; - // // L2.bOut = locals.b[locals.j]; - // // L2.wOut = locals.w[locals.j]; - // // L2.pxIn = locals.pxIn; - // // L2.pxOut = locals.pxOut; - // // L2.calcAmountScaled18 = 0; - - // // L2.poolDetails.noiseThresholdPercentage9 = locals.thr; - // // L2.poolDetails.noiseCapDeviationPercentage9 = locals.cap; - // // L2.poolDetails.noiseMaxSurgeFee9 = locals.maxp; - - // // L2.poolDetails.arbThresholdPercentage9 = locals.thr + 1; - // // L2.poolDetails.arbCapDeviationPercentage9 = locals.cap - 1; - // // L2.poolDetails.arbMaxSurgeFee9 = locals.maxp + 1; - - // // (, locals.feeOut) = mock.ComputeSurgeFee(L2, pOut, STATIC_SWAP_FEE); - - // // assertEq(locals.feeIn, locals.feeOut, "with equal lane params, kind should not change math result"); - // // } + // EXACT_IN vs EXACT_OUT: with identical lane params, the engine result must match. + // Correction: keep the *effective* lane params for the chosen direction the same, + // but make ARB and NOISE lanes different so a wrong-lane implementation would not hide here. + function testFuzz_internal_exactIn_equals_exactOut_whenParamsSame( + uint8 nSeed, + uint256 wSeed, + uint256 bSeed, + uint256 dSeed + ) public { + ExactInEqualsExactOutLocals memory locals; - // function testFuzz_view_missingPrices_reverts(uint8 nSeed, uint256 /* wSeed */, uint256 bSeed, uint8 iSeed) public { - // // --- Register pool and adapt to its actual token count --- - // uint8 nTarget = uint8(bound(nSeed, 2, 8)); - // _registerBasePoolWithN(nTarget); + locals.n = uint8(bound(nSeed, 2, 8)); + locals.w = fee_normWeights(locals.n, wSeed); + locals.b = fee_balances(locals.n, bSeed); - // uint256[] memory weights = WeightedPool(address(pool)).getNormalizedWeights(); - // uint256 m = weights.length; - // assertGe(m, 2, "pool must have at least 2 tokens"); + locals.i = uint8(bound(uint256(keccak256(abi.encode(dSeed, 41))), 0, locals.n - 1)); + locals.j = (locals.i + 1 + uint8(bound(uint256(keccak256(abi.encode(dSeed, 42))), 0, locals.n - 2))) % locals.n; + + locals.thr = 1_000_000; // 0.1% + locals.cap = 500_000_000; // 50% + locals.maxp = 50_000_000; // 5% - // // --- Random non-zero balances of exact pool length --- - // uint256[] memory b = fee_balances(uint8(m), bSeed); + locals.P = fee_pairSpotFromBW(locals.b[locals.i], locals.w[locals.i], locals.b[locals.j], locals.w[locals.j]); + vm.assume(locals.P > 0); - // // --- Pick a valid distinct pair (i != j) --- - // uint256 i = uint256(bound(iSeed, 0, m - 1)); - // uint256 j = (i + 1) % m; + locals.capDev = _convertTo18Decimals(locals.cap); + locals.D = uint256(keccak256(abi.encode(dSeed))) % (locals.capDev + locals.capDev / 2 + 1); + (locals.pxIn, locals.pxOut) = fee_localsForDeviation(locals.P, locals.D); - // // --- Build base swap params template with those balances --- - // PoolSwapParams memory p; - // p.balancesScaled18 = new uint256[](m); - // for (uint256 k = 0; k < m; ++k) { - // p.balancesScaled18[k] = b[k]; - // } - // p.indexIn = i; - // p.indexOut = j; + HyperSurgeHookMock mock = new HyperSurgeHookMock( + IVault(vault), + _convertTo18Decimals(locals.maxp), + _convertTo18Decimals(locals.thr), + _convertTo18Decimals(locals.cap), + "fee-io" + ); - // uint256 bIn = b[i]; - // uint256 bOut = b[j]; + // EXACT_IN + PoolSwapParams memory pIn = _createPoolSwapParams(SwapKind.EXACT_IN, locals.b, locals.i, locals.j, 0); - // uint256 safeInAmt = bIn / 1e6; - // if (safeInAmt == 0) safeInAmt = 1; - // uint256 safeOutAmt = bOut / 1e6; - // if (safeOutAmt == 0) safeOutAmt = 1; + // Build pool details with NOISE = (thr/cap/maxp) and ARB deliberately different + HyperSurgeHook.PoolDetails memory poolDetails; + poolDetails.numTokens = locals.n; - // // Sanity: amounts are indeed tiny relative to balances to avoid accidental reverts - // // (these checks also self-document the invariant we rely on) - // assertLt(safeInAmt, bIn / 10, "safeInAmt too large vs balanceIn"); // < 10% (much stricter in practice) - // assertLt(safeOutAmt, bOut / 10, "safeOutAmt too large vs balanceOut"); // < 10% + // Effective (chosen) lane params + poolDetails.noiseThresholdPercentage9 = locals.thr; + poolDetails.noiseCapDeviationPercentage9 = locals.cap; + poolDetails.noiseMaxSurgeFee9 = locals.maxp; - // p.kind = SwapKind.EXACT_IN; - // p.amountGivenScaled18 = safeInAmt; + // Different ARB lane params so wrong-lane usage wouldn’t accidentally match + poolDetails.arbThresholdPercentage9 = locals.thr + 1; + poolDetails.arbCapDeviationPercentage9 = locals.cap - 1; + poolDetails.arbMaxSurgeFee9 = locals.maxp + 1; - // vm.expectRevert(HyperSpotPricePrecompile.SpotPriceIsZero.selector); - // hook.onComputeDynamicSwapFeePercentage(p, address(pool), STATIC_SWAP_FEE); + (, locals.feeIn) = mock.ComputeSurgeFee( + pIn, + poolDetails, + STATIC_SWAP_FEE, + locals.w, + 0, + locals.pxOut.divDown(locals.pxIn) + ); - // p.kind = SwapKind.EXACT_OUT; - // p.amountGivenScaled18 = safeOutAmt; + // EXACT_OUT + PoolSwapParams memory pOut = _createPoolSwapParams(SwapKind.EXACT_OUT, locals.b, locals.i, locals.j, 0); - // vm.expectRevert(HyperSpotPricePrecompile.SpotPriceIsZero.selector); - // hook.onComputeDynamicSwapFeePercentage(p, address(pool), STATIC_SWAP_FEE); - // } + (, locals.feeOut) = mock.ComputeSurgeFee( + pOut, + poolDetails, + STATIC_SWAP_FEE, + locals.w, + 0, + locals.pxOut.divDown(locals.pxIn) + ); + + assertEq(locals.feeIn, locals.feeOut, "with equal lane params, kind should not change math result"); + } + + function testFuzz_view_missingPrices_reverts(uint8 nSeed, uint256 /* wSeed */, uint256 bSeed, uint8 iSeed) public { + // --- Register pool and adapt to its actual token count --- + uint8 nTarget = uint8(bound(nSeed, 2, 8)); + _registerBasePoolWithN(nTarget); + + uint256[] memory weights = WeightedPool(address(pool)).getNormalizedWeights(); + uint256 m = weights.length; + assertGe(m, 2, "pool must have at least 2 tokens"); + + // --- Random non-zero balances of exact pool length --- + uint256[] memory b = fee_balances(uint8(m), bSeed); + + // --- Pick a valid distinct pair (i != j) --- + uint256 i = uint256(bound(iSeed, 0, m - 1)); + uint256 j = (i + 1) % m; + + // --- Build base swap params template with those balances --- + uint256 bIn = b[i]; + uint256 bOut = b[j]; + + uint256 safeInAmt = bIn / 1e6; + if (safeInAmt == 0) safeInAmt = 1; + uint256 safeOutAmt = bOut / 1e6; + if (safeOutAmt == 0) safeOutAmt = 1; + + // Sanity: amounts are indeed tiny relative to balances to avoid accidental reverts + // (these checks also self-document the invariant we rely on) + assertLt(safeInAmt, bIn / 10, "safeInAmt too large vs balanceIn"); // < 10% (much stricter in practice) + assertLt(safeOutAmt, bOut / 10, "safeOutAmt too large vs balanceOut"); // < 10% + + PoolSwapParams memory p = _createPoolSwapParams(SwapKind.EXACT_IN, b, uint8(i), uint8(j), safeInAmt); + + vm.expectRevert(HyperSpotPricePrecompile.SpotPriceIsZero.selector); + hook.onComputeDynamicSwapFeePercentage(p, address(pool), STATIC_SWAP_FEE); + + p.kind = SwapKind.EXACT_OUT; + p.amountGivenScaled18 = safeOutAmt; + + vm.expectRevert(HyperSpotPricePrecompile.SpotPriceIsZero.selector); + hook.onComputeDynamicSwapFeePercentage(p, address(pool), STATIC_SWAP_FEE); + } // function testFuzz_view_readsLaneParams_reverts_onSafePath(uint8 nSeed) public { // uint8 n = uint8(bound(nSeed, 2, 8)); From d9a181b77f3de44b125a2659fb1c988f39158264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Mon, 15 Sep 2025 12:17:10 -0300 Subject: [PATCH 11/28] Fix test - WIP --- .../test/foundry/HyperSurgeFee.t.sol | 367 +++++++----------- 1 file changed, 147 insertions(+), 220 deletions(-) diff --git a/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol b/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol index 01ba5188..6724ea62 100644 --- a/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol +++ b/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol @@ -1072,223 +1072,154 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo hook.onComputeDynamicSwapFeePercentage(p, address(pool), STATIC_SWAP_FEE); } - // function testFuzz_view_readsLaneParams_reverts_onSafePath(uint8 nSeed) public { - // uint8 n = uint8(bound(nSeed, 2, 8)); - // _registerBasePoolWithN(n); + function testFuzz_view_readsLaneParams_reverts_onSafePath(uint8 nSeed) public { + uint8 n = uint8(bound(nSeed, 2, 8)); + _registerBasePoolWithN(n); - // // Diverge NOISE and ARB lane params (authorized admin) - // vm.startPrank(admin); - // hook.setSurgeThresholdPercentage(address(pool), 5_000_000 * 1e9, IHyperSurgeHook.TradeType.NOISE); // 0.5% - // hook.setCapDeviationPercentage(address(pool), 400_000_000 * 1e9, IHyperSurgeHook.TradeType.NOISE); // 40% - // hook.setMaxSurgeFeePercentage(address(pool), 25_000_000 * 1e9, IHyperSurgeHook.TradeType.NOISE); // 2.5% - - // hook.setSurgeThresholdPercentage(address(pool), 1_000_000 * 1e9, IHyperSurgeHook.TradeType.ARBITRAGE); // 0.1% - // hook.setCapDeviationPercentage(address(pool), 300_000_000 * 1e9, IHyperSurgeHook.TradeType.ARBITRAGE); // 30% - // hook.setMaxSurgeFeePercentage(address(pool), 50_000_000 * 1e9, IHyperSurgeHook.TradeType.ARBITRAGE); // 5% - // vm.stopPrank(); - - // // Adapt to the pool’s true size to avoid OOB / shape mismatches - // uint256[] memory weights = WeightedPool(address(pool)).getNormalizedWeights(); - // uint256 m = weights.length; - // assertGe(m, 2, "pool must have at least 2 tokens"); - - // // Build non-zero balances of correct length m - // uint256[] memory balances = new uint256[](m); - // for (uint256 k = 0; k < m; ++k) { - // balances[k] = 1e24 + k; - // } - - // PoolSwapParams memory p; - // p.amountGivenScaled18 = 1e18; // non-zero trade amount - // p.balancesScaled18 = balances; - // p.indexIn = 0; - // p.indexOut = (m > 1) ? 1 : 0; - - // // EXACT_IN: either revert or static fee (but never a computed dynamic fee) - // p.kind = SwapKind.EXACT_IN; - // vm.expectRevert(HyperSpotPricePrecompile.SpotPriceIsZero.selector); - // hook.onComputeDynamicSwapFeePercentage(p, address(pool), STATIC_SWAP_FEE); - - // // EXACT_OUT: same invariant - // p.kind = SwapKind.EXACT_OUT; - // vm.expectRevert(HyperSpotPricePrecompile.SpotPriceIsZero.selector); - // hook.onComputeDynamicSwapFeePercentage(p, address(pool), STATIC_SWAP_FEE); - // } - - // struct DeviationEqualsThreshold { - // uint256 staticFee; - // uint256 maxFee; - // uint32 thr9; - // uint32 cap9; - // uint32 max9; - // uint256 E; - // uint256 thr; - // uint256 fee; - // } - - // /// 1) deviation == threshold => returns static fee (boundary counted as "inside") - // // function test_cfg_fee_static_at_threshold_usingMockWrapper() public view { - // // DeviationEqualsThreshold memory locals; - - // // locals.staticFee = 30e14; // 30 bps = 0.003 * 1e18 - // // locals.maxFee = 120e14; // 120 bps - - // // // 9 lane params (contract upscales to 18dp) - // // locals.thr9 = 100_000_000; // 10% - // // locals.cap9 = 500_000_000; // 50% - // // locals.max9 = uint32(locals.maxFee / 1e9); - - // // HyperSurgeHookMock.ComputeSurgeFeeLocals memory computeLocals; - // // computeLocals.pxIn = 1e18; - // // computeLocals.pxOut = 10e18; // external price E = 10 - - // // // set both lanes the same (lane choice irrelevant for this edge) - // // computeLocals.poolDetails.noiseThresholdPercentage9 = locals.thr9; - // // computeLocals.poolDetails.noiseCapDeviationPercentage9 = locals.cap9; - // // computeLocals.poolDetails.noiseMaxSurgeFee9 = locals.max9; - // // computeLocals.poolDetails.arbThresholdPercentage9 = locals.thr9; - // // computeLocals.poolDetails.arbCapDeviationPercentage9 = locals.cap9; - // // computeLocals.poolDetails.arbMaxSurgeFee9 = locals.max9; - - // // PoolSwapParams memory p; - // // p.kind = SwapKind.EXACT_IN; - // // p.amountGivenScaled18 = 0; - - // // locals.E = 10e18; - // // locals.thr = uint256(locals.thr9) * 1e9; // 18dp - - // // locals.fee = _feeAtDeviation(computeLocals, p, locals.staticFee, locals.E, locals.thr); - // // assertEq(locals.fee, locals.staticFee, "fee must equal static when deviation == threshold"); - // // } - - // struct justAboveThreshold { - // uint256 staticFee; - // uint256 maxFee; - // uint32 thr9; - // uint32 cap9; - // uint32 max9; - // uint256 E; - // uint256 thr; - // uint256 cap; - // uint256 dev; - // uint256 span; - // uint256 ramp; - // uint256 expected; - // } + // Diverge NOISE and ARB lane params (authorized admin) + vm.startPrank(admin); + hook.setSurgeThresholdPercentage(address(pool), 5_000_000 * 1e9, IHyperSurgeHook.TradeType.NOISE); // 0.5% + hook.setCapDeviationPercentage(address(pool), 400_000_000 * 1e9, IHyperSurgeHook.TradeType.NOISE); // 40% + hook.setMaxSurgeFeePercentage(address(pool), 25_000_000 * 1e9, IHyperSurgeHook.TradeType.NOISE); // 2.5% - // // function test_cfg_fee_minimalRamp_just_above_threshold() public view { - // // justAboveThreshold memory locals; + hook.setSurgeThresholdPercentage(address(pool), 1_000_000 * 1e9, IHyperSurgeHook.TradeType.ARBITRAGE); // 0.1% + hook.setCapDeviationPercentage(address(pool), 300_000_000 * 1e9, IHyperSurgeHook.TradeType.ARBITRAGE); // 30% + hook.setMaxSurgeFeePercentage(address(pool), 50_000_000 * 1e9, IHyperSurgeHook.TradeType.ARBITRAGE); // 5% + vm.stopPrank(); - // // locals.staticFee = 30e14; // 30 bps - // // locals.maxFee = 120e14; // 120 bps + // Adapt to the pool’s true size to avoid OOB / shape mismatches + uint256[] memory weights = WeightedPool(address(pool)).getNormalizedWeights(); + uint256 m = weights.length; + assertGe(m, 2, "pool must have at least 2 tokens"); - // // locals.thr9 = 100_000_000; // 10% - // // locals.cap9 = 500_000_000; // 50% - // // locals.max9 = uint32(locals.maxFee / 1e9); + // Build non-zero balances of correct length m + uint256[] memory balances = new uint256[](m); + for (uint256 k = 0; k < m; ++k) { + balances[k] = 1e24 + k; + } - // // HyperSurgeHookMock.ComputeSurgeFeeLocals memory computeLocals; - // // computeLocals.pxIn = 1e18; - // // computeLocals.pxOut = 10e18; + PoolSwapParams memory p = _createPoolSwapParams(SwapKind.EXACT_IN, balances, 0, 1, 1e18); - // // computeLocals.poolDetails.noiseThresholdPercentage9 = locals.thr9; - // // computeLocals.poolDetails.noiseCapDeviationPercentage9 = locals.cap9; - // // computeLocals.poolDetails.noiseMaxSurgeFee9 = locals.max9; - // // computeLocals.poolDetails.arbThresholdPercentage9 = locals.thr9; - // // computeLocals.poolDetails.arbCapDeviationPercentage9 = locals.cap9; - // // computeLocals.poolDetails.arbMaxSurgeFee9 = locals.max9; + // EXACT_IN: either revert or static fee (but never a computed dynamic fee) + vm.expectRevert(HyperSpotPricePrecompile.SpotPriceIsZero.selector); + hook.onComputeDynamicSwapFeePercentage(p, address(pool), STATIC_SWAP_FEE); - // // PoolSwapParams memory p; - // // p.kind = SwapKind.EXACT_IN; - // // p.amountGivenScaled18 = 0; + // EXACT_OUT: same invariant + p.kind = SwapKind.EXACT_OUT; + vm.expectRevert(HyperSpotPricePrecompile.SpotPriceIsZero.selector); + hook.onComputeDynamicSwapFeePercentage(p, address(pool), STATIC_SWAP_FEE); + } - // // locals.E = 10e18; - // // locals.thr = uint256(locals.thr9) * 1e9; - // // locals.cap = uint256(locals.cap9) * 1e9; - // // locals.dev = (uint256(locals.thr9) + 1) * 1e9; // smallest 18dp step above threshold + /// 1) deviation == threshold => returns static fee (boundary counted as "inside") + function test_cfg_fee_static_at_threshold_usingMockWrapper() public view { + uint256 staticFee = 0.3e16; // 0.3% + uint256 maxFee = 1.2e16; // 1.2% - // // uint256 fee = _feeAtDeviation(computeLocals, p, locals.staticFee, locals.E, locals.dev); + // 9 lane params (contract upscales to 18dp) + uint32 thr9 = 100_000_000; // 10% + uint32 cap9 = 500_000_000; // 50% + uint32 max9 = uint32(maxFee / 1e9); - // // // Expected: static + (max - static) * (dev - thr) / (cap - thr) (div-down) - // // locals.span = locals.cap - locals.thr; - // // locals.ramp = ((locals.maxFee - locals.staticFee) * (locals.dev - locals.thr)) / locals.span; - // // locals.expected = locals.staticFee + locals.ramp; + // set both lanes the same (lane choice irrelevant for this edge) + HyperSurgeHook.PoolDetails memory poolDetails; + poolDetails.noiseThresholdPercentage9 = thr9; + poolDetails.noiseCapDeviationPercentage9 = cap9; + poolDetails.noiseMaxSurgeFee9 = max9; + poolDetails.arbThresholdPercentage9 = thr9; + poolDetails.arbCapDeviationPercentage9 = cap9; + poolDetails.arbMaxSurgeFee9 = max9; + poolDetails.numTokens = 2; - // // assertEq(fee, locals.expected, "minimal ramp just above threshold"); - // // assertGt(fee, locals.staticFee, "fee > static just above threshold"); - // // assertLt(fee, locals.maxFee, "fee < max when deviation < cap"); - // // } + PoolSwapParams memory p; + p.kind = SwapKind.EXACT_IN; + p.indexIn = 0; + p.indexOut = 1; + p.amountGivenScaled18 = 0; - // struct MaxEqualsStatic { - // uint256 staticFee; - // uint256 maxFee; - // uint32 thr9; - // uint32 cap9; - // uint32 max9; - // uint256 E; - // uint256 thr; - // uint256 cap; - // uint256 devAtThr; - // uint256 devMid; - // uint256 devAtCap; - // uint256 devBeyond; - // } + // External price (priceTokenOut / priceTokenIn). + uint256 E = 10e18; - // /// 3) degenerate: max == static => always static (even outside threshold) - // function test_cfg_fee_degenerateRamp_max_equals_static() public view { - // MaxEqualsStatic memory locals; + uint256 fee = _feeAtDeviation(p, poolDetails, staticFee, E, _convertTo18Decimals(thr9)); + assertEq(fee, staticFee, "fee must equal static when deviation == threshold"); + } - // locals.staticFee = 45e14; // 45 bps - // locals.maxFee = locals.staticFee; + function test_cfg_fee_minimalRamp_just_above_threshold() public view { + uint256 staticFee = 0.3e16; // 0.3% + uint256 maxFee = 1.2e16; // 1.2% - // locals.thr9 = 50_000_000; // 5% - // locals.cap9 = 250_000_000; // 25% - // locals.max9 = uint32(locals.maxFee / 1e9); + uint32 thr9 = 100_000_000; // 10% + uint32 cap9 = 500_000_000; // 50% + uint32 max9 = uint32(maxFee / 1e9); - // HyperSurgeHookMock.ComputeSurgeFeeLocals memory computeLocals; - // computeLocals.pxIn = 1e18; - // computeLocals.pxOut = 10e18; + HyperSurgeHook.PoolDetails memory poolDetails; + poolDetails.noiseThresholdPercentage9 = thr9; + poolDetails.noiseCapDeviationPercentage9 = cap9; + poolDetails.noiseMaxSurgeFee9 = max9; + poolDetails.arbThresholdPercentage9 = thr9; + poolDetails.arbCapDeviationPercentage9 = cap9; + poolDetails.arbMaxSurgeFee9 = max9; + poolDetails.numTokens = 2; - // computeLocals.poolDetails.noiseThresholdPercentage9 = locals.thr9; - // computeLocals.poolDetails.noiseCapDeviationPercentage9 = locals.cap9; - // computeLocals.poolDetails.noiseMaxSurgeFee9 = locals.max9; - // computeLocals.poolDetails.arbThresholdPercentage9 = locals.thr9; - // computeLocals.poolDetails.arbCapDeviationPercentage9 = locals.cap9; - // computeLocals.poolDetails.arbMaxSurgeFee9 = locals.max9; + PoolSwapParams memory p; + p.kind = SwapKind.EXACT_IN; + p.amountGivenScaled18 = 0; + p.indexIn = 0; + p.indexOut = 1; + + uint256 oraclePrice = 10e18; + uint256 deviation = _convertTo18Decimals(thr9 + 1); // smallest 9dp step above threshold + uint256 fee = _feeAtDeviation(p, poolDetails, staticFee, oraclePrice, deviation); + + // Expected: static + (max - static) * (dev - thr) / (cap - thr) (div-down) + uint256 span = _convertTo18Decimals(cap9 - thr9); + uint256 ramp = ((maxFee - staticFee) * (deviation - _convertTo18Decimals(thr9))) / span; + uint256 expected = staticFee + ramp; + + assertEq(fee, expected, "minimal ramp just above threshold"); + assertGt(fee, staticFee, "fee > static just above threshold"); + assertLt(fee, maxFee, "fee < max when deviation < cap"); + } - // PoolSwapParams memory p; - // p.kind = SwapKind.EXACT_IN; - // p.amountGivenScaled18 = 0; + /// 3) degenerate: max == static => always static (even outside threshold) + function test_cfg_fee_degenerateRamp_max_equals_static() public view { + uint256 staticFee = 0.45e16; // 0.45% + uint256 maxFee = staticFee; - // locals.E = 10e18; - // locals.thr = uint256(locals.thr9) * 1e9; - // locals.cap = uint256(locals.cap9) * 1e9; + uint32 thr9 = 50_000_000; // 5% + uint32 cap9 = 250_000_000; // 25% + uint32 max9 = uint32(maxFee / 1e9); - // locals.devAtThr = locals.thr; - // locals.devMid = locals.thr + (locals.cap - locals.thr) / 2; - // locals.devAtCap = locals.cap; - // locals.devBeyond = locals.cap + 12345; + HyperSurgeHook.PoolDetails memory poolDetails; + poolDetails.noiseThresholdPercentage9 = thr9; + poolDetails.noiseCapDeviationPercentage9 = cap9; + poolDetails.noiseMaxSurgeFee9 = max9; + poolDetails.arbThresholdPercentage9 = thr9; + poolDetails.arbCapDeviationPercentage9 = cap9; + poolDetails.arbMaxSurgeFee9 = max9; - // assertEq( - // _feeAtDeviation(computeLocals, p, locals.staticFee, locals.E, locals.devAtThr), - // locals.staticFee, - // "at thr => static" - // ); - // assertEq( - // _feeAtDeviation(computeLocals, p, locals.staticFee, locals.E, locals.devMid), - // locals.staticFee, - // "mid => static" - // ); - // assertEq( - // _feeAtDeviation(computeLocals, p, locals.staticFee, locals.E, locals.devAtCap), - // locals.staticFee, - // "at cap => static" - // ); - // assertEq( - // _feeAtDeviation(computeLocals, p, locals.staticFee, locals.E, locals.devBeyond), - // locals.staticFee, - // "beyond cap => static" - // ); - // } + PoolSwapParams memory p; + p.kind = SwapKind.EXACT_IN; + p.amountGivenScaled18 = 0; + p.indexIn = 0; + p.indexOut = 1; + + uint256 oraclePrice = 10e18; + uint256 thr = _convertTo18Decimals(thr9); + uint256 cap = _convertTo18Decimals(cap9); + + assertEq(_feeAtDeviation(p, poolDetails, staticFee, oraclePrice, thr), staticFee, "at thr => static"); + assertEq( + _feeAtDeviation(p, poolDetails, staticFee, oraclePrice, thr + (cap - thr) / 2), + staticFee, + "mid => static" + ); + assertEq(_feeAtDeviation(p, poolDetails, staticFee, oraclePrice, cap), staticFee, "at cap => static"); + assertEq( + _feeAtDeviation(p, poolDetails, staticFee, oraclePrice, cap + 12345), + staticFee, + "beyond cap => static" + ); + } // struct MaxBelowStatic { // uint256 staticFee; @@ -2961,30 +2892,26 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo vm.store(HyperTokenInfoPrecompile.TOKEN_INFO_PRECOMPILE_ADDRESS, slot, bytes32(uint256(sz))); } - // function _feeAtDeviation( - // HyperSurgeHookMock.ComputeSurgeFeeLocals memory computeLocals, - // PoolSwapParams memory p, - // uint256 staticFee, - // uint256 extPxE18, - // uint256 deviation18 - // ) internal view returns (uint256) { - // // pool price P = E * (1 + deviation) - // uint256 P = extPxE18 + (extPxE18 * deviation18) / 1e18; - - // // Make poolPx = P using simple weights/balances: - // // poolPx = (bOut * wIn) / (bIn * wOut) - // computeLocals.wIn = 1e18; - // computeLocals.wOut = 1e18; - // computeLocals.bIn = 1e18; - // computeLocals.bOut = P; - - // // Keep deltas zero so poolPx == poolPxBefore (no lane flip due to swap) - // computeLocals.calcAmountScaled18 = 0; - - // (bool ok, uint256 fee) = hook.ComputeSurgeFee(computeLocals, p, staticFee); - // assertTrue(ok, "compute ok"); - // return fee; - // } + function _feeAtDeviation( + PoolSwapParams memory p, + HyperSurgeHook.PoolDetails memory poolDetails, + uint256 staticFee, + uint256 extPxE18, + uint256 deviation18 + ) internal view returns (uint256) { + // pool price P = E * (1 + deviation) + uint256 P = extPxE18 + extPxE18.mulDown(deviation18); + + // Make poolPx = P using simple weights/balances: + // poolPx = (bOut * wIn) / (bIn * wOut) + uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); + p.balancesScaled18 = [uint256(1e18), uint256(P)].toMemoryArray(); + + // Keep deltas zero so poolPx == poolPxBefore (no lane flip due to swap) => calculatedAmount = 0 + (bool ok, uint256 fee) = hook.ComputeSurgeFee(p, poolDetails, staticFee, weights, 0, extPxE18); + assertTrue(ok, "compute ok"); + return fee; + } function fee_mulDown(uint256 a, uint256 b) internal pure returns (uint256) { return (a * b) / FEE_ONE; From 4b03180a320a8052bef5a242ef157ef424d15001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Mon, 15 Sep 2025 12:53:06 -0300 Subject: [PATCH 12/28] Fix tests - WIP --- .../test/foundry/HyperSurgeFee.t.sol | 451 +++++++----------- 1 file changed, 170 insertions(+), 281 deletions(-) diff --git a/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol b/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol index 6724ea62..8c3ff82e 100644 --- a/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol +++ b/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol @@ -310,7 +310,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo // bound amountIn to strictly inside the 30% guard params.MAX_RATIO = 30e16; // 30% in 1e18 - params.maxIn = (balances[p.indexIn] * params.MAX_RATIO) / 1e18; + params.maxIn = balances[p.indexIn].mulDown(params.MAX_RATIO); if (params.maxIn > 0) params.maxIn -= 1; p.amountGivenScaled18 = bound(amtSeed, 1, params.maxIn == 0 ? 1 : params.maxIn); @@ -400,7 +400,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo // bound amountOut to strictly inside the 30% guard params.MAX_RATIO = 30e16; // 30% - params.maxIn = (balances[p.indexOut] * params.MAX_RATIO) / 1e18; + params.maxIn = balances[p.indexOut].mulDown(params.MAX_RATIO); if (params.maxIn > 0) { params.maxIn -= 1; } @@ -519,7 +519,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo p.indexOut = locals.indexOut; locals.maxRatio = 30e16; // 30% in 1e18 basis - locals.maxIn = (locals.balances[p.indexIn] * locals.maxRatio) / 1e18; + locals.maxIn = locals.balances[p.indexIn].mulDown(locals.maxRatio); if (locals.maxIn > 0) { locals.maxIn -= 1; } @@ -540,8 +540,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo uint256 P; uint256 capDev; uint256 D; - uint256 pxIn; - uint256 pxOut; + uint256 oraclePrice; uint256 feeA; uint256 expected; bool ok; @@ -573,7 +572,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo locals.capDev = _convertTo18Decimals(poolDetails.arbCapDeviationPercentage9); locals.D = uint256(keccak256(abi.encode(dSeed))) % (locals.capDev + locals.capDev / 2 + 1); - (locals.pxIn, locals.pxOut) = fee_localsForDeviation(locals.P, locals.D); + (locals.oraclePrice) = fee_computeOraclePriceForDeviation(locals.P, locals.D); HyperSurgeHookMock mock = new HyperSurgeHookMock( IVault(vault), @@ -589,14 +588,13 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo STATIC_SWAP_FEE, locals.w, 0, - locals.pxOut.divDown(locals.pxIn) + locals.oraclePrice ); assertTrue(locals.ok, "compute must succeed"); locals.expected = fee_expectedFeeWithParams( locals.P, - locals.pxIn, - locals.pxOut, + locals.oraclePrice, STATIC_SWAP_FEE, poolDetails.arbThresholdPercentage9, poolDetails.arbCapDeviationPercentage9, @@ -615,10 +613,8 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo uint256 capDev; uint256 D1; uint256 D2; - uint256 pxIn1; - uint256 pxOut1; - uint256 pxIn2; - uint256 pxOut2; + uint256 oraclePrice1; + uint256 oraclePrice2; uint256 fee1; uint256 fee2; } @@ -653,8 +649,8 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo locals.D2 = uint256(keccak256(abi.encode(d2))) % (locals.capDev + locals.capDev / 2 + 1); if (locals.D2 < locals.D1) (locals.D1, locals.D2) = (locals.D2, locals.D1); - (locals.pxIn1, locals.pxOut1) = fee_localsForDeviation(locals.P, locals.D1); - (locals.pxIn2, locals.pxOut2) = fee_localsForDeviation(locals.P, locals.D2); + (locals.oraclePrice1) = fee_computeOraclePriceForDeviation(locals.P, locals.D1); + (locals.oraclePrice2) = fee_computeOraclePriceForDeviation(locals.P, locals.D2); HyperSurgeHookMock mock = new HyperSurgeHookMock( IVault(vault), @@ -664,22 +660,8 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo "fee-mono" ); - (, locals.fee1) = mock.ComputeSurgeFee( - p, - poolDetails, - STATIC_SWAP_FEE, - locals.w, - 0, - locals.pxOut1.divDown(locals.pxIn1) - ); - (, locals.fee2) = mock.ComputeSurgeFee( - p, - poolDetails, - STATIC_SWAP_FEE, - locals.w, - 0, - locals.pxOut2.divDown(locals.pxIn2) - ); + (, locals.fee1) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, locals.w, 0, locals.oraclePrice1); + (, locals.fee2) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, locals.w, 0, locals.oraclePrice2); assertLe(locals.fee1, locals.fee2, "fee must be non-decreasing in deviation"); } @@ -694,8 +676,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo uint256 capDev; uint256 scaleSeed; uint256 D; - uint256 pxIn; - uint256 pxOut; + uint256 oraclePrice; uint256 bMin; uint256 baseAmt; uint256 fee1; @@ -732,7 +713,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo locals.D = uint256(keccak256(abi.encode(dSeed))) % (locals.capDev + locals.capDev / 2 + 1); // External price inputs that produce the desired deviation - (locals.pxIn, locals.pxOut) = fee_localsForDeviation(locals.P, locals.D); + (locals.oraclePrice) = fee_computeOraclePriceForDeviation(locals.P, locals.D); // Scale factor k and a base amount small relative to balances to avoid overflow locals.scaleSeed = 1 + (uint256(scaleSeed) % 1_000_000_000); // k in [1 .. 1e9] @@ -770,23 +751,8 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo locals.baseAmt * locals.scaleSeed ); - (, locals.fee1) = mock.ComputeSurgeFee( - p1, - poolDetails, - STATIC_SWAP_FEE, - locals.w, - 0, - locals.pxOut.divDown(locals.pxIn) - ); - - (, locals.fee2) = mock.ComputeSurgeFee( - p2, - poolDetails, - STATIC_SWAP_FEE, - locals.w, - 0, - locals.pxOut.divDown(locals.pxIn) - ); + (, locals.fee1) = mock.ComputeSurgeFee(p1, poolDetails, STATIC_SWAP_FEE, locals.w, 0, locals.oraclePrice); + (, locals.fee2) = mock.ComputeSurgeFee(p2, poolDetails, STATIC_SWAP_FEE, locals.w, 0, locals.oraclePrice); // --- Branch-aware assertion (inferred) --- uint256 strictTol = 100; // knife-edge rounding flips @@ -823,8 +789,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo uint32 cap; uint32 maxp; uint256 D; - uint256 pxIn; - uint256 pxOut; + uint256 oraclePrice; uint256 feeA; uint256 feeB; uint256 feeC; @@ -857,7 +822,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo // Below threshold locals.D = _convertTo18Decimals(locals.thr) - 1; - (locals.pxIn, locals.pxOut) = fee_localsForDeviation(locals.P, locals.D); + (locals.oraclePrice) = fee_computeOraclePriceForDeviation(locals.P, locals.D); HyperSurgeHook.PoolDetails memory poolDetails; poolDetails.numTokens = 2; @@ -872,32 +837,17 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo poolDetails.noiseCapDeviationPercentage9 = locals.cap - 1; poolDetails.noiseMaxSurgeFee9 = locals.maxp + 1; - (, locals.feeA) = mock.ComputeSurgeFee( - p, - poolDetails, - STATIC_SWAP_FEE, - locals.w, - 0, - locals.pxOut.divDown(locals.pxIn) - ); + (, locals.feeA) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, locals.w, 0, locals.oraclePrice); assertEq(locals.feeA, STATIC_SWAP_FEE, "below threshold means static fee"); locals.D = (_convertTo18Decimals(locals.thr) + _convertTo18Decimals(locals.cap)) / 2; - (locals.pxIn, locals.pxOut) = fee_localsForDeviation(locals.P, locals.D); + (locals.oraclePrice) = fee_computeOraclePriceForDeviation(locals.P, locals.D); - (, locals.feeB) = mock.ComputeSurgeFee( - p, - poolDetails, - STATIC_SWAP_FEE, - locals.w, - 0, - locals.pxOut.divDown(locals.pxIn) - ); + (, locals.feeB) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, locals.w, 0, locals.oraclePrice); uint256 expected = fee_expectedFeeWithParams( locals.P, - locals.pxIn, - locals.pxOut, + locals.oraclePrice, STATIC_SWAP_FEE, locals.thr, locals.cap, @@ -908,29 +858,15 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo // At cap and above cap uint256 Dcap = _convertTo18Decimals(locals.cap); - (locals.pxIn, locals.pxOut) = fee_localsForDeviation(locals.P, Dcap); + (locals.oraclePrice) = fee_computeOraclePriceForDeviation(locals.P, Dcap); - (, locals.feeC) = mock.ComputeSurgeFee( - p, - poolDetails, - STATIC_SWAP_FEE, - locals.w, - 0, - locals.pxOut.divDown(locals.pxIn) - ); + (, locals.feeC) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, locals.w, 0, locals.oraclePrice); assertEq(locals.feeC, _convertTo18Decimals(locals.maxp), "at cap means max fee"); uint256 Dbeyond = Dcap + 1; - (locals.pxIn, locals.pxOut) = fee_localsForDeviation(locals.P, Dbeyond); + (locals.oraclePrice) = fee_computeOraclePriceForDeviation(locals.P, Dbeyond); - (, locals.feeD) = mock.ComputeSurgeFee( - p, - poolDetails, - STATIC_SWAP_FEE, - locals.w, - 0, - locals.pxOut.divDown(locals.pxIn) - ); + (, locals.feeD) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, locals.w, 0, locals.oraclePrice); assertEq(locals.feeD, _convertTo18Decimals(locals.maxp), "above cap means clamped to max fee"); } @@ -946,8 +882,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo uint256 P; uint256 capDev; uint256 D; - uint256 pxIn; - uint256 pxOut; + uint256 oraclePrice; uint256 feeIn; uint256 feeOut; } @@ -979,7 +914,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo locals.capDev = _convertTo18Decimals(locals.cap); locals.D = uint256(keccak256(abi.encode(dSeed))) % (locals.capDev + locals.capDev / 2 + 1); - (locals.pxIn, locals.pxOut) = fee_localsForDeviation(locals.P, locals.D); + (locals.oraclePrice) = fee_computeOraclePriceForDeviation(locals.P, locals.D); HyperSurgeHookMock mock = new HyperSurgeHookMock( IVault(vault), @@ -1006,26 +941,12 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo poolDetails.arbCapDeviationPercentage9 = locals.cap - 1; poolDetails.arbMaxSurgeFee9 = locals.maxp + 1; - (, locals.feeIn) = mock.ComputeSurgeFee( - pIn, - poolDetails, - STATIC_SWAP_FEE, - locals.w, - 0, - locals.pxOut.divDown(locals.pxIn) - ); + (, locals.feeIn) = mock.ComputeSurgeFee(pIn, poolDetails, STATIC_SWAP_FEE, locals.w, 0, locals.oraclePrice); // EXACT_OUT PoolSwapParams memory pOut = _createPoolSwapParams(SwapKind.EXACT_OUT, locals.b, locals.i, locals.j, 0); - (, locals.feeOut) = mock.ComputeSurgeFee( - pOut, - poolDetails, - STATIC_SWAP_FEE, - locals.w, - 0, - locals.pxOut.divDown(locals.pxIn) - ); + (, locals.feeOut) = mock.ComputeSurgeFee(pOut, poolDetails, STATIC_SWAP_FEE, locals.w, 0, locals.oraclePrice); assertEq(locals.feeIn, locals.feeOut, "with equal lane params, kind should not change math result"); } @@ -1221,184 +1142,155 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo ); } - // struct MaxBelowStatic { - // uint256 staticFee; - // uint256 maxFee; - // uint32 thr9; - // uint32 cap9; - // uint32 max9; - // uint256 E; - // uint256 thr; - // uint256 cap; - // uint256 devMid; - // uint256 feeMid; - // uint256 span; - // uint256 ramp; - // uint256 expected; - // } + function test_fee_misconfig_maxBelowStatic_usingMockWrapper() public { + // Misconfig: max < static + uint256 staticFee = 0.8e16; // 0.8% + uint256 maxFee = 0.2e16; // 0.2% -> lower than static + uint32 thr9 = 100_000_000; // 10% + uint32 cap9 = 300_000_000; // 30% + uint32 max9 = uint32(maxFee / 1e9); - // function test_fee_misconfig_maxBelowStatic_usingMockWrapper() public { - // MaxBelowStatic memory locals; + // Local mock (don’t rely on global `hook`) + HyperSurgeHookMock mock = new HyperSurgeHookMock( + IVault(vault), + _convertTo18Decimals(max9), + _convertTo18Decimals(thr9), + _convertTo18Decimals(cap9), + "misconfig-maxBelowStatic" + ); - // // Misconfig: max < static - // locals.staticFee = 80e14; // 80 bps (1e18 scale) - // locals.maxFee = 20e14; // 20 bps (1e18 scale) -> lower than static - // locals.thr9 = 100_000_000; // 10% in 1e9 - // locals.cap9 = 300_000_000; // 30% in 1e9 - // locals.max9 = uint32(locals.maxFee / 1e9); + uint256 oraclePrice = 10e18; - // // Local mock (don’t rely on global `hook`) - // HyperSurgeHookMock mock = new HyperSurgeHookMock( - // IVault(vault), - // _convertTo18Decimals(locals.max9), - // _convertTo18Decimals(locals.thr9), - // _convertTo18Decimals(locals.cap9), - // "misconfig-maxBelowStatic" - // ); + // Set BOTH lanes to the same (misconfigured) params so lane choice doesn't matter here. + HyperSurgeHook.PoolDetails memory poolDetails; + poolDetails.noiseThresholdPercentage9 = thr9; + poolDetails.noiseCapDeviationPercentage9 = cap9; + poolDetails.noiseMaxSurgeFee9 = max9; + poolDetails.arbThresholdPercentage9 = thr9; + poolDetails.arbCapDeviationPercentage9 = cap9; + poolDetails.arbMaxSurgeFee9 = max9; + poolDetails.numTokens = 2; - // // Base inputs used for both sub-tests - // locals.E = 10e18; // external price - // locals.thr = uint256(locals.thr9) * 1e9; // 18dp threshold - // locals.cap = uint256(locals.cap9) * 1e9; // 18dp cap - - // HyperSurgeHookMock.ComputeSurgeFeeLocals memory base; - // base.pxIn = 1e18; - // base.pxOut = locals.E; - - // // Set BOTH lanes to the same (misconfigured) params so lane choice doesn't matter here. - // base.poolDetails.noiseThresholdPercentage9 = locals.thr9; - // base.poolDetails.noiseCapDeviationPercentage9 = locals.cap9; - // base.poolDetails.noiseMaxSurgeFee9 = locals.max9; - // base.poolDetails.arbThresholdPercentage9 = locals.thr9; - // base.poolDetails.arbCapDeviationPercentage9 = locals.cap9; - // base.poolDetails.arbMaxSurgeFee9 = locals.max9; - - // PoolSwapParams memory p; - // p.kind = SwapKind.EXACT_IN; - // p.amountGivenScaled18 = 0; // keep balances-based price exact - // p.balancesScaled18 = new uint256[](2); - // p.balancesScaled18[0] = 1e18; - // p.balancesScaled18[1] = locals.E; - - // // Reused working struct - // HyperSurgeHookMock.ComputeSurgeFeeLocals memory T; - - // // ---------- (a) dev >= cap -> revert (underflow in mock ramp) ---------- - // uint256 dev = locals.cap + 999; // strictly beyond cap - // uint256 P = locals.E + (locals.E * dev) / 1e18; // P = E * (1 + dev) - // T = base; - // T.wIn = 1e18; - // T.wOut = 1e18; - // T.bIn = 1e18; - // T.bOut = P; - // T.calcAmountScaled18 = 0; - - // vm.expectRevert(stdError.arithmeticError); - // mock.ComputeSurgeFee(T, p, locals.staticFee); - - // // ---------- (b) thr < dev < cap -> revert (underflow in mock ramp) ---------- - // dev = locals.thr + (locals.cap - locals.thr) / 3; // strictly between thr & cap - // P = locals.E + (locals.E * dev) / 1e18; - // T = base; - // T.wIn = 1e18; - // T.wOut = 1e18; - // T.bIn = 1e18; - // T.bOut = P; - // T.calcAmountScaled18 = 0; - - // vm.expectRevert(stdError.arithmeticError); - // mock.ComputeSurgeFee(T, p, locals.staticFee); - // } + // ---------- (a) dev >= cap -> revert (underflow in mock ramp) ---------- + uint256 deviationAboveCap = _convertTo18Decimals(cap9) + 999; // strictly beyond cap - // struct OutsideDynamicAfterLocals { - // uint256 E; - // uint32 noiseThr9; - // uint32 noiseCap9; - // uint32 noiseMax9; - // uint32 arbThr9; - // uint32 arbCap9; - // uint32 arbMax9; - // uint256 thr; - // uint256 cap; - // uint256 deviationBefore; - // uint256 price_before; - // uint256 price_after; - // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - // PoolSwapParams p; - // uint256 expected; - // uint256 dyn; - // } + // `amountGivenScaled18 = 0` to keep balances-based price exact + PoolSwapParams memory p = _createPoolSwapParams( + SwapKind.EXACT_IN, + [FixedPoint.ONE, oraclePrice.mulDown(FixedPoint.ONE + deviationAboveCap)].toMemoryArray(), + 0, + 1, + 0 + ); + uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); - // /// 1) Noise: starts outside threshold, deviation worsens → NOISE lane, dynamic fee based on **after** deviation. - // function testFuzz_logic_noise_worsens_outside_dynamic_after( - // uint256 eSeed, - // uint32 noiseThrSeed, - // uint32 noiseCapSeed, - // uint32 noiseMaxSeed, - // uint64 amtSeed - // ) public { - // OutsideDynamicAfterLocals memory locals; + vm.expectRevert(stdError.arithmeticError); + mock.ComputeSurgeFee(p, poolDetails, staticFee, weights, 0, oraclePrice); - // // --- Fuzz + bounds --- - // locals.E = bound(eSeed, 1e16, 1e24); // pxOut - // locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); - // locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); - // locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); - // // ARB lane (unused here, but keep distinct) - // locals.arbThr9 = 1_000_000; - // locals.arbCap9 = 300_000_000; - // locals.arbMax9 = 50_000_000; + // ---------- (b) thr < dev < cap -> revert (underflow in mock ramp) ---------- + uint256 deviationBetweenThrAndCap = _convertTo18Decimals(thr9 + (cap9 - thr9) / 3); // strictly between thr & cap + p = _createPoolSwapParams( + SwapKind.EXACT_IN, + [FixedPoint.ONE, oraclePrice.mulDown(FixedPoint.ONE + deviationBetweenThrAndCap)].toMemoryArray(), + 0, + 1, + 0 + ); - // locals.thr = uint256(locals.noiseThr9) * 1e9; - // locals.cap = uint256(locals.noiseCap9) * 1e9; - // locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; // strictly outside + vm.expectRevert(stdError.arithmeticError); + mock.ComputeSurgeFee(p, poolDetails, staticFee, weights, 0, oraclePrice); + } - // // Start BELOW E: price_before = E * (1 - deviationBefore) - // locals.price_before = locals.E - (locals.E * locals.deviationBefore) / 1e18; + struct OutsideDynamicAfterLocals { + uint256 E; + uint32 noiseThr9; + uint32 noiseCap9; + uint32 noiseMax9; + uint32 arbThr9; + uint32 arbCap9; + uint32 arbMax9; + uint256 thr; + uint256 cap; + uint256 deviationBefore; + uint256 price_before; + uint256 price_after; + uint256 expected; + uint256 dyn; + } - // // Build compute locals + swap that worsens deviation (EXACT_IN; calc=0 → P decreases further) - // locals.comp.wIn = 1e18; - // locals.comp.wOut = 1e18; - // locals.comp.bIn = 1e18; - // locals.comp.bOut = locals.price_before; - // locals.comp.pxIn = 1e18; - // locals.comp.pxOut = locals.E; - // locals.comp.calcAmountScaled18 = 0; - // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; - // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - // locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; - // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + /// 1) Noise: starts outside threshold, deviation worsens → NOISE lane, dynamic fee based on **after** deviation. + function testFuzz_logic_noise_worsens_outside_dynamic_after( + uint256 eSeed, + uint32 noiseThrSeed, + uint32 noiseCapSeed, + uint32 noiseMaxSeed, + uint64 amtSeed + ) public { + OutsideDynamicAfterLocals memory locals; + + // --- Fuzz + bounds --- + uint256 oraclePrice = bound(eSeed, 1e16, 1e24); + locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); + locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); + locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + // ARB lane (unused here, but keep distinct) + locals.arbThr9 = 1_000_000; + locals.arbCap9 = 300_000_000; + locals.arbMax9 = 50_000_000; + + locals.thr = _convertTo18Decimals(locals.noiseThr9); + locals.cap = _convertTo18Decimals(locals.noiseCap9); + locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; // strictly outside + + // Start BELOW E: price_before = E * (1 - deviationBefore) + locals.price_before = oraclePrice - (oraclePrice * locals.deviationBefore) / 1e18; + + // Build compute locals + swap that worsens deviation (EXACT_IN; calc=0 → P decreases further) + uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); + uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.price_before].toMemoryArray(); - // locals.p.kind = SwapKind.EXACT_IN; - // // ensure deviation increases *measurably* in Q18 (avoid 1-wei changes) - // locals.p.amountGivenScaled18 = bound(uint256(amtSeed), 1e9, 5e17); // [1e9, 0.5e18] + HyperSurgeHook.PoolDetails memory poolDetails; + poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + poolDetails.arbThresholdPercentage9 = locals.arbThr9; + poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; + poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + poolDetails.numTokens = 2; - // // Expected (NOISE) uses AFTER deviation: price_after = price_before / (1 + x) - // locals.price_after = (locals.price_before * 1e18) / (1e18 + locals.p.amountGivenScaled18); - // locals.expected = fee_expectedFeeWithParams( - // locals.price_after, - // locals.comp.pxIn, - // locals.comp.pxOut, - // STATIC_SWAP_FEE, - // locals.noiseThr9, - // locals.noiseCap9, - // locals.noiseMax9 - // ); + // ensure deviation increases *measurably* in Q18 (avoid 1-wei changes) + PoolSwapParams memory p = _createPoolSwapParams( + SwapKind.EXACT_IN, + balancesScaled18, + 0, + 1, + bound(uint256(amtSeed), 1e9, 5e17) + ); - // HyperSurgeHookMock mock = new HyperSurgeHookMock( - // IVault(vault), - // _convertTo18Decimals(locals.arbMax9), - // _convertTo18Decimals(locals.arbThr9), - // _convertTo18Decimals(locals.arbCap9), - // "logic-1" - // ); - // (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); + // Expected (NOISE) uses AFTER deviation: price_after = price_before / (1 + x) + locals.price_after = locals.price_before.divDown(FixedPoint.ONE + p.amountGivenScaled18); + locals.expected = fee_expectedFeeWithParams( + locals.price_after, + oraclePrice, + STATIC_SWAP_FEE, + locals.noiseThr9, + locals.noiseCap9, + locals.noiseMax9 + ); - // assertEq(locals.dyn, locals.expected, "noise path must use AFTER deviation for dynamic fee"); - // assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee >= static"); - // } + HyperSurgeHookMock mock = new HyperSurgeHookMock( + IVault(vault), + _convertTo18Decimals(locals.arbMax9), + _convertTo18Decimals(locals.arbThr9), + _convertTo18Decimals(locals.arbCap9), + "logic-1" + ); + + (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, oraclePrice); + + assertEq(locals.dyn, locals.expected, "noise path must use AFTER deviation for dynamic fee"); + assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee >= static"); + } // struct BetterStillOutsideLocals { // uint256 E; @@ -2976,9 +2868,8 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo } // Choose deviation D, then set external px so that extPx = P / (1 + D) - function fee_localsForDeviation(uint256 P, uint256 D) internal pure returns (uint256 pxIn, uint256 pxOut) { - pxIn = FEE_ONE; - pxOut = fee_divDown(P, FEE_ONE + D); + function fee_computeOraclePriceForDeviation(uint256 P, uint256 deviation) internal pure returns (uint256) { + return P.divDown(FixedPoint.ONE + deviation); } function _convertTo18Decimals(uint32 valueScaled9) internal pure returns (uint256) { @@ -2988,15 +2879,13 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo // Expected fee (exact same rounding & clamping as the hook) function fee_expectedFeeWithParams( uint256 poolPx, - uint256 pxIn, - uint256 pxOut, + uint256 oraclePrice, uint256 staticSwapFee, uint32 thresholdPPM9, uint32 capDevPPM9, uint32 maxFeePPM9 ) internal pure returns (uint256) { - uint256 extPx = fee_divDown(pxOut, pxIn); - uint256 deviation = fee_relAbsDiff(poolPx, extPx); + uint256 deviation = fee_relAbsDiff(poolPx, oraclePrice); uint256 threshold = _convertTo18Decimals(thresholdPPM9); uint256 capDev = _convertTo18Decimals(capDevPPM9); From 2aec7a920a102cc0b9ff23a739b9435c4afc986a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Mon, 15 Sep 2025 19:10:04 -0300 Subject: [PATCH 13/28] Fix tests - WIP --- .../hooks-quantamm/HyperSurgeHook.sol | 15 +- .../test/foundry/HyperSurgeFee.t.sol | 190 +++++++++--------- 2 files changed, 103 insertions(+), 102 deletions(-) diff --git a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol index d06aa0b5..9b6d1fb7 100644 --- a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol +++ b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol @@ -567,16 +567,17 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi uint256 indexTokenIn, uint256 indexTokenOut ) internal pure returns (uint256) { - uint256 num = balancesScaled18[indexTokenOut].mulDown(weights[indexTokenIn]); - uint256 den = balancesScaled18[indexTokenIn].mulDown(weights[indexTokenOut]); - - //would be impossible given normal balances and weights but given - //it is on the withdraw path keep the defensive check - if (den == 0) { + // This would cause a division by zero error. In normal circumstances this should never happen, + // but we keep the defensive check since it is on the withdraw path. + if (balancesScaled18[indexTokenIn] == 0 || weights[indexTokenOut] == 0) { return 0; } - return num.divDown(den); + // Use pure math increases the precision of the operations and reduces gas cost. + return + ((balancesScaled18[indexTokenOut] * weights[indexTokenIn]) / weights[indexTokenOut]).divDown( + balancesScaled18[indexTokenIn] + ); } function _relAbsDiff(uint256 a, uint256 b) internal pure returns (uint256) { diff --git a/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol b/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol index 8c3ff82e..c672fbfe 100644 --- a/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol +++ b/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol @@ -1202,7 +1202,6 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo } struct OutsideDynamicAfterLocals { - uint256 E; uint32 noiseThr9; uint32 noiseCap9; uint32 noiseMax9; @@ -1288,115 +1287,116 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, oraclePrice); + // Small error expected due to precision loss. assertEq(locals.dyn, locals.expected, "noise path must use AFTER deviation for dynamic fee"); assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee >= static"); } - // struct BetterStillOutsideLocals { - // uint256 E; - // uint32 arbThr9; - // uint32 arbCap9; - // uint32 arbMax9; - // uint32 noiseThr9; - // uint32 noiseCap9; - // uint32 noiseMax9; - // uint256 thr; - // uint256 cap; - // uint256 deviationBefore; - // uint256 price_before; - // uint256 price_after; - // uint256 xMax; - // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - // PoolSwapParams p; - // uint256 expected; - // uint256 dyn; - // } + struct BetterStillOutsideLocals { + uint32 arbThr9; + uint32 arbCap9; + uint32 arbMax9; + uint32 noiseThr9; + uint32 noiseCap9; + uint32 noiseMax9; + uint256 thr; + uint256 cap; + uint256 deviationBefore; + uint256 price_before; + uint256 price_after; + uint256 xMax; + uint256 expected; + uint256 dyn; + } - // function testFuzz_logic_arb_outside_improves_but_outside_dynamic_before( - // uint256 eSeed, - // uint32 arbThrSeed, - // uint32 arbCapSeed, - // uint32 arbMaxSeed, - // uint64 amtSeed - // ) public { - // BetterStillOutsideLocals memory locals; + function testFuzz_logic_arb_outside_improves_but_outside_dynamic_before( + uint256 eSeed, + uint32 arbThrSeed, + uint32 arbCapSeed, + uint32 arbMaxSeed, + uint64 amtSeed + ) public { + BetterStillOutsideLocals memory locals; - // // --- Fuzz + bounds --- - // locals.E = bound(eSeed, 1e16, 1e24); - // locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); - // locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); - // locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); - // // NOISE lane different (unused in assertion) - // locals.noiseThr9 = 5_000_000; - // locals.noiseCap9 = 400_000_000; - // locals.noiseMax9 = 25_000_000; + // --- Fuzz + bounds --- + uint256 oraclePrice = bound(eSeed, 1e16, 1e24); + locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); + locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); + locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + // NOISE lane different (unused in assertion) + locals.noiseThr9 = 5_000_000; + locals.noiseCap9 = 400_000_000; + locals.noiseMax9 = 25_000_000; + + locals.thr = uint256(locals.arbThr9) * 1e9; + locals.cap = uint256(locals.arbCap9) * 1e9; + locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; // strictly outside - // locals.thr = uint256(locals.arbThr9) * 1e9; - // locals.cap = uint256(locals.arbCap9) * 1e9; - // locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; // strictly outside + // Start ABOVE E + locals.price_before = oraclePrice + (oraclePrice * locals.deviationBefore) / 1e18; + + // Compute xMax to remain outside after: price_after >= E*(1 + thr) + // price_after = price_before / (1 + x) means x less than or equal to (price_before / (E*(1+thr)) - 1) * 1e18 + vm.assume(oraclePrice * (1e18 + locals.thr) != 0); // defensive + uint256 denom = (oraclePrice * (1e18 + locals.thr)) / 1e18; + vm.assume(denom != 0); + uint256 ratio = (locals.price_before * 1e18) / denom; + vm.assume(ratio > 1e18); // Ensure room to remain outside + locals.xMax = ratio - 1e18; + if (locals.xMax > 9e17) { + locals.xMax = 9e17; + } // clamp - // // Start ABOVE E - // locals.price_before = locals.E + (locals.E * locals.deviationBefore) / 1e18; + uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); + uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.price_before].toMemoryArray(); - // // Compute xMax to remain outside after: price_after >= E*(1 + thr) - // // price_after = price_before / (1 + x) means x less than or equal to (price_before / (E*(1+thr)) - 1) * 1e18 - // vm.assume(locals.E * (1e18 + locals.thr) != 0); // defensive - // uint256 denom = (locals.E * (1e18 + locals.thr)) / 1e18; - // vm.assume(denom != 0); - // uint256 ratio = (locals.price_before * 1e18) / denom; - // vm.assume(ratio > 1e18); // Ensure room to remain outside - // locals.xMax = ratio - 1e18; - // if (locals.xMax > 9e17) { - // locals.xMax = 9e17; - // } // clamp + HyperSurgeHook.PoolDetails memory poolDetails; + poolDetails.arbThresholdPercentage9 = locals.arbThr9; + poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; + poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + poolDetails.numTokens = 2; - // locals.comp.wIn = 1e18; - // locals.comp.wOut = 1e18; - // locals.comp.bIn = 1e18; - // locals.comp.bOut = locals.price_before; - // locals.comp.pxIn = 1e18; - // locals.comp.pxOut = locals.E; - // locals.comp.calcAmountScaled18 = 0; - // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - // locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; - // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; - // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + PoolSwapParams memory p = _createPoolSwapParams( + SwapKind.EXACT_IN, + balancesScaled18, + 0, + 1, + bound(uint256(amtSeed), 1, locals.xMax == 0 ? 1 : locals.xMax) + ); - // locals.p.kind = SwapKind.EXACT_IN; - // locals.p.amountGivenScaled18 = bound(uint256(amtSeed), 1, locals.xMax == 0 ? 1 : locals.xMax); + // Expected (ARB) uses BEFORE deviation + locals.expected = fee_expectedFeeWithParams( + locals.price_before, + oraclePrice, + STATIC_SWAP_FEE, + locals.arbThr9, + locals.arbCap9, + locals.arbMax9 + ); - // // Expected (ARB) uses BEFORE deviation - // locals.expected = fee_expectedFeeWithParams( - // locals.price_before, - // locals.comp.pxIn, - // locals.comp.pxOut, - // STATIC_SWAP_FEE, - // locals.arbThr9, - // locals.arbCap9, - // locals.arbMax9 - // ); + HyperSurgeHookMock mock = new HyperSurgeHookMock( + IVault(vault), + _convertTo18Decimals(locals.arbMax9), + _convertTo18Decimals(locals.arbThr9), + _convertTo18Decimals(locals.arbCap9), + "logic-2" + ); - // HyperSurgeHookMock mock = new HyperSurgeHookMock( - // IVault(vault), - // _convertTo18Decimals(locals.arbMax9), - // _convertTo18Decimals(locals.arbThr9), - // _convertTo18Decimals(locals.arbCap9), - // "logic-2" - // ); - // (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); + (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, oraclePrice); - // // Still outside afterward (sanity) - // locals.price_after = (locals.price_before * 1e18) / (1e18 + locals.p.amountGivenScaled18); - // uint256 deviationAfter = (( - // locals.price_after > locals.E ? (locals.price_after - locals.E) : (locals.E - locals.price_after) - // ) * 1e18) / locals.E; - // assertGt(deviationAfter, locals.thr, "should remain outside threshold after improving"); + // Still outside afterward (sanity) + locals.price_after = locals.price_before.divDown(FixedPoint.ONE + p.amountGivenScaled18); + uint256 deviationAfter = ( + locals.price_after > oraclePrice ? (locals.price_after - oraclePrice) : (oraclePrice - locals.price_after) + ).divDown(oraclePrice); + assertGt(deviationAfter, locals.thr, "should remain outside threshold after improving"); - // assertEq(locals.dyn, locals.expected, "arb path must use BEFORE deviation for dynamic fee"); - // assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee >= static"); - // } + assertEq(locals.dyn, locals.expected, "arb path must use BEFORE deviation for dynamic fee"); + assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee >= static"); + } // struct NoiseWorsensInsideButStaysInsideLocals { // uint256 E; From 143d095968438b83968c539fe13f56d1f7622514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Mon, 15 Sep 2025 19:24:40 -0300 Subject: [PATCH 14/28] Fix tests - WIP --- .../test/foundry/HyperSurgeFee.t.sol | 681 +++++++++--------- 1 file changed, 345 insertions(+), 336 deletions(-) diff --git a/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol b/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol index c672fbfe..19203de7 100644 --- a/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol +++ b/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol @@ -1398,379 +1398,388 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee >= static"); } - // struct NoiseWorsensInsideButStaysInsideLocals { - // uint256 E; - // uint32 noiseThr9; - // uint32 noiseCap9; - // uint32 noiseMax9; - // uint32 arbThr9; - // uint32 arbCap9; - // uint32 arbMax9; - // uint256 thr; - // uint256 deviationBefore; - // uint256 price_before; - // uint256 price_after; - // uint256 xMax; - // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - // PoolSwapParams p; - // uint256 fee; - // } - - // /// 3) Noise: starts inside threshold, worsens but stays inside → NOISE lane, **base (static)** fee. - // function testFuzz_logic_noise_inside_worse_but_inside_static( - // uint256 eSeed, - // uint32 noiseThrSeed, - // uint32 noiseCapSeed, - // uint32 noiseMaxSeed, - // uint64 amtSeed - // ) public { - // NoiseWorsensInsideButStaysInsideLocals memory locals; - - // locals.E = bound(eSeed, 1e16, 1e24); - // locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 1_000_000_000 - 1)); // (0,1) - // locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); - // locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); - - // locals.arbThr9 = 1_000_000; - // locals.arbCap9 = 300_000_000; - // locals.arbMax9 = 50_000_000; - // locals.thr = uint256(locals.noiseThr9) * 1e9; - // locals.deviationBefore = locals.thr / 4 + 1; - // locals.price_before = locals.E - (locals.E * locals.deviationBefore) / 1e18; - - // uint256 R1e18 = (locals.price_before * 1e18) / locals.E; - // uint256 denom = 1e18 - locals.thr; - // uint256 q = (R1e18 * 1e18) / denom; - // locals.xMax = q > 1e18 ? (q - 1e18) : 0; - // if (locals.xMax > 5e17) { - // locals.xMax = 5e17; - // } - - // locals.comp.wIn = 1e18; - // locals.comp.wOut = 1e18; - // locals.comp.bIn = 1e18; - // locals.comp.bOut = locals.price_before; - // locals.comp.pxIn = 1e18; - // locals.comp.pxOut = locals.E; - // locals.comp.calcAmountScaled18 = 0; - // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; - // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - // locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; - // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; - - // locals.p.kind = SwapKind.EXACT_IN; + struct NoiseWorsensInsideButStaysInsideLocals { + uint256 oraclePrice; + uint32 noiseThr9; + uint32 noiseCap9; + uint32 noiseMax9; + uint32 arbThr9; + uint32 arbCap9; + uint32 arbMax9; + uint256 thr; + uint256 deviationBefore; + uint256 price_before; + uint256 price_after; + uint256 xMax; + uint256 fee; + uint256 R1e18; + uint256 denom; + uint256 q; + } - // // Ensure a *measurable* worsening so NOISE is chosen: - // // pick x with a lower floor (e.g., 1e9 wei) but never exceed xMax. - // uint256 lo = 1e9; // 1e-9 in t; safely above Q18 rounding noise - // uint256 hi = locals.xMax; - // if (hi < lo) { - // lo = 1; - // } // if xMax < floor, fall back to [1, xMax] - // if (hi < lo) { - // hi = lo; - // } // clamp - // locals.p.amountGivenScaled18 = bound(uint256(amtSeed), lo, hi); + /// 3) Noise: starts inside threshold, worsens but stays inside → NOISE lane, **base (static)** fee. + function testFuzz_logic_noise_inside_worse_but_inside_static( + uint256 eSeed, + uint32 noiseThrSeed, + uint32 noiseCapSeed, + uint32 noiseMaxSeed, + uint64 amtSeed + ) public { + NoiseWorsensInsideButStaysInsideLocals memory locals; - // HyperSurgeHookMock mock = new HyperSurgeHookMock( - // IVault(vault), - // _convertTo18Decimals(locals.arbMax9), - // _convertTo18Decimals(locals.arbThr9), - // _convertTo18Decimals(locals.arbCap9), - // "logic-3" - // ); - // (, locals.fee) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); + locals.oraclePrice = bound(eSeed, 1e16, 1e24); + locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 1_000_000_000 - 1)); // (0,1) + locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); + locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); - // // Sanity: still inside after worsening - // locals.price_after = (locals.price_before * 1e18) / (1e18 + locals.p.amountGivenScaled18); - // uint256 deviationAfter = (( - // locals.price_after > locals.E ? (locals.price_after - locals.E) : (locals.E - locals.price_after) - // ) * 1e18) / locals.E; - // assertLe(deviationAfter, locals.thr, "must remain inside threshold"); + locals.arbThr9 = 1_000_000; + locals.arbCap9 = 300_000_000; + locals.arbMax9 = 50_000_000; + locals.thr = uint256(locals.noiseThr9) * 1e9; + locals.deviationBefore = locals.thr / 4 + 1; + locals.price_before = locals.oraclePrice.mulDown(FixedPoint.ONE - locals.deviationBefore); + + locals.R1e18 = locals.price_before.divDown(locals.oraclePrice); + locals.denom = 1e18 - locals.thr; + locals.q = (locals.R1e18 * 1e18) / locals.denom; + locals.xMax = locals.q > 1e18 ? (locals.q - 1e18) : 0; + if (locals.xMax > 5e17) { + locals.xMax = 5e17; + } - // // Inside-after on NOISE → static - // assertEq(locals.fee, STATIC_SWAP_FEE, "inside threshold after worsening must still return static (noise path)"); - // } + uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); + uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.price_before].toMemoryArray(); - // struct NoiseCrossesPriceWorsensLocals { - // uint256 E; - // uint32 noiseThr9; - // uint32 noiseCap9; - // uint32 noiseMax9; - // uint32 arbThr9; - // uint32 arbCap9; - // uint32 arbMax9; - // uint256 thr; - // uint256 cap; - // uint256 deviationBefore; - // uint256 price_before; - // uint256 price_after; - // uint256 tCross; - // uint256 tWorse; - // uint256 tMin; - // uint256 x; - // uint256 num; // numerator for tWorse calculation - // uint256 den; // denominator for tWorse calculation - // uint256 q; // intermediate value for tWorse calculation - // uint256 epsT; // safety margin for tMin - // uint256 span; // range for x selection - // uint256 lo; // lower bound for x - // uint256 hi; // upper bound for x - // uint256 deviationAfter; // absolute deviation after - // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - // PoolSwapParams p; - // uint256 expected; - // uint256 dyn; - // } + HyperSurgeHook.PoolDetails memory poolDetails; + poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + poolDetails.arbThresholdPercentage9 = locals.arbThr9; + poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; + poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + poolDetails.numTokens = 2; - // function testFuzz_logic_noise_outside_crosses_and_worsens_dynamic_after( - // uint256 eSeed, - // uint32 noiseThrSeed, - // uint32 noiseCapSeed, - // uint32 noiseMaxSeed, - // uint64 amtSeed - // ) public { - // NoiseCrossesPriceWorsensLocals memory locals; + // Ensure a *measurable* worsening so NOISE is chosen: + // pick x with a lower floor (e.g., 1e9 wei) but never exceed xMax. + uint256 lo = 1e9; // 1e-9 in t; safely above Q18 rounding noise + uint256 hi = locals.xMax; + if (hi < lo) { + lo = 1; + } // if xMax < floor, fall back to [1, xMax] + if (hi < lo) { + hi = lo; + } // clamp - // // --- Fuzz + bounds --- - // locals.E = bound(eSeed, 1e16, 1e24); + PoolSwapParams memory p = _createPoolSwapParams( + SwapKind.EXACT_IN, + balancesScaled18, + 0, + 1, + bound(uint256(amtSeed), lo, hi) + ); - // // Keep thr < 1 so denominators stay positive and bands are non-degenerate - // locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); // (0, 0.9) - // locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); // (thr, 1] - // locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + HyperSurgeHookMock mock = new HyperSurgeHookMock( + IVault(vault), + _convertTo18Decimals(locals.arbMax9), + _convertTo18Decimals(locals.arbThr9), + _convertTo18Decimals(locals.arbCap9), + "logic-3" + ); + (, locals.fee) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, locals.oraclePrice); - // // ARB lane different (unused in assertion) - // locals.arbThr9 = 1_000_000; - // locals.arbCap9 = 300_000_000; - // locals.arbMax9 = 50_000_000; + // Sanity: still inside after worsening + locals.price_after = locals.price_before.divDown(FixedPoint.ONE + p.amountGivenScaled18); + uint256 deviationAfter = ( + locals.price_after > locals.oraclePrice + ? (locals.price_after - locals.oraclePrice) + : (locals.oraclePrice - locals.price_after) + ).divDown(locals.oraclePrice); + assertLe(deviationAfter, locals.thr, "must remain inside threshold"); + + // Inside-after on NOISE → static + assertEq(locals.fee, STATIC_SWAP_FEE, "inside threshold after worsening must still return static (noise path)"); + } - // locals.thr = uint256(locals.noiseThr9) * 1e9; - // locals.cap = uint256(locals.noiseCap9) * 1e9; + struct NoiseCrossesPriceWorsensLocals { + uint256 oraclePrice; + uint32 noiseThr9; + uint32 noiseCap9; + uint32 noiseMax9; + uint32 arbThr9; + uint32 arbCap9; + uint32 arbMax9; + uint256 thr; + uint256 cap; + uint256 deviationBefore; + uint256 price_before; + uint256 price_after; + uint256 tCross; + uint256 tWorse; + uint256 tMin; + uint256 x; + uint256 num; // numerator for tWorse calculation + uint256 den; // denominator for tWorse calculation + uint256 q; // intermediate value for tWorse calculation + uint256 epsT; // safety margin for tMin + uint256 span; // range for x selection + uint256 lo; // lower bound for x + uint256 hi; // upper bound for x + uint256 deviationAfter; // absolute deviation after + uint256 expected; + uint256 dyn; + } - // // Start ABOVE E with a deviation strictly outside the threshold: - // locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 4; - // locals.price_before = locals.E + (locals.E * locals.deviationBefore) / 1e18; + function testFuzz_logic_noise_outside_crosses_and_worsens_dynamic_after( + uint256 eSeed, + uint32 noiseThrSeed, + uint32 noiseCapSeed, + uint32 noiseMaxSeed, + uint64 amtSeed + ) public { + NoiseCrossesPriceWorsensLocals memory locals; - // // Build compute locals - // locals.comp.wIn = 1e18; - // locals.comp.wOut = 1e18; - // locals.comp.bIn = 1e18; - // locals.comp.bOut = locals.price_before; - // locals.comp.pxIn = 1e18; - // locals.comp.pxOut = locals.E; - // locals.comp.calcAmountScaled18 = 0; - // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; - // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - // locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; - // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + // --- Fuzz + bounds --- + locals.oraclePrice = bound(eSeed, 1e16, 1e24); - // locals.p.kind = SwapKind.EXACT_IN; + // Keep thr < 1 so denominators stay positive and bands are non-degenerate + locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); // (0, 0.9) + locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); // (thr, 1] + locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); - // // We need BOTH: - // // (1) Cross: price_after < E means t > Db (R = 1 + Db) - // // (2) Worsen: |after| > |before| when ending below: - // // 1 - R/(1+t) > Db means (1 - Db)(1 + t) > 1 + Db means t > 2Db/(1 - Db) - // locals.tCross = locals.deviationBefore; - // // tWorse = ceil( (2*Db) / (1 - Db) ) in Q18 - // locals.num = (2 * locals.deviationBefore) * 1e18; // Q36 - // locals.den = 1e18 - locals.deviationBefore; - // locals.q = (locals.num + locals.den - 1) / locals.den; // ceilDiv -> Q18 - // locals.tWorse = locals.q; - - // // Add a safety margin to overcome integer rounding in price_after and deviationAfter. - // // Use 1e13 in Q18 (i.e., 1e-5) which is ample even for E as large as 1e24. - // locals.epsT = 1e13; - // locals.tMin = (locals.tWorse > locals.tCross ? locals.tWorse : locals.tCross) + locals.epsT; - - // // Choose x = t*1e18 with t in [tMin, tMin + span] - // locals.span = 5e17; // allow up to +0.5 in t - // locals.lo = locals.tMin; - // locals.hi = locals.tMin + locals.span; - // if (locals.lo == 0) { - // locals.lo = 1; - // } // avoid x==0 + // ARB lane different (unused in assertion) + locals.arbThr9 = 1_000_000; + locals.arbCap9 = 300_000_000; + locals.arbMax9 = 50_000_000; - // if (locals.hi < locals.lo) { - // locals.hi = locals.lo; - // } // clamp on overflow + locals.thr = uint256(locals.noiseThr9) * 1e9; + locals.cap = uint256(locals.noiseCap9) * 1e9; - // locals.x = bound(uint256(amtSeed), locals.lo, locals.hi); - // locals.p.amountGivenScaled18 = locals.x; + // Start ABOVE E with a deviation strictly outside the threshold: + locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 4; + locals.price_before = locals.oraclePrice + (locals.oraclePrice * locals.deviationBefore) / 1e18; - // // Expected uses NOISE with AFTER deviation - // locals.price_after = (locals.price_before * 1e18) / (1e18 + locals.x); + // Build compute locals + uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); + uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.price_before].toMemoryArray(); - // // Sanity: crossed and worsened absolute deviation - // locals.deviationBefore = ((locals.price_before - locals.E) * 1e18) / locals.E; - // locals.deviationAfter = ((locals.E - locals.price_after) * 1e18) / locals.E; - // require(locals.price_after < locals.E, "must cross below E"); - // require(locals.deviationAfter > locals.deviationBefore, "must worsen absolute deviation after crossing"); + HyperSurgeHook.PoolDetails memory poolDetails; + poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + poolDetails.arbThresholdPercentage9 = locals.arbThr9; + poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; + poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + poolDetails.numTokens = 2; - // locals.expected = fee_expectedFeeWithParams( - // locals.price_after, - // locals.comp.pxIn, - // locals.comp.pxOut, - // STATIC_SWAP_FEE, - // locals.noiseThr9, - // locals.noiseCap9, - // locals.noiseMax9 - // ); + // We need BOTH: + // (1) Cross: price_after < E means t > Db (R = 1 + Db) + // (2) Worsen: |after| > |before| when ending below: + // 1 - R/(1+t) > Db means (1 - Db)(1 + t) > 1 + Db means t > 2Db/(1 - Db) + locals.tCross = locals.deviationBefore; + // tWorse = ceil( (2*Db) / (1 - Db) ) in Q18 + locals.num = (2 * locals.deviationBefore) * 1e18; // Q36 + locals.den = 1e18 - locals.deviationBefore; + locals.q = (locals.num + locals.den - 1) / locals.den; // ceilDiv -> Q18 + locals.tWorse = locals.q; + + // Add a safety margin to overcome integer rounding in price_after and deviationAfter. + // Use 1e13 in Q18 (i.e., 1e-5) which is ample even for E as large as 1e24. + locals.epsT = 1e13; + locals.tMin = (locals.tWorse > locals.tCross ? locals.tWorse : locals.tCross) + locals.epsT; + + // Choose x = t*1e18 with t in [tMin, tMin + span] + locals.span = 5e17; // allow up to +0.5 in t + locals.lo = locals.tMin; + locals.hi = locals.tMin + locals.span; + if (locals.lo == 0) { + locals.lo = 1; + } // avoid x==0 + + if (locals.hi < locals.lo) { + locals.hi = locals.lo; + } // clamp on overflow - // HyperSurgeHookMock mock = new HyperSurgeHookMock( - // IVault(vault), - // _convertTo18Decimals(locals.arbMax9), - // _convertTo18Decimals(locals.arbThr9), - // _convertTo18Decimals(locals.arbCap9), - // "logic-4" - // ); - // (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); + PoolSwapParams memory p = _createPoolSwapParams( + SwapKind.EXACT_IN, + balancesScaled18, + 0, + 1, + bound(uint256(amtSeed), locals.lo, locals.hi) + ); - // assertEq( - // locals.dyn, - // locals.expected, - // "noise path must use AFTER deviation even when crossing the price (worsening)" - // ); - // assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee >= static"); - // } + // Expected uses NOISE with AFTER deviation + locals.price_after = locals.price_before.divDown(FixedPoint.ONE + p.amountGivenScaled18); - // struct OutsideToInsideDynamicBefore { - // uint256 E; - // uint32 arbThr9; - // uint32 arbCap9; - // uint32 arbMax9; - // uint32 noiseThr9; - // uint32 noiseCap9; - // uint32 noiseMax9; - // uint256 thr; - // uint256 cap; - // uint256 deviationBefore; - // uint256 price_before; - // uint256 price_after; - // uint256 R1e18; // R in 1e18 scale: R = price_before / E - // uint256 xLower; // min x to get price_after less than or equal to E*(1+thr) - // uint256 xUpper; // max x to keep price_after greater than or equal to E*(1−thr) - // uint256 x; // chosen amountGivenScaled18 inside [xLower, xUpper] - // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - // PoolSwapParams p; - // uint256 expected; - // uint256 dyn; - // } + // Sanity: crossed and worsened absolute deviation + locals.deviationBefore = (locals.price_before - locals.oraclePrice).divDown(locals.oraclePrice); + locals.deviationAfter = (locals.oraclePrice - locals.price_after).divDown(locals.oraclePrice); + require(locals.price_after < locals.oraclePrice, "must cross below oraclePrice"); + require(locals.deviationAfter > locals.deviationBefore, "must worsen absolute deviation after crossing"); - // /// 5) Arb: starts outside, ends inside → ARB lane still uses **BEFORE** deviation (dynamic, not base). - // function testFuzz_logic_arb_outside_to_inside_dynamic_before( - // uint256 eSeed, - // uint32 arbThrSeed, - // uint32 arbCapSeed, - // uint32 arbMaxSeed, - // uint64 amtSeed - // ) public { - // OutsideToInsideDynamicBefore memory locals; + locals.expected = fee_expectedFeeWithParams( + locals.price_after, + locals.oraclePrice, + STATIC_SWAP_FEE, + locals.noiseThr9, + locals.noiseCap9, + locals.noiseMax9 + ); - // // --- Fuzz + bounds --- - // locals.E = bound(eSeed, 1e16, 1e24); - // // Keep thr strictly < 1e9 so (1e18 - thr) > 0 - // locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); - // locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); - // locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); - // // NOISE lane can be anything different; not used by this assertion - // locals.noiseThr9 = 5_000_000; - // locals.noiseCap9 = 400_000_000; - // locals.noiseMax9 = 25_000_000; + HyperSurgeHookMock mock = new HyperSurgeHookMock( + IVault(vault), + _convertTo18Decimals(locals.arbMax9), + _convertTo18Decimals(locals.arbThr9), + _convertTo18Decimals(locals.arbCap9), + "logic-4" + ); + (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, locals.oraclePrice); - // locals.thr = uint256(locals.arbThr9) * 1e9; - // locals.cap = uint256(locals.arbCap9) * 1e9; + assertEq( + locals.dyn, + locals.expected, + "noise path must use AFTER deviation even when crossing the price (worsening)" + ); + assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee >= static"); + } - // // Start ABOVE E with an outside deviation deviationBefore > thr - // locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; // strictly outside - // locals.price_before = locals.E + (locals.E * locals.deviationBefore) / 1e18; // price_before = E * (1 + deviationBefore) - // locals.R1e18 = (locals.price_before * 1e18) / locals.E; // R = 1e18 + deviationBefore + struct OutsideToInsideDynamicBefore { + uint256 oraclePrice; + uint32 arbThr9; + uint32 arbCap9; + uint32 arbMax9; + uint32 noiseThr9; + uint32 noiseCap9; + uint32 noiseMax9; + uint256 thr; + uint256 cap; + uint256 deviationBefore; + uint256 price_before; + uint256 price_after; + uint256 R1e18; // R in 1e18 scale: R = price_before / E + uint256 xLower; // min x to get price_after less than or equal to E*(1+thr) + uint256 xUpper; // max x to keep price_after greater than or equal to E*(1−thr) + uint256 expected; + uint256 dyn; + uint256 denomPlus; + uint256 numPlus; + uint256 qPlus; + uint256 denomMinus; + uint256 numMinus; + uint256 qMinus; + } - // // Two-sided “inside” band: 1 − thr less than or equal to price_after/E less than or equal to 1 + thr, - // // with price_after/E = R / (1 + t), t = x / 1e18. + /// 5) Arb: starts outside, ends inside → ARB lane still uses **BEFORE** deviation (dynamic, not base). + function testFuzz_logic_arb_outside_to_inside_dynamic_before( + uint256 eSeed, + uint32 arbThrSeed, + uint32 arbCapSeed, + uint32 arbMaxSeed, + uint64 amtSeed + ) public { + OutsideToInsideDynamicBefore memory locals; - // // Lower bound on t (bring down to less than or equal to 1+thr): - // // t greater than or equal to R/(1+thr) − 1 means xLower = ceil( (R1e18 * 1e18) / (1e18 + thr) ) − 1e18 - // uint256 denomPlus = 1e18 + locals.thr; - // uint256 numPlus = locals.R1e18 * 1e18; // Q36 - // uint256 qPlus = (numPlus + denomPlus - 1) / denomPlus; // ceilDiv to Q18 - // locals.xLower = qPlus > 1e18 ? (qPlus - 1e18) : 0; + // --- Fuzz + bounds --- + locals.oraclePrice = bound(eSeed, 1e16, 1e24); + // Keep thr strictly < 1e9 so (1e18 - thr) > 0 + locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); + locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); + locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + // NOISE lane can be anything different; not used by this assertion + locals.noiseThr9 = 5_000_000; + locals.noiseCap9 = 400_000_000; + locals.noiseMax9 = 25_000_000; - // // Upper bound on t (don’t overshoot below 1 − thr): - // // t less than or equal to R/(1−thr) − 1 means xUpper = floor( (R1e18 * 1e18) / (1e18 − thr) ) − 1e18 - // uint256 denomMinus = 1e18 - locals.thr; // > 0 by bound - // uint256 numMinus = locals.R1e18 * 1e18; // Q36 - // uint256 qMinus = numMinus / denomMinus; // floorDiv to Q18 - // locals.xUpper = qMinus > 1e18 ? (qMinus - 1e18) : 0; + locals.thr = uint256(locals.arbThr9) * 1e9; + locals.cap = uint256(locals.arbCap9) * 1e9; - // // Choose x inside [xLower, xUpper] using bound (no vm.assume). Collapse if inverted. - // uint256 lo = locals.xLower; - // uint256 hi = locals.xUpper; - // if (hi < lo) { - // hi = lo; - // } - // // avoid degenerate zero (x == 0 keeps price_after == price_before and won’t end inside) - // if (lo == 0) lo = 1; - // if (hi < lo) hi = lo; + // Start ABOVE E with an outside deviation deviationBefore > thr + locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; // strictly outside + locals.price_before = locals.oraclePrice + (locals.oraclePrice * locals.deviationBefore) / 1e18; // price_before = E * (1 + deviationBefore) + locals.R1e18 = (locals.price_before * 1e18) / locals.oraclePrice; // R = 1e18 + deviationBefore + + // Two-sided “inside” band: 1 − thr less than or equal to price_after/E less than or equal to 1 + thr, + // with price_after/E = R / (1 + t), t = x / 1e18. + + // Lower bound on t (bring down to less than or equal to 1+thr): + // t greater than or equal to R/(1+thr) − 1 means xLower = ceil( (R1e18 * 1e18) / (1e18 + thr) ) − 1e18 + locals.denomPlus = 1e18 + locals.thr; + locals.numPlus = locals.R1e18 * 1e18; // Q36 + locals.qPlus = (locals.numPlus + locals.denomPlus - 1) / locals.denomPlus; // ceilDiv to Q18 + locals.xLower = locals.qPlus > 1e18 ? (locals.qPlus - 1e18) : 0; + + // Upper bound on t (don’t overshoot below 1 − thr): + // t less than or equal to R/(1−thr) − 1 means xUpper = floor( (R1e18 * 1e18) / (1e18 − thr) ) − 1e18 + locals.denomMinus = 1e18 - locals.thr; // > 0 by bound + locals.numMinus = locals.R1e18 * 1e18; // Q36 + locals.qMinus = locals.numMinus / locals.denomMinus; // floorDiv to Q18 + locals.xUpper = locals.qMinus > 1e18 ? (locals.qMinus - 1e18) : 0; + + // Choose x inside [xLower, xUpper] using bound (no vm.assume). Collapse if inverted. + uint256 lo = locals.xLower; + uint256 hi = locals.xUpper; + if (hi < lo) { + hi = lo; + } + // avoid degenerate zero (x == 0 keeps price_after == price_before and won’t end inside) + if (lo == 0) lo = 1; + if (hi < lo) hi = lo; - // locals.x = bound(uint256(amtSeed), lo, hi); + // Build compute locals + uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); + uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.price_before].toMemoryArray(); - // // Build compute locals - // locals.comp.wIn = 1e18; - // locals.comp.wOut = 1e18; - // locals.comp.bIn = 1e18; - // locals.comp.bOut = locals.price_before; - // locals.comp.pxIn = 1e18; - // locals.comp.pxOut = locals.E; - // locals.comp.calcAmountScaled18 = 0; - // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - // locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; - // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; - // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + HyperSurgeHook.PoolDetails memory poolDetails; + poolDetails.arbThresholdPercentage9 = locals.arbThr9; + poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; + poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + poolDetails.numTokens = 2; - // locals.p.kind = SwapKind.EXACT_IN; - // locals.p.amountGivenScaled18 = locals.x; + PoolSwapParams memory p = _createPoolSwapParams( + SwapKind.EXACT_IN, + balancesScaled18, + 0, + 1, + bound(uint256(amtSeed), lo, hi) + ); - // // Expected (ARB) uses BEFORE deviation even though end is inside - // locals.expected = fee_expectedFeeWithParams( - // locals.price_before, - // locals.comp.pxIn, - // locals.comp.pxOut, - // STATIC_SWAP_FEE, - // locals.arbThr9, - // locals.arbCap9, - // locals.arbMax9 - // ); + // Expected (ARB) uses BEFORE deviation even though end is inside + locals.expected = fee_expectedFeeWithParams( + locals.price_before, + locals.oraclePrice, + STATIC_SWAP_FEE, + locals.arbThr9, + locals.arbCap9, + locals.arbMax9 + ); - // HyperSurgeHookMock mock = new HyperSurgeHookMock( - // IVault(vault), - // _convertTo18Decimals(locals.arbMax9), - // _convertTo18Decimals(locals.arbThr9), - // _convertTo18Decimals(locals.arbCap9), - // "logic-5" - // ); - // (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); + HyperSurgeHookMock mock = new HyperSurgeHookMock( + IVault(vault), + _convertTo18Decimals(locals.arbMax9), + _convertTo18Decimals(locals.arbThr9), + _convertTo18Decimals(locals.arbCap9), + "logic-5" + ); + (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, locals.oraclePrice); - // // Sanity: end is inside (two-sided) - // locals.price_after = (locals.price_before * 1e18) / (1e18 + locals.p.amountGivenScaled18); - // uint256 deviationAfter = (( - // locals.price_after > locals.E ? (locals.price_after - locals.E) : (locals.E - locals.price_after) - // ) * 1e18) / locals.E; - // assertLe(deviationAfter, locals.thr, "end should be inside threshold"); + // Sanity: end is inside (two-sided) + locals.price_after = locals.price_before.divDown(FixedPoint.ONE + p.amountGivenScaled18); + uint256 deviationAfter = ( + locals.price_after > locals.oraclePrice + ? (locals.price_after - locals.oraclePrice) + : (locals.oraclePrice - locals.price_after) + ).divDown(locals.oraclePrice); + assertLe(deviationAfter, locals.thr, "end should be inside threshold"); - // assertEq( - // locals.dyn, - // locals.expected, - // "arb path must use BEFORE deviation even if the end state is inside threshold" - // ); - // assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee >= static"); - // } + assertEq( + locals.dyn, + locals.expected, + "arb path must use BEFORE deviation even if the end state is inside threshold" + ); + assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee >= static"); + } // struct InsideToOutsideDynamicAfterLocals { // uint256 E; From 3b6543221d6370fee9478e191d6bbc430b8d9014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Mon, 15 Sep 2025 19:31:33 -0300 Subject: [PATCH 15/28] Fix tests - WIP --- .../test/foundry/HyperSurgeFee.t.sol | 740 +++++++++--------- 1 file changed, 367 insertions(+), 373 deletions(-) diff --git a/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol b/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol index 19203de7..4875c526 100644 --- a/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol +++ b/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol @@ -1781,422 +1781,416 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee >= static"); } - // struct InsideToOutsideDynamicAfterLocals { - // uint256 E; - // uint32 noiseThr9; - // uint32 noiseCap9; - // uint32 noiseMax9; - // uint32 arbThr9; - // uint32 arbCap9; - // uint32 arbMax9; - // uint256 thr; - // uint256 cap; - // uint256 deviationBefore; - // uint256 priceBefore; - // uint256 priceAfter; - // uint256 R1e18; // = priceBefore/E (Q18) - // uint256 tLower; // min t to make priceAfter/E less than or equal to 1 - thr (Q18) - // uint256 x; // = t * 1e18 (amount in) - // uint256 num; // numerator for tLower calculation - // uint256 den; // denominator for tLower calculation - // uint256 q; // intermediate value for tLower calculation - // uint256 eps; // epsilon for x calculation - // uint256 lo; // lower bound for x - // uint256 hi; // upper bound for x - // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - // PoolSwapParams p; - // uint256 expected; - // uint256 dyn; - // } + struct InsideToOutsideDynamicAfterLocals { + uint256 oraclePrice; + uint32 noiseThr9; + uint32 noiseCap9; + uint32 noiseMax9; + uint32 arbThr9; + uint32 arbCap9; + uint32 arbMax9; + uint256 thr; + uint256 cap; + uint256 deviationBefore; + uint256 priceBefore; + uint256 priceAfter; + uint256 R1e18; // = priceBefore/E (Q18) + uint256 tLower; // min t to make priceAfter/E less than or equal to 1 - thr (Q18) + uint256 num; // numerator for tLower calculation + uint256 den; // denominator for tLower calculation + uint256 q; // intermediate value for tLower calculation + uint256 eps; // epsilon for x calculation + uint256 lo; // lower bound for x + uint256 hi; // upper bound for x + uint256 expected; + uint256 dyn; + } - // /// [LANE] Inside → cross outside (NOISE, dynamic with AFTER) - // function testFuzz_logic_noise_inside_to_outside_dynamic_after( - // uint256 eSeed, - // uint32 noiseThrSeed, - // uint32 noiseCapSeed, - // uint32 noiseMaxSeed, - // uint64 amtSeed - // ) public { - // InsideToOutsideDynamicAfterLocals memory locals; + /// [LANE] Inside → cross outside (NOISE, dynamic with AFTER) + function testFuzz_logic_noise_inside_to_outside_dynamic_after( + uint256 eSeed, + uint32 noiseThrSeed, + uint32 noiseCapSeed, + uint32 noiseMaxSeed, + uint64 amtSeed + ) public { + InsideToOutsideDynamicAfterLocals memory locals; - // // Lane params (NOISE fuzzed, ARB fixed and different) - // locals.E = bound(eSeed, 1e16, 1e24); - // locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); - // locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); - // locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); - // locals.arbThr9 = 1_000_000; - // locals.arbCap9 = 300_000_000; - // locals.arbMax9 = 50_000_000; + // Lane params (NOISE fuzzed, ARB fixed and different) + locals.oraclePrice = bound(eSeed, 1e16, 1e24); + locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); + locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); + locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + locals.arbThr9 = 1_000_000; + locals.arbCap9 = 300_000_000; + locals.arbMax9 = 50_000_000; - // locals.thr = uint256(locals.noiseThr9) * 1e9; - // locals.cap = uint256(locals.noiseCap9) * 1e9; + locals.thr = uint256(locals.noiseThr9) * 1e9; + locals.cap = uint256(locals.noiseCap9) * 1e9; - // // Start BELOW E but inside: deviationBefore ∈ [0, thr) - // locals.deviationBefore = (locals.thr / 3) + 1; // safely inside - // locals.priceBefore = locals.E - (locals.E * locals.deviationBefore) / 1e18; // P/E = 1 - deviationBefore - // locals.R1e18 = (locals.priceBefore * 1e18) / locals.E; + // Start BELOW E but inside: deviationBefore ∈ [0, thr) + locals.deviationBefore = (locals.thr / 3) + 1; // safely inside + locals.priceBefore = locals.oraclePrice - (locals.oraclePrice * locals.deviationBefore) / 1e18; // P/E = 1 - deviationBefore + locals.R1e18 = (locals.priceBefore * 1e18) / locals.oraclePrice; - // // Need priceAfter/E less than or equal to 1 - thr ⇒ t greater than or equal to R/(1 - thr) - 1 + // Need priceAfter/E less than or equal to 1 - thr ⇒ t greater than or equal to R/(1 - thr) - 1 - // locals.num = locals.R1e18 * 1e18; // Q36 - // locals.den = 1e18 - locals.thr; - // locals.q = (locals.num + locals.den - 1) / locals.den; // ceilDiv → Q18 - // locals.tLower = locals.q > 1e18 ? (locals.q - 1e18) : 0; + locals.num = locals.R1e18 * 1e18; // Q36 + locals.den = 1e18 - locals.thr; + locals.q = (locals.num + locals.den - 1) / locals.den; // ceilDiv → Q18 + locals.tLower = locals.q > 1e18 ? (locals.q - 1e18) : 0; - // // Pick x greater than or equal to tLower (plus small epsilon) to cross outside - // locals.eps = 1e12; - // locals.lo = locals.tLower + locals.eps; - // if (locals.lo == 0) locals.lo = 1; - // locals.hi = locals.lo + 5e17; // allow up to +0.5 in t - // locals.x = bound(uint256(amtSeed), locals.lo, locals.hi); + // Pick x greater than or equal to tLower (plus small epsilon) to cross outside + locals.eps = 1e12; + locals.lo = locals.tLower + locals.eps; + if (locals.lo == 0) locals.lo = 1; + locals.hi = locals.lo + 5e17; // allow up to +0.5 in t - // // Build locals - // locals.comp.wIn = 1e18; - // locals.comp.wOut = 1e18; - // locals.comp.bIn = 1e18; - // locals.comp.bOut = locals.priceBefore; - // locals.comp.pxIn = 1e18; - // locals.comp.pxOut = locals.E; - // locals.comp.calcAmountScaled18 = 0; - // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; - // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - // locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; - // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + // Build locals + uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); + uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.priceBefore].toMemoryArray(); - // locals.p.kind = SwapKind.EXACT_IN; - // locals.p.amountGivenScaled18 = locals.x; + HyperSurgeHook.PoolDetails memory poolDetails; + poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + poolDetails.arbThresholdPercentage9 = locals.arbThr9; + poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; + poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + poolDetails.numTokens = 2; - // // Expected (NOISE) uses AFTER - // locals.priceAfter = (locals.priceBefore * 1e18) / (1e18 + locals.x); - // uint256 deviationAfter = (( - // locals.priceAfter > locals.E ? (locals.priceAfter - locals.E) : (locals.E - locals.priceAfter) - // ) * 1e18) / locals.E; - // assertGt(deviationAfter, locals.thr, "must end outside threshold (worsened)"); - // locals.expected = fee_expectedFeeWithParams( - // locals.priceAfter, - // locals.comp.pxIn, - // locals.comp.pxOut, - // STATIC_SWAP_FEE, - // locals.noiseThr9, - // locals.noiseCap9, - // locals.noiseMax9 - // ); + PoolSwapParams memory p = _createPoolSwapParams( + SwapKind.EXACT_IN, + balancesScaled18, + 0, + 1, + bound(uint256(amtSeed), locals.lo, locals.hi) + ); - // HyperSurgeHookMock mock = new HyperSurgeHookMock( - // IVault(vault), - // _convertTo18Decimals(locals.arbMax9), - // _convertTo18Decimals(locals.arbThr9), - // _convertTo18Decimals(locals.arbCap9), - // "lane-inside2outside" - // ); - // (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); + // Expected (NOISE) uses AFTER + locals.priceAfter = locals.priceBefore.divDown(FixedPoint.ONE + p.amountGivenScaled18); + uint256 deviationAfter = ( + locals.priceAfter > locals.oraclePrice + ? (locals.priceAfter - locals.oraclePrice) + : (locals.oraclePrice - locals.priceAfter) + ).divDown(locals.oraclePrice); + assertGt(deviationAfter, locals.thr, "must end outside threshold (worsened)"); + locals.expected = fee_expectedFeeWithParams( + locals.priceAfter, + locals.oraclePrice, + STATIC_SWAP_FEE, + locals.noiseThr9, + locals.noiseCap9, + locals.noiseMax9 + ); - // assertEq(locals.dyn, locals.expected, "noise/after: dynamic fee must match expected"); - // assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee greater than or equal to static"); - // } + HyperSurgeHookMock mock = new HyperSurgeHookMock( + IVault(vault), + _convertTo18Decimals(locals.arbMax9), + _convertTo18Decimals(locals.arbThr9), + _convertTo18Decimals(locals.arbCap9), + "lane-inside2outside" + ); + (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, locals.oraclePrice); - // struct OutsideToThresholdDynamicBeforeLocals { - // uint256 E; - // uint32 arbThr9; - // uint32 arbCap9; - // uint32 arbMax9; - // uint32 noiseThr9; - // uint32 noiseCap9; - // uint32 noiseMax9; - // uint256 thr; - // uint256 cap; - // uint256 deviationBefore; - // uint256 priceBefore; - // uint256 priceAfter; - // uint256 R1e18; - // uint256 tLower; - // uint256 tUpper; - // uint256 x; - // uint256 epsT; - // uint256 lo; - // uint256 hi; - // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - // PoolSwapParams p; - // uint256 expected; - // uint256 dyn; - // } + assertEq(locals.dyn, locals.expected, "noise/after: dynamic fee must match expected"); + assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee greater than or equal to static"); + } - // /// [LANE] Outside → to (or just inside) threshold (ARB, dynamic with BEFORE) - // function testFuzz_logic_arb_outside_to_threshold_dynamic_before( - // uint256 eSeed, - // uint32 arbThrSeed, - // uint32 arbCapSeed, - // uint32 arbMaxSeed, - // uint64 amtSeed - // ) public { - // OutsideToThresholdDynamicBeforeLocals memory locals; + struct OutsideToThresholdDynamicBeforeLocals { + uint256 oraclePrice; + uint32 arbThr9; + uint32 arbCap9; + uint32 arbMax9; + uint32 noiseThr9; + uint32 noiseCap9; + uint32 noiseMax9; + uint256 thr; + uint256 cap; + uint256 deviationBefore; + uint256 priceBefore; + uint256 priceAfter; + uint256 R1e18; + uint256 tLower; + uint256 tUpper; + uint256 epsT; + uint256 lo; + uint256 hi; + uint256 expected; + uint256 dyn; + uint256 denomPlus; + uint256 numPlus; + uint256 qPlus; + uint256 denomMinus; + uint256 numMinus; + uint256 qMinus; + } - // locals.E = bound(eSeed, 1e16, 1e24); - // locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); - // locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); - // locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); - // // Distinct NOISE lane (unused in expected but kept different) - // locals.noiseThr9 = 5_000_000; - // locals.noiseCap9 = 400_000_000; - // locals.noiseMax9 = 25_000_000; + /// [LANE] Outside → to (or just inside) threshold (ARB, dynamic with BEFORE) + function testFuzz_logic_arb_outside_to_threshold_dynamic_before( + uint256 eSeed, + uint32 arbThrSeed, + uint32 arbCapSeed, + uint32 arbMaxSeed, + uint64 amtSeed + ) public { + OutsideToThresholdDynamicBeforeLocals memory locals; - // locals.thr = uint256(locals.arbThr9) * 1e9; - // locals.cap = uint256(locals.arbCap9) * 1e9; + locals.oraclePrice = bound(eSeed, 1e16, 1e24); + locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); + locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); + locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + // Distinct NOISE lane (unused in expected but kept different) + locals.noiseThr9 = 5_000_000; + locals.noiseCap9 = 400_000_000; + locals.noiseMax9 = 25_000_000; - // // Start ABOVE, outside - // locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; // strictly outside - // locals.priceBefore = locals.E + (locals.E * locals.deviationBefore) / 1e18; - - // // R = priceBefore / E in Q18; compute both ceil and floor variants to bound tightly - // // R_up = ceil( (priceBefore * 1e18) / E ) - // // R_down = floor( (priceBefore * 1e18) / E ) - // uint256 numR = locals.priceBefore * 1e18; - // locals.R1e18 = (numR + locals.E - 1) / locals.E; - - // // We need 1 - thr less than or equal to priceAfter/E less than or equal to 1 + thr, and priceAfter/E = R / (1 + t), with t = x/1e18 (Q18). - // // Lower bound on t (to get under the upper edge 1 + thr): - // // t ≥ R/(1 + thr) − 1 - // // Use R_up and ceil-div to be conservative, then subtract 1e18. - // uint256 denomPlus = 1e18 + locals.thr; - // uint256 numPlus = locals.R1e18 * 1e18; // Q36 - // uint256 qPlus = (numPlus + denomPlus - 1) / denomPlus; // ceilDiv → Q18 - // locals.tLower = qPlus > 1e18 ? (qPlus - 1e18) : 0; - - // // Upper bound on t (don’t drop below the lower edge 1 − thr): - // // t less than or equal to R/(1 − thr) − 1 - // // Use R_down and floor-div to be conservative, then subtract 1e18. - // uint256 denomMinus = 1e18 - locals.thr; - // uint256 numMinus = locals.R1e18 * 1e18; // Q36 - // uint256 qMinus = numMinus / denomMinus; // floorDiv → Q18 - // locals.tUpper = qMinus > 1e18 ? (qMinus - 1e18) : 0; - - // // Choose t inside [tLower + eps, tUpper − eps] and map amtSeed with bound(...). - // // eps helps avoid equality-edge flips due to integer rounding. - // locals.epsT = 1; // one Q18 unit (~1e-18) is ample given we used ceil/floor conservatively - // locals.lo = locals.tLower + locals.epsT; - // locals.hi = (locals.tUpper > locals.epsT) ? (locals.tUpper - locals.epsT) : locals.tUpper; - - // // If interval collapses or inverted (can happen with extreme tiny thr), clamp to a point and proceed. - // if (locals.hi < locals.lo) { - // locals.hi = locals.lo; - // } - // if (locals.lo == 0) { - // locals.lo = 1; - // if (locals.hi < locals.lo) locals.hi = locals.lo; - // } + locals.thr = uint256(locals.arbThr9) * 1e9; + locals.cap = uint256(locals.arbCap9) * 1e9; - // locals.x = bound(uint256(amtSeed), locals.lo, locals.hi); + // Start ABOVE, outside + locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; // strictly outside + locals.priceBefore = locals.oraclePrice + (locals.oraclePrice * locals.deviationBefore) / 1e18; + + // R = priceBefore / E in Q18; compute both ceil and floor variants to bound tightly + // R_up = ceil( (priceBefore * 1e18) / E ) + // R_down = floor( (priceBefore * 1e18) / E ) + uint256 numR = locals.priceBefore * 1e18; + locals.R1e18 = (numR + locals.oraclePrice - 1) / locals.oraclePrice; + + // We need 1 - thr less than or equal to priceAfter/E less than or equal to 1 + thr, and priceAfter/E = R / (1 + t), with t = x/1e18 (Q18). + // Lower bound on t (to get under the upper edge 1 + thr): + // t ≥ R/(1 + thr) − 1 + // Use R_up and ceil-div to be conservative, then subtract 1e18. + locals.denomPlus = 1e18 + locals.thr; + locals.numPlus = locals.R1e18 * 1e18; // Q36 + locals.qPlus = (locals.numPlus + locals.denomPlus - 1) / locals.denomPlus; // ceilDiv → Q18 + locals.tLower = locals.qPlus > 1e18 ? (locals.qPlus - 1e18) : 0; - // // Build locals - // locals.comp.wIn = 1e18; - // locals.comp.wOut = 1e18; - // locals.comp.bIn = 1e18; - // locals.comp.bOut = locals.priceBefore; - // locals.comp.pxIn = 1e18; - // locals.comp.pxOut = locals.E; - // locals.comp.calcAmountScaled18 = 0; - // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - // locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; - // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; - // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + // Upper bound on t (don’t drop below the lower edge 1 − thr): + // t less than or equal to R/(1 − thr) − 1 + // Use R_down and floor-div to be conservative, then subtract 1e18. + locals.denomMinus = 1e18 - locals.thr; + locals.numMinus = locals.R1e18 * 1e18; // Q36 + locals.qMinus = locals.numMinus / locals.denomMinus; // floorDiv → Q18 + locals.tUpper = locals.qMinus > 1e18 ? (locals.qMinus - 1e18) : 0; - // locals.p.kind = SwapKind.EXACT_IN; - // locals.p.amountGivenScaled18 = locals.x; + // Choose t inside [tLower + eps, tUpper − eps] and map amtSeed with bound(...). + // eps helps avoid equality-edge flips due to integer rounding. + locals.epsT = 1; // one Q18 unit (~1e-18) is ample given we used ceil/floor conservatively + locals.lo = locals.tLower + locals.epsT; + locals.hi = (locals.tUpper > locals.epsT) ? (locals.tUpper - locals.epsT) : locals.tUpper; - // // Sanity: end is inside (two-sided) - // locals.priceAfter = (locals.priceBefore * 1e18) / (1e18 + locals.p.amountGivenScaled18); - // uint256 dAfter = (( - // locals.priceAfter > locals.E ? (locals.priceAfter - locals.E) : (locals.E - locals.priceAfter) - // ) * 1e18) / locals.E; - // assertLe(dAfter, locals.thr, "end should be at/inside threshold"); + // If interval collapses or inverted (can happen with extreme tiny thr), clamp to a point and proceed. + if (locals.hi < locals.lo) { + locals.hi = locals.lo; + } + if (locals.lo == 0) { + locals.lo = 1; + if (locals.hi < locals.lo) locals.hi = locals.lo; + } - // // Expected (ARB) uses BEFORE even if end is at/inside threshold - // locals.expected = fee_expectedFeeWithParams( - // locals.priceBefore, - // locals.comp.pxIn, - // locals.comp.pxOut, - // STATIC_SWAP_FEE, - // locals.arbThr9, - // locals.arbCap9, - // locals.arbMax9 - // ); + // Build locals + uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); + uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.priceBefore].toMemoryArray(); - // HyperSurgeHookMock mock = new HyperSurgeHookMock( - // IVault(vault), - // _convertTo18Decimals(locals.arbMax9), - // _convertTo18Decimals(locals.arbThr9), - // _convertTo18Decimals(locals.arbCap9), - // "lane-out2thr" - // ); - // (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); + HyperSurgeHook.PoolDetails memory poolDetails; + poolDetails.arbThresholdPercentage9 = locals.arbThr9; + poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; + poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + poolDetails.numTokens = 2; - // assertEq( - // locals.dyn, - // locals.expected, - // "arb/before: dynamic fee must use BEFORE deviation even at threshold end" - // ); - // assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee greater than or equal to static"); - // } + PoolSwapParams memory p = _createPoolSwapParams( + SwapKind.EXACT_IN, + balancesScaled18, + 0, + 1, + bound(uint256(amtSeed), locals.lo, locals.hi) + ); - // struct ArbNoMoveOutsideDynamicLocals { - // uint256 E; - // uint32 arbThr9; - // uint32 arbCap9; - // uint32 arbMax9; - // uint32 noiseThr9; - // uint32 noiseCap9; - // uint32 noiseMax9; - // uint256 thr; - // uint256 cap; - // uint256 deviationBefore; - // uint256 priceBefore; - // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - // PoolSwapParams p; - // uint256 expected; - // uint256 dyn; - // } + // Sanity: end is inside (two-sided) + locals.priceAfter = locals.priceBefore.divDown(FixedPoint.ONE + p.amountGivenScaled18); + uint256 dAfter = ( + locals.priceAfter > locals.oraclePrice + ? (locals.priceAfter - locals.oraclePrice) + : (locals.oraclePrice - locals.priceAfter) + ).divDown(locals.oraclePrice); + assertLe(dAfter, locals.thr, "end should be at/inside threshold"); - // function test_logic_arb_outside_nochange_dynamic_before( - // uint256 eSeed, - // uint32 arbThrSeed, - // uint32 arbCapSeed, - // uint32 arbMaxSeed - // ) public { - // ArbNoMoveOutsideDynamicLocals memory locals; + // Expected (ARB) uses BEFORE even if end is at/inside threshold + locals.expected = fee_expectedFeeWithParams( + locals.priceBefore, + locals.oraclePrice, + STATIC_SWAP_FEE, + locals.arbThr9, + locals.arbCap9, + locals.arbMax9 + ); - // locals.E = bound(eSeed, 1e16, 1e24); - // locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); - // locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); - // locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); - // // NOISE lane different (unused) - // locals.noiseThr9 = 5_000_000; - // locals.noiseCap9 = 400_000_000; - // locals.noiseMax9 = 25_000_000; + HyperSurgeHookMock mock = new HyperSurgeHookMock( + IVault(vault), + _convertTo18Decimals(locals.arbMax9), + _convertTo18Decimals(locals.arbThr9), + _convertTo18Decimals(locals.arbCap9), + "lane-out2thr" + ); + (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, locals.oraclePrice); - // locals.thr = uint256(locals.arbThr9) * 1e9; - // locals.cap = uint256(locals.arbCap9) * 1e9; + assertEq( + locals.dyn, + locals.expected, + "arb/before: dynamic fee must use BEFORE deviation even at threshold end" + ); + assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee greater than or equal to static"); + } - // // Start ABOVE, outside - // locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; - // locals.priceBefore = locals.E + (locals.E * locals.deviationBefore) / 1e18; + struct ArbNoMoveOutsideDynamicLocals { + uint256 oraclePrice; + uint32 arbThr9; + uint32 arbCap9; + uint32 arbMax9; + uint32 noiseThr9; + uint32 noiseCap9; + uint32 noiseMax9; + uint256 thr; + uint256 cap; + uint256 deviationBefore; + uint256 priceBefore; + uint256 expected; + uint256 dyn; + } - // // No movement: amount = 0, so deviationAfter == deviationBefore → ARB path - // locals.comp.wIn = 1e18; - // locals.comp.wOut = 1e18; - // locals.comp.bIn = 1e18; - // locals.comp.bOut = locals.priceBefore; - // locals.comp.pxIn = 1e18; - // locals.comp.pxOut = locals.E; - // locals.comp.calcAmountScaled18 = 0; - // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - // locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; - // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; - // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + function test_logic_arb_outside_nochange_dynamic_before( + uint256 eSeed, + uint32 arbThrSeed, + uint32 arbCapSeed, + uint32 arbMaxSeed + ) public { + ArbNoMoveOutsideDynamicLocals memory locals; - // locals.p.kind = SwapKind.EXACT_IN; - // locals.p.amountGivenScaled18 = 0; + locals.oraclePrice = bound(eSeed, 1e16, 1e24); + locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); + locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); + locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + // NOISE lane different (unused) + locals.noiseThr9 = 5_000_000; + locals.noiseCap9 = 400_000_000; + locals.noiseMax9 = 25_000_000; - // locals.expected = fee_expectedFeeWithParams( - // locals.priceBefore, - // locals.comp.pxIn, - // locals.comp.pxOut, - // STATIC_SWAP_FEE, - // locals.arbThr9, - // locals.arbCap9, - // locals.arbMax9 - // ); + locals.thr = uint256(locals.arbThr9) * 1e9; + locals.cap = uint256(locals.arbCap9) * 1e9; - // HyperSurgeHookMock mock = new HyperSurgeHookMock( - // IVault(vault), - // _convertTo18Decimals(locals.arbMax9), - // _convertTo18Decimals(locals.arbThr9), - // _convertTo18Decimals(locals.arbCap9), - // "lane-nomove-outside" - // ); - // (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); + // Start ABOVE, outside + locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; + locals.priceBefore = locals.oraclePrice + (locals.oraclePrice * locals.deviationBefore) / 1e18; - // assertEq(locals.dyn, locals.expected, "no-move/outside must be ARB, dynamic from BEFORE"); - // assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee greater than or equal to static"); - // } + // No movement: amount = 0, so deviationAfter == deviationBefore → ARB path + uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); + uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.priceBefore].toMemoryArray(); - // struct ArbNoMoveInsideLocals { - // uint256 E; - // uint32 arbThr9; - // uint32 arbCap9; - // uint32 arbMax9; - // uint32 noiseThr9; - // uint32 noiseCap9; - // uint32 noiseMax9; - // uint256 thr; - // uint256 deviationBefore; - // uint256 priceBefore; - // uint256 fee; - // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - // PoolSwapParams p; - // } + HyperSurgeHook.PoolDetails memory poolDetails; + poolDetails.arbThresholdPercentage9 = locals.arbThr9; + poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; + poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + poolDetails.numTokens = 2; - // /// [LANE] No movement, inside: ARB path, but STATIC fee (since BEFORE less than or equal to thr) - // function test_logic_arb_inside_nochange_static( - // uint256 eSeed, - // uint32 arbThrSeed, - // uint32 arbCapSeed, - // uint32 arbMaxSeed - // ) public { - // ArbNoMoveInsideLocals memory locals; + PoolSwapParams memory p = _createPoolSwapParams(SwapKind.EXACT_IN, balancesScaled18, 0, 1, 0); - // locals.E = bound(eSeed, 1e16, 1e24); - // locals.arbThr9 = uint32(bound(arbThrSeed, 1, 1_000_000_000 - 1)); - // locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); - // locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); - // locals.noiseThr9 = 5_000_000; - // locals.noiseCap9 = 400_000_000; - // locals.noiseMax9 = 25_000_000; + locals.expected = fee_expectedFeeWithParams( + locals.priceBefore, + locals.oraclePrice, + STATIC_SWAP_FEE, + locals.arbThr9, + locals.arbCap9, + locals.arbMax9 + ); - // locals.thr = uint256(locals.arbThr9) * 1e9; + HyperSurgeHookMock mock = new HyperSurgeHookMock( + IVault(vault), + _convertTo18Decimals(locals.arbMax9), + _convertTo18Decimals(locals.arbThr9), + _convertTo18Decimals(locals.arbCap9), + "lane-nomove-outside" + ); + (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, locals.oraclePrice); - // // Start BELOW, inside - // locals.deviationBefore = (locals.thr / 3) + 1; // strictly inside - // locals.priceBefore = locals.E - (locals.E * locals.deviationBefore) / 1e18; + assertEq(locals.dyn, locals.expected, "no-move/outside must be ARB, dynamic from BEFORE"); + assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee greater than or equal to static"); + } - // // No movement: deviationAfter == deviationBefore → ARB branch, but less than or equal to thr ⇒ static - // locals.comp.wIn = 1e18; - // locals.comp.wOut = 1e18; - // locals.comp.bIn = 1e18; - // locals.comp.bOut = locals.priceBefore; - // locals.comp.pxIn = 1e18; - // locals.comp.pxOut = locals.E; - // locals.comp.calcAmountScaled18 = 0; - // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - // locals.comp.poolDetails.arbCapDeviationPercentage9 = uint32(locals.arbCap9); - // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; - // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + struct ArbNoMoveInsideLocals { + uint256 oraclePrice; + uint32 arbThr9; + uint32 arbCap9; + uint32 arbMax9; + uint32 noiseThr9; + uint32 noiseCap9; + uint32 noiseMax9; + uint256 thr; + uint256 deviationBefore; + uint256 priceBefore; + uint256 fee; + } - // locals.p.kind = SwapKind.EXACT_IN; - // locals.p.amountGivenScaled18 = 0; + /// [LANE] No movement, inside: ARB path, but STATIC fee (since BEFORE less than or equal to thr) + function test_logic_arb_inside_nochange_static( + uint256 eSeed, + uint32 arbThrSeed, + uint32 arbCapSeed, + uint32 arbMaxSeed + ) public { + ArbNoMoveInsideLocals memory locals; - // HyperSurgeHookMock mock = new HyperSurgeHookMock( - // IVault(vault), - // _convertTo18Decimals(locals.arbMax9), - // _convertTo18Decimals(locals.arbThr9), - // _convertTo18Decimals(locals.arbCap9), - // "lane-nomove-inside" - // ); - // (, locals.fee) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); + locals.oraclePrice = bound(eSeed, 1e16, 1e24); + locals.arbThr9 = uint32(bound(arbThrSeed, 1, 1_000_000_000 - 1)); + locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); + locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + locals.noiseThr9 = 5_000_000; + locals.noiseCap9 = 400_000_000; + locals.noiseMax9 = 25_000_000; - // assertEq( - // locals.fee, - // STATIC_SWAP_FEE, - // "no-move/inside must return static (ARB branch, but less than or equal to thr)" - // ); - // } + locals.thr = uint256(locals.arbThr9) * 1e9; + + // Start BELOW, inside + locals.deviationBefore = (locals.thr / 3) + 1; // strictly inside + locals.priceBefore = locals.oraclePrice - (locals.oraclePrice * locals.deviationBefore) / 1e18; + + // No movement: deviationAfter == deviationBefore → ARB branch, but less than or equal to thr ⇒ static + uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); + uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.priceBefore].toMemoryArray(); + + HyperSurgeHook.PoolDetails memory poolDetails; + poolDetails.arbThresholdPercentage9 = locals.arbThr9; + poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; + poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + poolDetails.numTokens = 2; + + PoolSwapParams memory p = _createPoolSwapParams(SwapKind.EXACT_IN, balancesScaled18, 0, 1, 0); + + HyperSurgeHookMock mock = new HyperSurgeHookMock( + IVault(vault), + _convertTo18Decimals(locals.arbMax9), + _convertTo18Decimals(locals.arbThr9), + _convertTo18Decimals(locals.arbCap9), + "lane-nomove-inside" + ); + (, locals.fee) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, locals.oraclePrice); + + assertEq( + locals.fee, + STATIC_SWAP_FEE, + "no-move/inside must return static (ARB branch, but less than or equal to thr)" + ); + } // struct NoiseCrossesPriceWorsensDymanicLocals { // uint256 E; From 998acef11730fb471b0c87f8d8dc33ef612a569a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Mon, 15 Sep 2025 19:43:13 -0300 Subject: [PATCH 16/28] Fix HyperSurgeFee tests --- .../test/foundry/HyperSurgeFee.t.sol | 1013 +++++++++-------- 1 file changed, 514 insertions(+), 499 deletions(-) diff --git a/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol b/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol index 4875c526..f9046a39 100644 --- a/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol +++ b/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol @@ -2192,505 +2192,520 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo ); } - // struct NoiseCrossesPriceWorsensDymanicLocals { - // uint256 E; - // uint32 noiseThr9; - // uint32 noiseCap9; - // uint32 noiseMax9; - // uint32 arbThr9; - // uint32 arbCap9; - // uint32 arbMax9; - // uint256 thr; - // uint256 cap; - // uint256 deviationBefore; - // uint256 priceBefore; - // uint256 priceAfter; - // uint256 tCross; - // uint256 tWorse; - // uint256 tMin; - // uint256 x; - // uint256 num; - // uint256 den; - // uint256 q; - // uint256 epsT; - // uint256 lo; - // uint256 hi; - // uint256 deviationAfter; - // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - // PoolSwapParams p; - // uint256 expected; - // uint256 dyn; - // } - - // /// [LANE] Symmetric “below” case: start outside BELOW, worsen further BELOW (no cross) → NOISE uses AFTER - // /// Note: With calc=0 and this simplified price update, EXACT_IN can only decrease P, - // /// so a true below→above cross is not representable without changing the price update model. - // /// This test locks the symmetric NOISE/AFTER behavior from the “below” side. - // function testFuzz_logic_noise_outside_below_worsens_dynamic_after( - // uint256 eSeed, - // uint32 noiseThrSeed, - // uint32 noiseCapSeed, - // uint32 noiseMaxSeed, - // uint64 amtSeed - // ) public { - // NoiseCrossesPriceWorsensDymanicLocals memory locals; - - // // External price (pxOut/pxIn -> E); keep as in all other tests - // locals.E = bound(eSeed, 1e16, 1e24); - - // // Distinct NOISE lane params (fuzzed) and different ARB params (unused in expected but distinct to catch wrong-lane) - // locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); - // locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); - // locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); - // locals.arbThr9 = 1_000_000; - // locals.arbCap9 = 300_000_000; - // locals.arbMax9 = 50_000_000; - - // locals.thr = uint256(locals.noiseThr9) * 1e9; - // locals.cap = uint256(locals.noiseCap9) * 1e9; - - // // Start OUTSIDE BELOW price: priceBefore = E * (1 - D_before), with D_before in (thr, cap) - // locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; - // locals.priceBefore = locals.E - (locals.E * locals.deviationBefore) / 1e18; - - // // Build compute locals with the standard orientation (pxIn=1e18, pxOut=E) - // locals.comp.wIn = 1e18; - // locals.comp.wOut = 1e18; - // locals.comp.bIn = 1e18; - // locals.comp.bOut = locals.priceBefore; - // locals.comp.pxIn = 1e18; // keep the usual frame - // locals.comp.pxOut = locals.E; - // locals.comp.calcAmountScaled18 = 0; - // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; - // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - // locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; - // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; - - // // EXACT_IN reduces P further → deviation worsens from the BELOW side (NOISE lane) - // locals.p.kind = SwapKind.EXACT_IN; - // // ensure a measurable worsening but no overflow; avoid 1-wei knife edges - // uint256 lo = 1e9; - // uint256 hi = 5e17; - // locals.p.amountGivenScaled18 = bound(uint256(amtSeed), lo, hi); - - // // AFTER price for expected (NOISE uses AFTER) - // locals.priceAfter = (locals.priceBefore * 1e18) / (1e18 + locals.p.amountGivenScaled18); - - // // Sanity: still BELOW E and deviation increased - // uint256 dBefore = ((locals.E - locals.priceBefore) * 1e18) / locals.E; - // uint256 dAfter = ((locals.E - locals.priceAfter) * 1e18) / locals.E; - // assertGt(dAfter, dBefore, "deviation must worsen from the below side"); - - // // Expected NOISE fee from AFTER deviation - // locals.expected = fee_expectedFeeWithParams( - // locals.priceAfter, - // locals.comp.pxIn, - // locals.comp.pxOut, - // STATIC_SWAP_FEE, - // locals.noiseThr9, - // locals.noiseCap9, - // locals.noiseMax9 - // ); - - // HyperSurgeHookMock mock = new HyperSurgeHookMock( - // IVault(vault), - // _convertTo18Decimals(locals.arbMax9), - // _convertTo18Decimals(locals.arbThr9), - // _convertTo18Decimals(locals.arbCap9), - // "lane-below-worsen" - // ); - // (, locals.dyn) = mock.ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); - - // assertEq(locals.dyn, locals.expected, "noise/after (below side): dynamic fee must match expected"); - // assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee greater than or equal to static"); - // } - - // struct BoundArbBeforeClampToMaxLocals { - // uint256 E; - // uint32 arbThr9; - // uint32 arbCap9; - // uint32 arbMax9; - // uint32 noiseThr9; - // uint32 noiseCap9; - // uint32 noiseMax9; - // uint256 thr; - // uint256 cap; - // uint256 Db; - // uint256 priceBefore; - // uint256 priceAfter; - // uint256 tLower; - // uint256 tUpperNoCross; - // uint256 x; - // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - // PoolSwapParams p; - // uint256 fee; - // uint256 expected; - // } - - // /// [BOUND] ARB with BEFORE > cap, AFTER < cap: ARB clamps to maxArb (basis = BEFORE) - // /// Start ABOVE with BEFORE deviation > cap, improve so AFTER less than or equal to cap (stay above; no cross). - // /// Assert: ARB lane; fee == arbMax (clamped by BEFORE). - // function testFuzz_bound_arb_before_gt_cap_clamps_to_max_before( - // uint256 eSeed, - // uint32 arbThrSeed, - // uint32 arbCapSeed, - // uint32 arbMaxSeed, - // uint64 amtSeed - // ) public { - // BoundArbBeforeClampToMaxLocals memory locals; - - // // External price - // locals.E = bound(eSeed, 1e16, 1e24); - - // // ARB lane params (ensure thr < cap < 1.0) - // locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); // (0, 0.9) - // locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000 - 1)); // (thr, 1) - // locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9) + 1, 1_000_000_000)); - - // // Distinct NOISE params (unused in expected but kept different to catch wrong-lane) - // locals.noiseThr9 = 5_000_000; - // locals.noiseCap9 = 400_000_000; - // locals.noiseMax9 = 25_000_000; - - // locals.thr = uint256(locals.arbThr9) * 1e9; - // locals.cap = uint256(locals.arbCap9) * 1e9; - // assertLt(locals.cap, 1e18, "cap must be < 100%"); - - // // BEFORE deviation strictly above cap but < 1, with safe margin - // // margin = max(1, (1e18 - cap)/16) keeps Db < 1 while staying comfortably > cap - // uint256 margin = (1e18 - locals.cap) / 16; - // if (margin == 0) { - // margin = 1; - // } - // locals.Db = locals.cap + margin; - // if (locals.Db >= 1e18) { - // locals.Db = 1e18 - 1; - // } - - // // Sanity: BEFORE > cap - // assertGt(locals.Db, locals.cap, "setup must have BEFORE > cap"); - - // // Price ABOVE E with BEFORE deviation Db - // locals.priceBefore = locals.E + (locals.E * locals.Db) / 1e18; - - // // ABOVE side with EXACT_IN: - // // D_after_pos (no-cross) = (Db - t)/(1 + t). Want AFTER less than or equal to cap ⇒ t ≥ (Db - cap)/(1 + cap). - - // uint256 num = (locals.Db - locals.cap) * 1e18; // Q36 (Db > cap guaranteed) - // uint256 den = 1e18 + locals.cap; - // uint256 q = (num + den - 1) / den; // ceilDiv → Q18 - // locals.tLower = q; - - // // Avoid crossing E: need t < Db. Use tiny epsilon below Db to stay strictly above E. - // uint256 epsCross = 1; // one Q18 unit - // locals.tUpperNoCross = (locals.Db > epsCross) ? (locals.Db - epsCross) : 0; - - // uint256 lo = (locals.tLower == 0 ? 1 : locals.tLower); - // uint256 hi = locals.tUpperNoCross; - - // if (hi < lo) { - // hi = lo; - // } - // locals.x = bound(uint256(amtSeed), lo, hi); - - // locals.comp.wIn = 1e18; - // locals.comp.wOut = 1e18; - // locals.comp.bIn = 1e18; - // locals.comp.bOut = locals.priceBefore; - // locals.comp.pxIn = 1e18; - // locals.comp.pxOut = locals.E; - // locals.comp.calcAmountScaled18 = 0; - // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - // locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; - // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; - // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; - - // locals.p.kind = SwapKind.EXACT_IN; - // locals.p.amountGivenScaled18 = locals.x; - - // // AFTER should be less than or equal to cap (improved) and we shouldn’t have crossed E. - // locals.priceAfter = (locals.priceBefore * 1e18) / (1e18 + locals.x); - // uint256 dAfter = (( - // locals.priceAfter > locals.E ? (locals.priceAfter - locals.E) : (locals.E - locals.priceAfter) - // ) * 1e18) / locals.E; - // assertLe(dAfter, locals.cap, "AFTER should be less than or equal to cap (improved)"); - - // // ARB uses BEFORE and must clamp to maxArb - // (, locals.fee) = new HyperSurgeHookMock( - // IVault(vault), - // _convertTo18Decimals(locals.arbMax9), - // _convertTo18Decimals(locals.arbThr9), - // _convertTo18Decimals(locals.arbCap9), - // "arb-before-cap" - // ).ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); - - // locals.expected = fee_expectedFeeWithParams( - // locals.priceBefore, - // locals.comp.pxIn, - // locals.comp.pxOut, - // STATIC_SWAP_FEE, - // locals.arbThr9, - // locals.arbCap9, - // locals.arbMax9 - // ); - // assertEq(locals.fee, locals.expected, "ARB should compute from BEFORE and clamp at cap->max"); - // assertEq(locals.fee, _convertTo18Decimals(locals.arbMax9), "ARB fee must equal arbMax"); - // } - - // struct BoundNoiseExactThresholdLocals { - // uint256 E; - // uint32 noiseThr9; - // uint32 noiseCap9; - // uint32 noiseMax9; - // uint32 arbThr9; - // uint32 arbCap9; - // uint32 arbMax9; - // uint256 thr; - // uint256 Db; - // uint256 priceBefore; - // uint256 priceAfter; - // uint256 tEdge; - // uint256 x; - // uint256 fee; - // HyperSurgeHookMock.ComputeSurgeFeeLocals comp; - // PoolSwapParams p; - // } - - // function testFuzz_bound_noise_after_at_threshold_static( - // uint256 eSeed, - // uint32 noiseThrSeed, - // uint32 noiseCapSeed, - // uint32 noiseMaxSeed, - // uint64 amtSeed - // ) public { - // BoundNoiseExactThresholdLocals memory locals; - - // locals.E = bound(eSeed, 1e16, 1e24); - // locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); - // locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); - // locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); - // locals.arbThr9 = 1_000_000; - // locals.arbCap9 = 300_000_000; - // locals.arbMax9 = 50_000_000; - - // locals.thr = uint256(locals.noiseThr9) * 1e9; - - // locals.Db = locals.thr / 4 + 1; - // locals.priceBefore = locals.E - (locals.E * locals.Db) / 1e18; - - // uint256 num = (locals.thr - locals.Db) * 1e18; - // uint256 den = 1e18 - locals.thr; - // locals.tEdge = den == 0 ? 0 : (num / den); - - // uint256 epsT = 1e6; - // uint256 lo = (locals.tEdge > epsT) ? (locals.tEdge - epsT) : 1; - // uint256 hi = locals.tEdge; - // if (hi < lo) { - // hi = lo; - // } - - // locals.x = bound(uint256(amtSeed), lo, hi); - // locals.comp.wIn = 1e18; - // locals.comp.wOut = 1e18; - // locals.comp.bIn = 1e18; - // locals.comp.bOut = locals.priceBefore; - // locals.comp.pxIn = 1e18; - // locals.comp.pxOut = locals.E; - // locals.comp.calcAmountScaled18 = 0; - // locals.comp.poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; - // locals.comp.poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; - // locals.comp.poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; - // locals.comp.poolDetails.arbThresholdPercentage9 = locals.arbThr9; - // locals.comp.poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; - // locals.comp.poolDetails.arbMaxSurgeFee9 = locals.arbMax9; - // locals.p.kind = SwapKind.EXACT_IN; - // locals.p.amountGivenScaled18 = locals.x; - // locals.priceAfter = (locals.priceBefore * 1e18) / (1e18 + locals.p.amountGivenScaled18); - - // uint256 dBefore = ((locals.E - locals.priceBefore) * 1e18) / locals.E; - // uint256 dAfter = ((locals.E - locals.priceAfter) * 1e18) / locals.E; - // assertLe(dAfter, locals.thr, "AFTER should be less than or equal to threshold (at-or-just-inside)"); - // assertGt(dAfter, dBefore, "deviation must worsen (positive t)"); - - // (, locals.fee) = new HyperSurgeHookMock( - // IVault(vault), - // _convertTo18Decimals(locals.arbMax9), - // _convertTo18Decimals(locals.arbThr9), - // _convertTo18Decimals(locals.arbCap9), - // "noise-exact-thr" - // ).ComputeSurgeFee(locals.comp, locals.p, STATIC_SWAP_FEE); - - // assertEq(locals.fee, STATIC_SWAP_FEE, "At threshold end-state: NOISE must return static (no ramp)"); - // } - - // uint32 constant HL_IDX_SZ_0 = 100; - // uint32 constant HL_IDX_SZ_8 = 108; - - // bytes4 constant _SEL_SPOT_PRICE = bytes4(keccak256("spotPrice(uint32)")); - // address constant _HYPER_SPOT_PRICE_PRECOMPILE = 0x0000000000000000000000000000000000000808; - - // function _mockHyperSpotPrice(uint32 pairIndex, uint64 raw) internal { - // vm.mockCall( - // _HYPER_SPOT_PRICE_PRECOMPILE, - // abi.encode(pairIndex), // <- no selector - // abi.encode(raw) // 32-byte padded uint64 - // ); - // } - - // function testFuzz_Fee_FallbacksToStatic_When_ExtPxZero(bool givenIn, uint64 rawInHuge) public { - // TokenConfig[] memory cfg = new TokenConfig[](2); - // LiquidityManagement memory lm; - // vm.prank(address(vault)); - // hook.onRegister(poolFactory, address(pool), cfg, lm); - - // // 2) Use the same HL token index you used (108 -> sz=0 -> divisor=1e8) on BOTH tokens - // uint32 pairIn = 8001; - // uint32 pairOut = 8002; - - // vm.startPrank(admin); - // hook.setTokenPriceConfigIndex(address(pool), 0, pairIn, 108); // div=1e8 - // hook.setTokenPriceConfigIndex(address(pool), 1, pairOut, 108); // div=1e8 - // vm.stopPrank(); - - // // 3) Force extPx == 0 with NON-ZERO raws: - // // extPx = floor((pxOut*1e18)/pxIn) = floor((rawOut*1e18)/rawIn) - // // => choose rawOut=1 and rawIn > 1e18 (fits in uint64), so extPx == 0 - // rawInHuge = uint64(bound(uint256(rawInHuge), 1e18 + 1, type(uint64).max)); - - // // (optional) prove we hit the correct precompile and calldata (no selector) - // vm.expectCall(_HYPER_SPOT_PRICE_PRECOMPILE, abi.encode(pairIn)); - // vm.expectCall(_HYPER_SPOT_PRICE_PRECOMPILE, abi.encode(pairOut)); - - // // Mock the spot prices with the correct calldata (NO selector) - // _mockHyperSpotPrice(pairIn, rawInHuge); // pxIn = rawInHuge * 1e10 - // _mockHyperSpotPrice(pairOut, 1); // pxOut = 1 * 1e10 - - // // 4) Build params (all 7 fields) - // uint256[] memory balances = new uint256[](2); - // balances[0] = 1e18; - // balances[1] = 1e18; - - // SwapKind kind = givenIn ? SwapKind.EXACT_IN : SwapKind.EXACT_OUT; - - // PoolSwapParams memory p = PoolSwapParams({ - // kind: kind, - // amountGivenScaled18: 5e15, - // balancesScaled18: balances, - // indexIn: 0, - // indexOut: 1, - // router: address(0), - // userData: "" - // }); - - // // 5) Expect: NO revert; the hook falls back to pool static fee because extPx == 0 - // uint256 staticFee = WeightedPool(address(pool)).getStaticSwapFeePercentage(); - // (bool ok, uint256 dynFee) = hook.onComputeDynamicSwapFeePercentage(p, address(pool), staticFee); - - // assertTrue(ok, "extPx==0 must not block"); - // assertEq(dynFee, staticFee, "extPx==0 must return static fee"); - // } - - // function testFuzz_Fee_ClampsToMax_When_DeviationBeyondCap(bool givenIn, uint64 rawOutHuge) public { - // uint256 idxIn = 0; - // uint256 idxOut = 1; - // uint256 amountGiven = 5e15; - - // uint256[] memory balances = new uint256[](2); - // balances[0] = 1e18; - // balances[1] = 1e18; - - // TokenConfig[] memory cfg = new TokenConfig[](2); - // LiquidityManagement memory lm; - // vm.prank(address(vault)); - // hook.onRegister(poolFactory, address(pool), cfg, lm); - - // uint32 pairIn = 91001; - // uint32 pairOut = 91002; - // vm.startPrank(admin); - // hook.setTokenPriceConfigIndex(address(pool), uint8(idxIn), pairIn, HL_IDX_SZ_8); - // hook.setTokenPriceConfigIndex(address(pool), uint8(idxOut), pairOut, HL_IDX_SZ_8); - - // uint256 thr = 1e16; // 1% - // uint256 cap = 2e16; // 2% - // uint256 max = 15e15; // 1.5% - - // hook.setSurgeThresholdPercentage(address(pool), thr, IHyperSurgeHook.TradeType.ARBITRAGE); - // hook.setSurgeThresholdPercentage(address(pool), thr, IHyperSurgeHook.TradeType.NOISE); - // hook.setCapDeviationPercentage(address(pool), cap, IHyperSurgeHook.TradeType.ARBITRAGE); - // hook.setCapDeviationPercentage(address(pool), cap, IHyperSurgeHook.TradeType.NOISE); - // hook.setMaxSurgeFeePercentage(address(pool), max, IHyperSurgeHook.TradeType.ARBITRAGE); - // hook.setMaxSurgeFeePercentage(address(pool), max, IHyperSurgeHook.TradeType.NOISE); - // vm.stopPrank(); - - // // External price >> 1.0: - // // extPx = (pxOut / pxIn) with same divisor. Set pxOut very large, pxIn = 1 unit. - // // Use HL_IDX_SZ_8 (divisor 1e8) so raw numbers are easy: rawIn=1e8, rawOut in [5e9, max]. - // rawOutHuge = uint64(bound(uint256(rawOutHuge), 5e9, type(uint64).max)); - // _mockHyperSpotPrice(pairIn, uint64(1e8)); - // _mockHyperSpotPrice(pairOut, rawOutHuge); - - // SwapKind kind = givenIn ? SwapKind.EXACT_IN : SwapKind.EXACT_OUT; - // PoolSwapParams memory p = _makeParams(idxIn, idxOut, kind, amountGiven, balances); - - // uint256 staticFee = WeightedPool(address(pool)).getStaticSwapFeePercentage(); - // (bool ok, uint256 fee) = hook.onComputeDynamicSwapFeePercentage(p, address(pool), staticFee); - - // assertTrue(ok, "fee path must not block"); - // assertEq(fee, max, "fee must clamp at configured maxPct"); - // } - - // function testFuzz_Fee_ReturnsStatic_When_DeviationBelowThreshold(bool givenIn, uint64 rawBase) public { - // uint256 idxIn = 0; - // uint256 idxOut = 1; - // uint256 amountGiven = 5e15; - - // uint256[] memory balances = new uint256[](2); - // balances[0] = 1e18; - // balances[1] = 1e18; - - // TokenConfig[] memory cfg = new TokenConfig[](2); - // LiquidityManagement memory lm; - // vm.prank(address(vault)); - // hook.onRegister(poolFactory, address(pool), cfg, lm); - - // uint32 pairIn = 92001; - // uint32 pairOut = 92002; - // vm.startPrank(admin); - // hook.setTokenPriceConfigIndex(address(pool), uint8(idxIn), pairIn, HL_IDX_SZ_8); - // hook.setTokenPriceConfigIndex(address(pool), uint8(idxOut), pairOut, HL_IDX_SZ_8); - - // // Set a relatively generous threshold (5%) and a higher cap so we stay in "below threshold" - // uint256 thr = 5e16; // 5% - // uint256 cap = 20e16; // 20% (arbitrary > thr) - // uint256 max = 50e16; // 50% (irrelevant here) - - // hook.setSurgeThresholdPercentage(address(pool), thr, IHyperSurgeHook.TradeType.ARBITRAGE); - // hook.setSurgeThresholdPercentage(address(pool), thr, IHyperSurgeHook.TradeType.NOISE); - // hook.setCapDeviationPercentage(address(pool), cap, IHyperSurgeHook.TradeType.ARBITRAGE); - // hook.setCapDeviationPercentage(address(pool), cap, IHyperSurgeHook.TradeType.NOISE); - // hook.setMaxSurgeFeePercentage(address(pool), max, IHyperSurgeHook.TradeType.ARBITRAGE); - // hook.setMaxSurgeFeePercentage(address(pool), max, IHyperSurgeHook.TradeType.NOISE); - // vm.stopPrank(); - - // // Make extPx ≈ 1.0 within ~1e-8 relative drift, far below the 5% threshold. - // // Same divisor (1e8): extPx = (rawOut/rawIn). Pick rawOut = rawBase + 1, rawIn = rawBase. - // rawBase = uint64(bound(uint256(rawBase), 1e8, 5e9)); // ensure > 0 and leaves headroom for +1 - // _mockHyperSpotPrice(pairIn, rawBase); - // _mockHyperSpotPrice(pairOut, rawBase + 1); - - // SwapKind kind = givenIn ? SwapKind.EXACT_IN : SwapKind.EXACT_OUT; - // PoolSwapParams memory p = _makeParams(idxIn, idxOut, kind, amountGiven, balances); - - // uint256 staticFee = WeightedPool(address(pool)).getStaticSwapFeePercentage(); - // (bool ok, uint256 fee) = hook.onComputeDynamicSwapFeePercentage(p, address(pool), staticFee); - - // assertTrue(ok, "below-threshold path must not block"); - // assertEq(fee, staticFee, "below-threshold deviation must return static fee"); - // } + struct NoiseCrossesPriceWorsensDymanicLocals { + uint256 oraclePrice; + uint32 noiseThr9; + uint32 noiseCap9; + uint32 noiseMax9; + uint32 arbThr9; + uint32 arbCap9; + uint32 arbMax9; + uint256 thr; + uint256 cap; + uint256 deviationBefore; + uint256 priceBefore; + uint256 priceAfter; + uint256 tCross; + uint256 tWorse; + uint256 tMin; + uint256 x; + uint256 num; + uint256 den; + uint256 q; + uint256 epsT; + uint256 lo; + uint256 hi; + uint256 deviationAfter; + uint256 expected; + uint256 dyn; + } + + /// [LANE] Symmetric “below” case: start outside BELOW, worsen further BELOW (no cross) → NOISE uses AFTER + /// Note: With calc=0 and this simplified price update, EXACT_IN can only decrease P, + /// so a true below→above cross is not representable without changing the price update model. + /// This test locks the symmetric NOISE/AFTER behavior from the “below” side. + function testFuzz_logic_noise_outside_below_worsens_dynamic_after( + uint256 eSeed, + uint32 noiseThrSeed, + uint32 noiseCapSeed, + uint32 noiseMaxSeed, + uint64 amtSeed + ) public { + NoiseCrossesPriceWorsensDymanicLocals memory locals; + + // External price (pxOut/pxIn -> E); keep as in all other tests + locals.oraclePrice = bound(eSeed, 1e16, 1e24); + + // Distinct NOISE lane params (fuzzed) and different ARB params (unused in expected but distinct to catch wrong-lane) + locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); + locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); + locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + locals.arbThr9 = 1_000_000; + locals.arbCap9 = 300_000_000; + locals.arbMax9 = 50_000_000; + + locals.thr = uint256(locals.noiseThr9) * 1e9; + locals.cap = uint256(locals.noiseCap9) * 1e9; + + // Start OUTSIDE BELOW price: priceBefore = E * (1 - D_before), with D_before in (thr, cap) + locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; + locals.priceBefore = locals.oraclePrice - (locals.oraclePrice * locals.deviationBefore) / 1e18; + + // Build compute locals with the standard orientation (pxIn=1e18, pxOut=E) + uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); + uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.priceBefore].toMemoryArray(); + + HyperSurgeHook.PoolDetails memory poolDetails; + poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + poolDetails.arbThresholdPercentage9 = locals.arbThr9; + poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; + poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + poolDetails.numTokens = 2; + + // ensure a measurable worsening but no overflow; avoid 1-wei knife edges + locals.lo = 1e9; + locals.hi = 5e17; + + // EXACT_IN reduces P further → deviation worsens from the BELOW side (NOISE lane) + PoolSwapParams memory p = _createPoolSwapParams( + SwapKind.EXACT_IN, + balancesScaled18, + 0, + 1, + bound(uint256(amtSeed), locals.lo, locals.hi) + ); + + // AFTER price for expected (NOISE uses AFTER) + locals.priceAfter = locals.priceBefore.divDown(FixedPoint.ONE + p.amountGivenScaled18); + + // Sanity: still BELOW E and deviation increased + uint256 dBefore = (locals.oraclePrice - locals.priceBefore).divDown(locals.oraclePrice); + uint256 dAfter = (locals.oraclePrice - locals.priceAfter).divDown(locals.oraclePrice); + assertGt(dAfter, dBefore, "deviation must worsen from the below side"); + + // Expected NOISE fee from AFTER deviation + locals.expected = fee_expectedFeeWithParams( + locals.priceAfter, + locals.oraclePrice, + STATIC_SWAP_FEE, + locals.noiseThr9, + locals.noiseCap9, + locals.noiseMax9 + ); + + HyperSurgeHookMock mock = new HyperSurgeHookMock( + IVault(vault), + _convertTo18Decimals(locals.arbMax9), + _convertTo18Decimals(locals.arbThr9), + _convertTo18Decimals(locals.arbCap9), + "lane-below-worsen" + ); + (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, locals.oraclePrice); + + assertEq(locals.dyn, locals.expected, "noise/after (below side): dynamic fee must match expected"); + assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee greater than or equal to static"); + } + + struct BoundArbBeforeClampToMaxLocals { + uint256 oraclePrice; + uint32 arbThr9; + uint32 arbCap9; + uint32 arbMax9; + uint32 noiseThr9; + uint32 noiseCap9; + uint32 noiseMax9; + uint256 thr; + uint256 cap; + uint256 Db; + uint256 priceBefore; + uint256 priceAfter; + uint256 tLower; + uint256 tUpperNoCross; + uint256 fee; + uint256 expected; + uint256 num; + uint256 den; + uint256 q; + uint256 epsCross; + uint256 lo; + uint256 hi; + uint256 margin; + } + + /// [BOUND] ARB with BEFORE > cap, AFTER < cap: ARB clamps to maxArb (basis = BEFORE) + /// Start ABOVE with BEFORE deviation > cap, improve so AFTER less than or equal to cap (stay above; no cross). + /// Assert: ARB lane; fee == arbMax (clamped by BEFORE). + function testFuzz_bound_arb_before_gt_cap_clamps_to_max_before( + uint256 eSeed, + uint32 arbThrSeed, + uint32 arbCapSeed, + uint32 arbMaxSeed, + uint64 amtSeed + ) public { + BoundArbBeforeClampToMaxLocals memory locals; + + // External price + locals.oraclePrice = bound(eSeed, 1e16, 1e24); + + // ARB lane params (ensure thr < cap < 1.0) + locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); // (0, 0.9) + locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000 - 1)); // (thr, 1) + locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9) + 1, 1_000_000_000)); + + // Distinct NOISE params (unused in expected but kept different to catch wrong-lane) + locals.noiseThr9 = 5_000_000; + locals.noiseCap9 = 400_000_000; + locals.noiseMax9 = 25_000_000; + + locals.thr = uint256(locals.arbThr9) * 1e9; + locals.cap = uint256(locals.arbCap9) * 1e9; + assertLt(locals.cap, 1e18, "cap must be < 100%"); + + // BEFORE deviation strictly above cap but < 1, with safe margin + // margin = max(1, (1e18 - cap)/16) keeps Db < 1 while staying comfortably > cap + locals.margin = (1e18 - locals.cap) / 16; + if (locals.margin == 0) { + locals.margin = 1; + } + locals.Db = locals.cap + locals.margin; + if (locals.Db >= 1e18) { + locals.Db = 1e18 - 1; + } + + // Sanity: BEFORE > cap + assertGt(locals.Db, locals.cap, "setup must have BEFORE > cap"); + + // Price ABOVE E with BEFORE deviation Db + locals.priceBefore = locals.oraclePrice + (locals.oraclePrice * locals.Db) / 1e18; + + // ABOVE side with EXACT_IN: + // D_after_pos (no-cross) = (Db - t)/(1 + t). Want AFTER less than or equal to cap ⇒ t ≥ (Db - cap)/(1 + cap). + + locals.num = (locals.Db - locals.cap) * 1e18; // Q36 (Db > cap guaranteed) + locals.den = 1e18 + locals.cap; + locals.q = (locals.num + locals.den - 1) / locals.den; // ceilDiv → Q18 + locals.tLower = locals.q; + + // Avoid crossing E: need t < Db. Use tiny epsilon below Db to stay strictly above E. + locals.epsCross = 1; // one Q18 unit + locals.tUpperNoCross = (locals.Db > locals.epsCross) ? (locals.Db - locals.epsCross) : 0; + + locals.lo = (locals.tLower == 0 ? 1 : locals.tLower); + locals.hi = locals.tUpperNoCross; + + if (locals.hi < locals.lo) { + locals.hi = locals.lo; + } + + uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); + uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.priceBefore].toMemoryArray(); + + HyperSurgeHook.PoolDetails memory poolDetails; + poolDetails.arbThresholdPercentage9 = locals.arbThr9; + poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; + poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + poolDetails.numTokens = 2; + + PoolSwapParams memory p = _createPoolSwapParams( + SwapKind.EXACT_IN, + balancesScaled18, + 0, + 1, + bound(uint256(amtSeed), locals.lo, locals.hi) + ); + + // AFTER should be less than or equal to cap (improved) and we shouldn’t have crossed E. + locals.priceAfter = locals.priceBefore.divDown(FixedPoint.ONE + p.amountGivenScaled18); + uint256 dAfter = ( + locals.priceAfter > locals.oraclePrice + ? (locals.priceAfter - locals.oraclePrice) + : (locals.oraclePrice - locals.priceAfter) + ).divDown(locals.oraclePrice); + assertLe(dAfter, locals.cap, "AFTER should be less than or equal to cap (improved)"); + + // ARB uses BEFORE and must clamp to maxArb + (, locals.fee) = new HyperSurgeHookMock( + IVault(vault), + _convertTo18Decimals(locals.arbMax9), + _convertTo18Decimals(locals.arbThr9), + _convertTo18Decimals(locals.arbCap9), + "arb-before-cap" + ).ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, locals.oraclePrice); + + locals.expected = fee_expectedFeeWithParams( + locals.priceBefore, + locals.oraclePrice, + STATIC_SWAP_FEE, + locals.arbThr9, + locals.arbCap9, + locals.arbMax9 + ); + assertEq(locals.fee, locals.expected, "ARB should compute from BEFORE and clamp at cap->max"); + assertEq(locals.fee, _convertTo18Decimals(locals.arbMax9), "ARB fee must equal arbMax"); + } + + struct BoundNoiseExactThresholdLocals { + uint256 oraclePrice; + uint32 noiseThr9; + uint32 noiseCap9; + uint32 noiseMax9; + uint32 arbThr9; + uint32 arbCap9; + uint32 arbMax9; + uint256 thr; + uint256 Db; + uint256 priceBefore; + uint256 priceAfter; + uint256 tEdge; + uint256 fee; + uint256 num; + uint256 den; + uint256 epsT; + uint256 lo; + uint256 hi; + uint256 margin; + } + + function testFuzz_bound_noise_after_at_threshold_static( + uint256 eSeed, + uint32 noiseThrSeed, + uint32 noiseCapSeed, + uint32 noiseMaxSeed, + uint64 amtSeed + ) public { + BoundNoiseExactThresholdLocals memory locals; + + locals.oraclePrice = bound(eSeed, 1e16, 1e24); + locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); + locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); + locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + locals.arbThr9 = 1_000_000; + locals.arbCap9 = 300_000_000; + locals.arbMax9 = 50_000_000; + + locals.thr = uint256(locals.noiseThr9) * 1e9; + + locals.Db = locals.thr / 4 + 1; + locals.priceBefore = locals.oraclePrice - (locals.oraclePrice * locals.Db) / 1e18; + + locals.num = (locals.thr - locals.Db) * 1e18; + locals.den = 1e18 - locals.thr; + locals.tEdge = locals.den == 0 ? 0 : (locals.num / locals.den); + + locals.epsT = 1e6; + locals.lo = (locals.tEdge > locals.epsT) ? (locals.tEdge - locals.epsT) : 1; + locals.hi = locals.tEdge; + if (locals.hi < locals.lo) { + locals.hi = locals.lo; + } + + uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); + uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.priceBefore].toMemoryArray(); + + HyperSurgeHook.PoolDetails memory poolDetails; + poolDetails.noiseThresholdPercentage9 = locals.noiseThr9; + poolDetails.noiseCapDeviationPercentage9 = locals.noiseCap9; + poolDetails.noiseMaxSurgeFee9 = locals.noiseMax9; + poolDetails.arbThresholdPercentage9 = locals.arbThr9; + poolDetails.arbCapDeviationPercentage9 = locals.arbCap9; + poolDetails.arbMaxSurgeFee9 = locals.arbMax9; + poolDetails.numTokens = 2; + + PoolSwapParams memory p = _createPoolSwapParams( + SwapKind.EXACT_IN, + balancesScaled18, + 0, + 1, + bound(uint256(amtSeed), locals.lo, locals.hi) + ); + + locals.priceAfter = locals.priceBefore.divDown(FixedPoint.ONE + p.amountGivenScaled18); + + uint256 dBefore = (locals.oraclePrice - locals.priceBefore).divDown(locals.oraclePrice); + uint256 dAfter = (locals.oraclePrice - locals.priceAfter).divDown(locals.oraclePrice); + assertLe(dAfter, locals.thr, "AFTER should be less than or equal to threshold (at-or-just-inside)"); + assertGt(dAfter, dBefore, "deviation must worsen (positive t)"); + + (, locals.fee) = new HyperSurgeHookMock( + IVault(vault), + _convertTo18Decimals(locals.arbMax9), + _convertTo18Decimals(locals.arbThr9), + _convertTo18Decimals(locals.arbCap9), + "noise-exact-thr" + ).ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, locals.oraclePrice); + + assertEq(locals.fee, STATIC_SWAP_FEE, "At threshold end-state: NOISE must return static (no ramp)"); + } + + uint32 constant HL_IDX_SZ_0 = 100; + uint32 constant HL_IDX_SZ_8 = 108; + + bytes4 constant _SEL_SPOT_PRICE = bytes4(keccak256("spotPrice(uint32)")); + address constant _HYPER_SPOT_PRICE_PRECOMPILE = 0x0000000000000000000000000000000000000808; + + function _mockHyperSpotPrice(uint32 pairIndex, uint64 raw) internal { + vm.mockCall( + _HYPER_SPOT_PRICE_PRECOMPILE, + abi.encode(pairIndex), // <- no selector + abi.encode(raw) // 32-byte padded uint64 + ); + } + + function testFuzz_Fee_FallbacksToStatic_When_ExtPxZero(bool givenIn, uint64 rawInHuge) public { + TokenConfig[] memory cfg = new TokenConfig[](2); + LiquidityManagement memory lm; + vm.prank(address(vault)); + hook.onRegister(poolFactory, address(pool), cfg, lm); + + // 2) Use the same HL token index you used (108 -> sz=0 -> divisor=1e8) on BOTH tokens + uint32 pairIn = 8001; + uint32 pairOut = 8002; + + vm.startPrank(admin); + hook.setTokenPriceConfigIndex(address(pool), 0, pairIn, 108); // div=1e8 + hook.setTokenPriceConfigIndex(address(pool), 1, pairOut, 108); // div=1e8 + vm.stopPrank(); + + // 3) Force extPx == 0 with NON-ZERO raws: + // extPx = floor((pxOut*1e18)/pxIn) = floor((rawOut*1e18)/rawIn) + // => choose rawOut=1 and rawIn > 1e18 (fits in uint64), so extPx == 0 + rawInHuge = uint64(bound(uint256(rawInHuge), 1e18 + 1, type(uint64).max)); + + // (optional) prove we hit the correct precompile and calldata (no selector) + vm.expectCall(_HYPER_SPOT_PRICE_PRECOMPILE, abi.encode(pairIn)); + vm.expectCall(_HYPER_SPOT_PRICE_PRECOMPILE, abi.encode(pairOut)); + + // Mock the spot prices with the correct calldata (NO selector) + _mockHyperSpotPrice(pairIn, rawInHuge); // pxIn = rawInHuge * 1e10 + _mockHyperSpotPrice(pairOut, 1); // pxOut = 1 * 1e10 + + // 4) Build params (all 7 fields) + uint256[] memory balances = new uint256[](2); + balances[0] = 1e18; + balances[1] = 1e18; + + SwapKind kind = givenIn ? SwapKind.EXACT_IN : SwapKind.EXACT_OUT; + + PoolSwapParams memory p = PoolSwapParams({ + kind: kind, + amountGivenScaled18: 5e15, + balancesScaled18: balances, + indexIn: 0, + indexOut: 1, + router: address(0), + userData: "" + }); + + // 5) Expect: NO revert; the hook falls back to pool static fee because extPx == 0 + uint256 staticFee = WeightedPool(address(pool)).getStaticSwapFeePercentage(); + (bool ok, uint256 dynFee) = hook.onComputeDynamicSwapFeePercentage(p, address(pool), staticFee); + + assertTrue(ok, "extPx==0 must not block"); + assertEq(dynFee, staticFee, "extPx==0 must return static fee"); + } + + function testFuzz_Fee_ClampsToMax_When_DeviationBeyondCap(bool givenIn, uint64 rawOutHuge) public { + uint256 idxIn = 0; + uint256 idxOut = 1; + uint256 amountGiven = 5e15; + + uint256[] memory balances = new uint256[](2); + balances[0] = 1e18; + balances[1] = 1e18; + + TokenConfig[] memory cfg = new TokenConfig[](2); + LiquidityManagement memory lm; + vm.prank(address(vault)); + hook.onRegister(poolFactory, address(pool), cfg, lm); + + uint32 pairIn = 91001; + uint32 pairOut = 91002; + vm.startPrank(admin); + hook.setTokenPriceConfigIndex(address(pool), uint8(idxIn), pairIn, HL_IDX_SZ_8); + hook.setTokenPriceConfigIndex(address(pool), uint8(idxOut), pairOut, HL_IDX_SZ_8); + + uint256 thr = 1e16; // 1% + uint256 cap = 2e16; // 2% + uint256 max = 15e15; // 1.5% + + hook.setSurgeThresholdPercentage(address(pool), thr, IHyperSurgeHook.TradeType.ARBITRAGE); + hook.setSurgeThresholdPercentage(address(pool), thr, IHyperSurgeHook.TradeType.NOISE); + hook.setCapDeviationPercentage(address(pool), cap, IHyperSurgeHook.TradeType.ARBITRAGE); + hook.setCapDeviationPercentage(address(pool), cap, IHyperSurgeHook.TradeType.NOISE); + hook.setMaxSurgeFeePercentage(address(pool), max, IHyperSurgeHook.TradeType.ARBITRAGE); + hook.setMaxSurgeFeePercentage(address(pool), max, IHyperSurgeHook.TradeType.NOISE); + vm.stopPrank(); + + // External price >> 1.0: + // extPx = (pxOut / pxIn) with same divisor. Set pxOut very large, pxIn = 1 unit. + // Use HL_IDX_SZ_8 (divisor 1e8) so raw numbers are easy: rawIn=1e8, rawOut in [5e9, max]. + rawOutHuge = uint64(bound(uint256(rawOutHuge), 5e9, type(uint64).max)); + _mockHyperSpotPrice(pairIn, uint64(1e8)); + _mockHyperSpotPrice(pairOut, rawOutHuge); + + SwapKind kind = givenIn ? SwapKind.EXACT_IN : SwapKind.EXACT_OUT; + PoolSwapParams memory p = _makeParams(idxIn, idxOut, kind, amountGiven, balances); + + uint256 staticFee = WeightedPool(address(pool)).getStaticSwapFeePercentage(); + (bool ok, uint256 fee) = hook.onComputeDynamicSwapFeePercentage(p, address(pool), staticFee); + + assertTrue(ok, "fee path must not block"); + assertEq(fee, max, "fee must clamp at configured maxPct"); + } + + function testFuzz_Fee_ReturnsStatic_When_DeviationBelowThreshold(bool givenIn, uint64 rawBase) public { + uint256 idxIn = 0; + uint256 idxOut = 1; + uint256 amountGiven = 5e15; + + uint256[] memory balances = new uint256[](2); + balances[0] = 1e18; + balances[1] = 1e18; + + TokenConfig[] memory cfg = new TokenConfig[](2); + LiquidityManagement memory lm; + vm.prank(address(vault)); + hook.onRegister(poolFactory, address(pool), cfg, lm); + + uint32 pairIn = 92001; + uint32 pairOut = 92002; + vm.startPrank(admin); + hook.setTokenPriceConfigIndex(address(pool), uint8(idxIn), pairIn, HL_IDX_SZ_8); + hook.setTokenPriceConfigIndex(address(pool), uint8(idxOut), pairOut, HL_IDX_SZ_8); + + // Set a relatively generous threshold (5%) and a higher cap so we stay in "below threshold" + uint256 thr = 5e16; // 5% + uint256 cap = 20e16; // 20% (arbitrary > thr) + uint256 max = 50e16; // 50% (irrelevant here) + + hook.setSurgeThresholdPercentage(address(pool), thr, IHyperSurgeHook.TradeType.ARBITRAGE); + hook.setSurgeThresholdPercentage(address(pool), thr, IHyperSurgeHook.TradeType.NOISE); + hook.setCapDeviationPercentage(address(pool), cap, IHyperSurgeHook.TradeType.ARBITRAGE); + hook.setCapDeviationPercentage(address(pool), cap, IHyperSurgeHook.TradeType.NOISE); + hook.setMaxSurgeFeePercentage(address(pool), max, IHyperSurgeHook.TradeType.ARBITRAGE); + hook.setMaxSurgeFeePercentage(address(pool), max, IHyperSurgeHook.TradeType.NOISE); + vm.stopPrank(); + + // Make extPx ≈ 1.0 within ~1e-8 relative drift, far below the 5% threshold. + // Same divisor (1e8): extPx = (rawOut/rawIn). Pick rawOut = rawBase + 1, rawIn = rawBase. + rawBase = uint64(bound(uint256(rawBase), 1e8, 5e9)); // ensure > 0 and leaves headroom for +1 + _mockHyperSpotPrice(pairIn, rawBase); + _mockHyperSpotPrice(pairOut, rawBase + 1); + + SwapKind kind = givenIn ? SwapKind.EXACT_IN : SwapKind.EXACT_OUT; + PoolSwapParams memory p = _makeParams(idxIn, idxOut, kind, amountGiven, balances); + + uint256 staticFee = WeightedPool(address(pool)).getStaticSwapFeePercentage(); + (bool ok, uint256 fee) = hook.onComputeDynamicSwapFeePercentage(p, address(pool), staticFee); + + assertTrue(ok, "below-threshold path must not block"); + assertEq(fee, staticFee, "below-threshold deviation must return static fee"); + } function _makeParams( uint256 indexIn, From 4278aed749828b85abaa8c05d471a24b55fe55d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Mon, 15 Sep 2025 20:14:57 -0300 Subject: [PATCH 17/28] Clean test file --- .../test/foundry/HyperSurgeFee.t.sol | 523 +++++++++--------- 1 file changed, 274 insertions(+), 249 deletions(-) diff --git a/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol b/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol index f9046a39..eae4fdfa 100644 --- a/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol +++ b/pkg/pool-hooks/test/foundry/HyperSurgeFee.t.sol @@ -3,46 +3,33 @@ pragma solidity ^0.8.24; import "forge-std/Test.sol"; -// Base test utilities (provides: vault, poocomputeLocals, poolFactory, admin, authorizer, routers, tokens, etc.) -import { BaseVaultTest } from "@balancer-labs/v3-vault/test/foundry/utils/BaseVaultTest.sol"; +import { IAuthentication } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IAuthentication.sol"; +import { IHyperSurgeHook } from "@balancer-labs/v3-interfaces/contracts/pool-hooks/IHyperSurgeHook.sol"; +import { IAuthorizer } from "@balancer-labs/v3-interfaces/contracts/vault/IAuthorizer.sol"; +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol"; import { ArrayHelpers } from "@balancer-labs/v3-solidity-utils/contracts/test/ArrayHelpers.sol"; +import { BaseVaultTest } from "@balancer-labs/v3-vault/test/foundry/utils/BaseVaultTest.sol"; import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; +import { WeightedPool } from "@balancer-labs/v3-pool-weighted/contracts/WeightedPool.sol"; -// Hook interfaces -import { IHyperSurgeHook } from "@balancer-labs/v3-interfaces/contracts/pool-hooks/IHyperSurgeHook.sol"; -import { IAuthentication } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IAuthentication.sol"; -import { IAuthorizer } from "@balancer-labs/v3-interfaces/contracts/vault/IAuthorizer.sol"; - -// Vault interfaces/types -import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; import { - TokenConfig, - LiquidityManagement, - PoolSwapParams, - SwapKind, - PoolRoleAccounts -} from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; - -// Local deployer + mock -import { HyperSurgeHookDeployer } from "./utils/HyperSurgeHookDeployer.sol"; -import { HyperSurgeHookMock } from "../../contracts/test/HyperSurgeHookMock.sol"; -import { HyperSurgeHook } from "../../contracts/hooks-quantamm/HyperSurgeHook.sol"; + WeightedPoolContractsDeployer +} from "@balancer-labs/v3-pool-weighted/test/foundry/utils/WeightedPoolContractsDeployer.sol"; +import { + HypercorePrecompileMock +} from "@balancer-labs/v3-standalone-utils/test/foundry/utils/HypercorePrecompileMock.sol"; import { HyperSpotPricePrecompile } from "@balancer-labs/v3-standalone-utils/contracts/utils/HyperSpotPricePrecompile.sol"; import { HyperTokenInfoPrecompile } from "@balancer-labs/v3-standalone-utils/contracts/utils/HyperTokenInfoPrecompile.sol"; -import { - HypercorePrecompileMock -} from "@balancer-labs/v3-standalone-utils/test/foundry/utils/HypercorePrecompileMock.sol"; - -import { - WeightedPoolContractsDeployer -} from "@balancer-labs/v3-pool-weighted/test/foundry/utils/WeightedPoolContractsDeployer.sol"; -import { WeightedPool } from "@balancer-labs/v3-pool-weighted/contracts/WeightedPool.sol"; +import { HyperSurgeHook } from "../../contracts/hooks-quantamm/HyperSurgeHook.sol"; +import { HyperSurgeHookMock } from "../../contracts/test/HyperSurgeHookMock.sol"; +import { HyperSurgeHookDeployer } from "./utils/HyperSurgeHookDeployer.sol"; contract HLPriceStub { mapping(uint32 => uint32) internal px; // slot 0 @@ -165,12 +152,12 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo using FixedPoint for uint256; using ArrayHelpers for *; - uint256 constant ONE = 1e18; - uint256 constant STATIC_SWAP_FEE = 1e16; // 1% (1e18 scale) + uint256 constant STATIC_SWAP_FEE = 1e16; // 1% + uint256 constant ONE_SCALED_9 = 1e9; + uint256[] FIFTY_FIFTY; // MUST match addresses the hook libs read uint256 internal constant DEFAULT_SWAP_FEE = 1e16; // 1% - uint256 constant FEE_ONE = 1e18; HyperSurgeHookMock internal hook; @@ -185,7 +172,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo IVault(address(vault)), 0.02e18, // default max fee (2%) 0.02e18, // default threshold (2%) - 1e18, + FixedPoint.ONE, string("test") ); @@ -218,6 +205,8 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo IAuthentication(address(hook)).getActionId(IHyperSurgeHook.setTokenPriceConfigIndex.selector), admin ); + + FIFTY_FIFTY = [uint256(50e16), uint256(50e16)].toMemoryArray(); } struct HyperPriceSpotParams { @@ -227,9 +216,9 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo uint256 feeSeed; uint8 outSeed; uint256 n; - uint256 maxPct; - uint256 thr; - uint256 cap; + uint32 maxPct9; + uint32 thr9; + uint32 cap9; uint8 indexIn; uint8 indexOut; uint32 pairIdx; @@ -263,23 +252,43 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo assertTrue(hook.onRegister(poolFactory, address(pool), cfg, lm), "onRegister failed"); // --- fee knobs (1e9) - params.maxPct = bound(feeSeed, 3, 1e9); - params.thr = params.maxPct / 3; - params.cap = params.thr + (1e9 - params.thr) / 2; - if (params.cap == params.thr) params.cap = params.thr + 1; + params.maxPct9 = uint32(bound(feeSeed, 3, ONE_SCALED_9)); + params.thr9 = uint32(params.maxPct9 / 3); + params.cap9 = uint32(params.thr9 + (ONE_SCALED_9 - params.thr9) / 2); + if (params.cap9 == params.thr9) params.cap9 = params.thr9 + 1; // --- make NOISE lane different (keep maxPct same so staticFee bound remains valid) - uint256 noiseThr = (params.thr + 2 < params.cap) ? (params.thr + 1) : (params.thr - 1); - uint256 noiseCap = params.cap; + uint32 noiseThr9 = (params.thr9 + 2 < params.cap9) ? (params.thr9 + 1) : (params.thr9 - 1); + uint32 noiseCap9 = params.cap9; vm.startPrank(admin); - hook.setMaxSurgeFeePercentage(address(pool), params.maxPct * 1e9, IHyperSurgeHook.TradeType.ARBITRAGE); - hook.setSurgeThresholdPercentage(address(pool), params.thr * 1e9, IHyperSurgeHook.TradeType.ARBITRAGE); - hook.setCapDeviationPercentage(address(pool), params.cap * 1e9, IHyperSurgeHook.TradeType.ARBITRAGE); + hook.setMaxSurgeFeePercentage( + address(pool), + _convertTo18Decimals(params.maxPct9), + IHyperSurgeHook.TradeType.ARBITRAGE + ); + hook.setSurgeThresholdPercentage( + address(pool), + _convertTo18Decimals(params.thr9), + IHyperSurgeHook.TradeType.ARBITRAGE + ); + hook.setCapDeviationPercentage( + address(pool), + _convertTo18Decimals(params.cap9), + IHyperSurgeHook.TradeType.ARBITRAGE + ); - hook.setMaxSurgeFeePercentage(address(pool), params.maxPct * 1e9, IHyperSurgeHook.TradeType.NOISE); - hook.setSurgeThresholdPercentage(address(pool), noiseThr * 1e9, IHyperSurgeHook.TradeType.NOISE); - hook.setCapDeviationPercentage(address(pool), noiseCap * 1e9, IHyperSurgeHook.TradeType.NOISE); + hook.setMaxSurgeFeePercentage( + address(pool), + _convertTo18Decimals(params.maxPct9), + IHyperSurgeHook.TradeType.NOISE + ); + hook.setSurgeThresholdPercentage( + address(pool), + _convertTo18Decimals(noiseThr9), + IHyperSurgeHook.TradeType.NOISE + ); + hook.setCapDeviationPercentage(address(pool), _convertTo18Decimals(noiseCap9), IHyperSurgeHook.TradeType.NOISE); vm.stopPrank(); // --- configure external price sources for the two indices we’ll swap @@ -298,7 +307,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo // --- balancesScaled18 with length N (simple increasing balances) uint256[] memory balances = new uint256[](params.n); for (uint256 i = 0; i < params.n; ++i) { - balances[i] = 1e18 * (i + 1); + balances[i] = FixedPoint.ONE * (i + 1); } // --- build PoolSwapParams (EXACT_IN: 0 -> indexOut) @@ -309,13 +318,13 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo p.indexOut = params.indexOut; // bound amountIn to strictly inside the 30% guard - params.MAX_RATIO = 30e16; // 30% in 1e18 + params.MAX_RATIO = 30e16; // 30% params.maxIn = balances[p.indexIn].mulDown(params.MAX_RATIO); if (params.maxIn > 0) params.maxIn -= 1; p.amountGivenScaled18 = bound(amtSeed, 1, params.maxIn == 0 ? 1 : params.maxIn); // static fee (1e9) bounded to maxPct - params.staticFee = bound(feeSeed % 1e9, 0, params.maxPct); + params.staticFee = bound(feeSeed % ONE_SCALED_9, 0, params.maxPct9); // --- compute dynamic fee via hook vm.startPrank(address(vault)); @@ -324,7 +333,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo assertTrue(ok, "compute fee should succeed"); // returned value is in 1e9 scale here (hook keeps pct in 1e9) - assertLe(dyn, 1e18, "fee must be <= 100% (1e9)"); + assertLe(dyn, FixedPoint.ONE, "fee must be <= 100% (1e9)"); assertGe(dyn, params.staticFee, "dyn fee >= static fee"); } @@ -353,23 +362,43 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo assertTrue(hook.onRegister(poolFactory, address(pool), cfg, lm), "onRegister failed"); // --- fee knobs (1e9) - params.maxPct = bound(feeSeed, 3, 1e9); - params.thr = params.maxPct / 3; - params.cap = params.thr + (1e9 - params.thr) / 2; - if (params.cap == params.thr) params.cap = params.thr + 1; + params.maxPct9 = uint32(bound(feeSeed, 3, ONE_SCALED_9)); + params.thr9 = uint32(params.maxPct9 / 3); + params.cap9 = uint32(params.thr9 + (ONE_SCALED_9 - params.thr9) / 2); + if (params.cap9 == params.thr9) params.cap9 = params.thr9 + 1; // --- make NOISE lane different (keep maxPct same so staticFee bound remains valid) - uint256 noiseThr = (params.thr + 2 < params.cap) ? (params.thr + 1) : (params.thr - 1); - uint256 noiseCap = params.cap; + uint32 noiseThr9 = (params.thr9 + 2 < params.cap9) ? (params.thr9 + 1) : (params.thr9 - 1); + uint32 noiseCap9 = params.cap9; vm.startPrank(admin); - hook.setMaxSurgeFeePercentage(address(pool), params.maxPct * 1e9, IHyperSurgeHook.TradeType.ARBITRAGE); - hook.setSurgeThresholdPercentage(address(pool), params.thr * 1e9, IHyperSurgeHook.TradeType.ARBITRAGE); - hook.setCapDeviationPercentage(address(pool), params.cap * 1e9, IHyperSurgeHook.TradeType.ARBITRAGE); + hook.setMaxSurgeFeePercentage( + address(pool), + _convertTo18Decimals(params.maxPct9), + IHyperSurgeHook.TradeType.ARBITRAGE + ); + hook.setSurgeThresholdPercentage( + address(pool), + _convertTo18Decimals(params.thr9), + IHyperSurgeHook.TradeType.ARBITRAGE + ); + hook.setCapDeviationPercentage( + address(pool), + _convertTo18Decimals(params.cap9), + IHyperSurgeHook.TradeType.ARBITRAGE + ); - hook.setMaxSurgeFeePercentage(address(pool), params.maxPct * 1e9, IHyperSurgeHook.TradeType.NOISE); - hook.setSurgeThresholdPercentage(address(pool), noiseThr * 1e9, IHyperSurgeHook.TradeType.NOISE); - hook.setCapDeviationPercentage(address(pool), noiseCap * 1e9, IHyperSurgeHook.TradeType.NOISE); + hook.setMaxSurgeFeePercentage( + address(pool), + _convertTo18Decimals(params.maxPct9), + IHyperSurgeHook.TradeType.NOISE + ); + hook.setSurgeThresholdPercentage( + address(pool), + _convertTo18Decimals(noiseThr9), + IHyperSurgeHook.TradeType.NOISE + ); + hook.setCapDeviationPercentage(address(pool), _convertTo18Decimals(noiseCap9), IHyperSurgeHook.TradeType.NOISE); vm.stopPrank(); // --- configure price only for the two indices we use @@ -388,7 +417,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo // --- balancesScaled18 length N uint256[] memory balances = new uint256[](params.n); for (uint256 i = 0; i < params.n; ++i) { - balances[i] = 1e18 * (i + 1); + balances[i] = FixedPoint.ONE * (i + 1); } // --- build PoolSwapParams (EXACT_OUT: 0 -> indexOut) @@ -407,14 +436,14 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo p.amountGivenScaled18 = bound(amtSeed, 1, params.maxIn == 0 ? 1 : params.maxIn); // for EXACT_OUT this is amountOut // static fee (1e9) - params.staticFee = bound(feeSeed % 1e9, 0, params.maxPct); + params.staticFee = bound(feeSeed % ONE_SCALED_9, 0, params.maxPct9); vm.startPrank(address(vault)); (bool ok, uint256 dyn) = hook.onComputeDynamicSwapFeePercentage(p, address(pool), params.staticFee); vm.stopPrank(); assertTrue(ok, "compute fee should succeed"); - assertLe(dyn, 1e18, "fee must be <= 100% (1e9)"); + assertLe(dyn, FixedPoint.ONE, "fee must be <= 100% (1e9)"); assertGe(dyn, params.staticFee, "dyn fee >= static fee"); } @@ -434,9 +463,9 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo uint256 maxIn; bool ok; uint256 dyn; - uint256 max9; - uint256 thr9; - uint256 cap9; + uint32 max9; + uint32 thr9; + uint32 cap9; uint256 capRoom; uint256 staticSeed; uint256 i; @@ -462,19 +491,19 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo // 3) Build VALID lane params (9), then upscale ONCE to 18dp // max9 ∈ [1..1e9], thr9 ∈ [1..max9], cap9 ∈ (thr9..1e9] - locals.max9 = 1 + (marker % 1_000_000_000); // avoid 0 - locals.thr9 = 1 + ((marker >> 8) % locals.max9); // greater than or equal to1 and less than or equal to max9 - locals.capRoom = 1_000_000_000 - locals.thr9; // room above thr - locals.cap9 = locals.thr9 + 1; // strictly > thr + locals.max9 = uint32(1 + (marker % ONE_SCALED_9)); // avoid 0 + locals.thr9 = uint32(1 + ((marker >> 8) % locals.max9)); // greater than or equal to1 and less than or equal to max9 + locals.capRoom = ONE_SCALED_9 - locals.thr9; // room above thr + locals.cap9 = uint32(locals.thr9 + 1); // strictly > thr if (locals.capRoom > 0) { - locals.cap9 = locals.thr9 + 1 + ((marker >> 16) % locals.capRoom); // (thr9, 1e9] + locals.cap9 = uint32(locals.thr9 + 1 + ((marker >> 16) % locals.capRoom)); // (thr9, 1e9] } - if (locals.cap9 > 1_000_000_000) locals.cap9 = 1_000_000_000; // clamp just in case + if (locals.cap9 > ONE_SCALED_9) locals.cap9 = uint32(ONE_SCALED_9); // clamp just in case // Upscale once to 18dp - locals.maxPct = locals.max9 * 1e9; - locals.thr = locals.thr9 * 1e9; - locals.cap = locals.cap9 * 1e9; + locals.maxPct = _convertTo18Decimals(locals.max9); + locals.thr = _convertTo18Decimals(locals.thr9); + locals.cap = _convertTo18Decimals(locals.cap9); // static fee (18dp) ∈ [0..maxPct18] uint256 staticSeed = (uint256(keccak256(abi.encodePacked(marker))) << 32) | marker; @@ -508,7 +537,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo // 5) Balances array of length N (ascending 1e18, 2e18, ...) locals.balances = new uint256[](locals.n); for (locals.i = 0; locals.i < locals.n; ++locals.i) { - locals.balances[locals.i] = 1e18 * (locals.i + 1); + locals.balances[locals.i] = FixedPoint.ONE * (locals.i + 1); } // 6) Build swap params (EXACT_IN), amount within 30% guard @@ -518,7 +547,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo p.indexIn = locals.indexIn; p.indexOut = locals.indexOut; - locals.maxRatio = 30e16; // 30% in 1e18 basis + locals.maxRatio = 30e16; // 30% locals.maxIn = locals.balances[p.indexIn].mulDown(locals.maxRatio); if (locals.maxIn > 0) { locals.maxIn -= 1; @@ -716,7 +745,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo (locals.oraclePrice) = fee_computeOraclePriceForDeviation(locals.P, locals.D); // Scale factor k and a base amount small relative to balances to avoid overflow - locals.scaleSeed = 1 + (uint256(scaleSeed) % 1_000_000_000); // k in [1 .. 1e9] + locals.scaleSeed = 1 + (uint256(scaleSeed) % ONE_SCALED_9); // k in [1 .. 1e9] locals.bMin = locals.b[locals.i] < locals.b[locals.j] ? locals.b[locals.i] : locals.b[locals.j]; // base amount ~ bMin / 1e12 (but at least 1 wei); keeps amount*k << 2^256 @@ -999,13 +1028,33 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo // Diverge NOISE and ARB lane params (authorized admin) vm.startPrank(admin); - hook.setSurgeThresholdPercentage(address(pool), 5_000_000 * 1e9, IHyperSurgeHook.TradeType.NOISE); // 0.5% - hook.setCapDeviationPercentage(address(pool), 400_000_000 * 1e9, IHyperSurgeHook.TradeType.NOISE); // 40% - hook.setMaxSurgeFeePercentage(address(pool), 25_000_000 * 1e9, IHyperSurgeHook.TradeType.NOISE); // 2.5% - - hook.setSurgeThresholdPercentage(address(pool), 1_000_000 * 1e9, IHyperSurgeHook.TradeType.ARBITRAGE); // 0.1% - hook.setCapDeviationPercentage(address(pool), 300_000_000 * 1e9, IHyperSurgeHook.TradeType.ARBITRAGE); // 30% - hook.setMaxSurgeFeePercentage(address(pool), 50_000_000 * 1e9, IHyperSurgeHook.TradeType.ARBITRAGE); // 5% + hook.setSurgeThresholdPercentage( + address(pool), + _convertTo18Decimals(5_000_000), + IHyperSurgeHook.TradeType.NOISE + ); // 0.5% + hook.setCapDeviationPercentage( + address(pool), + _convertTo18Decimals(400_000_000), + IHyperSurgeHook.TradeType.NOISE + ); // 40% + hook.setMaxSurgeFeePercentage(address(pool), _convertTo18Decimals(25_000_000), IHyperSurgeHook.TradeType.NOISE); // 2.5% + + hook.setSurgeThresholdPercentage( + address(pool), + _convertTo18Decimals(1_000_000), + IHyperSurgeHook.TradeType.ARBITRAGE + ); // 0.1% + hook.setCapDeviationPercentage( + address(pool), + _convertTo18Decimals(300_000_000), + IHyperSurgeHook.TradeType.ARBITRAGE + ); // 30% + hook.setMaxSurgeFeePercentage( + address(pool), + _convertTo18Decimals(50_000_000), + IHyperSurgeHook.TradeType.ARBITRAGE + ); // 5% vm.stopPrank(); // Adapt to the pool’s true size to avoid OOB / shape mismatches @@ -1019,7 +1068,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo balances[k] = 1e24 + k; } - PoolSwapParams memory p = _createPoolSwapParams(SwapKind.EXACT_IN, balances, 0, 1, 1e18); + PoolSwapParams memory p = _createPoolSwapParams(SwapKind.EXACT_IN, balances, 0, 1, FixedPoint.ONE); // EXACT_IN: either revert or static fee (but never a computed dynamic fee) vm.expectRevert(HyperSpotPricePrecompile.SpotPriceIsZero.selector); @@ -1039,7 +1088,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo // 9 lane params (contract upscales to 18dp) uint32 thr9 = 100_000_000; // 10% uint32 cap9 = 500_000_000; // 50% - uint32 max9 = uint32(maxFee / 1e9); + uint32 max9 = uint32(maxFee / ONE_SCALED_9); // set both lanes the same (lane choice irrelevant for this edge) HyperSurgeHook.PoolDetails memory poolDetails; @@ -1070,7 +1119,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo uint32 thr9 = 100_000_000; // 10% uint32 cap9 = 500_000_000; // 50% - uint32 max9 = uint32(maxFee / 1e9); + uint32 max9 = uint32(maxFee / ONE_SCALED_9); HyperSurgeHook.PoolDetails memory poolDetails; poolDetails.noiseThresholdPercentage9 = thr9; @@ -1108,7 +1157,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo uint32 thr9 = 50_000_000; // 5% uint32 cap9 = 250_000_000; // 25% - uint32 max9 = uint32(maxFee / 1e9); + uint32 max9 = uint32(maxFee / ONE_SCALED_9); HyperSurgeHook.PoolDetails memory poolDetails; poolDetails.noiseThresholdPercentage9 = thr9; @@ -1148,7 +1197,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo uint256 maxFee = 0.2e16; // 0.2% -> lower than static uint32 thr9 = 100_000_000; // 10% uint32 cap9 = 300_000_000; // 30% - uint32 max9 = uint32(maxFee / 1e9); + uint32 max9 = uint32(maxFee / ONE_SCALED_9); // Local mock (don’t rely on global `hook`) HyperSurgeHookMock mock = new HyperSurgeHookMock( @@ -1182,10 +1231,9 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo 1, 0 ); - uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); vm.expectRevert(stdError.arithmeticError); - mock.ComputeSurgeFee(p, poolDetails, staticFee, weights, 0, oraclePrice); + mock.ComputeSurgeFee(p, poolDetails, staticFee, FIFTY_FIFTY, 0, oraclePrice); // ---------- (b) thr < dev < cap -> revert (underflow in mock ramp) ---------- uint256 deviationBetweenThrAndCap = _convertTo18Decimals(thr9 + (cap9 - thr9) / 3); // strictly between thr & cap @@ -1198,7 +1246,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo ); vm.expectRevert(stdError.arithmeticError); - mock.ComputeSurgeFee(p, poolDetails, staticFee, weights, 0, oraclePrice); + mock.ComputeSurgeFee(p, poolDetails, staticFee, FIFTY_FIFTY, 0, oraclePrice); } struct OutsideDynamicAfterLocals { @@ -1230,8 +1278,8 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo // --- Fuzz + bounds --- uint256 oraclePrice = bound(eSeed, 1e16, 1e24); locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); - locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); - locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, ONE_SCALED_9)); + locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / ONE_SCALED_9), ONE_SCALED_9)); // ARB lane (unused here, but keep distinct) locals.arbThr9 = 1_000_000; locals.arbCap9 = 300_000_000; @@ -1242,10 +1290,9 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; // strictly outside // Start BELOW E: price_before = E * (1 - deviationBefore) - locals.price_before = oraclePrice - (oraclePrice * locals.deviationBefore) / 1e18; + locals.price_before = oraclePrice.mulDown(FixedPoint.ONE - locals.deviationBefore); // Build compute locals + swap that worsens deviation (EXACT_IN; calc=0 → P decreases further) - uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.price_before].toMemoryArray(); HyperSurgeHook.PoolDetails memory poolDetails; @@ -1263,7 +1310,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo balancesScaled18, 0, 1, - bound(uint256(amtSeed), 1e9, 5e17) + bound(uint256(amtSeed), ONE_SCALED_9, 5e17) ); // Expected (NOISE) uses AFTER deviation: price_after = price_before / (1 + x) @@ -1285,7 +1332,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo "logic-1" ); - (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, oraclePrice); + (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, FIFTY_FIFTY, 0, oraclePrice); // Small error expected due to precision loss. assertEq(locals.dyn, locals.expected, "noise path must use AFTER deviation for dynamic fee"); @@ -1321,33 +1368,32 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo // --- Fuzz + bounds --- uint256 oraclePrice = bound(eSeed, 1e16, 1e24); locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); - locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); - locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, ONE_SCALED_9)); + locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / ONE_SCALED_9), ONE_SCALED_9)); // NOISE lane different (unused in assertion) locals.noiseThr9 = 5_000_000; locals.noiseCap9 = 400_000_000; locals.noiseMax9 = 25_000_000; - locals.thr = uint256(locals.arbThr9) * 1e9; - locals.cap = uint256(locals.arbCap9) * 1e9; + locals.thr = _convertTo18Decimals(locals.arbThr9); + locals.cap = _convertTo18Decimals(locals.arbCap9); locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; // strictly outside // Start ABOVE E - locals.price_before = oraclePrice + (oraclePrice * locals.deviationBefore) / 1e18; + locals.price_before = oraclePrice.mulDown(FixedPoint.ONE + locals.deviationBefore); // Compute xMax to remain outside after: price_after >= E*(1 + thr) // price_after = price_before / (1 + x) means x less than or equal to (price_before / (E*(1+thr)) - 1) * 1e18 - vm.assume(oraclePrice * (1e18 + locals.thr) != 0); // defensive - uint256 denom = (oraclePrice * (1e18 + locals.thr)) / 1e18; + vm.assume(oraclePrice * (FixedPoint.ONE + locals.thr) != 0); // defensive + uint256 denom = oraclePrice.mulDown(FixedPoint.ONE + locals.thr); vm.assume(denom != 0); - uint256 ratio = (locals.price_before * 1e18) / denom; - vm.assume(ratio > 1e18); // Ensure room to remain outside - locals.xMax = ratio - 1e18; + uint256 ratio = locals.price_before.divDown(denom); + vm.assume(ratio > FixedPoint.ONE); // Ensure room to remain outside + locals.xMax = ratio - FixedPoint.ONE; if (locals.xMax > 9e17) { locals.xMax = 9e17; } // clamp - uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.price_before].toMemoryArray(); HyperSurgeHook.PoolDetails memory poolDetails; @@ -1385,7 +1431,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo "logic-2" ); - (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, oraclePrice); + (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, FIFTY_FIFTY, 0, oraclePrice); // Still outside afterward (sanity) locals.price_after = locals.price_before.divDown(FixedPoint.ONE + p.amountGivenScaled18); @@ -1428,26 +1474,25 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo NoiseWorsensInsideButStaysInsideLocals memory locals; locals.oraclePrice = bound(eSeed, 1e16, 1e24); - locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 1_000_000_000 - 1)); // (0,1) - locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); - locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, ONE_SCALED_9 - 1)); // (0,1) + locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, ONE_SCALED_9)); + locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / ONE_SCALED_9), ONE_SCALED_9)); locals.arbThr9 = 1_000_000; locals.arbCap9 = 300_000_000; locals.arbMax9 = 50_000_000; - locals.thr = uint256(locals.noiseThr9) * 1e9; + locals.thr = _convertTo18Decimals(locals.noiseThr9); locals.deviationBefore = locals.thr / 4 + 1; locals.price_before = locals.oraclePrice.mulDown(FixedPoint.ONE - locals.deviationBefore); locals.R1e18 = locals.price_before.divDown(locals.oraclePrice); - locals.denom = 1e18 - locals.thr; - locals.q = (locals.R1e18 * 1e18) / locals.denom; - locals.xMax = locals.q > 1e18 ? (locals.q - 1e18) : 0; + locals.denom = FixedPoint.ONE - locals.thr; + locals.q = locals.R1e18.divDown(locals.denom); + locals.xMax = locals.q > FixedPoint.ONE ? (locals.q - FixedPoint.ONE) : 0; if (locals.xMax > 5e17) { locals.xMax = 5e17; } - uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.price_before].toMemoryArray(); HyperSurgeHook.PoolDetails memory poolDetails; @@ -1461,7 +1506,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo // Ensure a *measurable* worsening so NOISE is chosen: // pick x with a lower floor (e.g., 1e9 wei) but never exceed xMax. - uint256 lo = 1e9; // 1e-9 in t; safely above Q18 rounding noise + uint256 lo = ONE_SCALED_9; // 1e-9 in t; safely above Q18 rounding noise uint256 hi = locals.xMax; if (hi < lo) { lo = 1; @@ -1485,7 +1530,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo _convertTo18Decimals(locals.arbCap9), "logic-3" ); - (, locals.fee) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, locals.oraclePrice); + (, locals.fee) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, FIFTY_FIFTY, 0, locals.oraclePrice); // Sanity: still inside after worsening locals.price_after = locals.price_before.divDown(FixedPoint.ONE + p.amountGivenScaled18); @@ -1543,23 +1588,22 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo // Keep thr < 1 so denominators stay positive and bands are non-degenerate locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); // (0, 0.9) - locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); // (thr, 1] - locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, ONE_SCALED_9)); // (thr, 1] + locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / ONE_SCALED_9), ONE_SCALED_9)); // ARB lane different (unused in assertion) locals.arbThr9 = 1_000_000; locals.arbCap9 = 300_000_000; locals.arbMax9 = 50_000_000; - locals.thr = uint256(locals.noiseThr9) * 1e9; - locals.cap = uint256(locals.noiseCap9) * 1e9; + locals.thr = _convertTo18Decimals(locals.noiseThr9); + locals.cap = _convertTo18Decimals(locals.noiseCap9); // Start ABOVE E with a deviation strictly outside the threshold: locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 4; - locals.price_before = locals.oraclePrice + (locals.oraclePrice * locals.deviationBefore) / 1e18; + locals.price_before = locals.oraclePrice.mulDown(FixedPoint.ONE + locals.deviationBefore); // Build compute locals - uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.price_before].toMemoryArray(); HyperSurgeHook.PoolDetails memory poolDetails; @@ -1577,8 +1621,8 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo // 1 - R/(1+t) > Db means (1 - Db)(1 + t) > 1 + Db means t > 2Db/(1 - Db) locals.tCross = locals.deviationBefore; // tWorse = ceil( (2*Db) / (1 - Db) ) in Q18 - locals.num = (2 * locals.deviationBefore) * 1e18; // Q36 - locals.den = 1e18 - locals.deviationBefore; + locals.num = (2 * locals.deviationBefore) * FixedPoint.ONE; // Q36 + locals.den = FixedPoint.ONE - locals.deviationBefore; locals.q = (locals.num + locals.den - 1) / locals.den; // ceilDiv -> Q18 locals.tWorse = locals.q; @@ -1632,7 +1676,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo _convertTo18Decimals(locals.arbCap9), "logic-4" ); - (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, locals.oraclePrice); + (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, FIFTY_FIFTY, 0, locals.oraclePrice); assertEq( locals.dyn, @@ -1682,37 +1726,37 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo locals.oraclePrice = bound(eSeed, 1e16, 1e24); // Keep thr strictly < 1e9 so (1e18 - thr) > 0 locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); - locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); - locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, ONE_SCALED_9)); + locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / ONE_SCALED_9), ONE_SCALED_9)); // NOISE lane can be anything different; not used by this assertion locals.noiseThr9 = 5_000_000; locals.noiseCap9 = 400_000_000; locals.noiseMax9 = 25_000_000; - locals.thr = uint256(locals.arbThr9) * 1e9; - locals.cap = uint256(locals.arbCap9) * 1e9; + locals.thr = _convertTo18Decimals(locals.arbThr9); + locals.cap = _convertTo18Decimals(locals.arbCap9); // Start ABOVE E with an outside deviation deviationBefore > thr locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; // strictly outside - locals.price_before = locals.oraclePrice + (locals.oraclePrice * locals.deviationBefore) / 1e18; // price_before = E * (1 + deviationBefore) - locals.R1e18 = (locals.price_before * 1e18) / locals.oraclePrice; // R = 1e18 + deviationBefore + locals.price_before = locals.oraclePrice.mulDown(FixedPoint.ONE + locals.deviationBefore); // price_before = E * (1 + deviationBefore) + locals.R1e18 = locals.price_before.divDown(locals.oraclePrice); // R = 1e18 + deviationBefore // Two-sided “inside” band: 1 − thr less than or equal to price_after/E less than or equal to 1 + thr, // with price_after/E = R / (1 + t), t = x / 1e18. // Lower bound on t (bring down to less than or equal to 1+thr): // t greater than or equal to R/(1+thr) − 1 means xLower = ceil( (R1e18 * 1e18) / (1e18 + thr) ) − 1e18 - locals.denomPlus = 1e18 + locals.thr; - locals.numPlus = locals.R1e18 * 1e18; // Q36 + locals.denomPlus = FixedPoint.ONE + locals.thr; + locals.numPlus = locals.R1e18 * FixedPoint.ONE; // Q36 locals.qPlus = (locals.numPlus + locals.denomPlus - 1) / locals.denomPlus; // ceilDiv to Q18 - locals.xLower = locals.qPlus > 1e18 ? (locals.qPlus - 1e18) : 0; + locals.xLower = locals.qPlus > FixedPoint.ONE ? (locals.qPlus - FixedPoint.ONE) : 0; // Upper bound on t (don’t overshoot below 1 − thr): // t less than or equal to R/(1−thr) − 1 means xUpper = floor( (R1e18 * 1e18) / (1e18 − thr) ) − 1e18 - locals.denomMinus = 1e18 - locals.thr; // > 0 by bound - locals.numMinus = locals.R1e18 * 1e18; // Q36 + locals.denomMinus = FixedPoint.ONE - locals.thr; // > 0 by bound + locals.numMinus = locals.R1e18 * FixedPoint.ONE; // Q36 locals.qMinus = locals.numMinus / locals.denomMinus; // floorDiv to Q18 - locals.xUpper = locals.qMinus > 1e18 ? (locals.qMinus - 1e18) : 0; + locals.xUpper = locals.qMinus > FixedPoint.ONE ? (locals.qMinus - FixedPoint.ONE) : 0; // Choose x inside [xLower, xUpper] using bound (no vm.assume). Collapse if inverted. uint256 lo = locals.xLower; @@ -1725,7 +1769,6 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo if (hi < lo) hi = lo; // Build compute locals - uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.price_before].toMemoryArray(); HyperSurgeHook.PoolDetails memory poolDetails; @@ -1762,7 +1805,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo _convertTo18Decimals(locals.arbCap9), "logic-5" ); - (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, locals.oraclePrice); + (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, FIFTY_FIFTY, 0, locals.oraclePrice); // Sanity: end is inside (two-sided) locals.price_after = locals.price_before.divDown(FixedPoint.ONE + p.amountGivenScaled18); @@ -1819,26 +1862,26 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo // Lane params (NOISE fuzzed, ARB fixed and different) locals.oraclePrice = bound(eSeed, 1e16, 1e24); locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); - locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); - locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, ONE_SCALED_9)); + locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / ONE_SCALED_9), ONE_SCALED_9)); locals.arbThr9 = 1_000_000; locals.arbCap9 = 300_000_000; locals.arbMax9 = 50_000_000; - locals.thr = uint256(locals.noiseThr9) * 1e9; - locals.cap = uint256(locals.noiseCap9) * 1e9; + locals.thr = _convertTo18Decimals(locals.noiseThr9); + locals.cap = _convertTo18Decimals(locals.noiseCap9); // Start BELOW E but inside: deviationBefore ∈ [0, thr) locals.deviationBefore = (locals.thr / 3) + 1; // safely inside - locals.priceBefore = locals.oraclePrice - (locals.oraclePrice * locals.deviationBefore) / 1e18; // P/E = 1 - deviationBefore - locals.R1e18 = (locals.priceBefore * 1e18) / locals.oraclePrice; + locals.priceBefore = locals.oraclePrice.mulDown(FixedPoint.ONE - locals.deviationBefore); // P/E = 1 - deviationBefore + locals.R1e18 = locals.priceBefore.divDown(locals.oraclePrice); // Need priceAfter/E less than or equal to 1 - thr ⇒ t greater than or equal to R/(1 - thr) - 1 - locals.num = locals.R1e18 * 1e18; // Q36 - locals.den = 1e18 - locals.thr; + locals.num = locals.R1e18 * FixedPoint.ONE; // Q36 + locals.den = FixedPoint.ONE - locals.thr; locals.q = (locals.num + locals.den - 1) / locals.den; // ceilDiv → Q18 - locals.tLower = locals.q > 1e18 ? (locals.q - 1e18) : 0; + locals.tLower = locals.q > FixedPoint.ONE ? (locals.q - FixedPoint.ONE) : 0; // Pick x greater than or equal to tLower (plus small epsilon) to cross outside locals.eps = 1e12; @@ -1847,7 +1890,6 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo locals.hi = locals.lo + 5e17; // allow up to +0.5 in t // Build locals - uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.priceBefore].toMemoryArray(); HyperSurgeHook.PoolDetails memory poolDetails; @@ -1891,7 +1933,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo _convertTo18Decimals(locals.arbCap9), "lane-inside2outside" ); - (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, locals.oraclePrice); + (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, FIFTY_FIFTY, 0, locals.oraclePrice); assertEq(locals.dyn, locals.expected, "noise/after: dynamic fee must match expected"); assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee greater than or equal to static"); @@ -1938,42 +1980,42 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo locals.oraclePrice = bound(eSeed, 1e16, 1e24); locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); - locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); - locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, ONE_SCALED_9)); + locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / ONE_SCALED_9), ONE_SCALED_9)); // Distinct NOISE lane (unused in expected but kept different) locals.noiseThr9 = 5_000_000; locals.noiseCap9 = 400_000_000; locals.noiseMax9 = 25_000_000; - locals.thr = uint256(locals.arbThr9) * 1e9; - locals.cap = uint256(locals.arbCap9) * 1e9; + locals.thr = _convertTo18Decimals(locals.arbThr9); + locals.cap = _convertTo18Decimals(locals.arbCap9); // Start ABOVE, outside locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; // strictly outside - locals.priceBefore = locals.oraclePrice + (locals.oraclePrice * locals.deviationBefore) / 1e18; + locals.priceBefore = locals.oraclePrice.mulDown(FixedPoint.ONE + locals.deviationBefore); // R = priceBefore / E in Q18; compute both ceil and floor variants to bound tightly // R_up = ceil( (priceBefore * 1e18) / E ) // R_down = floor( (priceBefore * 1e18) / E ) - uint256 numR = locals.priceBefore * 1e18; + uint256 numR = locals.priceBefore * FixedPoint.ONE; locals.R1e18 = (numR + locals.oraclePrice - 1) / locals.oraclePrice; // We need 1 - thr less than or equal to priceAfter/E less than or equal to 1 + thr, and priceAfter/E = R / (1 + t), with t = x/1e18 (Q18). // Lower bound on t (to get under the upper edge 1 + thr): // t ≥ R/(1 + thr) − 1 // Use R_up and ceil-div to be conservative, then subtract 1e18. - locals.denomPlus = 1e18 + locals.thr; - locals.numPlus = locals.R1e18 * 1e18; // Q36 + locals.denomPlus = FixedPoint.ONE + locals.thr; + locals.numPlus = locals.R1e18 * FixedPoint.ONE; // Q36 locals.qPlus = (locals.numPlus + locals.denomPlus - 1) / locals.denomPlus; // ceilDiv → Q18 - locals.tLower = locals.qPlus > 1e18 ? (locals.qPlus - 1e18) : 0; + locals.tLower = locals.qPlus > FixedPoint.ONE ? (locals.qPlus - FixedPoint.ONE) : 0; // Upper bound on t (don’t drop below the lower edge 1 − thr): // t less than or equal to R/(1 − thr) − 1 // Use R_down and floor-div to be conservative, then subtract 1e18. - locals.denomMinus = 1e18 - locals.thr; - locals.numMinus = locals.R1e18 * 1e18; // Q36 + locals.denomMinus = FixedPoint.ONE - locals.thr; + locals.numMinus = locals.R1e18 * FixedPoint.ONE; // Q36 locals.qMinus = locals.numMinus / locals.denomMinus; // floorDiv → Q18 - locals.tUpper = locals.qMinus > 1e18 ? (locals.qMinus - 1e18) : 0; + locals.tUpper = locals.qMinus > FixedPoint.ONE ? (locals.qMinus - FixedPoint.ONE) : 0; // Choose t inside [tLower + eps, tUpper − eps] and map amtSeed with bound(...). // eps helps avoid equality-edge flips due to integer rounding. @@ -1991,7 +2033,6 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo } // Build locals - uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.priceBefore].toMemoryArray(); HyperSurgeHook.PoolDetails memory poolDetails; @@ -2037,7 +2078,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo _convertTo18Decimals(locals.arbCap9), "lane-out2thr" ); - (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, locals.oraclePrice); + (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, FIFTY_FIFTY, 0, locals.oraclePrice); assertEq( locals.dyn, @@ -2073,22 +2114,21 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo locals.oraclePrice = bound(eSeed, 1e16, 1e24); locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); - locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); - locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, ONE_SCALED_9)); + locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / ONE_SCALED_9), ONE_SCALED_9)); // NOISE lane different (unused) locals.noiseThr9 = 5_000_000; locals.noiseCap9 = 400_000_000; locals.noiseMax9 = 25_000_000; - locals.thr = uint256(locals.arbThr9) * 1e9; - locals.cap = uint256(locals.arbCap9) * 1e9; + locals.thr = _convertTo18Decimals(locals.arbThr9); + locals.cap = _convertTo18Decimals(locals.arbCap9); // Start ABOVE, outside locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; - locals.priceBefore = locals.oraclePrice + (locals.oraclePrice * locals.deviationBefore) / 1e18; + locals.priceBefore = locals.oraclePrice.mulDown(FixedPoint.ONE + locals.deviationBefore); // No movement: amount = 0, so deviationAfter == deviationBefore → ARB path - uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.priceBefore].toMemoryArray(); HyperSurgeHook.PoolDetails memory poolDetails; @@ -2118,7 +2158,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo _convertTo18Decimals(locals.arbCap9), "lane-nomove-outside" ); - (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, locals.oraclePrice); + (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, FIFTY_FIFTY, 0, locals.oraclePrice); assertEq(locals.dyn, locals.expected, "no-move/outside must be ARB, dynamic from BEFORE"); assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee greater than or equal to static"); @@ -2148,21 +2188,20 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo ArbNoMoveInsideLocals memory locals; locals.oraclePrice = bound(eSeed, 1e16, 1e24); - locals.arbThr9 = uint32(bound(arbThrSeed, 1, 1_000_000_000 - 1)); - locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000)); - locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + locals.arbThr9 = uint32(bound(arbThrSeed, 1, ONE_SCALED_9 - 1)); + locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, ONE_SCALED_9)); + locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / ONE_SCALED_9), ONE_SCALED_9)); locals.noiseThr9 = 5_000_000; locals.noiseCap9 = 400_000_000; locals.noiseMax9 = 25_000_000; - locals.thr = uint256(locals.arbThr9) * 1e9; + locals.thr = _convertTo18Decimals(locals.arbThr9); // Start BELOW, inside locals.deviationBefore = (locals.thr / 3) + 1; // strictly inside - locals.priceBefore = locals.oraclePrice - (locals.oraclePrice * locals.deviationBefore) / 1e18; + locals.priceBefore = locals.oraclePrice.mulDown(FixedPoint.ONE - locals.deviationBefore); // No movement: deviationAfter == deviationBefore → ARB branch, but less than or equal to thr ⇒ static - uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.priceBefore].toMemoryArray(); HyperSurgeHook.PoolDetails memory poolDetails; @@ -2183,7 +2222,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo _convertTo18Decimals(locals.arbCap9), "lane-nomove-inside" ); - (, locals.fee) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, locals.oraclePrice); + (, locals.fee) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, FIFTY_FIFTY, 0, locals.oraclePrice); assertEq( locals.fee, @@ -2238,21 +2277,20 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo // Distinct NOISE lane params (fuzzed) and different ARB params (unused in expected but distinct to catch wrong-lane) locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); - locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); - locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, ONE_SCALED_9)); + locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / ONE_SCALED_9), ONE_SCALED_9)); locals.arbThr9 = 1_000_000; locals.arbCap9 = 300_000_000; locals.arbMax9 = 50_000_000; - locals.thr = uint256(locals.noiseThr9) * 1e9; - locals.cap = uint256(locals.noiseCap9) * 1e9; + locals.thr = _convertTo18Decimals(locals.noiseThr9); + locals.cap = _convertTo18Decimals(locals.noiseCap9); // Start OUTSIDE BELOW price: priceBefore = E * (1 - D_before), with D_before in (thr, cap) locals.deviationBefore = locals.thr + (locals.cap - locals.thr) / 3; - locals.priceBefore = locals.oraclePrice - (locals.oraclePrice * locals.deviationBefore) / 1e18; + locals.priceBefore = locals.oraclePrice.mulDown(FixedPoint.ONE - locals.deviationBefore); // Build compute locals with the standard orientation (pxIn=1e18, pxOut=E) - uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.priceBefore].toMemoryArray(); HyperSurgeHook.PoolDetails memory poolDetails; @@ -2265,7 +2303,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo poolDetails.numTokens = 2; // ensure a measurable worsening but no overflow; avoid 1-wei knife edges - locals.lo = 1e9; + locals.lo = ONE_SCALED_9; locals.hi = 5e17; // EXACT_IN reduces P further → deviation worsens from the BELOW side (NOISE lane) @@ -2302,7 +2340,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo _convertTo18Decimals(locals.arbCap9), "lane-below-worsen" ); - (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, locals.oraclePrice); + (, locals.dyn) = mock.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, FIFTY_FIFTY, 0, locals.oraclePrice); assertEq(locals.dyn, locals.expected, "noise/after (below side): dynamic fee must match expected"); assertGe(locals.dyn, STATIC_SWAP_FEE, "dynamic fee greater than or equal to static"); @@ -2351,40 +2389,40 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo // ARB lane params (ensure thr < cap < 1.0) locals.arbThr9 = uint32(bound(arbThrSeed, 1, 900_000_000 - 1)); // (0, 0.9) - locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, 1_000_000_000 - 1)); // (thr, 1) - locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / 1e9) + 1, 1_000_000_000)); + locals.arbCap9 = uint32(bound(arbCapSeed, locals.arbThr9 + 1, ONE_SCALED_9 - 1)); // (thr, 1) + locals.arbMax9 = uint32(bound(arbMaxSeed, uint32(STATIC_SWAP_FEE / ONE_SCALED_9) + 1, ONE_SCALED_9)); // Distinct NOISE params (unused in expected but kept different to catch wrong-lane) locals.noiseThr9 = 5_000_000; locals.noiseCap9 = 400_000_000; locals.noiseMax9 = 25_000_000; - locals.thr = uint256(locals.arbThr9) * 1e9; - locals.cap = uint256(locals.arbCap9) * 1e9; - assertLt(locals.cap, 1e18, "cap must be < 100%"); + locals.thr = _convertTo18Decimals(locals.arbThr9); + locals.cap = _convertTo18Decimals(locals.arbCap9); + assertLt(locals.cap, FixedPoint.ONE, "cap must be < 100%"); // BEFORE deviation strictly above cap but < 1, with safe margin // margin = max(1, (1e18 - cap)/16) keeps Db < 1 while staying comfortably > cap - locals.margin = (1e18 - locals.cap) / 16; + locals.margin = (FixedPoint.ONE - locals.cap) / 16; if (locals.margin == 0) { locals.margin = 1; } locals.Db = locals.cap + locals.margin; - if (locals.Db >= 1e18) { - locals.Db = 1e18 - 1; + if (locals.Db >= FixedPoint.ONE) { + locals.Db = FixedPoint.ONE - 1; } // Sanity: BEFORE > cap assertGt(locals.Db, locals.cap, "setup must have BEFORE > cap"); // Price ABOVE E with BEFORE deviation Db - locals.priceBefore = locals.oraclePrice + (locals.oraclePrice * locals.Db) / 1e18; + locals.priceBefore = locals.oraclePrice.mulDown(FixedPoint.ONE + locals.Db); // ABOVE side with EXACT_IN: // D_after_pos (no-cross) = (Db - t)/(1 + t). Want AFTER less than or equal to cap ⇒ t ≥ (Db - cap)/(1 + cap). - locals.num = (locals.Db - locals.cap) * 1e18; // Q36 (Db > cap guaranteed) - locals.den = 1e18 + locals.cap; + locals.num = (locals.Db - locals.cap) * FixedPoint.ONE; // Q36 (Db > cap guaranteed) + locals.den = FixedPoint.ONE + locals.cap; locals.q = (locals.num + locals.den - 1) / locals.den; // ceilDiv → Q18 locals.tLower = locals.q; @@ -2399,7 +2437,6 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo locals.hi = locals.lo; } - uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.priceBefore].toMemoryArray(); HyperSurgeHook.PoolDetails memory poolDetails; @@ -2435,7 +2472,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo _convertTo18Decimals(locals.arbThr9), _convertTo18Decimals(locals.arbCap9), "arb-before-cap" - ).ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, locals.oraclePrice); + ).ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, FIFTY_FIFTY, 0, locals.oraclePrice); locals.expected = fee_expectedFeeWithParams( locals.priceBefore, @@ -2482,19 +2519,19 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo locals.oraclePrice = bound(eSeed, 1e16, 1e24); locals.noiseThr9 = uint32(bound(noiseThrSeed, 1, 900_000_000 - 1)); - locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, 1_000_000_000)); - locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / 1e9), 1_000_000_000)); + locals.noiseCap9 = uint32(bound(noiseCapSeed, locals.noiseThr9 + 1, ONE_SCALED_9)); + locals.noiseMax9 = uint32(bound(noiseMaxSeed, uint32(STATIC_SWAP_FEE / ONE_SCALED_9), ONE_SCALED_9)); locals.arbThr9 = 1_000_000; locals.arbCap9 = 300_000_000; locals.arbMax9 = 50_000_000; - locals.thr = uint256(locals.noiseThr9) * 1e9; + locals.thr = _convertTo18Decimals(locals.noiseThr9); locals.Db = locals.thr / 4 + 1; - locals.priceBefore = locals.oraclePrice - (locals.oraclePrice * locals.Db) / 1e18; + locals.priceBefore = locals.oraclePrice.mulDown(FixedPoint.ONE - locals.Db); - locals.num = (locals.thr - locals.Db) * 1e18; - locals.den = 1e18 - locals.thr; + locals.num = (locals.thr - locals.Db) * FixedPoint.ONE; + locals.den = FixedPoint.ONE - locals.thr; locals.tEdge = locals.den == 0 ? 0 : (locals.num / locals.den); locals.epsT = 1e6; @@ -2504,7 +2541,6 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo locals.hi = locals.lo; } - uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); uint256[] memory balancesScaled18 = [FixedPoint.ONE, locals.priceBefore].toMemoryArray(); HyperSurgeHook.PoolDetails memory poolDetails; @@ -2537,7 +2573,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo _convertTo18Decimals(locals.arbThr9), _convertTo18Decimals(locals.arbCap9), "noise-exact-thr" - ).ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, weights, 0, locals.oraclePrice); + ).ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, FIFTY_FIFTY, 0, locals.oraclePrice); assertEq(locals.fee, STATIC_SWAP_FEE, "At threshold end-state: NOISE must return static (no ramp)"); } @@ -2574,7 +2610,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo // 3) Force extPx == 0 with NON-ZERO raws: // extPx = floor((pxOut*1e18)/pxIn) = floor((rawOut*1e18)/rawIn) // => choose rawOut=1 and rawIn > 1e18 (fits in uint64), so extPx == 0 - rawInHuge = uint64(bound(uint256(rawInHuge), 1e18 + 1, type(uint64).max)); + rawInHuge = uint64(bound(uint256(rawInHuge), FixedPoint.ONE + 1, type(uint64).max)); // (optional) prove we hit the correct precompile and calldata (no selector) vm.expectCall(_HYPER_SPOT_PRICE_PRECOMPILE, abi.encode(pairIn)); @@ -2586,8 +2622,8 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo // 4) Build params (all 7 fields) uint256[] memory balances = new uint256[](2); - balances[0] = 1e18; - balances[1] = 1e18; + balances[0] = FixedPoint.ONE; + balances[1] = FixedPoint.ONE; SwapKind kind = givenIn ? SwapKind.EXACT_IN : SwapKind.EXACT_OUT; @@ -2615,8 +2651,8 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo uint256 amountGiven = 5e15; uint256[] memory balances = new uint256[](2); - balances[0] = 1e18; - balances[1] = 1e18; + balances[0] = FixedPoint.ONE; + balances[1] = FixedPoint.ONE; TokenConfig[] memory cfg = new TokenConfig[](2); LiquidityManagement memory lm; @@ -2664,8 +2700,8 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo uint256 amountGiven = 5e15; uint256[] memory balances = new uint256[](2); - balances[0] = 1e18; - balances[1] = 1e18; + balances[0] = FixedPoint.ONE; + balances[1] = FixedPoint.ONE; TokenConfig[] memory cfg = new TokenConfig[](2); LiquidityManagement memory lm; @@ -2739,7 +2775,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo weights = new uint256[](tokens.length); for (uint256 i = 0; i < tokens.length; i++) { - weights[i] = 1e18 / tokens.length; // Equal weights + weights[i] = FixedPoint.ONE / tokens.length; // Equal weights } } @@ -2814,62 +2850,51 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo // Make poolPx = P using simple weights/balances: // poolPx = (bOut * wIn) / (bIn * wOut) - uint256[] memory weights = [uint256(50e16), uint256(50e16)].toMemoryArray(); - p.balancesScaled18 = [uint256(1e18), uint256(P)].toMemoryArray(); + p.balancesScaled18 = [uint256(FixedPoint.ONE), uint256(P)].toMemoryArray(); // Keep deltas zero so poolPx == poolPxBefore (no lane flip due to swap) => calculatedAmount = 0 - (bool ok, uint256 fee) = hook.ComputeSurgeFee(p, poolDetails, staticFee, weights, 0, extPxE18); + (bool ok, uint256 fee) = hook.ComputeSurgeFee(p, poolDetails, staticFee, FIFTY_FIFTY, 0, extPxE18); assertTrue(ok, "compute ok"); return fee; } - function fee_mulDown(uint256 a, uint256 b) internal pure returns (uint256) { - return (a * b) / FEE_ONE; - } - - function fee_divDown(uint256 a, uint256 b) internal pure returns (uint256) { - return (a * FEE_ONE) / b; - } - function fee_relAbsDiff(uint256 a, uint256 b) internal pure returns (uint256) { - return a > b ? fee_divDown(a - b, b) : fee_divDown(b - a, b); + return a > b ? (a - b).divDown(b) : (b - a).divDown(b); } // Pool pair-spot with the SAME staging & rounding the hook uses: // P = (B_out * w_in) / (B_in * w_out) function fee_pairSpotFromBW(uint256 bIn, uint256 wIn, uint256 bOut, uint256 wOut) internal pure returns (uint256) { - uint256 num = fee_mulDown(bOut, wIn); - uint256 den = fee_mulDown(bIn, wOut); - return den == 0 ? 0 : fee_divDown(num, den); + return (bIn == 0 || wOut == 0) ? 0 : ((bOut * wIn) / wOut).divDown(bIn); } // Weights: normalized with 1% floor, deterministic from a seed function fee_normWeights(uint8 n, uint256 seed) internal pure returns (uint256[] memory w) { uint256 WEIGHT_MIN = 1e16; // 1% - require(uint256(n) * WEIGHT_MIN <= FEE_ONE, "min too big"); + require(uint256(n) * WEIGHT_MIN <= FixedPoint.ONE, "min too big"); w = new uint256[](n); uint256[] memory r = new uint256[](n); uint256 sumR; unchecked { for (uint8 i = 0; i < n; ++i) { - r[i] = 1 + (uint256(keccak256(abi.encode(seed, i))) % 1e9); + r[i] = 1 + (uint256(keccak256(abi.encode(seed, i))) % ONE_SCALED_9); sumR += r[i]; } } uint256 base = uint256(n) * WEIGHT_MIN; - uint256 rem = FEE_ONE - base; + uint256 rem = FixedPoint.ONE - base; uint256 acc; for (uint8 i = 0; i < n; ++i) { uint256 share = (r[i] * rem) / sumR; w[i] = WEIGHT_MIN + share; acc += w[i]; } - if (acc != FEE_ONE) { - if (acc < FEE_ONE) w[0] += (FEE_ONE - acc); + if (acc != FixedPoint.ONE) { + if (acc < FixedPoint.ONE) w[0] += (FixedPoint.ONE - acc); else { - uint256 over = acc - FEE_ONE; + uint256 over = acc - FixedPoint.ONE; w[0] = w[0] > over + WEIGHT_MIN ? (w[0] - over) : WEIGHT_MIN; } } @@ -2891,7 +2916,7 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo } function _convertTo18Decimals(uint32 valueScaled9) internal pure returns (uint256) { - return uint256(valueScaled9) * 1e9; + return uint256(valueScaled9) * ONE_SCALED_9; } // Expected fee (exact same rounding & clamping as the hook) @@ -2914,12 +2939,12 @@ contract HyperSurgeFeeTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedPoo } uint256 span = capDev - threshold; - uint256 norm = fee_divDown(deviation - threshold, span); - if (norm > FEE_ONE) { - norm = FEE_ONE; + uint256 norm = (deviation - threshold).divDown(span); + if (norm > FixedPoint.ONE) { + norm = FixedPoint.ONE; } - uint256 incr = fee_mulDown(maxPct - staticSwapFee, norm); + uint256 incr = (maxPct - staticSwapFee).mulDown(norm); uint256 fee = staticSwapFee + incr; if (fee > maxPct) { fee = maxPct; From 5ce5a526274a18bc7b47c512c3a8de0a2ca2cf2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Tue, 16 Sep 2025 10:25:30 -0300 Subject: [PATCH 18/28] Fix tests --- .../foundry/HyperSurgeLiquidityChecks.t.sol | 57 ++- .../test/foundry/HyperSurgeMaxDeviation.t.sol | 341 ++++++++---------- 2 files changed, 183 insertions(+), 215 deletions(-) diff --git a/pkg/pool-hooks/test/foundry/HyperSurgeLiquidityChecks.t.sol b/pkg/pool-hooks/test/foundry/HyperSurgeLiquidityChecks.t.sol index b4ad0da2..35ca9f73 100644 --- a/pkg/pool-hooks/test/foundry/HyperSurgeLiquidityChecks.t.sol +++ b/pkg/pool-hooks/test/foundry/HyperSurgeLiquidityChecks.t.sol @@ -27,9 +27,9 @@ import { } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; // Local deployer + mock -import { HyperSurgeHookDeployer } from "./utils/HyperSurgeHookDeployer.sol"; +import { HyperSurgeHook } from "../../contracts/hooks-quantamm/HyperSurgeHook.sol"; import { HyperSurgeHookMock } from "../../contracts/test/HyperSurgeHookMock.sol"; -import { HyperSurgeHook } from ".../../contracts/hooks-quantamm/HyperSurgeHook.sol"; +import { HyperSurgeHookDeployer } from "./utils/HyperSurgeHookDeployer.sol"; import { WeightedPoolContractsDeployer } from "@balancer-labs/v3-pool-weighted/test/foundry/utils/WeightedPoolContractsDeployer.sol"; @@ -1192,8 +1192,7 @@ contract HyperSurgeLiquidityCheckTest is BaseVaultTest, HyperSurgeHookDeployer, struct DefenciveZeroCheck { uint256 bIn; uint256 bOut; - uint256 pxIn; - uint256 pxOut; + uint256 oraclePrice; uint256 pxBase; uint256 amountGiven; uint256 calcAmount; @@ -1210,37 +1209,28 @@ contract HyperSurgeLiquidityCheckTest is BaseVaultTest, HyperSurgeHookDeployer, uint256 calcAmtRaw ) public view { DefenciveZeroCheck memory check; - check.bIn = bound(bInRaw, 1e18, 1e22); - check.bOut = bound(bOutRaw, 1e18, 1e22); - check.pxIn = 1e18; - check.pxOut = 1e18; + check.bIn = bound(bInRaw, FixedPoint.ONE, 1e22); + check.bOut = bound(bOutRaw, FixedPoint.ONE, 1e22); + check.oraclePrice = FixedPoint.ONE; check.amountGiven = bound(amtGivenRaw, 1, check.bIn / 1_000_000); // ≤ 1e-6 of bIn check.calcAmount = bound(calcAmtRaw, 1, check.bOut / 1_000_000); // ≤ 1e-6 of bOut - HyperSurgeHookMock.ComputeSurgeFeeLocals memory L; - L.bIn = check.bIn; - L.bOut = check.bOut; - L.wIn = 1e18; - L.wOut = 0; // <<< makes den = bIn.mulDown(wOut) == 0 → poolPx == 0 - L.pxIn = check.pxIn; - L.pxOut = check.pxOut; - L.calcAmountScaled18 = check.calcAmount; - L.poolDetails.noiseThresholdPercentage9 = 10_000_000; // 1% - L.poolDetails.noiseCapDeviationPercentage9 = 50_000_000; // 5% - L.poolDetails.noiseMaxSurgeFee9 = 100_000_000; // 10% - L.poolDetails.arbThresholdPercentage9 = 10_000_000; // 1% - L.poolDetails.arbCapDeviationPercentage9 = 50_000_000; // 5% - L.poolDetails.arbMaxSurgeFee9 = 200_000_000; // 20% - L.poolDetails.numTokens = 2; - - uint256[] memory balances = new uint256[](2); - balances[0] = check.bIn; - balances[1] = check.bOut; + uint256[] memory balancesScaled18 = [check.bIn, check.bOut].toMemoryArray(); + uint256[] memory weights = [FixedPoint.ONE, uint256(0)].toMemoryArray(); // <<< makes den = bIn.mulDown(wOut) == 0 → poolPx == 0 + + HyperSurgeHook.PoolDetails memory poolDetails; + poolDetails.noiseThresholdPercentage9 = 10_000_000; // 1% + poolDetails.noiseCapDeviationPercentage9 = 50_000_000; // 5% + poolDetails.noiseMaxSurgeFee9 = 100_000_000; // 10% + poolDetails.arbThresholdPercentage9 = 10_000_000; // 1% + poolDetails.arbCapDeviationPercentage9 = 50_000_000; // 5% + poolDetails.arbMaxSurgeFee9 = 200_000_000; // 20% + poolDetails.numTokens = 2; PoolSwapParams memory p = PoolSwapParams({ kind: exactIn ? SwapKind.EXACT_IN : SwapKind.EXACT_OUT, amountGivenScaled18: check.amountGiven, - balancesScaled18: balances, + balancesScaled18: balancesScaled18, indexIn: 0, indexOut: 1, router: address(0), @@ -1248,10 +1238,17 @@ contract HyperSurgeLiquidityCheckTest is BaseVaultTest, HyperSurgeHookDeployer, }); check.staticFee = 1e16; // 1% - (check.ok, check.fee) = hook.ComputeSurgeFee(L, p, check.staticFee); + (check.ok, check.fee) = hook.ComputeSurgeFee( + p, + poolDetails, + check.staticFee, + weights, + check.calcAmount, + check.oraclePrice + ); assertTrue(check.ok, "compute fee must not block when pool spot denominator is zero"); - assertLe(check.fee, 1e18, "fee must be a valid 18-dec percentage"); + assertLe(check.fee, FixedPoint.ONE, "fee must be a valid 18-dec percentage"); assertGe(check.fee, check.staticFee, "fee must be at least the static fee"); } } diff --git a/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol b/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol index 8af458cc..5666a27b 100644 --- a/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol +++ b/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol @@ -3,10 +3,15 @@ pragma solidity ^0.8.24; import "forge-std/Test.sol"; -import { HyperSurgeHookMock } from "../../contracts/test/HyperSurgeHookMock.sol"; import { PoolSwapParams, SwapKind } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; + +import { ArrayHelpers } from "@balancer-labs/v3-solidity-utils/contracts/test/ArrayHelpers.sol"; import { BaseVaultTest } from "@balancer-labs/v3-vault/test/foundry/utils/BaseVaultTest.sol"; +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; + +import { HyperSurgeHookMock } from "../../contracts/test/HyperSurgeHookMock.sol"; +import { HyperSurgeHook } from "../../contracts/hooks-quantamm/HyperSurgeHook.sol"; /// @notice Drop-in replacement for the "find max deviation" fuzz tests. /// This suite focuses on the surge-fee ramp behavior by fuzzing the @@ -15,7 +20,9 @@ import { BaseVaultTest } from "@balancer-labs/v3-vault/test/foundry/utils/BaseVa /// It mirrors the helper-style used in the original tests and uses /// the hook's ComputeSurgeFee pure entrypoint. contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { - uint256 constant ONE = 1e18; + using FixedPoint for uint256; + using ArrayHelpers for *; + uint256 constant DEFAULT_MAX_SURGE_FEE_PPM9 = 0.05e9; // 5% uint256 constant DEFAULT_THRESHOLD_PPM9 = 0.1e9; // 0.1% uint256 constant DEFAULT_CAP_DEV_PPM9 = 0.5e9; // 50% @@ -39,7 +46,7 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { // Simple normalized weights with a 1% floor, deterministic from a seed. function _normWeights(uint8 n, uint256 seed) internal pure returns (uint256[] memory w) { - require(uint256(n) * WEIGHT_MIN <= ONE, "min too big"); + require(uint256(n) * WEIGHT_MIN <= FixedPoint.ONE, "min too big"); w = new uint256[](n); uint256[] memory r = new uint256[](n); @@ -52,17 +59,17 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { } uint256 base = uint256(n) * WEIGHT_MIN; - uint256 rem = ONE - base; + uint256 rem = FixedPoint.ONE - base; uint256 acc; for (uint8 i = 0; i < n; ++i) { uint256 share = (r[i] * rem) / sumR; w[i] = WEIGHT_MIN + share; acc += w[i]; } - if (acc != ONE) { - if (acc < ONE) w[0] += (ONE - acc); + if (acc != FixedPoint.ONE) { + if (acc < FixedPoint.ONE) w[0] += (FixedPoint.ONE - acc); else { - uint256 over = acc - ONE; + uint256 over = acc - FixedPoint.ONE; w[0] = w[0] > over + WEIGHT_MIN ? (w[0] - over) : WEIGHT_MIN; } } @@ -78,55 +85,27 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { } } // Build a locals struct with two overridden prices targeting a desired deviation `D` (1e18 scale). - // We set pxIn = 1e18 and pxOut so that extPx = pxOut/pxIn = P / (1 + D), using the same divDown rounding. - function _localsForDeviation( - uint256 P, // pair spot (1e18) - uint256 D // target deviation (1e18) - ) internal pure returns (uint256 pxIn, uint256 pxOut) { - pxIn = ONE; - // extPx = P / (1 + D) (use hook-style rounding) - pxOut = _divDown(P, ONE + D); + // Choose deviation D, then set external px so that extPx = P / (1 + D) + function fee_computeOraclePriceForDeviation(uint256 P, uint256 deviation) internal pure returns (uint256) { + return P.divDown(FixedPoint.ONE + deviation); } - // Instantiate ComputeSurgeFeeLocals with common pool details (NOISE lane), - // with b/w and px values provided by the caller. - function _makeLocals( - uint256 bIn, - uint256 wIn, - uint256 bOut, - uint256 wOut, - uint256 pxIn, - uint256 pxOut - ) internal pure returns (HyperSurgeHookMock.ComputeSurgeFeeLocals memory L) { - L.bIn = bIn; - L.wIn = wIn; - L.bOut = bOut; - L.wOut = wOut; - L.pxIn = pxIn; - L.pxOut = pxOut; - + function _getDefaultPoolDetails() internal pure returns (HyperSurgeHook.PoolDetails memory poolDetails) { // Configure NOISE lane (used when deviation does not worsen). - L.poolDetails.noiseThresholdPercentage9 = uint32(DEFAULT_THRESHOLD_PPM9); - L.poolDetails.noiseMaxSurgeFee9 = uint32(DEFAULT_MAX_SURGE_FEE_PPM9); - L.poolDetails.noiseCapDeviationPercentage9 = uint32(DEFAULT_CAP_DEV_PPM9); + poolDetails.noiseThresholdPercentage9 = uint32(DEFAULT_THRESHOLD_PPM9); + poolDetails.noiseMaxSurgeFee9 = uint32(DEFAULT_MAX_SURGE_FEE_PPM9); + poolDetails.noiseCapDeviationPercentage9 = uint32(DEFAULT_CAP_DEV_PPM9); // Set ARB lane too (not used here, but keep consistent). - L.poolDetails.arbThresholdPercentage9 = uint32(DEFAULT_THRESHOLD_PPM9); - L.poolDetails.arbMaxSurgeFee9 = uint32(DEFAULT_MAX_SURGE_FEE_PPM9); - L.poolDetails.arbCapDeviationPercentage9 = uint32(DEFAULT_CAP_DEV_PPM9); - } + poolDetails.arbThresholdPercentage9 = uint32(DEFAULT_THRESHOLD_PPM9); + poolDetails.arbMaxSurgeFee9 = uint32(DEFAULT_MAX_SURGE_FEE_PPM9); + poolDetails.arbCapDeviationPercentage9 = uint32(DEFAULT_CAP_DEV_PPM9); - // 1e18 fixed-point helpers identical to Balancer's FixedPoint - function _mulDown(uint256 a, uint256 b) internal pure returns (uint256) { - return (a * b) / 1e18; - } - - function _divDown(uint256 a, uint256 b) internal pure returns (uint256) { - return (a * 1e18) / b; + poolDetails.numTokens = 2; } function _relAbsDiff(uint256 a, uint256 b) internal pure returns (uint256) { - return a > b ? _divDown(a - b, b) : _divDown(b - a, b); + return a > b ? (a - b).divDown(b) : (b - a).divDown(b); } // Replace any existing pair-spot helper with this: @@ -136,15 +115,13 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { uint256 bOut, uint256 wOut ) internal pure returns (uint256) { - uint256 num = _mulDown(bOut, wIn); - uint256 den = _mulDown(bIn, wOut); - if (den == 0) return 0; - return _divDown(num, den); + // If the denominator is zero, the pool price is zero. + if (bIn == 0 || wOut == 0) return 0; + return ((bOut * wIn) / wOut).divDown(bIn); } - function _expectedFeeFromLocals(uint256 poolPx, uint256 pxIn, uint256 pxOut) internal pure returns (uint256) { - uint256 extPx = _divDown(pxOut, pxIn); // identical to hook’s locals.extPx - uint256 deviation = _relAbsDiff(poolPx, extPx); + function _expectedFeeFromLocals(uint256 poolPx, uint256 oraclePrice) internal pure returns (uint256) { + uint256 deviation = _relAbsDiff(poolPx, oraclePrice); uint256 threshold = DEFAULT_THRESHOLD_PPM9 * 1e9; uint256 capDev = DEFAULT_CAP_DEV_PPM9 * 1e9; @@ -153,10 +130,10 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { if (deviation <= threshold) return STATIC_SWAP_FEE; uint256 span = capDev - threshold; - uint256 norm = _divDown(deviation - threshold, span); - if (norm > ONE) norm = ONE; + uint256 norm = (deviation - threshold).divDown(span); + if (norm > FixedPoint.ONE) norm = FixedPoint.ONE; - uint256 incr = _mulDown(maxPct - STATIC_SWAP_FEE, norm); + uint256 incr = (maxPct - STATIC_SWAP_FEE).mulDown(norm); uint256 fee = STATIC_SWAP_FEE + incr; if (fee > maxPct) fee = maxPct; return fee; @@ -181,13 +158,17 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { // target deviation in [0 .. threshold] (inclusive lower range) uint256 D = uint256(keccak256(abi.encode(dSeed))) % (threshold + 1); - (uint256 pxIn, uint256 pxOut) = _localsForDeviation(P, D); - HyperSurgeHookMock.ComputeSurgeFeeLocals memory L = _makeLocals(b[i], w[i], b[j], w[j], pxIn, pxOut); + uint256 oraclePrice = fee_computeOraclePriceForDeviation(P, D); + HyperSurgeHook.PoolDetails memory poolDetails = _getDefaultPoolDetails(); PoolSwapParams memory p; // zero-initialized; p.kind defaults to 0 (= EXACT_IN) p.kind = SwapKind.EXACT_IN; // keep before==after so we take the NOISE lane + p.balancesScaled18 = b; + p.indexIn = i; + p.indexOut = j; + p.amountGivenScaled18 = 0; - (bool ok, uint256 fee) = hook.ComputeSurgeFee(L, p, STATIC_SWAP_FEE); + (bool ok, uint256 fee) = hook.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, w, 0, oraclePrice); assertTrue(ok, "compute must succeed"); assertEq(fee, STATIC_SWAP_FEE, "below threshold must return static fee"); } @@ -208,16 +189,20 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { uint256 capDev = DEFAULT_CAP_DEV_PPM9 * 1e9; // Choose a deviation D >= capDev (push comfortably above to avoid rounding back below). - uint256 extra = (ONE - capDev) / 4; // up to +25% beyond cap (bounded to keep pxOut > 0) + uint256 extra = (FixedPoint.ONE - capDev) / 4; // up to +25% beyond cap (bounded to keep pxOut > 0) uint256 D = capDev + (uint256(keccak256(abi.encode(dSeed, 5))) % (extra + 1)); - (uint256 pxIn, uint256 pxOut) = _localsForDeviation(P, D); - HyperSurgeHookMock.ComputeSurgeFeeLocals memory L = _makeLocals(b[i], w[i], b[j], w[j], pxIn, pxOut); + uint256 oraclePrice = fee_computeOraclePriceForDeviation(P, D); + HyperSurgeHook.PoolDetails memory poolDetails = _getDefaultPoolDetails(); PoolSwapParams memory p; p.kind = SwapKind.EXACT_IN; + p.balancesScaled18 = b; + p.indexIn = i; + p.indexOut = j; + p.amountGivenScaled18 = 0; - (bool ok, uint256 fee) = hook.ComputeSurgeFee(L, p, STATIC_SWAP_FEE); + (bool ok, uint256 fee) = hook.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, w, 0, oraclePrice); assertTrue(ok, "compute must succeed"); uint256 maxPct = DEFAULT_MAX_SURGE_FEE_PPM9 * 1e9; @@ -244,17 +229,21 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { // Target a deviation strictly inside (threshold, capDev): uint256 D = threshold + 1 + (uint256(keccak256(abi.encode(dSeed, 8))) % (span - 1)); - (uint256 pxIn, uint256 pxOut) = _localsForDeviation(P, D); - HyperSurgeHookMock.ComputeSurgeFeeLocals memory L = _makeLocals(b[i], w[i], b[j], w[j], pxIn, pxOut); + uint256 oraclePrice = fee_computeOraclePriceForDeviation(P, D); + HyperSurgeHook.PoolDetails memory poolDetails = _getDefaultPoolDetails(); PoolSwapParams memory p; p.kind = SwapKind.EXACT_IN; + p.balancesScaled18 = b; + p.indexIn = i; + p.indexOut = j; + p.amountGivenScaled18 = 0; - (bool ok, uint256 fee) = hook.ComputeSurgeFee(L, p, STATIC_SWAP_FEE); + (bool ok, uint256 fee) = hook.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, w, 0, oraclePrice); assertTrue(ok, "compute must succeed"); // Compute expected with identical rounding. - uint256 expected = _expectedFeeFromLocals(P, pxIn, pxOut); + uint256 expected = _expectedFeeFromLocals(P, oraclePrice); assertEq(fee, expected, "fee must follow linear ramp between min and max"); } @@ -266,15 +255,13 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { // Expected fee with custom lane parameters (all in ppm9 for the lane fields). function _expectedFeeWithParams( uint256 poolPx, - uint256 pxIn, - uint256 pxOut, + uint256 oraclePrice, uint256 staticSwapFee, uint32 thresholdPPM9, uint32 capDevPPM9, uint32 maxFeePPM9 ) internal pure returns (uint256) { - uint256 extPx = _divDown(pxOut, pxIn); - uint256 deviation = _relAbsDiff(poolPx, extPx); + uint256 deviation = _relAbsDiff(poolPx, oraclePrice); uint256 threshold = _ppm9To1e18(thresholdPPM9); uint256 capDev = _ppm9To1e18(capDevPPM9); @@ -283,10 +270,10 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { if (deviation <= threshold) return staticSwapFee; uint256 span = capDev - threshold; - uint256 norm = _divDown(deviation - threshold, span); - if (norm > ONE) norm = ONE; + uint256 norm = (deviation - threshold).divDown(span); + if (norm > FixedPoint.ONE) norm = FixedPoint.ONE; - uint256 incr = _mulDown(maxPct - staticSwapFee, norm); + uint256 incr = (maxPct - staticSwapFee).mulDown(norm); uint256 fee = staticSwapFee + incr; if (fee > maxPct) fee = maxPct; return fee; @@ -300,8 +287,7 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { uint256 capDev1e18; uint256 price; uint256 expected; - uint256 pxIn; - uint256 pxOut; + uint256 oraclePrice; bool ok; uint256 fee; } @@ -331,31 +317,20 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { uint256 D2raw = uint256(keccak256(abi.encode(dSeed2))) % (locals.capDev1e18 + locals.capDev1e18 / 2 + 1); (locals.deviation, locals.expected) = D1 <= D2raw ? (D1, D2raw) : (D2raw, D1); - (locals.pxIn, locals.pxOut) = _localsForDeviation(locals.price, locals.deviation); - (uint256 pxIn2, uint256 pxOut2) = _localsForDeviation(locals.price, locals.expected); + (locals.oraclePrice) = fee_computeOraclePriceForDeviation(locals.price, locals.deviation); + uint256 oraclePrice2 = fee_computeOraclePriceForDeviation(locals.price, locals.expected); - HyperSurgeHookMock.ComputeSurgeFeeLocals memory L1 = _makeLocals( - b[locals.i], - w[locals.i], - b[locals.j], - w[locals.j], - locals.pxIn, - locals.pxOut - ); - HyperSurgeHookMock.ComputeSurgeFeeLocals memory L2 = _makeLocals( - b[locals.i], - w[locals.i], - b[locals.j], - w[locals.j], - pxIn2, - pxOut2 - ); + HyperSurgeHook.PoolDetails memory poolDetails = _getDefaultPoolDetails(); PoolSwapParams memory p; p.kind = SwapKind.EXACT_IN; + p.balancesScaled18 = b; + p.indexIn = locals.i; + p.indexOut = locals.j; + p.amountGivenScaled18 = 0; - (locals.ok, locals.fee) = hook.ComputeSurgeFee(L1, p, STATIC_SWAP_FEE); - (, uint256 fee2) = hook.ComputeSurgeFee(L2, p, STATIC_SWAP_FEE); + (locals.ok, locals.fee) = hook.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, w, 0, locals.oraclePrice); + (, uint256 fee2) = hook.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, w, 0, oraclePrice2); assertLe(locals.fee, fee2, "fee must be non-decreasing with deviation"); } @@ -381,28 +356,33 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { uint256 capDev = DEFAULT_CAP_DEV_PPM9; uint256 D = uint256(keccak256(abi.encode(dSeed))) % (capDev + capDev / 2 + 1); - (uint256 pxIn, uint256 pxOut) = _localsForDeviation(P_ij, D); + uint256 oraclePrice = fee_computeOraclePriceForDeviation(P_ij, D); - // Orientation A (i -> j) - HyperSurgeHookMock.ComputeSurgeFeeLocals memory LA = _makeLocals(b[i], w[i], b[j], w[j], pxIn, pxOut); - // Orientation B (j -> i) with inverted external prices - HyperSurgeHookMock.ComputeSurgeFeeLocals memory LB = _makeLocals(b[j], w[j], b[i], w[i], pxOut, pxIn); + HyperSurgeHook.PoolDetails memory poolDetails = _getDefaultPoolDetails(); PoolSwapParams memory p; p.kind = SwapKind.EXACT_IN; - (bool okA, uint256 feeA) = hook.ComputeSurgeFee(LA, p, STATIC_SWAP_FEE); - (bool okB, uint256 feeB) = hook.ComputeSurgeFee(LB, p, STATIC_SWAP_FEE); + // Orientation A (i -> j) + (bool okA, uint256 feeA) = hook.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, w, 0, oraclePrice); + // Orientation B (j -> i) + p.balancesScaled18 = [b[1], b[0]].toMemoryArray(); + (bool okB, uint256 feeB) = hook.ComputeSurgeFee( + p, + poolDetails, + STATIC_SWAP_FEE, + [w[1], w[0]].toMemoryArray(), + 0, + FixedPoint.ONE.divDown(oraclePrice) + ); assertTrue(okA && okB, "compute must succeed"); // Measure deviations exactly like the hook does in each orientation - uint256 extA = _divDown(LA.pxOut, LA.pxIn); - uint256 devA = _relAbsDiff(P_ij, extA); + uint256 devA = _relAbsDiff(P_ij, oraclePrice); // Compute the swapped pool spot with the SAME rounding (don’t assume 1/P) - uint256 P_ji = _pairSpotFromBalancesWeights(LB.bIn, LB.wIn, LB.bOut, LB.wOut); - uint256 extB = _divDown(LB.pxOut, LB.pxIn); - uint256 devB = _relAbsDiff(P_ji, extB); + uint256 P_ji = _pairSpotFromBalancesWeights(b[1], w[1], b[0], w[0]); + uint256 devB = _relAbsDiff(P_ji, FixedPoint.ONE.divDown(oraclePrice)); // Correct directional assertion: if (devA > devB) { @@ -423,8 +403,7 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { uint256 capDev1e18; uint256 price; uint256 expected; - uint256 pxIn; - uint256 pxOut; + uint256 oraclePrice; bool ok; uint256 fee; } @@ -451,31 +430,27 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { locals.capDev1e18 = DEFAULT_CAP_DEV_PPM9; locals.deviation = uint256(keccak256(abi.encode(dSeed))) % (locals.capDev1e18 + locals.capDev1e18 / 2 + 1); - (locals.pxIn, locals.pxOut) = _localsForDeviation(locals.price, locals.deviation); + (locals.oraclePrice) = fee_computeOraclePriceForDeviation(locals.price, locals.deviation); // Choose static fee in [0 .. maxPct] uint256 maxPct = DEFAULT_MAX_SURGE_FEE_PPM9; uint256 staticFee = uint256(staticFeeSeed) % (maxPct + 1); - HyperSurgeHookMock.ComputeSurgeFeeLocals memory L = _makeLocals( - b[locals.i], - w[locals.i], - b[locals.j], - w[locals.j], - locals.pxIn, - locals.pxOut - ); + HyperSurgeHook.PoolDetails memory poolDetails = _getDefaultPoolDetails(); PoolSwapParams memory p; p.kind = SwapKind.EXACT_IN; + p.balancesScaled18 = b; + p.indexIn = locals.i; + p.indexOut = locals.j; + p.amountGivenScaled18 = 0; - (locals.ok, locals.fee) = hook.ComputeSurgeFee(L, p, staticFee); + (locals.ok, locals.fee) = hook.ComputeSurgeFee(p, poolDetails, staticFee, w, 0, locals.oraclePrice); assertTrue(locals.ok, "compute must succeed"); locals.expected = _expectedFeeWithParams( _pairSpotFromBalancesWeights(b[locals.i], w[locals.i], b[locals.j], w[locals.j]), - locals.pxIn, - locals.pxOut, + locals.oraclePrice, staticFee, uint32(DEFAULT_THRESHOLD_PPM9), uint32(DEFAULT_CAP_DEV_PPM9), @@ -492,8 +467,7 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { uint256 capDev1e18; uint256 price; uint256 expected; - uint256 pxIn; - uint256 pxOut; + uint256 oraclePrice; bool ok; uint256 fee; } @@ -521,39 +495,37 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { locals.capDev1e18 = DEFAULT_CAP_DEV_PPM9; locals.deviation = uint256(keccak256(abi.encode(dSeed))) % (locals.capDev1e18 + locals.capDev1e18 / 2 + 1); - (locals.pxIn, locals.pxOut) = _localsForDeviation(locals.price, locals.deviation); + (locals.oraclePrice) = fee_computeOraclePriceForDeviation(locals.price, locals.deviation); // Orientation A (i -> j) - HyperSurgeHookMock.ComputeSurgeFeeLocals memory LA = _makeLocals( - b[locals.i], - w[locals.i], - b[locals.j], - w[locals.j], - locals.pxIn, - locals.pxOut - ); - // Orientation B (j -> i) with inverted external prices - HyperSurgeHookMock.ComputeSurgeFeeLocals memory LB = _makeLocals( - b[locals.j], - w[locals.j], - b[locals.i], - w[locals.i], - locals.pxOut, - locals.pxIn - ); + HyperSurgeHook.PoolDetails memory poolDetails = _getDefaultPoolDetails(); PoolSwapParams memory p; p.kind = SwapKind.EXACT_IN; + p.balancesScaled18 = b; + p.indexIn = locals.i; + p.indexOut = locals.j; + p.amountGivenScaled18 = 0; + (locals.ok, locals.fee) = hook.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, w, 0, locals.oraclePrice); - (locals.ok, locals.fee) = hook.ComputeSurgeFee(LA, p, STATIC_SWAP_FEE); - (bool okB, uint256 feeB) = hook.ComputeSurgeFee(LB, p, STATIC_SWAP_FEE); + // Orientation B (j -> i) with inverted external prices + p.balancesScaled18 = [b[1], b[0]].toMemoryArray(); + (bool okB, uint256 feeB) = hook.ComputeSurgeFee( + p, + poolDetails, + STATIC_SWAP_FEE, + [w[1], w[0]].toMemoryArray(), + 0, + FixedPoint.ONE.divDown(locals.oraclePrice) + ); assertTrue(locals.ok && okB, "compute must succeed"); // Measure deviations exactly like the hook does - uint256 extA = _divDown(LA.pxOut, LA.pxIn); - uint256 extB = _divDown(LB.pxOut, LB.pxIn); - uint256 devA = _relAbsDiff(locals.price, extA); - uint256 devB = _relAbsDiff(_pairSpotFromBalancesWeights(LB.bIn, LB.wIn, LB.bOut, LB.wOut), extB); // equals 1/P vs 1/ext due to swap + uint256 devA = _relAbsDiff(locals.price, locals.oraclePrice); + uint256 devB = _relAbsDiff( + _pairSpotFromBalancesWeights(b[1], w[1], b[0], w[0]), + FixedPoint.ONE.divDown(locals.oraclePrice) + ); // equals 1/P vs 1/ext due to swap // Directional ordering with ±1 wei tolerance for knife-edge rounding if (devA > devB) { @@ -574,13 +546,11 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { uint256 capDev; int8[5] offs; uint256 Dt; - uint256 pxInT; - uint256 pxOutT; + uint256 oraclePriceT; uint256 extT; uint256 expectedT; uint256 Dc; - uint256 pxInC; - uint256 pxOutC; + uint256 oraclePriceC; uint256 expectedC; } @@ -616,24 +586,20 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { } else { locals.Dt = locals.threshold + uint256(uint8(locals.offs[k])); } - (locals.pxInT, locals.pxOutT) = _localsForDeviation(locals.P, locals.Dt); - HyperSurgeHookMock.ComputeSurgeFeeLocals memory LT = _makeLocals( - b[locals.i], - w[locals.i], - b[locals.j], - w[locals.j], - locals.pxInT, - locals.pxOutT - ); + (locals.oraclePriceT) = fee_computeOraclePriceForDeviation(locals.P, locals.Dt); + HyperSurgeHook.PoolDetails memory poolDetails = _getDefaultPoolDetails(); PoolSwapParams memory p; p.kind = SwapKind.EXACT_IN; + p.balancesScaled18 = b; + p.indexIn = locals.i; + p.indexOut = locals.j; + p.amountGivenScaled18 = 0; - (bool okT, uint256 feeT) = hook.ComputeSurgeFee(LT, p, STATIC_SWAP_FEE); + (bool okT, uint256 feeT) = hook.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, w, 0, locals.oraclePriceT); assertTrue(okT, "compute must succeed (threshold ring)"); - locals.extT = _divDown(locals.pxOutT, locals.pxInT); - locals.expectedT = _expectedFeeFromLocals(locals.P, locals.pxInT, locals.pxOutT); + locals.expectedT = _expectedFeeFromLocals(locals.P, locals.oraclePriceT); // Exact match to the hook’s rounding-based expected value assertEq(feeT, locals.expectedT, "threshold ring fee mismatch"); @@ -643,24 +609,16 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { locals.Dc = locals.capDev > deltaC ? locals.capDev - deltaC : 0; } else { // guard upper bound to avoid overflow in _localsForDeviation denominator - uint256 room = ONE > locals.capDev ? (ONE - locals.capDev) : 0; + uint256 room = FixedPoint.ONE > locals.capDev ? (FixedPoint.ONE - locals.capDev) : 0; uint256 add = uint256(uint8(locals.offs[k])); locals.Dc = locals.capDev + (add <= room ? add : room); } - (locals.pxInC, locals.pxOutC) = _localsForDeviation(locals.P, locals.Dc); - HyperSurgeHookMock.ComputeSurgeFeeLocals memory LC = _makeLocals( - b[locals.i], - w[locals.i], - b[locals.j], - w[locals.j], - locals.pxInC, - locals.pxOutC - ); - - (bool okC, uint256 feeC) = hook.ComputeSurgeFee(LC, p, STATIC_SWAP_FEE); + (locals.oraclePriceC) = fee_computeOraclePriceForDeviation(locals.P, locals.Dc); + (bool okC, uint256 feeC) = hook.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, w, 0, locals.oraclePriceC); + assertTrue(okC, "compute must succeed (cap ring)"); - locals.expectedC = _expectedFeeFromLocals(locals.P, locals.pxInC, locals.pxOutC); + locals.expectedC = _expectedFeeFromLocals(locals.P, locals.oraclePriceC); assertEq(feeC, locals.expectedC, "cap ring fee mismatch"); } } @@ -686,18 +644,31 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { uint256 capDev = DEFAULT_CAP_DEV_PPM9; uint256 D = uint256(keccak256(abi.encode(dSeed))) % (capDev + capDev / 3 + 1); - (uint256 pxIn, uint256 pxOut) = _localsForDeviation(P, D); + uint256 oraclePrice = fee_computeOraclePriceForDeviation(P, D); - HyperSurgeHookMock.ComputeSurgeFeeLocals memory L1 = _makeLocals(b[i], w[i], b[j], w[j], pxIn, pxOut); - - uint256 k = 1 + (uint256(scaleSeed) % 1_000_000_000); // [1 .. 1e9] - HyperSurgeHookMock.ComputeSurgeFeeLocals memory L2 = _makeLocals(b[i] * k, w[i], b[j] * k, w[j], pxIn, pxOut); + HyperSurgeHook.PoolDetails memory poolDetails = _getDefaultPoolDetails(); PoolSwapParams memory p; p.kind = SwapKind.EXACT_IN; + p.balancesScaled18 = b; + p.indexIn = i; + p.indexOut = j; + p.amountGivenScaled18 = 0; + + (, uint256 fee1) = hook.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, w, 0, oraclePrice); - (, uint256 fee1) = hook.ComputeSurgeFee(L1, p, STATIC_SWAP_FEE); - (, uint256 fee2) = hook.ComputeSurgeFee(L2, p, STATIC_SWAP_FEE); + uint256 k = 1 + (uint256(scaleSeed) % 1_000_000_000); // [1 .. 1e9] + + p.balancesScaled18 = [b[i] * k, b[j] * k].toMemoryArray(); + + (, uint256 fee2) = hook.ComputeSurgeFee( + p, + poolDetails, + STATIC_SWAP_FEE, + [w[i] * k, w[j] * k].toMemoryArray(), + 0, + oraclePrice + ); assertApproxEqAbs(fee1, fee2, 1, "fee must be invariant to balance scaling"); } From 25eb6bf70856c96f8f6b3953bea1c447e074a896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Tue, 16 Sep 2025 10:37:28 -0300 Subject: [PATCH 19/28] Fix stack-too-deep --- .../test/foundry/HyperSurgeMaxDeviation.t.sol | 88 ++++++++++++------- 1 file changed, 56 insertions(+), 32 deletions(-) diff --git a/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol b/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol index 5666a27b..5cf41cf5 100644 --- a/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol +++ b/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol @@ -143,14 +143,20 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { function testFuzz_feeBelowThreshold_min(uint8 nSeed, uint256 wSeed, uint256 bSeed, uint256 dSeed) public view { uint8 n = uint8(bound(nSeed, 2, 8)); uint256[] memory w = _normWeights(n, wSeed); - uint256[] memory b = _balances(n, bSeed); // Pick a pair i!=j. uint8 i = uint8(bound(uint256(keccak256(abi.encode(dSeed, 1))), 0, n - 1)); uint8 j = uint8(bound(uint256(keccak256(abi.encode(dSeed, 2))), 0, n - 1)); if (j == i) j = (i + 1) % n; - uint256 P = _pairSpotFromBalancesWeights(b[i], w[i], b[j], w[j]); + PoolSwapParams memory p; // zero-initialized; p.kind defaults to 0 (= EXACT_IN) + p.kind = SwapKind.EXACT_IN; // keep before==after so we take the NOISE lane + p.balancesScaled18 = _balances(n, bSeed); + p.indexIn = i; + p.indexOut = j; + p.amountGivenScaled18 = 0; + + uint256 P = _pairSpotFromBalancesWeights(p.balancesScaled18[i], w[i], p.balancesScaled18[j], w[j]); vm.assume(P > 0); @@ -161,13 +167,6 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { uint256 oraclePrice = fee_computeOraclePriceForDeviation(P, D); HyperSurgeHook.PoolDetails memory poolDetails = _getDefaultPoolDetails(); - PoolSwapParams memory p; // zero-initialized; p.kind defaults to 0 (= EXACT_IN) - p.kind = SwapKind.EXACT_IN; // keep before==after so we take the NOISE lane - p.balancesScaled18 = b; - p.indexIn = i; - p.indexOut = j; - p.amountGivenScaled18 = 0; - (bool ok, uint256 fee) = hook.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, w, 0, oraclePrice); assertTrue(ok, "compute must succeed"); assertEq(fee, STATIC_SWAP_FEE, "below threshold must return static fee"); @@ -335,54 +334,79 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { assertLe(locals.fee, fee2, "fee must be non-decreasing with deviation"); } - function testFuzz_swapSymmetry_sameLaneParams( - uint8 nSeed, - uint256 wSeed, - uint256 bSeed, - uint256 dSeed - ) public view { - uint8 n = uint8(bound(nSeed, 2, 8)); - uint256[] memory w = _normWeights(n, wSeed); - uint256[] memory b = _balances(n, bSeed); + struct SwapSymmetryLocals { + uint256[] w; + uint8 i; + uint8 j; + uint256 P; + uint256 oraclePrice; + } - uint8 i = uint8(bound(uint256(keccak256(abi.encode(dSeed, 1))), 0, n - 1)); - uint8 j = (i + 1 + uint8(bound(uint256(keccak256(abi.encode(dSeed, 2))), 0, n - 2))) % n; + function testFuzz_swapSymmetry_sameLaneParams(uint8 n, uint256 wSeed, uint256 bSeed, uint256 dSeed) public view { + SwapSymmetryLocals memory locals; + + n = uint8(bound(n, 2, 8)); + locals.w = _normWeights(n, wSeed); + + locals.i = uint8(bound(uint256(keccak256(abi.encode(dSeed, 1))), 0, n - 1)); + locals.j = (locals.i + 1 + uint8(bound(uint256(keccak256(abi.encode(dSeed, 2))), 0, n - 2))) % n; + + PoolSwapParams memory p; + p.kind = SwapKind.EXACT_IN; + p.balancesScaled18 = _balances(n, bSeed); + p.indexIn = locals.i; + p.indexOut = locals.j; + p.amountGivenScaled18 = 0; // Pool spot for (i -> j) using the same rounding/staging as the hook - uint256 P_ij = _pairSpotFromBalancesWeights(b[i], w[i], b[j], w[j]); + uint256 P_ij = _pairSpotFromBalancesWeights( + p.balancesScaled18[locals.i], + locals.w[locals.i], + p.balancesScaled18[locals.j], + locals.w[locals.j] + ); vm.assume(P_ij > 0); // Pick some deviation (bounded safely below 1 to keep pxOut > 0 in _localsForDeviation) uint256 capDev = DEFAULT_CAP_DEV_PPM9; uint256 D = uint256(keccak256(abi.encode(dSeed))) % (capDev + capDev / 2 + 1); - uint256 oraclePrice = fee_computeOraclePriceForDeviation(P_ij, D); + locals.oraclePrice = fee_computeOraclePriceForDeviation(P_ij, D); HyperSurgeHook.PoolDetails memory poolDetails = _getDefaultPoolDetails(); - PoolSwapParams memory p; - p.kind = SwapKind.EXACT_IN; - // Orientation A (i -> j) - (bool okA, uint256 feeA) = hook.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, w, 0, oraclePrice); + (bool okA, uint256 feeA) = hook.ComputeSurgeFee( + p, + poolDetails, + STATIC_SWAP_FEE, + locals.w, + 0, + locals.oraclePrice + ); // Orientation B (j -> i) - p.balancesScaled18 = [b[1], b[0]].toMemoryArray(); + p.balancesScaled18 = [p.balancesScaled18[1], p.balancesScaled18[0]].toMemoryArray(); (bool okB, uint256 feeB) = hook.ComputeSurgeFee( p, poolDetails, STATIC_SWAP_FEE, - [w[1], w[0]].toMemoryArray(), + [locals.w[1], locals.w[0]].toMemoryArray(), 0, - FixedPoint.ONE.divDown(oraclePrice) + FixedPoint.ONE.divDown(locals.oraclePrice) ); assertTrue(okA && okB, "compute must succeed"); // Measure deviations exactly like the hook does in each orientation - uint256 devA = _relAbsDiff(P_ij, oraclePrice); + uint256 devA = _relAbsDiff(P_ij, locals.oraclePrice); // Compute the swapped pool spot with the SAME rounding (don’t assume 1/P) - uint256 P_ji = _pairSpotFromBalancesWeights(b[1], w[1], b[0], w[0]); - uint256 devB = _relAbsDiff(P_ji, FixedPoint.ONE.divDown(oraclePrice)); + uint256 P_ji = _pairSpotFromBalancesWeights( + p.balancesScaled18[1], + locals.w[1], + p.balancesScaled18[0], + locals.w[0] + ); + uint256 devB = _relAbsDiff(P_ji, FixedPoint.ONE.divDown(locals.oraclePrice)); // Correct directional assertion: if (devA > devB) { From d43a44adf8eed5c19cd85391ab46055b73dfabda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Tue, 16 Sep 2025 10:49:37 -0300 Subject: [PATCH 20/28] Fix stack-too-deep --- .../test/foundry/HyperSurgeMaxDeviation.t.sol | 252 ++++++++++++------ 1 file changed, 177 insertions(+), 75 deletions(-) diff --git a/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol b/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol index 5cf41cf5..91eff432 100644 --- a/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol +++ b/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol @@ -172,78 +172,131 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { assertEq(fee, STATIC_SWAP_FEE, "below threshold must return static fee"); } + struct FeeAboveCapLocals { + uint8 n; + uint8 i; + uint8 j; + uint256[] w; + uint256[] b; + uint256 P; + uint256 capDev; + uint256 D; + uint256 oraclePrice; + uint256 extra; + bool ok; + uint256 fee; + uint256 maxPct; + } + /// 2) Above cap deviation ⇒ the dynamic fee must equal the configured maximum. function testFuzz_feeAboveCap_max(uint8 nSeed, uint256 wSeed, uint256 bSeed, uint256 dSeed) public view { - uint8 n = uint8(bound(nSeed, 2, 8)); - uint256[] memory w = _normWeights(n, wSeed); - uint256[] memory b = _balances(n, bSeed); + FeeAboveCapLocals memory locals; - uint8 i = uint8(bound(uint256(keccak256(abi.encode(dSeed, 3))), 0, n - 1)); - uint8 j = uint8(bound(uint256(keccak256(abi.encode(dSeed, 4))), 0, n - 1)); - if (j == i) j = (i + 1) % n; + locals.n = uint8(bound(nSeed, 2, 8)); + locals.w = _normWeights(locals.n, wSeed); + locals.b = _balances(locals.n, bSeed); - uint256 P = _pairSpotFromBalancesWeights(b[i], w[i], b[j], w[j]); - vm.assume(P > 0); + locals.i = uint8(bound(uint256(keccak256(abi.encode(dSeed, 3))), 0, locals.n - 1)); + locals.j = uint8(bound(uint256(keccak256(abi.encode(dSeed, 4))), 0, locals.n - 1)); + if (locals.j == locals.i) locals.j = (locals.i + 1) % locals.n; - uint256 capDev = DEFAULT_CAP_DEV_PPM9 * 1e9; + locals.P = _pairSpotFromBalancesWeights( + locals.b[locals.i], + locals.w[locals.i], + locals.b[locals.j], + locals.w[locals.j] + ); + vm.assume(locals.P > 0); + + locals.capDev = DEFAULT_CAP_DEV_PPM9 * 1e9; // Choose a deviation D >= capDev (push comfortably above to avoid rounding back below). - uint256 extra = (FixedPoint.ONE - capDev) / 4; // up to +25% beyond cap (bounded to keep pxOut > 0) - uint256 D = capDev + (uint256(keccak256(abi.encode(dSeed, 5))) % (extra + 1)); + locals.extra = (FixedPoint.ONE - locals.capDev) / 4; // up to +25% beyond cap (bounded to keep pxOut > 0) + locals.D = locals.capDev + (uint256(keccak256(abi.encode(dSeed, 5))) % (locals.extra + 1)); - uint256 oraclePrice = fee_computeOraclePriceForDeviation(P, D); + uint256 oraclePrice = fee_computeOraclePriceForDeviation(locals.P, locals.D); HyperSurgeHook.PoolDetails memory poolDetails = _getDefaultPoolDetails(); PoolSwapParams memory p; p.kind = SwapKind.EXACT_IN; - p.balancesScaled18 = b; - p.indexIn = i; - p.indexOut = j; + p.balancesScaled18 = locals.b; + p.indexIn = locals.i; + p.indexOut = locals.j; p.amountGivenScaled18 = 0; - (bool ok, uint256 fee) = hook.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, w, 0, oraclePrice); - assertTrue(ok, "compute must succeed"); + (locals.ok, locals.fee) = hook.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, locals.w, 0, oraclePrice); + assertTrue(locals.ok, "compute must succeed"); - uint256 maxPct = DEFAULT_MAX_SURGE_FEE_PPM9 * 1e9; - assertEq(fee, maxPct, "above cap must return max fee"); + locals.maxPct = DEFAULT_MAX_SURGE_FEE_PPM9 * 1e9; + assertEq(locals.fee, locals.maxPct, "above cap must return max fee"); + } + + struct FeeBetweenLinearLocals { + uint8 n; + uint8 i; + uint8 j; + uint256[] w; + uint256[] b; + uint256 P; + uint256 capDev; + uint256 D; + uint256 oraclePrice; + bool ok; + uint256 fee; + uint256 threshold; + uint256 span; } /// 3) Between threshold and cap ⇒ the dynamic fee must be a linear ramp between static and max. function testFuzz_feeBetween_linear(uint8 nSeed, uint256 wSeed, uint256 bSeed, uint256 dSeed) public view { - uint8 n = uint8(bound(nSeed, 2, 8)); - uint256[] memory w = _normWeights(n, wSeed); - uint256[] memory b = _balances(n, bSeed); + FeeBetweenLinearLocals memory locals; - uint8 i = uint8(bound(uint256(keccak256(abi.encode(dSeed, 6))), 0, n - 1)); - uint8 j = uint8(bound(uint256(keccak256(abi.encode(dSeed, 7))), 0, n - 1)); - if (j == i) j = (i + 1) % n; + locals.n = uint8(bound(nSeed, 2, 8)); + locals.w = _normWeights(locals.n, wSeed); + locals.b = _balances(locals.n, bSeed); - uint256 P = _pairSpotFromBalancesWeights(b[i], w[i], b[j], w[j]); - vm.assume(P > 0); + locals.i = uint8(bound(uint256(keccak256(abi.encode(dSeed, 6))), 0, locals.n - 1)); + locals.j = uint8(bound(uint256(keccak256(abi.encode(dSeed, 7))), 0, locals.n - 1)); + if (locals.j == locals.i) locals.j = (locals.i + 1) % locals.n; - uint256 threshold = DEFAULT_THRESHOLD_PPM9; - uint256 capDev = DEFAULT_CAP_DEV_PPM9; - uint256 span = capDev - threshold; + locals.P = _pairSpotFromBalancesWeights( + locals.b[locals.i], + locals.w[locals.i], + locals.b[locals.j], + locals.w[locals.j] + ); + vm.assume(locals.P > 0); + + locals.threshold = DEFAULT_THRESHOLD_PPM9; + locals.capDev = DEFAULT_CAP_DEV_PPM9; + locals.span = locals.capDev - locals.threshold; // Target a deviation strictly inside (threshold, capDev): - uint256 D = threshold + 1 + (uint256(keccak256(abi.encode(dSeed, 8))) % (span - 1)); + locals.D = locals.threshold + 1 + (uint256(keccak256(abi.encode(dSeed, 8))) % (locals.span - 1)); - uint256 oraclePrice = fee_computeOraclePriceForDeviation(P, D); + locals.oraclePrice = fee_computeOraclePriceForDeviation(locals.P, locals.D); HyperSurgeHook.PoolDetails memory poolDetails = _getDefaultPoolDetails(); PoolSwapParams memory p; p.kind = SwapKind.EXACT_IN; - p.balancesScaled18 = b; - p.indexIn = i; - p.indexOut = j; + p.balancesScaled18 = locals.b; + p.indexIn = locals.i; + p.indexOut = locals.j; p.amountGivenScaled18 = 0; - (bool ok, uint256 fee) = hook.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, w, 0, oraclePrice); - assertTrue(ok, "compute must succeed"); + (locals.ok, locals.fee) = hook.ComputeSurgeFee( + p, + poolDetails, + STATIC_SWAP_FEE, + locals.w, + 0, + locals.oraclePrice + ); + assertTrue(locals.ok, "compute must succeed"); // Compute expected with identical rounding. - uint256 expected = _expectedFeeFromLocals(P, oraclePrice); - assertEq(fee, expected, "fee must follow linear ramp between min and max"); + uint256 expected = _expectedFeeFromLocals(locals.P, locals.oraclePrice); + assertEq(locals.fee, expected, "fee must follow linear ramp between min and max"); } function _ppm9To1e18(uint32 v) internal pure returns (uint256) { @@ -340,6 +393,12 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { uint8 j; uint256 P; uint256 oraclePrice; + bool okA; + uint256 feeA; + uint256 devA; + bool okB; + uint256 feeB; + uint256 devB; } function testFuzz_swapSymmetry_sameLaneParams(uint8 n, uint256 wSeed, uint256 bSeed, uint256 dSeed) public view { @@ -376,7 +435,7 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { HyperSurgeHook.PoolDetails memory poolDetails = _getDefaultPoolDetails(); // Orientation A (i -> j) - (bool okA, uint256 feeA) = hook.ComputeSurgeFee( + (locals.okA, locals.feeA) = hook.ComputeSurgeFee( p, poolDetails, STATIC_SWAP_FEE, @@ -386,7 +445,7 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { ); // Orientation B (j -> i) p.balancesScaled18 = [p.balancesScaled18[1], p.balancesScaled18[0]].toMemoryArray(); - (bool okB, uint256 feeB) = hook.ComputeSurgeFee( + (locals.okB, locals.feeB) = hook.ComputeSurgeFee( p, poolDetails, STATIC_SWAP_FEE, @@ -394,10 +453,10 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { 0, FixedPoint.ONE.divDown(locals.oraclePrice) ); - assertTrue(okA && okB, "compute must succeed"); + assertTrue(locals.okA && locals.okB, "compute must succeed"); // Measure deviations exactly like the hook does in each orientation - uint256 devA = _relAbsDiff(P_ij, locals.oraclePrice); + locals.devA = _relAbsDiff(P_ij, locals.oraclePrice); // Compute the swapped pool spot with the SAME rounding (don’t assume 1/P) uint256 P_ji = _pairSpotFromBalancesWeights( @@ -406,16 +465,16 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { p.balancesScaled18[0], locals.w[0] ); - uint256 devB = _relAbsDiff(P_ji, FixedPoint.ONE.divDown(locals.oraclePrice)); + locals.devB = _relAbsDiff(P_ji, FixedPoint.ONE.divDown(locals.oraclePrice)); // Correct directional assertion: - if (devA > devB) { + if (locals.devA > locals.devB) { // allow 1 wei to avoid knife-edge floor rounding flips - assertGe(feeA + 1, feeB, "larger deviation must not yield smaller fee (A vs B)"); - } else if (devB > devA) { - assertGe(feeB + 1, feeA, "larger deviation must not yield smaller fee (B vs A)"); + assertGe(locals.feeA + 1, locals.feeB, "larger deviation must not yield smaller fee (A vs B)"); + } else if (locals.devB > locals.devA) { + assertGe(locals.feeB + 1, locals.feeA, "larger deviation must not yield smaller fee (B vs A)"); } else { - assertApproxEqAbs(feeA, feeB, 1, "equal deviations should give equal fees (1 wei)"); + assertApproxEqAbs(locals.feeA, locals.feeB, 1, "equal deviations should give equal fees (1 wei)"); } } @@ -565,6 +624,8 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { uint8 n; uint8 i; uint8 j; + uint256[] w; + uint256[] b; uint256 P; uint256 threshold; uint256 capDev; @@ -588,13 +649,18 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { ) public view { ThresholdAndCap memory locals; locals.n = uint8(bound(nSeed, 2, 8)); - uint256[] memory w = _normWeights(locals.n, wSeed); - uint256[] memory b = _balances(locals.n, bSeed); + locals.w = _normWeights(locals.n, wSeed); + locals.b = _balances(locals.n, bSeed); locals.i = uint8(bound(uint256(keccak256(abi.encode(dSeed, 1))), 0, locals.n - 1)); locals.j = (locals.i + 1 + uint8(bound(uint256(keccak256(abi.encode(dSeed, 2))), 0, locals.n - 2))) % locals.n; - locals.P = _pairSpotFromBalancesWeights(b[locals.i], w[locals.i], b[locals.j], w[locals.j]); + locals.P = _pairSpotFromBalancesWeights( + locals.b[locals.i], + locals.w[locals.i], + locals.b[locals.j], + locals.w[locals.j] + ); vm.assume(locals.P > 0); locals.threshold = DEFAULT_THRESHOLD_PPM9; @@ -615,12 +681,19 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { PoolSwapParams memory p; p.kind = SwapKind.EXACT_IN; - p.balancesScaled18 = b; + p.balancesScaled18 = locals.b; p.indexIn = locals.i; p.indexOut = locals.j; p.amountGivenScaled18 = 0; - (bool okT, uint256 feeT) = hook.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, w, 0, locals.oraclePriceT); + (bool okT, uint256 feeT) = hook.ComputeSurgeFee( + p, + poolDetails, + STATIC_SWAP_FEE, + locals.w, + 0, + locals.oraclePriceT + ); assertTrue(okT, "compute must succeed (threshold ring)"); locals.expectedT = _expectedFeeFromLocals(locals.P, locals.oraclePriceT); @@ -638,7 +711,14 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { locals.Dc = locals.capDev + (add <= room ? add : room); } (locals.oraclePriceC) = fee_computeOraclePriceForDeviation(locals.P, locals.Dc); - (bool okC, uint256 feeC) = hook.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, w, 0, locals.oraclePriceC); + (bool okC, uint256 feeC) = hook.ComputeSurgeFee( + p, + poolDetails, + STATIC_SWAP_FEE, + locals.w, + 0, + locals.oraclePriceC + ); assertTrue(okC, "compute must succeed (cap ring)"); @@ -647,6 +727,21 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { } } + struct BalanceScalingInvarianceLocals { + uint8 n; + uint8 i; + uint8 j; + uint256[] w; + uint256[] b; + uint256 P; + uint256 capDev; + uint256 D; + uint256 oraclePrice; + uint256 fee1; + uint256 fee2; + uint256 k; + } + /// Balance scaling invariance (unchanged idea, included for completeness). function testFuzz_balanceScalingInvariance( uint8 nSeed, @@ -655,46 +750,53 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { uint256 dSeed, uint64 scaleSeed ) public view { - uint8 n = uint8(bound(nSeed, 2, 8)); - uint256[] memory w = _normWeights(n, wSeed); - uint256[] memory b = _balances(n, bSeed); + BalanceScalingInvarianceLocals memory locals; - uint8 i = uint8(bound(uint256(keccak256(abi.encode(dSeed, 1))), 0, n - 1)); - uint8 j = (i + 1 + uint8(bound(uint256(keccak256(abi.encode(dSeed, 2))), 0, n - 2))) % n; + locals.n = uint8(bound(nSeed, 2, 8)); + locals.w = _normWeights(locals.n, wSeed); + locals.b = _balances(locals.n, bSeed); - uint256 P = _pairSpotFromBalancesWeights(b[i], w[i], b[j], w[j]); - vm.assume(P > 0); + locals.i = uint8(bound(uint256(keccak256(abi.encode(dSeed, 1))), 0, locals.n - 1)); + locals.j = (locals.i + 1 + uint8(bound(uint256(keccak256(abi.encode(dSeed, 2))), 0, locals.n - 2))) % locals.n; - uint256 capDev = DEFAULT_CAP_DEV_PPM9; - uint256 D = uint256(keccak256(abi.encode(dSeed))) % (capDev + capDev / 3 + 1); + locals.P = _pairSpotFromBalancesWeights( + locals.b[locals.i], + locals.w[locals.i], + locals.b[locals.j], + locals.w[locals.j] + ); + vm.assume(locals.P > 0); - uint256 oraclePrice = fee_computeOraclePriceForDeviation(P, D); + locals.capDev = DEFAULT_CAP_DEV_PPM9; + locals.D = uint256(keccak256(abi.encode(dSeed))) % (locals.capDev + locals.capDev / 3 + 1); + + locals.oraclePrice = fee_computeOraclePriceForDeviation(locals.P, locals.D); HyperSurgeHook.PoolDetails memory poolDetails = _getDefaultPoolDetails(); PoolSwapParams memory p; p.kind = SwapKind.EXACT_IN; - p.balancesScaled18 = b; - p.indexIn = i; - p.indexOut = j; + p.balancesScaled18 = locals.b; + p.indexIn = locals.i; + p.indexOut = locals.j; p.amountGivenScaled18 = 0; - (, uint256 fee1) = hook.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, w, 0, oraclePrice); + (, locals.fee1) = hook.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, locals.w, 0, locals.oraclePrice); - uint256 k = 1 + (uint256(scaleSeed) % 1_000_000_000); // [1 .. 1e9] + locals.k = 1 + (uint256(scaleSeed) % 1_000_000_000); // [1 .. 1e9] - p.balancesScaled18 = [b[i] * k, b[j] * k].toMemoryArray(); + p.balancesScaled18 = [locals.b[locals.i] * locals.k, locals.b[locals.j] * locals.k].toMemoryArray(); - (, uint256 fee2) = hook.ComputeSurgeFee( + (, locals.fee2) = hook.ComputeSurgeFee( p, poolDetails, STATIC_SWAP_FEE, - [w[i] * k, w[j] * k].toMemoryArray(), + [locals.w[locals.i] * locals.k, locals.w[locals.j] * locals.k].toMemoryArray(), 0, - oraclePrice + locals.oraclePrice ); - assertApproxEqAbs(fee1, fee2, 1, "fee must be invariant to balance scaling"); + assertApproxEqAbs(locals.fee1, locals.fee2, 1, "fee must be invariant to balance scaling"); } struct ExactOutArbLaneBoundaryLocals { From 124b896dd60b99fb99a512d98bf0ea135a7fd423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Tue, 16 Sep 2025 10:54:38 -0300 Subject: [PATCH 21/28] Fix test --- .../test/foundry/HyperSurgeMaxDeviation.t.sol | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol b/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol index 91eff432..266e9095 100644 --- a/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol +++ b/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol @@ -773,6 +773,7 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { locals.oraclePrice = fee_computeOraclePriceForDeviation(locals.P, locals.D); HyperSurgeHook.PoolDetails memory poolDetails = _getDefaultPoolDetails(); + poolDetails.numTokens = locals.n; PoolSwapParams memory p; p.kind = SwapKind.EXACT_IN; @@ -785,16 +786,11 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { locals.k = 1 + (uint256(scaleSeed) % 1_000_000_000); // [1 .. 1e9] - p.balancesScaled18 = [locals.b[locals.i] * locals.k, locals.b[locals.j] * locals.k].toMemoryArray(); + for (uint256 bl = 0; bl < locals.n; bl++) { + p.balancesScaled18[bl] = p.balancesScaled18[bl] * locals.k; + } - (, locals.fee2) = hook.ComputeSurgeFee( - p, - poolDetails, - STATIC_SWAP_FEE, - [locals.w[locals.i] * locals.k, locals.w[locals.j] * locals.k].toMemoryArray(), - 0, - locals.oraclePrice - ); + (, locals.fee2) = hook.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, locals.w, 0, locals.oraclePrice); assertApproxEqAbs(locals.fee1, locals.fee2, 1, "fee must be invariant to balance scaling"); } From a20dbc4df3d95760974629cab30f8284a45f3813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Tue, 16 Sep 2025 10:58:15 -0300 Subject: [PATCH 22/28] Fix HyperSurgeMaxDeviation file --- .../test/foundry/HyperSurgeMaxDeviation.t.sol | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol b/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol index 266e9095..433ce2a0 100644 --- a/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol +++ b/pkg/pool-hooks/test/foundry/HyperSurgeMaxDeviation.t.sol @@ -444,12 +444,13 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { locals.oraclePrice ); // Orientation B (j -> i) - p.balancesScaled18 = [p.balancesScaled18[1], p.balancesScaled18[0]].toMemoryArray(); + p.indexIn = locals.j; + p.indexOut = locals.i; (locals.okB, locals.feeB) = hook.ComputeSurgeFee( p, poolDetails, STATIC_SWAP_FEE, - [locals.w[1], locals.w[0]].toMemoryArray(), + locals.w, 0, FixedPoint.ONE.divDown(locals.oraclePrice) ); @@ -460,10 +461,10 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { // Compute the swapped pool spot with the SAME rounding (don’t assume 1/P) uint256 P_ji = _pairSpotFromBalancesWeights( - p.balancesScaled18[1], - locals.w[1], - p.balancesScaled18[0], - locals.w[0] + p.balancesScaled18[locals.j], + locals.w[locals.j], + p.balancesScaled18[locals.i], + locals.w[locals.i] ); locals.devB = _relAbsDiff(P_ji, FixedPoint.ONE.divDown(locals.oraclePrice)); @@ -589,15 +590,17 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { p.indexIn = locals.i; p.indexOut = locals.j; p.amountGivenScaled18 = 0; + (locals.ok, locals.fee) = hook.ComputeSurgeFee(p, poolDetails, STATIC_SWAP_FEE, w, 0, locals.oraclePrice); // Orientation B (j -> i) with inverted external prices - p.balancesScaled18 = [b[1], b[0]].toMemoryArray(); + p.indexIn = locals.j; + p.indexOut = locals.i; (bool okB, uint256 feeB) = hook.ComputeSurgeFee( p, poolDetails, STATIC_SWAP_FEE, - [w[1], w[0]].toMemoryArray(), + w, 0, FixedPoint.ONE.divDown(locals.oraclePrice) ); @@ -606,7 +609,7 @@ contract HyperSurgeFindMaxFeeRampTest is BaseVaultTest { // Measure deviations exactly like the hook does uint256 devA = _relAbsDiff(locals.price, locals.oraclePrice); uint256 devB = _relAbsDiff( - _pairSpotFromBalancesWeights(b[1], w[1], b[0], w[0]), + _pairSpotFromBalancesWeights(b[locals.j], w[locals.j], b[locals.i], w[locals.i]), FixedPoint.ONE.divDown(locals.oraclePrice) ); // equals 1/P vs 1/ext due to swap From 18121e7e285e0b1e7e5dd8d07db940ea49a41090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Tue, 16 Sep 2025 11:21:16 -0300 Subject: [PATCH 23/28] Fix test --- .../contracts/hooks-quantamm/HyperSurgeHook.sol | 9 +++++---- .../test/foundry/HyperSurgeLiquidityChecks.t.sol | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol index 9b6d1fb7..9381d7ef 100644 --- a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol +++ b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol @@ -651,21 +651,21 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi function _findMaxDeviation( ComputeOracleDeviationLocals memory locals, uint256[] memory balancesScaled18, - uint256[] memory w + uint256[] memory weights ) internal pure returns (uint256) { // Pairwise check (O(n^2), n<=8). for (locals.i = 0; locals.i < balancesScaled18.length; ++locals.i) { locals.bi = balancesScaled18[locals.i]; - locals.wi = w[locals.i]; + locals.wi = weights[locals.i]; locals.pxi = locals.px[locals.i]; for (locals.j = locals.i + 1; locals.j < balancesScaled18.length; ++locals.j) { locals.bj = balancesScaled18[locals.j]; - locals.wj = w[locals.j]; + locals.wj = weights[locals.j]; locals.pxj = locals.px[locals.j]; // Pool-implied spot for j vs i: (Bj/wj) / (Bi/wi) - locals.poolPx = _pairSpotFromBalancesWeights(balancesScaled18, w, locals.i, locals.j); + locals.poolPx = _pairSpotFromBalancesWeights(balancesScaled18, weights, locals.i, locals.j); if (locals.poolPx == 0) { continue; @@ -673,6 +673,7 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi // External ratio j/i locals.extPx = locals.pxj.divDown(locals.pxi); + locals.dev = _relAbsDiff(locals.poolPx, locals.extPx); if (locals.dev > locals.maxDev) { diff --git a/pkg/pool-hooks/test/foundry/HyperSurgeLiquidityChecks.t.sol b/pkg/pool-hooks/test/foundry/HyperSurgeLiquidityChecks.t.sol index 35ca9f73..ca7f1798 100644 --- a/pkg/pool-hooks/test/foundry/HyperSurgeLiquidityChecks.t.sol +++ b/pkg/pool-hooks/test/foundry/HyperSurgeLiquidityChecks.t.sol @@ -999,7 +999,7 @@ contract HyperSurgeLiquidityCheckTest is BaseVaultTest, HyperSurgeHookDeployer, ); vm.stopPrank(); - assertTrue(ok, "must be greater than, equal is fine"); + assertFalse(ok, "must be greater than, equal is fine"); } /// CASE 6 (worsened): Starts outside BELOW-price, ends outside ABOVE-price with *larger* deviation ⇒ must BLOCK. From ae9110a678f3ba6992a0b3fe3e4ad83a2488a18b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Tue, 16 Sep 2025 18:43:43 -0300 Subject: [PATCH 24/28] Remove check of token length (it's already checked in the vault) --- .../hooks-quantamm/HyperSurgeHook.sol | 5 -- .../test/foundry/HyperSurgeAdmin.t.sol | 64 ------------------- 2 files changed, 69 deletions(-) diff --git a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol index 9381d7ef..ab6b9985 100644 --- a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol +++ b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol @@ -30,7 +30,6 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi error InvalidArrayLengths(); error TokenIndexOutOfRange(); - error NumTokensOutOfRange(); error InvalidPairIndex(); error PoolNotInitialized(); error InvalidDecimals(); @@ -109,10 +108,6 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi TokenConfig[] memory tokenCfgs, LiquidityManagement calldata ) public override onlyVault returns (bool) { - if (tokenCfgs.length < 2 || tokenCfgs.length > 8) { - revert NumTokensOutOfRange(); - } - PoolDetails memory details; details.numTokens = uint8(tokenCfgs.length); // Set the pool details, so we can use the setters and emit the proper events. diff --git a/pkg/pool-hooks/test/foundry/HyperSurgeAdmin.t.sol b/pkg/pool-hooks/test/foundry/HyperSurgeAdmin.t.sol index 3599bf3d..1ae28053 100644 --- a/pkg/pool-hooks/test/foundry/HyperSurgeAdmin.t.sol +++ b/pkg/pool-hooks/test/foundry/HyperSurgeAdmin.t.sol @@ -999,70 +999,6 @@ contract HyperSurgeAdminTest is BaseVaultTest, HyperSurgeHookDeployer, WeightedP assertEq(hook.getCapDeviationPercentage(address(pool), IHyperSurgeHook.TradeType.ARBITRAGE), 1e18); } - function testFuzz_onRegister_RevertWhenTokenCountBelowTwo( - uint8 n, - uint256 defaultThreshold, - uint256 defaultMaxFee, - uint256 defaultCap - ) public { - n = uint8(bound(n, 0, 1)); - defaultThreshold = bound(defaultThreshold, 1, 1e9 - 1); - defaultCap = bound(defaultCap, defaultThreshold + 1, 1e9); - defaultMaxFee = bound(defaultMaxFee, 1, 1e9); - - defaultThreshold *= 1e9; - defaultCap *= 1e9; - defaultMaxFee *= 1e9; - - HyperSurgeHookMock h = new HyperSurgeHookMock( - IVault(vault), - defaultMaxFee, - defaultThreshold, - defaultCap, - "test" - ); - - TokenConfig[] memory cfgs = new TokenConfig[](n); - LiquidityManagement memory lm; - - vm.startPrank(address(vault)); - vm.expectRevert(HyperSurgeHook.NumTokensOutOfRange.selector); - h.onRegister(address(0), address(0), cfgs, lm); - vm.stopPrank(); - } - - function testFuzz_onRegister_RevertWhenTokenCountAboveEight( - uint256 n, - uint256 defaultThreshold, - uint256 defaultMaxFee, - uint256 defaultCap - ) public { - n = bound(n, 9, type(uint8).max); - defaultThreshold = bound(defaultThreshold, 1, 1e9 - 1); - defaultCap = bound(defaultCap, defaultThreshold + 1, 1e9); - defaultMaxFee = bound(defaultMaxFee, 1, 1e9); - - defaultThreshold *= 1e9; - defaultCap *= 1e9; - defaultMaxFee *= 1e9; - - HyperSurgeHookMock h = new HyperSurgeHookMock( - IVault(vault), - defaultMaxFee, - defaultThreshold, - defaultCap, - "test" - ); - - TokenConfig[] memory cfgs = new TokenConfig[](n); - LiquidityManagement memory lm; - - vm.startPrank(address(vault)); - vm.expectRevert(HyperSurgeHook.NumTokensOutOfRange.selector); - h.onRegister(address(0), address(0), cfgs, lm); - vm.stopPrank(); - } - function test_getHookFlags_SignalsAreSet( uint256 defaultThreshold, uint256 defaultMaxFee, From adebde478a6b6897be37b2177b92bac5bc6baa72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Tue, 16 Sep 2025 18:45:19 -0300 Subject: [PATCH 25/28] Remove unnecessary struct --- .../contracts/hooks-quantamm/HyperSurgeHook.sol | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol index ab6b9985..2b8481cf 100644 --- a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol +++ b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol @@ -208,11 +208,6 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi _setTokenPriceConfigIndex(pool, tokenIndex, hlPairIdx, hlTokenIdx, details); } - struct SetBatchConfigs { - TokenPriceCfg tempCfg; - uint256 i; - } - /// @notice Batch version (indices). /// @param pool the pool address /// @param tokenIndices the indices of the token configs being changed @@ -224,14 +219,13 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi uint32[] calldata hlTokenIdx ) external onlySwapFeeManagerOrGovernance(pool) { PoolDetails storage detail = _poolCfg[pool].details; - SetBatchConfigs memory cfg; if (tokenIndices.length != pairIdx.length) { revert InvalidArrayLengths(); } - for (cfg.i = 0; cfg.i < tokenIndices.length; ++cfg.i) { - _setTokenPriceConfigIndex(pool, tokenIndices[cfg.i], pairIdx[cfg.i], hlTokenIdx[cfg.i], detail); + for (uint256 i = 0; i < tokenIndices.length; ++i) { + _setTokenPriceConfigIndex(pool, tokenIndices[i], pairIdx[i], hlTokenIdx[i], detail); } } From 4de2a3e10f937766a71ad5d0a93b79a60f5e85a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Tue, 16 Sep 2025 18:54:44 -0300 Subject: [PATCH 26/28] Fix function comments --- .../hooks-quantamm/HyperSurgeHook.sol | 79 +++++++++++-------- 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol index 2b8481cf..186cce66 100644 --- a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol +++ b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol @@ -193,11 +193,13 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi Setters **************************************************/ - /// @notice Configure a single token’s Hyperliquid mapping for a given pool by token index (0..7). - /// @param pool The pool address to configure. - /// @param tokenIndex The balancer index of the token to configure (0..7). - /// @param hlPairIdx the index of the pair being set - /// @param hlTokenIdx the index of the token being set + /** + * @notice Configure a single token’s Hyperliquid mapping for a given pool by token index (0..7). + * @param pool The pool address to configure. + * @param tokenIndex The balancer index of the token to configure (0..7). + * @param hlPairIdx the index of the pair being set + * @param hlTokenIdx the index of the token being set + */ function setTokenPriceConfigIndex( address pool, uint8 tokenIndex, @@ -208,10 +210,13 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi _setTokenPriceConfigIndex(pool, tokenIndex, hlPairIdx, hlTokenIdx, details); } - /// @notice Batch version (indices). - /// @param pool the pool address - /// @param tokenIndices the indices of the token configs being changed - /// @param pairIdx the index of the pair being changed + /** + * @notice Batch version (indices). + * @param pool the pool address + * @param tokenIndices the indices of the token configs being changed + * @param pairIdx the index of the pair being changed + * @param hlTokenIdx the index of the token being set + */ function setTokenPriceConfigBatchIndex( address pool, uint8[] calldata tokenIndices, @@ -264,7 +269,7 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi address pool, uint256 newMaxSurgeFeePercentageScaled18, TradeType tradeType - ) external override onlySwapFeeManagerOrGovernance(pool) { + ) external override onlySwapFeeManagerOrGovernance(pool) ensureValidPercentage(newMaxSurgeFeePercentageScaled18) { _setMaxSurgeFeePercentage(pool, newMaxSurgeFeePercentageScaled18, tradeType); } @@ -272,7 +277,7 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi address pool, uint256 newMaxSurgeFeePercentageScaled18, TradeType tradeType - ) internal ensureValidPercentage(newMaxSurgeFeePercentageScaled18) { + ) internal { if (tradeType == TradeType.ARBITRAGE) { _poolCfg[pool].details.arbMaxSurgeFee9 = _safeConvertTo9Decimals(newMaxSurgeFeePercentageScaled18); } else { @@ -287,7 +292,7 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi address pool, uint256 newThresholdPercentageScaled18, TradeType tradeType - ) external override onlySwapFeeManagerOrGovernance(pool) { + ) external override onlySwapFeeManagerOrGovernance(pool) ensureValidPercentage(newThresholdPercentageScaled18) { _setSurgeThresholdPercentage(pool, newThresholdPercentageScaled18, tradeType); } @@ -295,7 +300,7 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi address pool, uint256 newThresholdPercentageScaled18, TradeType tradeType - ) internal ensureValidPercentage(newThresholdPercentageScaled18) { + ) internal { uint256 capDeviationPercentageScaled18; PoolDetails memory poolDetails = _poolCfg[pool].details; if (tradeType == TradeType.ARBITRAGE) { @@ -321,7 +326,7 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi address pool, uint256 newCapDeviationPercentageScaled18, TradeType tradeType - ) external override onlySwapFeeManagerOrGovernance(pool) { + ) external override onlySwapFeeManagerOrGovernance(pool) ensureValidPercentage(newCapDeviationPercentageScaled18) { _setCapDeviationPercentage(pool, newCapDeviationPercentageScaled18, tradeType); } @@ -329,7 +334,7 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi address pool, uint256 newCapDeviationPercentageScaled18, TradeType tradeType - ) internal ensureValidPercentage(newCapDeviationPercentageScaled18) { + ) internal { uint256 thresholdPercentageScaled18; PoolDetails memory poolDetails = _poolCfg[pool].details; if (tradeType == TradeType.ARBITRAGE) { @@ -440,27 +445,27 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi } ( - uint256 deviation18, - uint256 capDevPct18, - uint256 maxPct18, - uint256 threshold18 + uint256 deviationScaled18, + uint256 capDeviationPercentageScaled18, + uint256 maxSurgeFeeScaled18, + uint256 thresholdScaled18 ) = _computeDeviationAndSelectPoolDetails(params, weights, calculatedAmountScaled18, oraclePrice, poolDetails); - if (deviation18 <= threshold18) { + if (deviationScaled18 <= thresholdScaled18) { return (true, staticSwapFee); } - uint256 span = capDevPct18 - threshold18; // > 0 by fallback above - uint256 norm = (deviation18 - threshold18).divDown(span); + uint256 span = capDeviationPercentageScaled18 - thresholdScaled18; // > 0 by fallback above + uint256 norm = (deviationScaled18 - thresholdScaled18).divDown(span); if (norm > FixedPoint.ONE) { norm = FixedPoint.ONE; } - uint256 increment = (maxPct18 - staticSwapFee).mulDown(norm); - uint256 surgeFee18 = staticSwapFee + increment; + uint256 increment = (maxSurgeFeeScaled18 - staticSwapFee).mulDown(norm); + uint256 surgeFeeScaled18 = staticSwapFee + increment; - return (true, surgeFee18); + return (true, surgeFeeScaled18); } function _computeDeviationAndSelectPoolDetails( @@ -473,13 +478,13 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi internal pure returns ( - uint256 deviation18, + uint256 deviationScaled18, uint256 capDeviationScaled18, uint256 maxSurgeFeeScaled18, uint256 thresholdScaled18 ) { - uint256 deviationBefore18; + uint256 deviationBeforeScaled18; { uint256 poolPriceBefore = _pairSpotFromBalancesWeights( params.balancesScaled18, @@ -487,7 +492,7 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi params.indexIn, params.indexOut ); - deviationBefore18 = _relAbsDiff(poolPriceBefore, oraclePrice); + deviationBeforeScaled18 = _relAbsDiff(poolPriceBefore, oraclePrice); } uint256[] memory newBalancesScaled18 = new uint256[](params.balancesScaled18.length); @@ -511,11 +516,11 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi params.indexIn, params.indexOut ); - deviation18 = _relAbsDiff(poolPriceAfter, oraclePrice); // |pool - ext| / ext + deviationScaled18 = _relAbsDiff(poolPriceAfter, oraclePrice); // |pool - ext| / ext } // Check if the swap is a noise (deviation is worsening) or an arbitrage (deviation is improving). - if (deviation18 > deviationBefore18) { + if (deviationScaled18 > deviationBeforeScaled18) { // Deviation is worsening, use noise details. capDeviationScaled18 = _convertTo18Decimals(poolDetails.noiseCapDeviationPercentage9); maxSurgeFeeScaled18 = _convertTo18Decimals(poolDetails.noiseMaxSurgeFee9); @@ -531,7 +536,7 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi // decreases the closer you get to market price, another arb opportunity presents itself once the first arb // is taken. This means a large fee != a large no arb region and the pool stays close to market. For more // information, check the HyperSurgeHook-README.md file. - deviation18 = deviationBefore18; + deviationScaled18 = deviationBeforeScaled18; } } @@ -609,9 +614,15 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi uint256 priceDivisor; } - /// @dev Computes the pool-wide oracle deviation as the MAX pairwise deviation - /// across all token pairs (ij) - P_ext(i->j)| / P_ext(i->j). - /// Uses the same spot & external price conventions as the swap-fee compute. + /** + * @dev Computes the pool-wide oracle deviation as the MAX pairwise deviation across all token pairs (ij) - P_ext(i->j)| / P_ext(i->j). Uses the same spot & external price conventions as the swap-fee + * compute. + * + * @param pool The pool address + * @param balancesScaled18 The balances of the pool + * @param w The weights of the pool + */ function _computeOracleDeviationPct( address pool, uint256[] memory balancesScaled18, From 076677a3c93dc7a05a9c52f63c207d87f1a99823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Tue, 16 Sep 2025 19:05:18 -0300 Subject: [PATCH 27/28] Fix PR comments --- .../contracts/hooks-quantamm/HyperSurgeHook.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol index 186cce66..a70bef31 100644 --- a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol +++ b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol @@ -9,6 +9,7 @@ import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol" import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; import { SingletonAuthentication } from "@balancer-labs/v3-vault/contracts/SingletonAuthentication.sol"; +import { InputHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/InputHelpers.sol"; import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; import { WeightedPool } from "@balancer-labs/v3-pool-weighted/contracts/WeightedPool.sol"; import { Version } from "@balancer-labs/v3-solidity-utils/contracts/helpers/Version.sol"; @@ -223,11 +224,9 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi uint32[] calldata pairIdx, uint32[] calldata hlTokenIdx ) external onlySwapFeeManagerOrGovernance(pool) { - PoolDetails storage detail = _poolCfg[pool].details; + InputHelpers.ensureInputLengthMatch(tokenIndices.length, pairIdx.length); - if (tokenIndices.length != pairIdx.length) { - revert InvalidArrayLengths(); - } + PoolDetails storage detail = _poolCfg[pool].details; for (uint256 i = 0; i < tokenIndices.length; ++i) { _setTokenPriceConfigIndex(pool, tokenIndices[i], pairIdx[i], hlTokenIdx[i], detail); @@ -689,6 +688,7 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi * @notice Checks if the pool price deviation is worsening after a add/remove liquidity operation. * @dev The pool price deviation is worsening if the deviation between oracle and pool price increased and the * deviation is greater than the surge threshold. + * * @param pool The pool address * @param oldBalancesScaled18 The balances before the add/remove liquidity operation * @param newBalancesScaled18 The balances after the add/remove liquidity operation From d484ddc6a8e335574851f480b244ed845a4a8686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Tue, 16 Sep 2025 19:06:04 -0300 Subject: [PATCH 28/28] Fix natspec --- .../hooks-quantamm/HyperSurgeHook.sol | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol index a70bef31..b26765c9 100644 --- a/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol +++ b/pkg/pool-hooks/contracts/hooks-quantamm/HyperSurgeHook.sol @@ -95,7 +95,7 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi Hooks **************************************************/ - ///@inheritdoc IHooks + /// @inheritdoc IHooks function getHookFlags() public pure override returns (HookFlags memory hookFlags) { hookFlags.shouldCallComputeDynamicSwapFee = true; hookFlags.shouldCallAfterAddLiquidity = true; @@ -263,7 +263,7 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi emit TokenPriceConfiguredIndex(pool, tokenIndex, tempCfg.pairIndex, hlTokenIdx, tempCfg.sz); } - ///@inheritdoc IHyperSurgeHook + /// @inheritdoc IHyperSurgeHook function setMaxSurgeFeePercentage( address pool, uint256 newMaxSurgeFeePercentageScaled18, @@ -286,7 +286,7 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi emit MaxSurgeFeePercentageChanged(msg.sender, pool, newMaxSurgeFeePercentageScaled18, tradeType); } - ///@inheritdoc IHyperSurgeHook + /// @inheritdoc IHyperSurgeHook function setSurgeThresholdPercentage( address pool, uint256 newThresholdPercentageScaled18, @@ -367,7 +367,7 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi } } - ///@inheritdoc IHyperSurgeHook + /// @inheritdoc IHyperSurgeHook function getMaxSurgeFeePercentage(address pool, TradeType tradeType) external view override returns (uint256) { if (tradeType == TradeType.ARBITRAGE) { return _convertTo18Decimals(_poolCfg[pool].details.arbMaxSurgeFee9); @@ -376,7 +376,7 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi } } - ///@inheritdoc IHyperSurgeHook + /// @inheritdoc IHyperSurgeHook function getCapDeviationPercentage(address pool, TradeType tradeType) external view override returns (uint256) { if (tradeType == TradeType.ARBITRAGE) { return _convertTo18Decimals(_poolCfg[pool].details.arbCapDeviationPercentage9); @@ -385,7 +385,7 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi } } - ///@inheritdoc IHyperSurgeHook + /// @inheritdoc IHyperSurgeHook function getTokenPriceConfigIndex( address pool, uint8 tokenIndex @@ -394,7 +394,7 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi return (cfg.pairIndex, _divisorFromSz(cfg.sz)); } - ///@inheritdoc IHyperSurgeHook + /// @inheritdoc IHyperSurgeHook function getTokenPriceConfigs( address pool ) external view override returns (uint32[] memory pairIndexArr, uint32[] memory priceDivisorArr) { @@ -410,22 +410,22 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi } } - ///@inheritdoc IHyperSurgeHook + /// @inheritdoc IHyperSurgeHook function getDefaultMaxSurgeFeePercentage() external view override returns (uint256) { return _defaultMaxSurgeFeePercentage18; } - ///@inheritdoc IHyperSurgeHook + /// @inheritdoc IHyperSurgeHook function getDefaultSurgeThresholdPercentage() external view override returns (uint256) { return _defaultThresholdPercentage18; } - ///@inheritdoc IHyperSurgeHook + /// @inheritdoc IHyperSurgeHook function getDefaultCapDeviationPercentage() external view override returns (uint256) { return _defaultCapDeviationPercentage18; } - ///@inheritdoc IHyperSurgeHook + /// @inheritdoc IHyperSurgeHook function getNumTokens(address pool) external view override returns (uint8) { return _poolCfg[pool].details.numTokens; } @@ -707,12 +707,12 @@ contract HyperSurgeHook is BaseHooks, VaultGuard, SingletonAuthentication, Versi return (priceDeviationAfter > priceDeviationBefore) && (priceDeviationAfter > surgeThreshold); } - ///@notice Converts a 9 decimal places fixed point number to 18 decimal places. + /// @notice Converts a 9 decimal places fixed point number to 18 decimal places. function _convertTo18Decimals(uint32 valueScaled9) internal pure returns (uint256) { return uint256(valueScaled9) * 1e9; } - ///@notice Converts a 18 decimal places fixed point number to 9 decimal places. + /// @notice Converts a 18 decimal places fixed point number to 9 decimal places. function _safeConvertTo9Decimals(uint256 valueScaled18) internal pure returns (uint32) { return (valueScaled18 / 1e9).toUint32(); }