Skip to content

Commit

Permalink
Merge pull request #20 from uniswapfoundation/dynamic-fees
Browse files Browse the repository at this point in the history
Dynamic Swap Fees
  • Loading branch information
saucepoint authored Jan 23, 2024
2 parents f83dc26 + 81780e4 commit 57dd289
Show file tree
Hide file tree
Showing 19 changed files with 645 additions and 7 deletions.
1 change: 1 addition & 0 deletions .forge-snapshots/autodynamic fee.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
242952
2 changes: 1 addition & 1 deletion .forge-snapshots/counter.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
339161
358444
2 changes: 1 addition & 1 deletion .forge-snapshots/hookFee.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
246486
265804
1 change: 1 addition & 0 deletions .forge-snapshots/manual dynamic fee.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
219217
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,5 @@ Generate react, register routes, and rebuild search index
```bash
npm run generate
```

Update the [changelog](src/pages/index.tsx#L11)
6 changes: 4 additions & 2 deletions forge-test/Counter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ contract CounterTest is HookTest, GasSnapshot {
HookTest.initHookTestEnv();

// Deploy the hook to an address with the correct flags
uint160 flags =
uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG);
uint160 flags = uint160(
Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG
| Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG
);
(address hookAddress, bytes32 salt) =
HookMiner.find(address(this), flags, type(Counter).creationCode, abi.encode(address(manager)));
counter = new Counter{salt: salt}(IPoolManager(address(manager)));
Expand Down
207 changes: 207 additions & 0 deletions forge-test/DynamicFees.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "forge-std/Test.sol";
import {IHooks} from "v4-core/interfaces/IHooks.sol";
import {Hooks} from "v4-core/libraries/Hooks.sol";
import {TickMath} from "v4-core/libraries/TickMath.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {BalanceDelta} from "v4-core/types/BalanceDelta.sol";
import {PoolId, PoolIdLibrary} from "v4-core/types/PoolId.sol";
import {Constants} from "v4-core/../test/utils/Constants.sol";
import {CurrencyLibrary, Currency} from "v4-core/types/Currency.sol";
import {FeeLibrary} from "v4-core/libraries/FeeLibrary.sol";
import {HookTest} from "@v4-by-example/utils/HookTest.sol";
import {HookMiner} from "./utils/HookMiner.sol";
import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol";
import {PoolSwapTest} from "v4-core/test/PoolSwapTest.sol";
import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol";

import {ManualDynamicFee} from "@v4-by-example/pages/fees/dynamic-fee/ManualDynamicFee.sol";
import {AutoDynamicFee} from "@v4-by-example/pages/fees/dynamic-fee/AutoDynamicFee.sol";

contract DynamicFeesTest is HookTest, GasSnapshot {
using FixedPointMathLib for uint256;
using PoolIdLibrary for PoolKey;
using CurrencyLibrary for Currency;

AutoDynamicFee autoDynamicFee;
ManualDynamicFee manualDynamicFee;

PoolKey autoDynamicFeePoolKey;
PoolKey manualDynamicFeePoolKey;

function setUp() public {
// creates the pool manager, test tokens, and other utility routers
HookTest.initHookTestEnv();

// Deploy the hook to an address with the correct flags
uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG);
(address hookAddress, bytes32 salt) =
HookMiner.find(address(this), flags, type(AutoDynamicFee).creationCode, abi.encode(address(manager)));
autoDynamicFee = new AutoDynamicFee{salt: salt}(IPoolManager(address(manager)));
require(address(autoDynamicFee) == hookAddress, "hook address mismatch");

(hookAddress, salt) =
HookMiner.find(address(this), uint160(0), type(ManualDynamicFee).creationCode, abi.encode(address(manager)));
manualDynamicFee = new ManualDynamicFee{salt: salt}(IPoolManager(address(manager)));
require(address(manualDynamicFee) == hookAddress, "hook address mismatch");

// Create the pools
autoDynamicFeePoolKey = PoolKey(
Currency.wrap(address(token0)),
Currency.wrap(address(token1)),
FeeLibrary.DYNAMIC_FEE_FLAG,
60,
IHooks(autoDynamicFee)
);
initializeRouter.initialize(autoDynamicFeePoolKey, Constants.SQRT_RATIO_1_1, ZERO_BYTES);

manualDynamicFeePoolKey = PoolKey(
Currency.wrap(address(token0)),
Currency.wrap(address(token1)),
FeeLibrary.DYNAMIC_FEE_FLAG,
60,
IHooks(manualDynamicFee)
);
initializeRouter.initialize(manualDynamicFeePoolKey, Constants.SQRT_RATIO_1_1, ZERO_BYTES);

// Provide liquidity to the pool
modifyPositionRouter.modifyLiquidity(
autoDynamicFeePoolKey,
IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 100000 ether),
ZERO_BYTES
);

modifyPositionRouter.modifyLiquidity(
manualDynamicFeePoolKey,
IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 100000 ether),
ZERO_BYTES
);
}

