Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

beforeSwap returns dynamic fee #648

Merged
merged 26 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
36e7653
allow beforeSwap to return and set the lpFee
saucepoint May 13, 2024
07a9d30
fix tests; default to a sentinel for Hooks.beforeSwap
saucepoint May 13, 2024
28311ce
merge in main; regenerate snapshots
saucepoint May 14, 2024
7406eb9
define constant
saucepoint May 14, 2024
761ccb9
very smol optimization
saucepoint May 14, 2024
f424c1b
Optimise before swap return fee (#650)
hensha256 May 14, 2024
ad6dcf6
Try shared parse lib (#652)
snreynolds May 14, 2024
cd95832
Update src/interfaces/IHooks.sol
saucepoint May 14, 2024
52ef386
misc
saucepoint May 14, 2024
132a877
merge in main; regenerate snapshots
saucepoint May 14, 2024
616bc84
merge in main; regenerate snapshots; fix test
saucepoint May 15, 2024
f508681
review comments; abstract fee checks; add new test
saucepoint May 15, 2024
b5929ae
make ProtocolFeeLibrary similar to LPFeeLibrary
saucepoint May 15, 2024
3a9747c
fix test
saucepoint May 15, 2024
160e4c6
rename how protocol fee is validated
saucepoint May 16, 2024
196af36
fee masking
saucepoint May 16, 2024
525108e
trial new override mechanism
hensha256 May 16, 2024
fdfa579
remove assembly block
hensha256 May 16, 2024
afcda81
cleanup; some natspec
saucepoint May 16, 2024
2ffaa92
revert if override fee exceeds the maximum
saucepoint May 16, 2024
60e1060
review comments; consolidate fee flag
saucepoint May 16, 2024
9499de9
use 2nd bit of uint24 to signal override
saucepoint May 16, 2024
bac221f
naming
saucepoint May 16, 2024
ef84cb0
Update src/libraries/LPFeeLibrary.sol
saucepoint May 17, 2024
35a1402
Update src/libraries/LPFeeLibrary.sol
saucepoint May 17, 2024
abb6988
additional code comment
saucepoint May 17, 2024
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
2 changes: 1 addition & 1 deletion .forge-snapshots/addLiquidity CA fee.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
331831
331641
2 changes: 1 addition & 1 deletion .forge-snapshots/addLiquidity with empty hook.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
286606
286634
2 changes: 1 addition & 1 deletion .forge-snapshots/poolManager bytecode size.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
22099
22121
2 changes: 1 addition & 1 deletion .forge-snapshots/removeLiquidity CA fee.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
187273
187083
2 changes: 1 addition & 1 deletion .forge-snapshots/simple swap with native.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
117304
117431
2 changes: 1 addition & 1 deletion .forge-snapshots/simple swap.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
132465
132592
2 changes: 1 addition & 1 deletion .forge-snapshots/swap CA custom curve + swap noop.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
134799
134711
2 changes: 1 addition & 1 deletion .forge-snapshots/swap CA fee on unspecified.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
183793
183730
Original file line number Diff line number Diff line change
@@ -1 +1 @@
112929
113056
2 changes: 1 addition & 1 deletion .forge-snapshots/swap against liquidity.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
124272
124399
2 changes: 1 addition & 1 deletion .forge-snapshots/swap burn 6909 for input.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
136325
136452
2 changes: 1 addition & 1 deletion .forge-snapshots/swap burn native 6909 for input.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
125453
125580
2 changes: 1 addition & 1 deletion .forge-snapshots/swap mint native output as 6909.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
147514
147641
2 changes: 1 addition & 1 deletion .forge-snapshots/swap mint output as 6909.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
164319
164446
Original file line number Diff line number Diff line change
@@ -1 +1 @@
223093
223312
2 changes: 1 addition & 1 deletion .forge-snapshots/swap with dynamic fee.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
148520
148647
2 changes: 1 addition & 1 deletion .forge-snapshots/swap with hooks.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
124284
124411
2 changes: 1 addition & 1 deletion .forge-snapshots/swap with lp fee and protocol fee.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
180775
180884
1 change: 1 addition & 0 deletions .forge-snapshots/swap with return dynamic fee.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
156517
2 changes: 1 addition & 1 deletion .forge-snapshots/update dynamic fee in before swap.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
159006
159121
32 changes: 19 additions & 13 deletions src/PoolManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -227,19 +227,25 @@ contract PoolManager is IPoolManager, ProtocolFees, NoDelegateCall, ERC6909Claim
PoolId id = key.toId();
_checkPoolInitialized(id);

(int256 amountToSwap, BeforeSwapDelta beforeSwapDelta) = key.hooks.beforeSwap(key, params, hookData);

// execute swap, account protocol fees, and emit swap event
swapDelta = _swap(
id,
Pool.SwapParams({
tickSpacing: key.tickSpacing,
zeroForOne: params.zeroForOne,
amountSpecified: amountToSwap,
sqrtPriceLimitX96: params.sqrtPriceLimitX96
}),
params.zeroForOne ? key.currency0 : key.currency1 // input token
);
BeforeSwapDelta beforeSwapDelta;
{
hensha256 marked this conversation as resolved.
Show resolved Hide resolved
int256 amountToSwap;
uint24 lpFeeOverride;
(amountToSwap, beforeSwapDelta, lpFeeOverride) = key.hooks.beforeSwap(key, params, hookData);

// execute swap, account protocol fees, and emit swap event
swapDelta = _swap(
id,
Pool.SwapParams({
tickSpacing: key.tickSpacing,
zeroForOne: params.zeroForOne,
amountSpecified: amountToSwap,
sqrtPriceLimitX96: params.sqrtPriceLimitX96,
lpFeeOverride: lpFeeOverride
}),
params.zeroForOne ? key.currency0 : key.currency1 // input token
);
}

BalanceDelta hookDelta;
(swapDelta, hookDelta) = key.hooks.afterSwap(key, params, swapDelta, hookData, beforeSwapDelta);
Expand Down
4 changes: 2 additions & 2 deletions src/ProtocolFees.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ abstract contract ProtocolFees is IProtocolFees, Owned {
/// @inheritdoc IProtocolFees
function setProtocolFee(PoolKey memory key, uint24 newProtocolFee) external {
if (msg.sender != address(protocolFeeController)) revert InvalidCaller();
if (!newProtocolFee.validate()) revert InvalidProtocolFee();
if (!newProtocolFee.isValidProtocolFee()) revert InvalidProtocolFee();
PoolId id = key.toId();
_getPool(id).setProtocolFee(newProtocolFee);
emit ProtocolFeeUpdated(id, newProtocolFee);
Expand Down Expand Up @@ -77,7 +77,7 @@ abstract contract ProtocolFees is IProtocolFees, Owned {
returnData := mload(add(_data, 0x20))
}
// Ensure return data does not overflow a uint24 and that the underlying fees are within bounds.
(success, protocolFees) = (returnData == uint24(returnData)) && uint24(returnData).validate()
(success, protocolFees) = (returnData == uint24(returnData)) && uint24(returnData).isValidProtocolFee()
? (true, uint24(returnData))
: (false, 0);
}
Expand Down
3 changes: 2 additions & 1 deletion src/interfaces/IHooks.sol
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,13 @@ interface IHooks {
/// @param hookData Arbitrary data handed into the PoolManager by the swapper to be be passed on to the hook
/// @return bytes4 The function selector for the hook
/// @return BeforeSwapDelta The hook's delta in specified and unspecified currencies. Positive: the hook is owed/took currency, negative: the hook owes/sent currency
/// @return uint24 Optionally override the lp fee, only used if three conditions are met: 1) the Pool has a dynamic fee, 2) the value's leading bit is set to 1 (24th bit, 0x800000), 3) the value is less than or equal to the maximum fee (1 million)
saucepoint marked this conversation as resolved.
Show resolved Hide resolved
function beforeSwap(
address sender,
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
bytes calldata hookData
) external returns (bytes4, BeforeSwapDelta);
) external returns (bytes4, BeforeSwapDelta, uint24);

/// @notice The hook called after a swap
/// @param sender The initial msg.sender for the swap call
Expand Down
34 changes: 15 additions & 19 deletions src/libraries/Hooks.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {LPFeeLibrary} from "./LPFeeLibrary.sol";
import {BalanceDelta, toBalanceDelta, BalanceDeltaLibrary} from "../types/BalanceDelta.sol";
import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "../types/BeforeSwapDelta.sol";
import {IPoolManager} from "../interfaces/IPoolManager.sol";
import {ParseBytes} from "../libraries/ParseBytes.sol";

/// @notice V4 decides whether to invoke specific hooks by inspecting the leading bits of the address that
/// the hooks contract is deployed to.
Expand All @@ -18,6 +19,7 @@ library Hooks {
using Hooks for IHooks;
using SafeCast for int256;
using BeforeSwapDeltaLibrary for BeforeSwapDelta;
using ParseBytes for bytes;

uint256 internal constant BEFORE_INITIALIZE_FLAG = 1 << 159;
uint256 internal constant AFTER_INITIALIZE_FLAG = 1 << 158;
Expand Down Expand Up @@ -125,14 +127,8 @@ library Hooks {
(success, result) = address(self).call(data);
if (!success) _revert(result);

bytes4 expectedSelector;
bytes4 selector;
assembly {
expectedSelector := mload(add(data, 0x20))
selector := mload(add(result, 0x20))
}

if (selector != expectedSelector) revert InvalidHookResponse();
// Check expected selector and returned selector match.
if (result.parseSelector() != data.parseSelector()) revert InvalidHookResponse();
hensha256 marked this conversation as resolved.
Show resolved Hide resolved
}

/// @notice performs a hook call using the given calldata on the given hook
Expand All @@ -145,7 +141,7 @@ library Hooks {

// If this hook wasnt meant to return something, default to 0 delta
if (!parseReturn) return 0;
(, delta) = abi.decode(result, (bytes4, int256));
return result.parseReturnDelta();
}

/// @notice modifier to prevent calling a hook if they initiated the action
Expand Down Expand Up @@ -236,22 +232,22 @@ library Hooks {
/// @notice calls beforeSwap hook if permissioned and validates return value
function beforeSwap(IHooks self, PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData)
internal
returns (int256 amountToSwap, BeforeSwapDelta hookReturn)
returns (int256 amountToSwap, BeforeSwapDelta hookReturn, uint24 lpFeeOverride)
{
amountToSwap = params.amountSpecified;
if (msg.sender == address(self)) return (amountToSwap, BeforeSwapDeltaLibrary.ZERO_DELTA);
if (msg.sender == address(self)) return (amountToSwap, BeforeSwapDeltaLibrary.ZERO_DELTA, lpFeeOverride);

if (self.hasPermission(BEFORE_SWAP_FLAG)) {
bool canReturnDelta = self.hasPermission(BEFORE_SWAP_RETURNS_DELTA_FLAG);
hookReturn = BeforeSwapDelta.wrap(
self.callHookWithReturnDelta(
abi.encodeWithSelector(IHooks.beforeSwap.selector, msg.sender, key, params, hookData),
canReturnDelta
)
);
bytes memory result =
callHook(self, abi.encodeWithSelector(IHooks.beforeSwap.selector, msg.sender, key, params, hookData));

// dynamic fee pools that do not want to override the cache fee, return 0 otherwise they return a valid fee with the override flag
if (key.fee.isDynamicFee()) lpFeeOverride = result.parseFee();

// skip this logic for the case where the hook return is 0
if (canReturnDelta) {
if (self.hasPermission(BEFORE_SWAP_RETURNS_DELTA_FLAG)) {
hookReturn = BeforeSwapDelta.wrap(result.parseReturnDelta());

// any return in unspecified is passed to the afterSwap hook for handling
int128 hookDeltaSpecified = hookReturn.getSpecifiedDelta();

Expand Down
34 changes: 31 additions & 3 deletions src/libraries/LPFeeLibrary.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,52 @@ library LPFeeLibrary {
/// @notice Thrown when the static or dynamic fee on a pool exceeds 100%.
error FeeTooLarge();

uint24 public constant STATIC_FEE_MASK = 0x7FFFFF;
uint24 public constant FEE_MASK = 0x7FFFFF;
uint24 public constant OVERRIDE_MASK = 0xBFFFFF;

// the top bit of the fee in a PoolKey is used to signal if a Pool's LP fee is dynamic
uint24 public constant DYNAMIC_FEE_FLAG = 0x800000;

// the second bit of the fee returned by beforeSwap is used to signal if the stored LP fee should be overridden in this swap
// only dynamic-fee pools can return a fee via the beforeSwap hook
uint24 public constant OVERRIDE_FEE_FLAG = 0x400000;

// the lp fee is represented in hundredths of a bip, so the max is 100%
uint24 public constant MAX_LP_FEE = 1000000;

function isDynamicFee(uint24 self) internal pure returns (bool) {
return self & DYNAMIC_FEE_FLAG != 0;
}

function isValid(uint24 self) internal pure returns (bool) {
return self <= MAX_LP_FEE;
}

function validate(uint24 self) internal pure {
if (self > MAX_LP_FEE) revert FeeTooLarge();
if (!self.isValid()) revert FeeTooLarge();
}

function getInitialLPFee(uint24 self) internal pure returns (uint24 lpFee) {
// the initial fee for a dynamic fee pool is 0
if (self.isDynamicFee()) return 0;
lpFee = self & STATIC_FEE_MASK;
lpFee = self & FEE_MASK;
lpFee.validate();
}

/// @notice returns true if the fee has the override flag set (top bit of the uint24)
function isOverride(uint24 self) internal pure returns (bool) {
return self & OVERRIDE_FEE_FLAG != 0;
}

/// @notice returns a fee with the override flag removed
function removeOverrideFlag(uint24 self) internal pure returns (uint24) {
return self & OVERRIDE_MASK;
}

/// @notice Removes the override flag and validates the fee (reverts if the fee is too large)
function removeOverrideAndValidate(uint24 self) internal pure returns (uint24) {
uint24 fee = self.removeOverrideFlag();
fee.validate();
return fee;
}
}
29 changes: 29 additions & 0 deletions src/libraries/ParseBytes.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity ^0.8.20;

/// @notice Parses bytes returned from hooks and the byte selector used to check return selectors from hooks.
/// @dev parseSelector also is used to parse the expected selector
/// For parsing hook returns, note that all hooks return either bytes4 or (bytes4, 32-byte-delta) or (bytes4, 32-byte-delta, uint24).
library ParseBytes {
function parseSelector(bytes memory result) internal pure returns (bytes4 selector) {
// equivalent: (selector,) = abi.decode(result, (bytes4, int256));
assembly {
selector := mload(add(result, 0x20))
}
}

function parseFee(bytes memory result) internal pure returns (uint24 lpFee) {
// equivalent: (,, lpFee) = abi.decode(result, (bytes4, int256, uint24));
assembly {
lpFee := mload(add(result, 0x60))
}
}

function parseReturnDelta(bytes memory result) internal pure returns (int256 hookReturn) {
// equivalent: (, hookReturnDelta) = abi.decode(result, (bytes4, int256));
assembly {
hookReturn := mload(add(result, 0x40))
}
}
}
9 changes: 7 additions & 2 deletions src/libraries/Pool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ library Pool {
using Position for Position.Info;
using Pool for State;
using ProtocolFeeLibrary for uint24;
using LPFeeLibrary for uint24;

/// @notice Thrown when tickLower is not below tickUpper
/// @param tickLower The invalid tickLower
Expand Down Expand Up @@ -294,6 +295,7 @@ library Pool {
bool zeroForOne;
int256 amountSpecified;
uint160 sqrtPriceLimitX96;
uint24 lpFeeOverride;
}

/// @notice Executes a swap against the state, and returns the amount deltas of the pool
Expand All @@ -317,8 +319,11 @@ library Pool {
state.feeGrowthGlobalX128 = zeroForOne ? self.feeGrowthGlobal0X128 : self.feeGrowthGlobal1X128;
state.liquidity = cache.liquidityStart;

swapFee =
cache.protocolFee == 0 ? slot0Start.lpFee : uint24(cache.protocolFee).calculateSwapFee(slot0Start.lpFee);
// if the beforeSwap hook returned a valid fee override, use that as the LP fee, otherwise load from storage
uint24 lpFee =
params.lpFeeOverride.isOverride() ? params.lpFeeOverride.removeOverrideAndValidate() : slot0Start.lpFee;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sooo much better ❤️


swapFee = cache.protocolFee == 0 ? lpFee : uint24(cache.protocolFee).calculateSwapFee(lpFee);

bool exactInput = params.amountSpecified < 0;

Expand Down
2 changes: 1 addition & 1 deletion src/libraries/ProtocolFeeLibrary.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ library ProtocolFeeLibrary {
return uint16(self >> 12);
}

function validate(uint24 self) internal pure returns (bool) {
function isValidProtocolFee(uint24 self) internal pure returns (bool) {
if (self != 0) {
uint16 fee0 = getZeroForOneFee(self);
uint16 fee1 = getOneForZeroFee(self);
Expand Down
2 changes: 1 addition & 1 deletion src/test/BaseTestHooks.sol
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ contract BaseTestHooks is IHooks {
PoolKey calldata, /* key **/
IPoolManager.SwapParams calldata, /* params **/
bytes calldata /* hookData **/
) external virtual returns (bytes4, BeforeSwapDelta) {
) external virtual returns (bytes4, BeforeSwapDelta, uint24) {
revert HookNotImplemented();
}

Expand Down
4 changes: 2 additions & 2 deletions src/test/CustomCurveHook.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ contract CustomCurveHook is BaseTestHooks {
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
bytes calldata /* hookData **/
) external override onlyPoolManager returns (bytes4, BeforeSwapDelta) {
) external override onlyPoolManager returns (bytes4, BeforeSwapDelta, uint24) {
(Currency inputCurrency, Currency outputCurrency, uint256 amount) = _getInputOutputAndAmount(key, params);

// this "custom curve" is a line, 1-1
Expand All @@ -47,7 +47,7 @@ contract CustomCurveHook is BaseTestHooks {

// return -amountSpecified as specified to no-op the concentrated liquidity swap
BeforeSwapDelta hookDelta = toBeforeSwapDelta(int128(-params.amountSpecified), int128(params.amountSpecified));
return (IHooks.beforeSwap.selector, hookDelta);
return (IHooks.beforeSwap.selector, hookDelta, 0);
}

function afterAddLiquidity(
Expand Down
Loading
Loading