diff --git a/snapshots/CloneERC20FactoryDopplerHookMulticurveInitializerNoOpGovernanceFactoryNoOpMigrator.json b/snapshots/CloneERC20FactoryDopplerHookMulticurveInitializerNoOpGovernanceFactoryNoOpMigrator.json index dc891a3c..484875b8 100644 --- a/snapshots/CloneERC20FactoryDopplerHookMulticurveInitializerNoOpGovernanceFactoryNoOpMigrator.json +++ b/snapshots/CloneERC20FactoryDopplerHookMulticurveInitializerNoOpGovernanceFactoryNoOpMigrator.json @@ -1,3 +1,3 @@ { - "create": "9533617" + "create": "9533588" } \ No newline at end of file diff --git a/src/base/BaseDopplerHook.sol b/src/base/BaseDopplerHook.sol index afeeb73a..c7a73271 100644 --- a/src/base/BaseDopplerHook.sol +++ b/src/base/BaseDopplerHook.sol @@ -10,14 +10,17 @@ import { IDopplerHook } from "src/interfaces/IDopplerHook.sol"; /// @dev Flag for the `onInitialization` callback uint256 constant ON_INITIALIZATION_FLAG = 1 << 0; -/// @dev Flag for the `onSwap` callback -uint256 constant ON_SWAP_FLAG = 1 << 1; +/// @dev Flag for the `onBeforeSwap` callback +uint256 constant ON_BEFORE_SWAP_FLAG = 1 << 1; + +/// @dev Flag for the `onSwap` (afterSwap) callback +uint256 constant ON_AFTER_SWAP_FLAG = 1 << 2; /// @dev Flag for the `onGraduation` callback -uint256 constant ON_GRADUATION_FLAG = 1 << 2; +uint256 constant ON_GRADUATION_FLAG = 1 << 3; /// @dev Flag indicating the hook requires a dynamic LP fee pool -uint256 constant REQUIRES_DYNAMIC_LP_FEE_FLAG = 1 << 3; +uint256 constant REQUIRES_DYNAMIC_LP_FEE_FLAG = 1 << 4; /// @notice Thrown when the `msg.sender` is not the DopplerHookInitializer contract error SenderNotInitializer(); @@ -52,14 +55,24 @@ abstract contract BaseDopplerHook is IDopplerHook { } /// @inheritdoc IDopplerHook - function onSwap( + function onBeforeSwap( + address sender, + PoolKey calldata key, + IPoolManager.SwapParams calldata params, + bytes calldata data + ) external onlyInitializer returns (uint24) { + return _onBeforeSwap(sender, key, params, data); + } + + /// @inheritdoc IDopplerHook + function onAfterSwap( address sender, PoolKey calldata key, IPoolManager.SwapParams calldata params, BalanceDelta balanceDelta, bytes calldata data ) external onlyInitializer returns (Currency, int128) { - return _onSwap(sender, key, params, balanceDelta, data); + return _onAfterSwap(sender, key, params, balanceDelta, data); } /// @inheritdoc IDopplerHook @@ -70,8 +83,16 @@ abstract contract BaseDopplerHook is IDopplerHook { /// @dev Internal function to be overridden for initialization logic function _onInitialization(address asset, PoolKey calldata key, bytes calldata data) internal virtual { } - /// @dev Internal function to be overridden for swap logic - function _onSwap( + /// @dev Internal function to be overridden for before-swap logic (with LP fee override support) + function _onBeforeSwap( + address sender, + PoolKey calldata key, + IPoolManager.SwapParams calldata params, + bytes calldata data + ) internal virtual returns (uint24) { } + + /// @dev Internal function to be overridden for after swap logic + function _onAfterSwap( address sender, PoolKey calldata key, IPoolManager.SwapParams calldata params, diff --git a/src/dopplerHooks/RehypeDopplerHook.sol b/src/dopplerHooks/RehypeDopplerHook.sol index e36638eb..b426a957 100644 --- a/src/dopplerHooks/RehypeDopplerHook.sol +++ b/src/dopplerHooks/RehypeDopplerHook.sol @@ -16,6 +16,7 @@ import { DopplerHookInitializer } from "src/initializers/DopplerHookInitializer. import { MigrationMath } from "src/libraries/MigrationMath.sol"; import { BeneficiaryData } from "src/types/BeneficiaryData.sol"; import { Position } from "src/types/Position.sol"; +import { SafeCastLib } from "@solady/utils/SafeCastLib.sol"; import { AIRLOCK_OWNER_FEE_BPS, AirlockOwnerFeesClaimed, @@ -25,8 +26,14 @@ import { FeeDistributionMustAddUpToWAD, FeeRoutingMode, FeeRoutingModeUpdated, + FeeSchedule, + FeeScheduleSet, + FeeTooHigh, + FeeUpdated, HookFees, InitData, + InvalidDurationSeconds, + InvalidFeeRange, InvalidInitializationDataLength, MAX_REBALANCE_ITERATIONS, MAX_SWAP_FEE, @@ -47,6 +54,7 @@ import { WAD } from "src/types/Wad.sol"; contract RehypeDopplerHook is BaseDopplerHook { using StateLibrary for IPoolManager; using CurrencyLibrary for Currency; + using SafeCastLib for uint256; /// @notice Address of the Uniswap V4 Pool Manager IPoolManager public immutable poolManager; @@ -69,6 +77,9 @@ contract RehypeDopplerHook is BaseDopplerHook { /// @notice Fee routing mode for each pool mapping(PoolId poolId => FeeRoutingMode feeRoutingMode) public getFeeRoutingMode; + /// @notice Fee schedule for each pool (decaying fee) + mapping(PoolId poolId => FeeSchedule feeSchedule) public getFeeSchedule; + receive() external payable { } /** @@ -91,7 +102,33 @@ contract RehypeDopplerHook is BaseDopplerHook { _validateFeeDistribution(initData.feeDistributionInfo); getFeeDistributionInfo[poolId] = initData.feeDistributionInfo; getFeeRoutingMode[poolId] = initData.feeRoutingMode; - getHookFees[poolId].customFee = initData.customFee; + + // Validate and store fee schedule + require(initData.startFee <= uint24(MAX_SWAP_FEE), FeeTooHigh(initData.startFee)); + require(initData.endFee <= uint24(MAX_SWAP_FEE), FeeTooHigh(initData.endFee)); + require(initData.startFee >= initData.endFee, InvalidFeeRange(initData.startFee, initData.endFee)); + + bool isDescending = initData.startFee > initData.endFee; + if (isDescending) { + require(initData.durationSeconds > 0, InvalidDurationSeconds(initData.durationSeconds)); + } + + uint32 normalizedStart = + (initData.startingTime == 0 || initData.startingTime <= uint32(block.timestamp)) + ? uint32(block.timestamp) + : initData.startingTime; + + getFeeSchedule[poolId] = FeeSchedule({ + startingTime: normalizedStart, + startFee: initData.startFee, + endFee: initData.endFee, + lastFee: initData.startFee, + durationSeconds: initData.durationSeconds + }); + + getHookFees[poolId].customFee = initData.startFee; + + emit FeeScheduleSet(poolId, normalizedStart, initData.startFee, initData.endFee, initData.durationSeconds); // Initialize position getPosition[poolId] = Position({ @@ -103,7 +140,7 @@ contract RehypeDopplerHook is BaseDopplerHook { } /// @inheritdoc BaseDopplerHook - function _onSwap( + function _onAfterSwap( address sender, PoolKey calldata key, IPoolManager.SwapParams calldata params, @@ -610,18 +647,24 @@ contract RehypeDopplerHook is BaseDopplerHook { function collectFees(address asset) external returns (BalanceDelta fees) { (,,,,, PoolKey memory poolKey,) = DopplerHookInitializer(payable(INITIALIZER)).getState(asset); PoolId poolId = poolKey.toId(); - HookFees memory hookFees = getHookFees[poolId]; + HookFees storage hookFees = getHookFees[poolId]; address beneficiary = getPoolInfo[poolId].buybackDst; - fees = toBalanceDelta(int128(uint128(hookFees.beneficiaryFees0)), int128(uint128(hookFees.beneficiaryFees1))); + uint128 beneficiaryFees0 = hookFees.beneficiaryFees0; + uint128 beneficiaryFees1 = hookFees.beneficiaryFees1; + + fees = toBalanceDelta(int128(uint128(beneficiaryFees0)), int128(uint128(beneficiaryFees1))); - if (hookFees.beneficiaryFees0 > 0) { + if (beneficiaryFees0 > 0 || beneficiaryFees1 > 0) { getHookFees[poolId].beneficiaryFees0 = 0; - poolKey.currency0.transfer(beneficiary, hookFees.beneficiaryFees0); - } - if (hookFees.beneficiaryFees1 > 0) { getHookFees[poolId].beneficiaryFees1 = 0; - poolKey.currency1.transfer(beneficiary, hookFees.beneficiaryFees1); + } + + if (beneficiaryFees0 > 0) { + poolKey.currency0.transfer(beneficiary, beneficiaryFees0); + } + if (beneficiaryFees1 > 0) { + poolKey.currency1.transfer(beneficiary, beneficiaryFees1); } return fees; @@ -669,6 +712,58 @@ contract RehypeDopplerHook is BaseDopplerHook { ); } + /** + * @dev Computes the current fee based on linear interpolation of the fee schedule + * @param schedule The fee schedule + * @param elapsed Time elapsed since schedule start + * @return The interpolated fee + */ + function _computeCurrentFee(FeeSchedule memory schedule, uint256 elapsed) internal pure returns (uint24) { + uint256 feeRange = uint256(schedule.startFee - schedule.endFee); + uint256 feeDelta_ = feeRange * elapsed / schedule.durationSeconds; + return uint24(uint256(schedule.startFee) - feeDelta_); + } + + /** + * @dev Returns the current fee for a pool, applying the decaying fee schedule + * @param poolId Uniswap V4 poolId + * @return currentFee The current fee rate + */ + function _getCurrentFee(PoolId poolId) internal returns (uint24 currentFee) { + FeeSchedule memory schedule = getFeeSchedule[poolId]; + + // No decay: startFee == endFee or durationSeconds == 0 + if (schedule.startFee == schedule.endFee || schedule.durationSeconds == 0) { + return schedule.startFee; + } + + // Already fully decayed + if (schedule.lastFee == schedule.endFee) { + return schedule.endFee; + } + + // Before schedule start + if (block.timestamp <= schedule.startingTime) { + return schedule.startFee; + } + + uint256 elapsed = block.timestamp - schedule.startingTime; + + if (elapsed >= schedule.durationSeconds) { + currentFee = schedule.endFee; + } else { + currentFee = _computeCurrentFee(schedule, elapsed); + } + + // Only write to storage if the fee has changed (optimization to avoid redundant writes) + if (currentFee < schedule.lastFee) { + getFeeSchedule[poolId].lastFee = currentFee; + emit FeeUpdated(poolId, currentFee); + } + + return currentFee; + } + /** * @dev Collects swap fees from a swap and updates hook fee tracking * @param params Parameters of the swap @@ -706,7 +801,8 @@ contract RehypeDopplerHook is BaseDopplerHook { feeBase = uint256(-inputAmount); } - uint256 feeAmount = FullMath.mulDiv(feeBase, getHookFees[poolId].customFee, MAX_SWAP_FEE); + uint24 currentFee = _getCurrentFee(poolId); + uint256 feeAmount = FullMath.mulDiv(feeBase, currentFee, MAX_SWAP_FEE); uint256 balanceOfFeeCurrency = feeCurrency.balanceOf(address(poolManager)); if (balanceOfFeeCurrency < feeAmount) { diff --git a/src/dopplerHooks/RehypeDopplerHookMigrator.sol b/src/dopplerHooks/RehypeDopplerHookMigrator.sol index 6017a4cf..a112a6d9 100644 --- a/src/dopplerHooks/RehypeDopplerHookMigrator.sol +++ b/src/dopplerHooks/RehypeDopplerHookMigrator.sol @@ -15,6 +15,7 @@ import { BaseDopplerHookMigrator } from "src/base/BaseDopplerHookMigrator.sol"; import { MigrationMath } from "src/libraries/MigrationMath.sol"; import { DopplerHookMigrator } from "src/migrators/DopplerHookMigrator.sol"; import { Position } from "src/types/Position.sol"; +import { SafeCastLib } from "@solady/utils/SafeCastLib.sol"; import { AIRLOCK_OWNER_FEE_BPS, AirlockOwnerFeesClaimed, @@ -24,8 +25,14 @@ import { FeeDistributionMustAddUpToWAD, FeeRoutingMode, FeeRoutingModeUpdated, + FeeSchedule, + FeeScheduleSet, + FeeTooHigh, + FeeUpdated, HookFees, InitData, + InvalidDurationSeconds, + InvalidFeeRange, InvalidInitializationDataLength, MAX_REBALANCE_ITERATIONS, MAX_SWAP_FEE, @@ -46,6 +53,7 @@ import { WAD } from "src/types/Wad.sol"; contract RehypeDopplerHookMigrator is BaseDopplerHookMigrator { using StateLibrary for IPoolManager; using CurrencyLibrary for Currency; + using SafeCastLib for uint256; /// @notice Address of the Uniswap V4 Pool Manager IPoolManager public immutable poolManager; @@ -68,6 +76,9 @@ contract RehypeDopplerHookMigrator is BaseDopplerHookMigrator { /// @notice Fee routing mode for each pool mapping(PoolId poolId => FeeRoutingMode feeRoutingMode) public getFeeRoutingMode; + /// @notice Fee schedule for each pool (decaying fee) + mapping(PoolId poolId => FeeSchedule feeSchedule) public getFeeSchedule; + /// @notice Fallback function to receive ETH receive() external payable { } @@ -91,7 +102,33 @@ contract RehypeDopplerHookMigrator is BaseDopplerHookMigrator { _validateFeeDistribution(initData.feeDistributionInfo); getFeeDistributionInfo[poolId] = initData.feeDistributionInfo; getFeeRoutingMode[poolId] = initData.feeRoutingMode; - getHookFees[poolId].customFee = initData.customFee; + + // Validate and store fee schedule + require(initData.startFee <= uint24(MAX_SWAP_FEE), FeeTooHigh(initData.startFee)); + require(initData.endFee <= uint24(MAX_SWAP_FEE), FeeTooHigh(initData.endFee)); + require(initData.startFee >= initData.endFee, InvalidFeeRange(initData.startFee, initData.endFee)); + + bool isDescending = initData.startFee > initData.endFee; + if (isDescending) { + require(initData.durationSeconds > 0, InvalidDurationSeconds(initData.durationSeconds)); + } + + uint32 normalizedStart = + (initData.startingTime == 0 || initData.startingTime <= uint32(block.timestamp)) + ? uint32(block.timestamp) + : initData.startingTime; + + getFeeSchedule[poolId] = FeeSchedule({ + startingTime: normalizedStart, + startFee: initData.startFee, + endFee: initData.endFee, + lastFee: initData.startFee, + durationSeconds: initData.durationSeconds + }); + + getHookFees[poolId].customFee = initData.startFee; + + emit FeeScheduleSet(poolId, normalizedStart, initData.startFee, initData.endFee, initData.durationSeconds); // Initialize position getPosition[poolId] = Position({ @@ -601,18 +638,24 @@ contract RehypeDopplerHookMigrator is BaseDopplerHookMigrator { (address token0, address token1) = MIGRATOR.getPair(asset); (, PoolKey memory poolKey,,,,,,) = MIGRATOR.getAssetData(token0, token1); PoolId poolId = poolKey.toId(); - HookFees memory hookFees = getHookFees[poolId]; + HookFees storage hookFees = getHookFees[poolId]; address beneficiary = getPoolInfo[poolId].buybackDst; - fees = toBalanceDelta(int128(uint128(hookFees.beneficiaryFees0)), int128(uint128(hookFees.beneficiaryFees1))); + uint128 beneficiaryFees0 = hookFees.beneficiaryFees0; + uint128 beneficiaryFees1 = hookFees.beneficiaryFees1; + + fees = toBalanceDelta(int128(uint128(beneficiaryFees0)), int128(uint128(beneficiaryFees1))); - if (hookFees.beneficiaryFees0 > 0) { + if (beneficiaryFees0 > 0 || beneficiaryFees1 > 0) { getHookFees[poolId].beneficiaryFees0 = 0; - poolKey.currency0.transfer(beneficiary, hookFees.beneficiaryFees0); - } - if (hookFees.beneficiaryFees1 > 0) { getHookFees[poolId].beneficiaryFees1 = 0; - poolKey.currency1.transfer(beneficiary, hookFees.beneficiaryFees1); + } + + if (beneficiaryFees0 > 0) { + poolKey.currency0.transfer(beneficiary, beneficiaryFees0); + } + if (beneficiaryFees1 > 0) { + poolKey.currency1.transfer(beneficiary, beneficiaryFees1); } return fees; @@ -701,6 +744,58 @@ contract RehypeDopplerHookMigrator is BaseDopplerHookMigrator { ); } + /** + * @dev Computes the current fee based on linear interpolation of the fee schedule + * @param schedule The fee schedule + * @param elapsed Time elapsed since schedule start + * @return The interpolated fee + */ + function _computeCurrentFee(FeeSchedule memory schedule, uint256 elapsed) internal pure returns (uint24) { + uint256 feeRange = uint256(schedule.startFee - schedule.endFee); + uint256 feeDelta_ = feeRange * elapsed / schedule.durationSeconds; + return uint24(uint256(schedule.startFee) - feeDelta_); + } + + /** + * @dev Returns the current fee for a pool, applying the decaying fee schedule + * @param poolId Uniswap V4 poolId + * @return currentFee The current fee rate + */ + function _getCurrentFee(PoolId poolId) internal returns (uint24 currentFee) { + FeeSchedule memory schedule = getFeeSchedule[poolId]; + + // No decay: startFee == endFee or durationSeconds == 0 + if (schedule.startFee == schedule.endFee || schedule.durationSeconds == 0) { + return schedule.startFee; + } + + // Already fully decayed + if (schedule.lastFee == schedule.endFee) { + return schedule.endFee; + } + + // Before schedule start + if (block.timestamp <= schedule.startingTime) { + return schedule.startFee; + } + + uint256 elapsed = block.timestamp - schedule.startingTime; + + if (elapsed >= schedule.durationSeconds) { + currentFee = schedule.endFee; + } else { + currentFee = _computeCurrentFee(schedule, elapsed); + } + + // Only write to storage if the fee has changed (optimization to avoid redundant writes) + if (currentFee < schedule.lastFee) { + getFeeSchedule[poolId].lastFee = currentFee; + emit FeeUpdated(poolId, currentFee); + } + + return currentFee; + } + /** * @dev Collects swap fees from a swap and updates hook fee tracking * @param params Parameters of the swap @@ -738,7 +833,8 @@ contract RehypeDopplerHookMigrator is BaseDopplerHookMigrator { feeBase = uint256(-inputAmount); } - uint256 feeAmount = FullMath.mulDiv(feeBase, getHookFees[poolId].customFee, MAX_SWAP_FEE); + uint24 currentFee = _getCurrentFee(poolId); + uint256 feeAmount = FullMath.mulDiv(feeBase, currentFee, MAX_SWAP_FEE); uint256 balanceOfFeeCurrency = feeCurrency.balanceOf(address(poolManager)); if (balanceOfFeeCurrency < feeAmount) { diff --git a/src/dopplerHooks/ScheduledLaunchDopplerHook.sol b/src/dopplerHooks/ScheduledLaunchDopplerHook.sol index 11d81c81..58975f91 100644 --- a/src/dopplerHooks/ScheduledLaunchDopplerHook.sol +++ b/src/dopplerHooks/ScheduledLaunchDopplerHook.sol @@ -36,7 +36,7 @@ contract ScheduledLaunchDopplerHook is BaseDopplerHook { } /// @inheritdoc BaseDopplerHook - function _onSwap( + function _onAfterSwap( address, PoolKey calldata key, IPoolManager.SwapParams calldata, diff --git a/src/dopplerHooks/SwapRestrictorDopplerHook.sol b/src/dopplerHooks/SwapRestrictorDopplerHook.sol index 610bd983..f28bfa84 100644 --- a/src/dopplerHooks/SwapRestrictorDopplerHook.sol +++ b/src/dopplerHooks/SwapRestrictorDopplerHook.sol @@ -44,7 +44,7 @@ contract SwapRestrictorDopplerHook is BaseDopplerHook { } /// @inheritdoc BaseDopplerHook - function _onSwap( + function _onAfterSwap( address sender, PoolKey calldata key, IPoolManager.SwapParams calldata params, diff --git a/src/initializers/DopplerHookInitializer.sol b/src/initializers/DopplerHookInitializer.sol index 1b9f363d..37e1bb73 100644 --- a/src/initializers/DopplerHookInitializer.sol +++ b/src/initializers/DopplerHookInitializer.sol @@ -9,11 +9,17 @@ import { LPFeeLibrary } from "@v4-core/libraries/LPFeeLibrary.sol"; import { StateLibrary } from "@v4-core/libraries/StateLibrary.sol"; import { TickMath } from "@v4-core/libraries/TickMath.sol"; import { BalanceDelta, BalanceDeltaLibrary } from "@v4-core/types/BalanceDelta.sol"; +import { BeforeSwapDelta, BeforeSwapDeltaLibrary } from "@v4-core/types/BeforeSwapDelta.sol"; import { Currency, CurrencyLibrary } from "@v4-core/types/Currency.sol"; import { PoolId } from "@v4-core/types/PoolId.sol"; import { PoolKey } from "@v4-core/types/PoolKey.sol"; import { ImmutableState } from "@v4-periphery/base/ImmutableState.sol"; -import { ON_GRADUATION_FLAG, ON_INITIALIZATION_FLAG, ON_SWAP_FLAG } from "src/base/BaseDopplerHook.sol"; +import { + ON_AFTER_SWAP_FLAG, + ON_BEFORE_SWAP_FLAG, + ON_GRADUATION_FLAG, + ON_INITIALIZATION_FLAG +} from "src/base/BaseDopplerHook.sol"; import { BaseHook } from "src/base/BaseHook.sol"; import { FeesManager } from "src/base/FeesManager.sol"; import { ImmutableAirlock, SenderNotAirlock } from "src/base/ImmutableAirlock.sol"; @@ -168,7 +174,15 @@ struct PoolState { } /// @dev Maximum LP fee allowed (1_000_000 = 100%) -uint24 constant MAX_LP_FEE = 100_000; +uint24 constant MAX_LP_FEE = 800_000; + +/// @dev Ordered flag layout version (beforeSwap=bit1, afterSwap=bit2, graduation=bit3) +uint8 constant FLAG_LAYOUT_VERSION_ORDERED = 2; + +/// @dev Legacy flag bits for backward compatibility with previously stored values. +uint256 constant LEGACY_ON_AFTER_SWAP_FLAG = 1 << 1; +uint256 constant LEGACY_ON_GRADUATION_FLAG = 1 << 2; +uint256 constant LEGACY_ON_BEFORE_SWAP_FLAG = 1 << 4; /** * @title Doppler Hook Uniswap V4 Multicurve Initializer @@ -192,6 +206,9 @@ contract DopplerHookInitializer is ImmutableAirlock, BaseHook, MiniV4Manager, Fe /// @notice Returns a non-zero value if a Doppler hook is enabled mapping(address dopplerHook => uint256 flags) public isDopplerHookEnabled; + /// @notice Flag layout version per Doppler hook (0 = legacy layout, 2 = ordered layout) + mapping(address dopplerHook => uint8 version) public getDopplerHookFlagLayoutVersion; + /// @notice Returns the delegated authority for a user mapping(address user => address authority) public getAuthority; @@ -405,7 +422,7 @@ contract DopplerHookInitializer is ImmutableAirlock, BaseHook, MiniV4Manager, Fe address dopplerHook = state.dopplerHook; uint256 flags = isDopplerHookEnabled[dopplerHook]; - if (dopplerHook == address(0) || flags & ON_GRADUATION_FLAG == 0) { + if (dopplerHook == address(0) || !_hasGraduationFlag(dopplerHook, flags)) { revert CannotMigratePoolNoProvidedDopplerHook(); } @@ -446,6 +463,7 @@ contract DopplerHookInitializer is ImmutableAirlock, BaseHook, MiniV4Manager, Fe for (uint256 i; i != length; i++) { isDopplerHookEnabled[dopplerHooks[i]] = flags[i]; + getDopplerHookFlagLayoutVersion[dopplerHooks[i]] = flags[i] == 0 ? 0 : FLAG_LAYOUT_VERSION_ORDERED; emit SetDopplerHookState(dopplerHooks[i], flags[i]); } } @@ -524,6 +542,28 @@ contract DopplerHookInitializer is ImmutableAirlock, BaseHook, MiniV4Manager, Fe return (BaseHook.afterRemoveLiquidity.selector, BalanceDeltaLibrary.ZERO_DELTA); } + /// @inheritdoc BaseHook + function _beforeSwap( + address sender, + PoolKey calldata key, + IPoolManager.SwapParams calldata params, + bytes calldata data + ) internal override returns (bytes4, BeforeSwapDelta, uint24) { + address asset = getAsset[key.toId()]; + address dopplerHook = getState[asset].dopplerHook; + uint24 lpFeeOverride; + + uint256 flags = isDopplerHookEnabled[dopplerHook]; + if (dopplerHook != address(0) && _hasBeforeSwapFlag(dopplerHook, flags)) { + uint24 fee = IDopplerHook(dopplerHook).onBeforeSwap(sender, key, params, data); + if (fee > 0) { + lpFeeOverride = fee | LPFeeLibrary.OVERRIDE_FEE_FLAG; + } + } + + return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, lpFeeOverride); + } + /// @inheritdoc BaseHook function _afterSwap( address sender, @@ -538,9 +578,10 @@ contract DopplerHookInitializer is ImmutableAirlock, BaseHook, MiniV4Manager, Fe int128 delta; - if (dopplerHook != address(0) && isDopplerHookEnabled[dopplerHook] & ON_SWAP_FLAG != 0) { + uint256 flags = isDopplerHookEnabled[dopplerHook]; + if (dopplerHook != address(0) && _hasAfterSwapFlag(dopplerHook, flags)) { Currency feeCurrency; - (feeCurrency, delta) = IDopplerHook(dopplerHook).onSwap(sender, key, params, balanceDelta, data); + (feeCurrency, delta) = IDopplerHook(dopplerHook).onAfterSwap(sender, key, params, balanceDelta, data); if (delta > 0) { poolManager.take(feeCurrency, address(this), uint128(delta)); @@ -568,7 +609,7 @@ contract DopplerHookInitializer is ImmutableAirlock, BaseHook, MiniV4Manager, Fe beforeRemoveLiquidity: false, afterAddLiquidity: true, afterRemoveLiquidity: true, - beforeSwap: false, + beforeSwap: true, afterSwap: true, beforeDonate: false, afterDonate: false, @@ -578,4 +619,25 @@ contract DopplerHookInitializer is ImmutableAirlock, BaseHook, MiniV4Manager, Fe afterRemoveLiquidityReturnDelta: false }); } + + function _hasBeforeSwapFlag(address dopplerHook, uint256 flags) internal view returns (bool) { + if (getDopplerHookFlagLayoutVersion[dopplerHook] == FLAG_LAYOUT_VERSION_ORDERED) { + return flags & ON_BEFORE_SWAP_FLAG != 0; + } + return flags & LEGACY_ON_BEFORE_SWAP_FLAG != 0; + } + + function _hasAfterSwapFlag(address dopplerHook, uint256 flags) internal view returns (bool) { + if (getDopplerHookFlagLayoutVersion[dopplerHook] == FLAG_LAYOUT_VERSION_ORDERED) { + return flags & ON_AFTER_SWAP_FLAG != 0; + } + return flags & LEGACY_ON_AFTER_SWAP_FLAG != 0; + } + + function _hasGraduationFlag(address dopplerHook, uint256 flags) internal view returns (bool) { + if (getDopplerHookFlagLayoutVersion[dopplerHook] == FLAG_LAYOUT_VERSION_ORDERED) { + return flags & ON_GRADUATION_FLAG != 0; + } + return flags & LEGACY_ON_GRADUATION_FLAG != 0; + } } diff --git a/src/interfaces/IDopplerHook.sol b/src/interfaces/IDopplerHook.sol index d4bbc049..15295fec 100644 --- a/src/interfaces/IDopplerHook.sol +++ b/src/interfaces/IDopplerHook.sol @@ -23,6 +23,21 @@ interface IDopplerHook { */ function onInitialization(address asset, PoolKey calldata key, bytes calldata data) external; + /** + * @notice Called before every swap executed, allows overriding the LP fee for the current swap + * @param sender Address of the swap sender + * @param key Key of the Uniswap V4 pool where the swap is executed + * @param params Swap parameters as defined in IPoolManager + * @param data Extra data to pass to the hook + * @return lpFeeOverride LP fee override for this swap (0 = no override) + */ + function onBeforeSwap( + address sender, + PoolKey calldata key, + IPoolManager.SwapParams calldata params, + bytes calldata data + ) external returns (uint24 lpFeeOverride); + /** * @notice Called after every swap executed * @param sender Address of the swap sender @@ -33,7 +48,7 @@ interface IDopplerHook { * @return feeCurrency Currency being charged (unspecified currency derived from the swap) * @return hookDelta Positive amount if the hook is owed currency, false otherwise */ - function onSwap( + function onAfterSwap( address sender, PoolKey calldata key, IPoolManager.SwapParams calldata params, diff --git a/src/types/RehypeTypes.sol b/src/types/RehypeTypes.sol index 365c9ab8..31613a99 100644 --- a/src/types/RehypeTypes.sol +++ b/src/types/RehypeTypes.sol @@ -47,8 +47,53 @@ uint256 constant AIRLOCK_OWNER_FEE_BPS = 500; /// @dev Basis points denominator uint256 constant BPS_DENOMINATOR = 10_000; -/// @dev Rehype init payload words (with fee routing mode) -uint256 constant REHYPE_INIT_WORDS = 12; +/// @dev Rehype init payload words (with fee routing mode and decaying fee fields) +uint256 constant REHYPE_INIT_WORDS = 15; + +/// @notice Thrown when a fee exceeds the maximum swap fee +error FeeTooHigh(uint24 fee); + +/// @notice Thrown when startFee < endFee +error InvalidFeeRange(uint24 startFee, uint24 endFee); + +/// @notice Thrown when durationSeconds is zero for a descending fee schedule +error InvalidDurationSeconds(uint32 durationSeconds); + +/** + * @notice Emitted when a fee schedule is configured for a pool + * @param poolId Pool id + * @param startingTime Schedule start timestamp + * @param startFee Fee at schedule start + * @param endFee Terminal fee after schedule completion + * @param durationSeconds Number of seconds over which fee linearly descends + */ +event FeeScheduleSet( + PoolId indexed poolId, uint32 startingTime, uint24 startFee, uint24 endFee, uint32 durationSeconds +); + +/** + * @notice Emitted when the custom fee is updated for a pool + * @param poolId Pool id + * @param fee New fee + */ +event FeeUpdated(PoolId indexed poolId, uint24 fee); + +/** + * @notice Packed fee schedule for a pool. + * @dev Fits in a single storage slot to minimize read/write cost. + * @param startingTime Timestamp where schedule starts + * @param startFee Fee at schedule start + * @param endFee Fee at schedule end + * @param lastFee Last applied fee + * @param durationSeconds Schedule duration in seconds + */ +struct FeeSchedule { + uint32 startingTime; + uint24 startFee; + uint24 endFee; + uint24 lastFee; + uint32 durationSeconds; +} /** * @notice Routing mode for buyback-designated fees @@ -64,14 +109,20 @@ enum FeeRoutingMode { * @notice Initialization data for a Rehype-managed pool * @param numeraire Address of the numeraire token * @param buybackDst Address receiving direct buyback proceeds and beneficiary fees - * @param customFee Custom swap fee rate applied to the pool (in millionths, e.g. 5000 = 0.5%) + * @param startFee Fee at schedule start (in millionths, e.g. 5000 = 0.5%) + * @param endFee Terminal fee after decay completes (in millionths) + * @param durationSeconds Duration of linear fee decay (0 = no decay, fee stays at startFee) + * @param startingTime Timestamp when decay begins (0 = use block.timestamp at initialization) * @param feeRoutingMode Routing mode for buyback-designated fees * @param feeDistributionInfo Fee routing matrix percentages for the pool */ struct InitData { address numeraire; address buybackDst; - uint24 customFee; + uint24 startFee; + uint24 endFee; + uint32 durationSeconds; + uint32 startingTime; FeeRoutingMode feeRoutingMode; FeeDistributionInfo feeDistributionInfo; } diff --git a/test/integration/DopplerHookInitializer.t.sol b/test/integration/DopplerHookInitializer.t.sol index 4223211a..f449a530 100644 --- a/test/integration/DopplerHookInitializer.t.sol +++ b/test/integration/DopplerHookInitializer.t.sol @@ -24,7 +24,7 @@ function deployDopplerHookMulticurveInitializer( payable(address( uint160( Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_ADD_LIQUIDITY_FLAG | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG - | Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG + | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG ) ^ (0x4444 << 144) )) ); diff --git a/test/integration/DopplerHookMigratorIntegration.t.sol b/test/integration/DopplerHookMigratorIntegration.t.sol index ea8bc753..14d9f47a 100644 --- a/test/integration/DopplerHookMigratorIntegration.t.sol +++ b/test/integration/DopplerHookMigratorIntegration.t.sol @@ -64,7 +64,7 @@ contract DopplerHookMigratorIntegrationTest is Deployers { payable(address( uint160( Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_ADD_LIQUIDITY_FLAG - | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_SWAP_FLAG + | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG ) ^ (0x4444 << 144) )) @@ -206,7 +206,10 @@ contract DopplerHookMigratorIntegrationTest is Deployers { RehypeInitData({ numeraire: address(0), buybackDst: address(0xBEEF), - customFee: 3000, + startFee: 3000, + endFee: 3000, + durationSeconds: 0, + startingTime: 0, feeRoutingMode: FeeRoutingMode.DirectBuyback, feeDistributionInfo: FeeDistributionInfo({ assetFeesToAssetBuybackWad: 0.2e18, @@ -295,7 +298,7 @@ contract DopplerHookMigratorIntegrationTest is Deployers { vm.expectRevert(abi.encodeWithSelector(SaleHasNotStartedYet.selector, startTime, block.timestamp)); vm.prank(address(migrator)); - scheduledLaunchHook.onSwap( + scheduledLaunchHook.onAfterSwap( address(0), poolKey, IPoolManager.SwapParams(false, 0, 0), BalanceDeltaLibrary.ZERO_DELTA, new bytes(0) ); @@ -353,7 +356,7 @@ contract DopplerHookMigratorIntegrationTest is Deployers { }); vm.prank(address(migrator)); - swapRestrictorHook.onSwap(allowedBuyer, poolKey, swapParams, delta, new bytes(0)); + swapRestrictorHook.onAfterSwap(allowedBuyer, poolKey, swapParams, delta, new bytes(0)); assertLt(swapRestrictorHook.amountLeftOf(poolKey.toId(), allowedBuyer), 1 ether); @@ -362,7 +365,7 @@ contract DopplerHookMigratorIntegrationTest is Deployers { vm.expectRevert( abi.encodeWithSelector(InsufficientAmountLeft.selector, poolKey.toId(), unapproved, 0.01 ether, 0) ); - swapRestrictorHook.onSwap(unapproved, poolKey, swapParams, delta, new bytes(0)); + swapRestrictorHook.onAfterSwap(unapproved, poolKey, swapParams, delta, new bytes(0)); } function test_fullFlow_CreateAndMigrate_DynamicFee() public { diff --git a/test/integration/ImmediateMigration.t.sol b/test/integration/ImmediateMigration.t.sol index 21095ea1..d1d2f80e 100644 --- a/test/integration/ImmediateMigration.t.sol +++ b/test/integration/ImmediateMigration.t.sol @@ -73,8 +73,8 @@ contract ImmediateMigrationTest is Deployers { payable(address( uint160( Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_ADD_LIQUIDITY_FLAG - | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_SWAP_FLAG - | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG + | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG | Hooks.BEFORE_SWAP_FLAG + | Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG ) ^ (0x4444 << 144) )) ); diff --git a/test/integration/RehypeDopplerHook.t.sol b/test/integration/RehypeDopplerHook.t.sol index 0e8121c3..b2d12aba 100644 --- a/test/integration/RehypeDopplerHook.t.sol +++ b/test/integration/RehypeDopplerHook.t.sol @@ -16,7 +16,12 @@ import { console } from "forge-std/console.sol"; import "forge-std/console.sol"; import { Airlock, CreateParams, ModuleState } from "src/Airlock.sol"; -import { ON_INITIALIZATION_FLAG, ON_SWAP_FLAG } from "src/base/BaseDopplerHook.sol"; +import { + ON_AFTER_SWAP_FLAG, + ON_BEFORE_SWAP_FLAG, + ON_GRADUATION_FLAG, + ON_INITIALIZATION_FLAG +} from "src/base/BaseDopplerHook.sol"; import { RehypeDopplerHook } from "src/dopplerHooks/RehypeDopplerHook.sol"; import { GovernanceFactory } from "src/governance/GovernanceFactory.sol"; import { DopplerHookInitializer, InitData, PoolStatus } from "src/initializers/DopplerHookInitializer.sol"; @@ -70,7 +75,7 @@ contract RehypeDopplerHookIntegrationTest is Deployers { payable(address( uint160( Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_ADD_LIQUIDITY_FLAG - | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_SWAP_FLAG + | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG ) ^ (0x4444 << 144) )) @@ -103,7 +108,7 @@ contract RehypeDopplerHookIntegrationTest is Deployers { address[] memory dopplerHooks = new address[](1); dopplerHooks[0] = address(rehypeDopplerHook); uint256[] memory flags = new uint256[](1); - flags[0] = ON_INITIALIZATION_FLAG | ON_SWAP_FLAG; + flags[0] = ON_INITIALIZATION_FLAG | ON_AFTER_SWAP_FLAG; initializer.setDopplerHookState(dopplerHooks, flags); vm.stopPrank(); } @@ -659,7 +664,7 @@ contract RehypeDopplerHookIntegrationTest is Deployers { /* Airlock Owner Fee Tests */ /* ----------------------------------------------------------------------------- */ - function test_airlockOwnerFees_AccumulateOnSwap() public { + function test_airlockOwnerFees_AccumulateonAfterSwap() public { bytes32 salt = bytes32(uint256(14)); (bool isToken0, address asset) = _createToken(salt); @@ -889,7 +894,10 @@ contract RehypeDopplerHookIntegrationTest is Deployers { RehypeInitData({ numeraire: address(numeraire), buybackDst: buybackDst, - customFee: customFee, + startFee: customFee, + endFee: customFee, + durationSeconds: 0, + startingTime: 0, feeRoutingMode: feeRoutingMode, feeDistributionInfo: feeDistribution }) diff --git a/test/integration/RehypeDopplerHookMigratorIntegration.t.sol b/test/integration/RehypeDopplerHookMigratorIntegration.t.sol index 7054ccd9..07c51b7b 100644 --- a/test/integration/RehypeDopplerHookMigratorIntegration.t.sol +++ b/test/integration/RehypeDopplerHookMigratorIntegration.t.sol @@ -62,7 +62,7 @@ contract RehypeDopplerHookMigratorIntegrationTest is Deployers { payable(address( uint160( Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_ADD_LIQUIDITY_FLAG - | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_SWAP_FLAG + | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG ) ^ (0x4444 << 144) )) @@ -713,7 +713,10 @@ contract RehypeDopplerHookMigratorIntegrationTest is Deployers { RehypeInitData({ numeraire: address(0), buybackDst: BUYBACK_DST, - customFee: customFee, + startFee: customFee, + endFee: customFee, + durationSeconds: 0, + startingTime: 0, feeRoutingMode: feeRoutingMode, feeDistributionInfo: FeeDistributionInfo({ assetFeesToAssetBuybackWad: 0.2e18, diff --git a/test/invariant/RehypeHandler.sol b/test/invariant/RehypeHandler.sol index d1cb465a..a1649a30 100644 --- a/test/invariant/RehypeHandler.sol +++ b/test/invariant/RehypeHandler.sol @@ -19,7 +19,7 @@ import { IV4Quoter, V4Quoter } from "@v4-periphery/lens/V4Quoter.sol"; import { Test } from "forge-std/Test.sol"; import { console } from "forge-std/console.sol"; import { Airlock } from "src/Airlock.sol"; -import { ON_GRADUATION_FLAG, ON_INITIALIZATION_FLAG, ON_SWAP_FLAG } from "src/base/BaseDopplerHook.sol"; +import { ON_AFTER_SWAP_FLAG, ON_GRADUATION_FLAG, ON_INITIALIZATION_FLAG } from "src/base/BaseDopplerHook.sol"; import { RehypeDopplerHook } from "src/dopplerHooks/RehypeDopplerHook.sol"; import { DopplerHookInitializer, InitData } from "src/initializers/DopplerHookInitializer.sol"; import { Curve } from "src/libraries/Multicurve.sol"; @@ -50,7 +50,7 @@ contract RehyperInvariantTests is Deployers { payable(address( uint160( Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_ADD_LIQUIDITY_FLAG - | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_SWAP_FLAG + | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG ) ^ (0x4444 << 144) )) @@ -79,7 +79,7 @@ contract RehyperInvariantTests is Deployers { address[] memory hooks = new address[](1); hooks[0] = address(rehypeHook); uint256[] memory flags = new uint256[](1); - flags[0] = ON_INITIALIZATION_FLAG | ON_GRADUATION_FLAG | ON_SWAP_FLAG; + flags[0] = ON_INITIALIZATION_FLAG | ON_GRADUATION_FLAG | ON_AFTER_SWAP_FLAG; vm.prank(AIRLOCK_OWNER); dopplerHookInitializer.setDopplerHookState(hooks, flags); } @@ -256,7 +256,10 @@ contract RehypeHandler is Test { RehypeInitData({ numeraire: numeraire, buybackDst: address(0xbeef), - customFee: 3000, + startFee: 3000, + endFee: 3000, + durationSeconds: 0, + startingTime: 0, feeRoutingMode: FeeRoutingMode.DirectBuyback, feeDistributionInfo: FeeDistributionInfo({ assetFeesToAssetBuybackWad: settings.assetBuybackPercentWad, diff --git a/test/invariant/RehypeMigratorHandler.sol b/test/invariant/RehypeMigratorHandler.sol index ee4fee16..2b814483 100644 --- a/test/invariant/RehypeMigratorHandler.sol +++ b/test/invariant/RehypeMigratorHandler.sol @@ -219,7 +219,10 @@ contract RehypeMigratorHandler is Test { RehypeInitData({ numeraire: settings.numeraire, buybackDst: settings.buybackDst, - customFee: settings.customFee, + startFee: settings.customFee, + endFee: settings.customFee, + durationSeconds: 0, + startingTime: 0, feeRoutingMode: FeeRoutingMode.DirectBuyback, feeDistributionInfo: FeeDistributionInfo({ assetFeesToAssetBuybackWad: settings.assetBuybackPercentWad, diff --git a/test/unit/base/BaseDopplerHook.sol b/test/unit/base/BaseDopplerHook.sol index 4da89d68..1f445d7a 100644 --- a/test/unit/base/BaseDopplerHook.sol +++ b/test/unit/base/BaseDopplerHook.sol @@ -60,12 +60,12 @@ contract BaseDopplerHookTest is Test { } /* ------------------------------------------------------------------------------ */ - /* onSwap() */ + /* onAfterSwap() */ /* ------------------------------------------------------------------------------ */ function test_onSwap_RevertsWhenMsgSenderNotInitializer() public { vm.expectRevert(SenderNotInitializer.selector); - dopplerHook.onSwap( + dopplerHook.onAfterSwap( address(0), PoolKey(Currency.wrap(address(0)), Currency.wrap(address(0)), 0, 0, IHooks(address(0))), IPoolManager.SwapParams(false, 0, 0), @@ -76,7 +76,7 @@ contract BaseDopplerHookTest is Test { function test_onSwap_PassesWhenMsgSenderInitializer() public { vm.prank(initializer); - dopplerHook.onSwap( + dopplerHook.onAfterSwap( address(0), PoolKey(Currency.wrap(address(0)), Currency.wrap(address(0)), 0, 0, IHooks(address(0))), IPoolManager.SwapParams(false, 0, 0), diff --git a/test/unit/dopplerHooks/ScheduledLaunchDopplerHook.t.sol b/test/unit/dopplerHooks/ScheduledLaunchDopplerHook.t.sol index 88f7e3c4..adf57fe7 100644 --- a/test/unit/dopplerHooks/ScheduledLaunchDopplerHook.t.sol +++ b/test/unit/dopplerHooks/ScheduledLaunchDopplerHook.t.sol @@ -45,7 +45,7 @@ contract ScheduledLaunchDopplerHookTest is Test { } /* ---------------------------------------------------------------------- */ - /* onSwap() */ + /* onAfterSwap() */ /* ---------------------------------------------------------------------- */ function test_onSwap_RevertsWhenSenderNotInitializer( @@ -53,7 +53,7 @@ contract ScheduledLaunchDopplerHookTest is Test { IPoolManager.SwapParams calldata swapParams ) public { vm.expectRevert(SenderNotInitializer.selector); - dopplerHook.onSwap(address(0), poolKey, swapParams, BalanceDeltaLibrary.ZERO_DELTA, new bytes(0)); + dopplerHook.onAfterSwap(address(0), poolKey, swapParams, BalanceDeltaLibrary.ZERO_DELTA, new bytes(0)); } function test_onSwap_RevertsWhenSaleNotStarted( @@ -70,7 +70,7 @@ contract ScheduledLaunchDopplerHookTest is Test { vm.expectRevert(abi.encodeWithSelector(SaleHasNotStartedYet.selector, startingTime, block.timestamp)); vm.prank(initializer); - dopplerHook.onSwap(address(0), poolKey, swapParams, BalanceDeltaLibrary.ZERO_DELTA, new bytes(0)); + dopplerHook.onAfterSwap(address(0), poolKey, swapParams, BalanceDeltaLibrary.ZERO_DELTA, new bytes(0)); } function test_onSwap_PassesAfterStartingTime( @@ -86,6 +86,6 @@ contract ScheduledLaunchDopplerHookTest is Test { dopplerHook.onInitialization(address(0), poolKey, abi.encode(startingTime)); vm.prank(initializer); - dopplerHook.onSwap(address(0), poolKey, swapParams, BalanceDeltaLibrary.ZERO_DELTA, new bytes(0)); + dopplerHook.onAfterSwap(address(0), poolKey, swapParams, BalanceDeltaLibrary.ZERO_DELTA, new bytes(0)); } } diff --git a/test/unit/dopplerHooks/SwapRestrictorDopplerHook.t.sol b/test/unit/dopplerHooks/SwapRestrictorDopplerHook.t.sol index 79b4d7ca..06a33273 100644 --- a/test/unit/dopplerHooks/SwapRestrictorDopplerHook.t.sol +++ b/test/unit/dopplerHooks/SwapRestrictorDopplerHook.t.sol @@ -49,7 +49,7 @@ contract SwapRestrictorDopplerHookTest is Test { } /* ---------------------------------------------------------------------- */ - /* onSwap() */ + /* onAfterSwap() */ /* ---------------------------------------------------------------------- */ function test_onSwap_RevertsWhenSenderNotInitializer( @@ -57,7 +57,7 @@ contract SwapRestrictorDopplerHookTest is Test { IPoolManager.SwapParams calldata swapParams ) public { vm.expectRevert(SenderNotInitializer.selector); - dopplerHook.onSwap(address(0), poolKey, swapParams, BalanceDeltaLibrary.ZERO_DELTA, new bytes(0)); + dopplerHook.onAfterSwap(address(0), poolKey, swapParams, BalanceDeltaLibrary.ZERO_DELTA, new bytes(0)); } function test_onSwap_DecreasesAmountLeftWhenBuyingAsset(bool isTokenZero, PoolKey calldata poolKey) public { @@ -78,7 +78,7 @@ contract SwapRestrictorDopplerHookTest is Test { emit UpdatedAmountLeft(poolKey.toId(), approved[0], 0); vm.prank(initializer); - dopplerHook.onSwap( + dopplerHook.onAfterSwap( approved[0], poolKey, swapParams, @@ -104,7 +104,7 @@ contract SwapRestrictorDopplerHookTest is Test { IPoolManager.SwapParams({ zeroForOne: !isTokenZero, amountSpecified: 0, sqrtPriceLimitX96: 0 }); vm.prank(initializer); - dopplerHook.onSwap( + dopplerHook.onAfterSwap( approved[0], poolKey, swapParams, @@ -140,7 +140,7 @@ contract SwapRestrictorDopplerHookTest is Test { ); vm.prank(initializer); - dopplerHook.onSwap( + dopplerHook.onAfterSwap( approved[0], poolKey, swapParams, diff --git a/test/unit/dopplerHooks/rehypeHook/RehypeDopplerHook.t.sol b/test/unit/dopplerHooks/rehypeHook/RehypeDopplerHook.t.sol index b3a0bc19..02c63283 100644 --- a/test/unit/dopplerHooks/rehypeHook/RehypeDopplerHook.t.sol +++ b/test/unit/dopplerHooks/rehypeHook/RehypeDopplerHook.t.sol @@ -103,7 +103,10 @@ contract RehypeDopplerHookTest is Test { InitData({ numeraire: numeraire, buybackDst: buybackDst, - customFee: customFee, + startFee: customFee, + endFee: customFee, + durationSeconds: 0, + startingTime: 0, feeRoutingMode: FeeRoutingMode.DirectBuyback, feeDistributionInfo: FeeDistributionInfo({ assetFeesToAssetBuybackWad: assetBuybackPercentWad, @@ -208,7 +211,10 @@ contract RehypeDopplerHookTest is Test { InitData({ numeraire: numeraire, buybackDst: address(0), - customFee: 0, + startFee: 0, + endFee: 0, + durationSeconds: 0, + startingTime: 0, feeRoutingMode: FeeRoutingMode.DirectBuyback, feeDistributionInfo: FeeDistributionInfo({ assetFeesToAssetBuybackWad: 0.25e18, @@ -237,7 +243,10 @@ contract RehypeDopplerHookTest is Test { InitData({ numeraire: numeraire, buybackDst: address(0), - customFee: 0, + startFee: 0, + endFee: 0, + durationSeconds: 0, + startingTime: 0, feeRoutingMode: FeeRoutingMode.DirectBuyback, feeDistributionInfo: FeeDistributionInfo({ assetFeesToAssetBuybackWad: 0.5e18, @@ -287,6 +296,9 @@ contract RehypeDopplerHookTest is Test { numeraire, buybackDst, uint24(3000), + uint24(3000), + uint32(0), + uint32(0), uint8(2), uint256(0.25e18), uint256(0.25e18), @@ -304,18 +316,18 @@ contract RehypeDopplerHookTest is Test { } /* ---------------------------------------------------------------------- */ - /* onSwap() */ + /* onAfterSwap() */ /* ---------------------------------------------------------------------- */ - function test_onSwap_RevertsWhenSenderNotInitializer( + function test_onAfterSwap_RevertsWhenSenderNotInitializer( PoolKey memory poolKey, IPoolManager.SwapParams memory swapParams ) public { vm.expectRevert(SenderNotInitializer.selector); - dopplerHook.onSwap(address(0), poolKey, swapParams, BalanceDeltaLibrary.ZERO_DELTA, new bytes(0)); + dopplerHook.onAfterSwap(address(0), poolKey, swapParams, BalanceDeltaLibrary.ZERO_DELTA, new bytes(0)); } - function test_onSwap_AccumulatesFees(PoolKey memory poolKey) public { + function test_onAfterSwap_AccumulatesFees(PoolKey memory poolKey) public { poolKey.tickSpacing = 60; address asset = Currency.unwrap(poolKey.currency0); @@ -328,7 +340,10 @@ contract RehypeDopplerHookTest is Test { InitData({ numeraire: numeraire, buybackDst: buybackDst, - customFee: customFee, + startFee: customFee, + endFee: customFee, + durationSeconds: 0, + startingTime: 0, feeRoutingMode: FeeRoutingMode.DirectBuyback, feeDistributionInfo: FeeDistributionInfo({ assetFeesToAssetBuybackWad: 0, @@ -351,7 +366,7 @@ contract RehypeDopplerHookTest is Test { IPoolManager.SwapParams({ zeroForOne: true, amountSpecified: -1e18, sqrtPriceLimitX96: 0 }); vm.prank(address(initializer)); - dopplerHook.onSwap(address(0x123), poolKey, swapParams, BalanceDeltaLibrary.ZERO_DELTA, new bytes(0)); + dopplerHook.onAfterSwap(address(0x123), poolKey, swapParams, BalanceDeltaLibrary.ZERO_DELTA, new bytes(0)); PoolId poolId = poolKey.toId(); @@ -361,11 +376,11 @@ contract RehypeDopplerHookTest is Test { // Note: Actual fee accumulation depends on the fee logic, but fees0 should have been set } - function test_onSwap_SkipsWhenSenderIsHook(PoolKey memory poolKey) public { + function test_onAfterSwap_SkipsWhenSenderIsHook(PoolKey memory poolKey) public { poolKey.tickSpacing = 60; vm.prank(address(initializer)); - (Currency feeCurrency, int128 delta) = dopplerHook.onSwap( + (Currency feeCurrency, int128 delta) = dopplerHook.onAfterSwap( address(dopplerHook), poolKey, IPoolManager.SwapParams(false, 1, 0), BalanceDeltaLibrary.ZERO_DELTA, "" ); @@ -383,7 +398,10 @@ contract RehypeDopplerHookTest is Test { InitData({ numeraire: numeraire, buybackDst: address(0), - customFee: 0, + startFee: 0, + endFee: 0, + durationSeconds: 0, + startingTime: 0, feeRoutingMode: FeeRoutingMode.DirectBuyback, feeDistributionInfo: FeeDistributionInfo({ assetFeesToAssetBuybackWad: 0.5e18, @@ -461,7 +479,10 @@ contract RehypeDopplerHookTest is Test { return InitData({ numeraire: numeraire, buybackDst: buybackDst, - customFee: customFee, + startFee: customFee, + endFee: customFee, + durationSeconds: 0, + startingTime: 0, feeRoutingMode: feeRoutingMode, feeDistributionInfo: FeeDistributionInfo({ assetFeesToAssetBuybackWad: 0.25e18, diff --git a/test/unit/dopplerHooks/rehypeHookMigrator/RehypeDopplerHookMigrator.t.sol b/test/unit/dopplerHooks/rehypeHookMigrator/RehypeDopplerHookMigrator.t.sol index 7ad95ab3..ba7ac226 100644 --- a/test/unit/dopplerHooks/rehypeHookMigrator/RehypeDopplerHookMigrator.t.sol +++ b/test/unit/dopplerHooks/rehypeHookMigrator/RehypeDopplerHookMigrator.t.sol @@ -122,7 +122,10 @@ contract RehypeDopplerHookMigratorTest is Test { InitData({ numeraire: numeraire, buybackDst: buybackDst, - customFee: customFee, + startFee: customFee, + endFee: customFee, + durationSeconds: 0, + startingTime: 0, feeRoutingMode: FeeRoutingMode.DirectBuyback, feeDistributionInfo: FeeDistributionInfo({ assetFeesToAssetBuybackWad: assetBuybackPercentWad, @@ -224,7 +227,10 @@ contract RehypeDopplerHookMigratorTest is Test { InitData({ numeraire: numeraire, buybackDst: address(0), - customFee: 0, + startFee: 0, + endFee: 0, + durationSeconds: 0, + startingTime: 0, feeRoutingMode: FeeRoutingMode.DirectBuyback, feeDistributionInfo: FeeDistributionInfo({ assetFeesToAssetBuybackWad: 0.25e18, @@ -253,7 +259,10 @@ contract RehypeDopplerHookMigratorTest is Test { InitData({ numeraire: numeraire, buybackDst: address(0), - customFee: 0, + startFee: 0, + endFee: 0, + durationSeconds: 0, + startingTime: 0, feeRoutingMode: FeeRoutingMode.DirectBuyback, feeDistributionInfo: FeeDistributionInfo({ assetFeesToAssetBuybackWad: 0.5e18, @@ -577,7 +586,10 @@ contract RehypeDopplerHookMigratorTest is Test { InitData({ numeraire: numeraire, buybackDst: buybackDst, - customFee: 10_000, + startFee: 10_000, + endFee: 10_000, + durationSeconds: 0, + startingTime: 0, feeRoutingMode: FeeRoutingMode.DirectBuyback, feeDistributionInfo: FeeDistributionInfo({ assetFeesToAssetBuybackWad: 0, @@ -624,7 +636,10 @@ contract RehypeDopplerHookMigratorTest is Test { InitData({ numeraire: numeraire, buybackDst: buybackDst, - customFee: 5000, + startFee: 5000, + endFee: 5000, + durationSeconds: 0, + startingTime: 0, feeRoutingMode: FeeRoutingMode.DirectBuyback, feeDistributionInfo: FeeDistributionInfo({ assetFeesToAssetBuybackWad: 0, @@ -726,7 +741,10 @@ contract RehypeDopplerHookMigratorTest is Test { InitData({ numeraire: numeraire, buybackDst: buybackDst, - customFee: customFee, + startFee: customFee, + endFee: customFee, + durationSeconds: 0, + startingTime: 0, feeRoutingMode: FeeRoutingMode.DirectBuyback, feeDistributionInfo: FeeDistributionInfo({ assetFeesToAssetBuybackWad: 0, @@ -773,7 +791,10 @@ contract RehypeDopplerHookMigratorTest is Test { return InitData({ numeraire: numeraire, buybackDst: buybackDst, - customFee: customFee, + startFee: customFee, + endFee: customFee, + durationSeconds: 0, + startingTime: 0, feeRoutingMode: feeRoutingMode, feeDistributionInfo: FeeDistributionInfo({ assetFeesToAssetBuybackWad: 0.25e18, diff --git a/test/unit/initializers/DecayMulticurveInitializer.t.sol b/test/unit/initializers/DecayMulticurveInitializer.t.sol index aba34387..1862f367 100644 --- a/test/unit/initializers/DecayMulticurveInitializer.t.sol +++ b/test/unit/initializers/DecayMulticurveInitializer.t.sol @@ -316,7 +316,7 @@ contract DecayMulticurveInitializerTest is Deployers { assertEq(duration, durationSeconds, "Incorrect schedule duration"); } - function testFuzz_initialize_FlatScheduleCompletesImmediatelyAndNoOpsOnSwap( + function testFuzz_initialize_FlatScheduleCompletesImmediatelyAndNoOpsonAfterSwap( bool isToken0, uint24 rawFee, uint32 rawStartOffset, diff --git a/test/unit/initializers/DopplerHookInitializer.t.sol b/test/unit/initializers/DopplerHookInitializer.t.sol index 1e3da1dc..ccd275da 100644 --- a/test/unit/initializers/DopplerHookInitializer.t.sol +++ b/test/unit/initializers/DopplerHookInitializer.t.sol @@ -17,7 +17,12 @@ import { PoolId } from "@v4-core/types/PoolId.sol"; import { ImmutableState } from "@v4-periphery/base/ImmutableState.sol"; import { Airlock } from "src/Airlock.sol"; -import { ON_GRADUATION_FLAG, ON_INITIALIZATION_FLAG, ON_SWAP_FLAG } from "src/base/BaseDopplerHook.sol"; +import { + ON_AFTER_SWAP_FLAG, + ON_BEFORE_SWAP_FLAG, + ON_GRADUATION_FLAG, + ON_INITIALIZATION_FLAG +} from "src/base/BaseDopplerHook.sol"; import { SenderNotAirlock } from "src/base/ImmutableAirlock.sol"; import { ArrayLengthsMismatch, @@ -52,7 +57,13 @@ import { WAD } from "src/types/Wad.sol"; contract MockDopplerHook is IDopplerHook { function onInitialization(address, PoolKey calldata, bytes calldata) external { } - function onSwap( + function onBeforeSwap( + address, + PoolKey calldata, + IPoolManager.SwapParams calldata, + bytes calldata + ) external returns (uint24) { } + function onAfterSwap( address, PoolKey calldata, IPoolManager.SwapParams calldata, @@ -84,7 +95,7 @@ contract DopplerHookMulticurveInitializerTest is Deployers { payable(address( uint160( Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_ADD_LIQUIDITY_FLAG - | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_SWAP_FLAG + | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG ) ^ (0x4444 << 144) )) @@ -96,7 +107,7 @@ contract DopplerHookMulticurveInitializerTest is Deployers { address[] memory dopplerHooks = new address[](1); uint256[] memory flags = new uint256[](1); dopplerHooks[0] = address(dopplerHook); - flags[0] = ON_INITIALIZATION_FLAG | ON_GRADUATION_FLAG | ON_SWAP_FLAG; + flags[0] = ON_INITIALIZATION_FLAG | ON_GRADUATION_FLAG | ON_AFTER_SWAP_FLAG; vm.prank(airlockOwner); initializer.setDopplerHookState(dopplerHooks, flags); } @@ -230,7 +241,7 @@ contract DopplerHookMulticurveInitializerTest is Deployers { address[] memory dopplerHooks = new address[](1); uint256[] memory flags = new uint256[](1); dopplerHooks[0] = address(dopplerHook); - flags[0] = ON_INITIALIZATION_FLAG | ON_GRADUATION_FLAG | ON_SWAP_FLAG; + flags[0] = ON_INITIALIZATION_FLAG | ON_GRADUATION_FLAG | ON_AFTER_SWAP_FLAG; initializer.setDopplerHookState(dopplerHooks, flags); vm.expectCall( @@ -254,7 +265,7 @@ contract DopplerHookMulticurveInitializerTest is Deployers { address[] memory dopplerHooks = new address[](1); uint256[] memory flags = new uint256[](1); dopplerHooks[0] = address(dopplerHook); - flags[0] = ON_GRADUATION_FLAG | ON_SWAP_FLAG; + flags[0] = ON_GRADUATION_FLAG | ON_AFTER_SWAP_FLAG; initializer.setDopplerHookState(dopplerHooks, flags); vm.expectCall( @@ -705,7 +716,7 @@ contract DopplerHookMulticurveInitializerTest is Deployers { test_initialize_LocksPoolWithDopplerHook(initParams, isToken0); vm.expectCall( address(dopplerHook), - abi.encodeWithSelector(IDopplerHook.onSwap.selector, sender, poolKey, swapParams, balanceDelta, data) + abi.encodeWithSelector(IDopplerHook.onAfterSwap.selector, sender, poolKey, swapParams, balanceDelta, data) ); vm.prank(address(manager)); initializer.afterSwap(sender, poolKey, swapParams, balanceDelta, data);