function test_start_autoFee() public {
// Perform a test swap //
int256 amount = 1e18;
bool zeroForOne = true;
BalanceDelta swapDelta = swap(autoDynamicFeePoolKey, amount, zeroForOne, ZERO_BYTES);
// ------------------- //

// fee on output token, so expect ~0.95e18 output
assertLt(uint256(-int256(swapDelta.amount1())), 0.95e18);
assertGt(uint256(-int256(swapDelta.amount1())), 0.94e18);
assertApproxEqAbs(
uint256(-int256(swapDelta.amount1())), uint256(amount), uint256(amount).mulWadDown(0.05001e18)
);
}

function test_floor_autoFee() public {
// skip 496,000 seconds, fee is now floored at 0.05%
skip(496000);

// Perform a test swap //
int256 amount = 1e18;
bool zeroForOne = true;
BalanceDelta swapDelta = swap(autoDynamicFeePoolKey, amount, zeroForOne, ZERO_BYTES);
// ------------------- //

assertLt(uint256(-int256(swapDelta.amount1())), 0.9995e18);
assertGt(uint256(-int256(swapDelta.amount1())), 0.9994e18);
assertApproxEqAbs(
uint256(-int256(swapDelta.amount1())), uint256(amount), uint256(amount).mulWadDown(0.00051e18)
);
}

function test_start_manualFee() public {
// Perform a test swap //
int256 amount = 1e18;
bool zeroForOne = true;
BalanceDelta swapDelta = swap(manualDynamicFeePoolKey, amount, zeroForOne, ZERO_BYTES);
// ------------------- //

// fee on output token, so expect ~0.95e18 output
assertLt(uint256(-int256(swapDelta.amount1())), 0.95e18);
assertGt(uint256(-int256(swapDelta.amount1())), 0.94e18);
assertApproxEqAbs(
uint256(-int256(swapDelta.amount1())), uint256(amount), uint256(amount).mulWadDown(0.05001e18)
);
}

function test_floor_manualFee() public {
// skip 496,000 seconds, fee is now floored at 0.05%
skip(496000);

// poke the pool manager
manager.updateDynamicSwapFee(manualDynamicFeePoolKey);

// Perform a test swap //
int256 amount = 1e18;
bool zeroForOne = true;
BalanceDelta swapDelta = swap(manualDynamicFeePoolKey, amount, zeroForOne, ZERO_BYTES);
// ------------------- //

assertLt(uint256(-int256(swapDelta.amount1())), 0.9995e18);
assertGt(uint256(-int256(swapDelta.amount1())), 0.9994e18);
assertApproxEqAbs(
uint256(-int256(swapDelta.amount1())), uint256(amount), uint256(amount).mulWadDown(0.00051e18)
);
}

function test_staleFee_manualFee() public {
// skip 496,000 seconds, but the fee is still 5% since its stale and wasnt updated
skip(496000);

// Perform a test swap //
int256 amount = 1e18;
bool zeroForOne = true;
BalanceDelta swapDelta = swap(manualDynamicFeePoolKey, amount, zeroForOne, ZERO_BYTES);
// ------------------- //

// fee on output token, so expect ~0.95e18 output
assertLt(uint256(-int256(swapDelta.amount1())), 0.95e18);
assertGt(uint256(-int256(swapDelta.amount1())), 0.94e18);
assertApproxEqAbs(
uint256(-int256(swapDelta.amount1())), uint256(amount), uint256(amount).mulWadDown(0.05001e18)
);
}

function test_snapshot_autoFee() public {
skip(100_000);
int256 amount = 1e18;
bool zeroForOne = true;
IPoolManager.SwapParams memory params = IPoolManager.SwapParams({
zeroForOne: zeroForOne,
amountSpecified: amount,
sqrtPriceLimitX96: zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT // unlimited impact
});

PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true, currencyAlreadySent: false});

snapStart("autodynamic fee");
swapRouter.swap(autoDynamicFeePoolKey, params, testSettings, ZERO_BYTES);
snapEnd();
}

function test_snapshot_manualFee() public {
skip(100_000);
// poke the pool manager
manager.updateDynamicSwapFee(manualDynamicFeePoolKey);

int256 amount = 1e18;
bool zeroForOne = true;
IPoolManager.SwapParams memory params = IPoolManager.SwapParams({
zeroForOne: zeroForOne,
amountSpecified: amount,
sqrtPriceLimitX96: zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT // unlimited impact
});

PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true, currencyAlreadySent: false});

