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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"create": "9533617"
"create": "9533588"
}
37 changes: 29 additions & 8 deletions src/base/BaseDopplerHook.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down
116 changes: 106 additions & 10 deletions src/dopplerHooks/RehypeDopplerHook.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -25,8 +26,14 @@ import {
FeeDistributionMustAddUpToWAD,
FeeRoutingMode,
FeeRoutingModeUpdated,
FeeSchedule,
FeeScheduleSet,
FeeTooHigh,
FeeUpdated,
HookFees,
InitData,
InvalidDurationSeconds,
InvalidFeeRange,
InvalidInitializationDataLength,
MAX_REBALANCE_ITERATIONS,
MAX_SWAP_FEE,
Expand All @@ -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;
Expand All @@ -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 { }

/**
Expand All @@ -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({
Expand All @@ -103,7 +140,7 @@ contract RehypeDopplerHook is BaseDopplerHook {
}

/// @inheritdoc BaseDopplerHook
function _onSwap(
function _onAfterSwap(
address sender,
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
Loading
Loading