snapStart("manual dynamic fee");
swapRouter.swap(manualDynamicFeePoolKey, params, testSettings, ZERO_BYTES);
snapEnd();
}
}
2 changes: 1 addition & 1 deletion forge-test/utils/HookMiner.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ library HookMiner {
uint160 constant FLAG_MASK = 0xFFF << 148;

// Maximum number of iterations to find a salt, avoid infinite loops
uint256 constant MAX_LOOP = 10_000;
uint256 constant MAX_LOOP = 20_000;

/// @notice Find a salt that produces a hook address with the desired `flags`
/// @param deployer The address that will deploy the hook. In `forge test`, this will be the test contract `address(this)` or the pranking address
Expand Down
7 changes: 7 additions & 0 deletions src/keywords.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@
"static fee",
"hook fee"
],
"/fees/dynamic-fee": [
"fee",
"fees",
"dynamic fee",
"dynamic",
"poke"
],
"/create-liquidity": [
"liquidity",
"LP",
Expand Down
4 changes: 4 additions & 0 deletions src/nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ const FEE_ROUTES: Route[] = [
{
path: "fixed-hook-fee",
title: "Static Hook Fee"
},
{
path: "dynamic-fee",
title: "Dynamic Swap Fee"
}
]

Expand Down
63 changes: 63 additions & 0 deletions src/pages/fees/dynamic-fee/AutoDynamicFee.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

// TODO: update to v4-periphery/BaseHook.sol when its compatible
import {BaseHook} from "@v4-by-example/utils/BaseHook.sol";

import {Hooks} from "v4-core/libraries/Hooks.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {IDynamicFeeManager} from "v4-core/interfaces/IDynamicFeeManager.sol";

/// @notice A time-decaying dynamically fee, updated automatically with beforeSwap()
contract AutoDynamicFee is BaseHook, IDynamicFeeManager {
uint256 public immutable startTimestamp;

// Start at 5% fee, decaying at rate of 0.00001% per second
// after 495,000 seconds (5.72 days), fee will be a minimum of 0.05%
// NOTE: because fees are uint24, we will lose some precision
uint128 public constant START_FEE = 500000; // represents 5%
uint128 public constant MIN_FEE = 500; // minimum fee of 0.05%

uint128 public constant decayRate = 1; // 0.00001% per second

constructor(IPoolManager _poolManager) BaseHook(_poolManager) {
startTimestamp = block.timestamp;
}

/// @inheritdoc IDynamicFeeManager
function getFee(address, PoolKey calldata) external view override returns (uint24 _currentFee) {
unchecked {
uint256 timeElapsed = block.timestamp - startTimestamp;
_currentFee = timeElapsed > 495000 ? uint24(MIN_FEE) : uint24((START_FEE - (timeElapsed * decayRate)) / 10);
}
}

/// @dev this example hook contract does not implement any hooks
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeInitialize: false,
afterInitialize: false,
beforeAddLiquidity: false,
afterAddLiquidity: false,
beforeRemoveLiquidity: false,
afterRemoveLiquidity: false,
beforeSwap: true,
afterSwap: false,
beforeDonate: false,
afterDonate: false,
noOp: false,
accessLock: false
});
}

function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata)
external
override
returns (bytes4)
{
// poke the poolmanager to update the fee for every swap
poolManager.updateDynamicSwapFee(key);
return BaseHook.beforeSwap.selector;
}
}
55 changes: 55 additions & 0 deletions src/pages/fees/dynamic-fee/ManualDynamicFee.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

// TODO: update to v4-periphery/BaseHook.sol when its compatible
import {BaseHook} from "@v4-by-example/utils/BaseHook.sol";

import {Hooks} from "v4-core/libraries/Hooks.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {IDynamicFeeManager} from "v4-core/interfaces/IDynamicFeeManager.sol";

/// @notice A time-decaying dynamically fee, updated manually with external PoolManager.updateDynamicSwapFee() calls
contract ManualDynamicFee is BaseHook, IDynamicFeeManager {
uint256 public immutable startTimestamp;

// Start at 5% fee, decaying at rate of 0.00001% per second
// after 495,000 seconds (5.72 days), fee will be a minimum of 0.05%
// NOTE: because fees are uint24, we will lose some precision
uint128 public constant START_FEE = 500000; // represents 5%
uint128 public constant MIN_FEE = 500; // minimum fee of 0.05%

uint128 public constant decayRate = 1; // 0.00001% per second

constructor(IPoolManager _poolManager) BaseHook(_poolManager) {
startTimestamp = block.timestamp;
}

/// @dev Deteremines a Pool's swap fee. Called and cached by PoolManager.updateDynamicFee()
function getFee(address, PoolKey calldata) external view override returns (uint24 _currentFee) {
// Linearly decaying fee, y = mx + b
// After 495,000 seconds (5.72 days), fee will be a minimum of 0.05%
unchecked {
uint256 timeElapsed = block.timestamp - startTimestamp;
_currentFee = timeElapsed > 495000 ? uint24(MIN_FEE) : uint24((START_FEE - (timeElapsed * decayRate)) / 10);
}
}

/// @dev this example hook contract does not implement any hooks
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeInitialize: false,
afterInitialize: false,
beforeAddLiquidity: false,
afterAddLiquidity: false,
beforeRemoveLiquidity: false,
afterRemoveLiquidity: false,
beforeSwap: false,
afterSwap: false,
beforeDonate: false,
afterDonate: false,
noOp: false,
accessLock: false
});
}
}
Loading

0 comments on commit 57dd289

Please sign in to comment.