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

Static Hook Fees #9

Merged
merged 4 commits into from
Dec 3, 2023
Merged
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
2 changes: 1 addition & 1 deletion contracts/lib/v4-core
Submodule v4-core updated 45 files
+1 −1 .forge-snapshots/before swap hook, already cached dynamic fee.snap
+1 −1 .forge-snapshots/cached dynamic fee, no hooks.snap
+1 −1 .forge-snapshots/donate gas with 1 token.snap
+1 −1 .forge-snapshots/donate gas with 2 tokens.snap
+1 −1 .forge-snapshots/erc20 collect protocol fees.snap
+1 −1 .forge-snapshots/gas overhead of no-op lock.snap
+1 −1 .forge-snapshots/initialize.snap
+1 −1 .forge-snapshots/mint with empty hook.snap
+1 −1 .forge-snapshots/mint with native token.snap
+1 −1 .forge-snapshots/mint.snap
+1 −1 .forge-snapshots/modify position with noop.snap
+1 −1 .forge-snapshots/native collect protocol fees.snap
+1 −1 .forge-snapshots/poolManager bytecode size.snap
+1 −1 .forge-snapshots/simple swap with native.snap
+1 −1 .forge-snapshots/simple swap.snap
+1 −1 .forge-snapshots/swap against liquidity with native token.snap
+1 −1 .forge-snapshots/swap against liquidity.snap
+1 −1 .forge-snapshots/swap burn claim for input.snap
+1 −1 .forge-snapshots/swap mint output as claim.snap
+1 −1 .forge-snapshots/swap with dynamic fee.snap
+1 −1 .forge-snapshots/swap with hooks.snap
+1 −1 .forge-snapshots/swap with noop.snap
+1 −1 .forge-snapshots/update dynamic fee in before swap.snap
+8 −10 CONTRIBUTING.md
+1 −1 README.md
+1 −1 lib/forge-std
+57 −12 src/PoolManager.sol
+5 −1 src/interfaces/IPoolManager.sol
+20 −12 src/libraries/Hooks.sol
+35 −0 src/libraries/Lockers.sol
+222 −0 src/test/AccessLockHook.sol
+4 −3 src/test/EmptyTestHooks.sol
+4 −4 src/test/HooksTest.sol
+4 −3 src/test/NoOpTestHooks.sol
+15 −7 src/test/PoolDonateTest.sol
+16 −4 src/test/PoolInitializeTest.sol
+16 −10 src/test/PoolModifyPositionTest.sol
+46 −29 src/test/PoolSwapTest.sol
+1 −1 src/test/PoolTestBase.sol
+803 −0 test/AccessLock.t.sol
+46 −0 test/CurrentHookAddress.t.sol
+163 −90 test/Hooks.t.sol
+6 −5 test/PoolManager.t.sol
+4 −0 test/utils/Constants.sol
+10 −5 test/utils/Deployers.sol
10 changes: 6 additions & 4 deletions contracts/src/Counter.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

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

import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol";
import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol";
Expand All @@ -25,8 +26,8 @@ contract Counter is BaseHook {

constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}

function getHooksCalls() public pure override returns (Hooks.Calls memory) {
return Hooks.Calls({
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeInitialize: false,
afterInitialize: false,
beforeModifyPosition: true,
Expand All @@ -35,7 +36,8 @@ contract Counter is BaseHook {
afterSwap: true,
beforeDonate: false,
afterDonate: false,
noOp: false
noOp: false,
accessLock: false
});
}

Expand Down
72 changes: 72 additions & 0 deletions contracts/src/examples/FixedHookFee.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

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

import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol";
import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol";
import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol";
import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol";
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/contracts/types/Currency.sol";

contract FixedHookFee is BaseHook {
using PoolIdLibrary for PoolKey;
using CurrencyLibrary for Currency;

uint256 public constant FIXED_HOOK_FEE = 0.0001e18;

constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}

function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeInitialize: false,
afterInitialize: false,
beforeModifyPosition: false,
afterModifyPosition: false,
beforeSwap: true,
afterSwap: false,
beforeDonate: false,
afterDonate: false,
noOp: false,
accessLock: true // -- Required to take a fee -- //
});
}

function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata params, bytes calldata)
external
override
returns (bytes4)
{
// take a fixed fee of 0.0001 of the input token
params.zeroForOne
? poolManager.mint(key.currency0, address(this), FIXED_HOOK_FEE)
: poolManager.mint(key.currency1, address(this), FIXED_HOOK_FEE);

return BaseHook.beforeSwap.selector;
}

/// @dev Hook fees are kept as PoolManager claims, so collecting ERC20s will require locking
function collectFee(address recipient, Currency currency) external returns (uint256 amount) {
amount = abi.decode(
poolManager.lock(
abi.encodeCall(
this.handleCollectFee,
(recipient, currency)
)
),
(uint256)
);
}

/// @dev requires the lock pattern in order to call poolManager.burn
function handleCollectFee(address recipient, Currency currency) external returns (uint256 amount) {
// convert the fee (Claims) into ERC20 tokens
amount = poolManager.balanceOf(address(this), currency);
poolManager.burn(currency, amount);

// direct claims (the tokens) to the recipient
poolManager.take(currency, recipient, amount);
}
}
9 changes: 5 additions & 4 deletions contracts/src/examples/NoOpSwap.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {BaseHook} from "v4-periphery/BaseHook.sol";
import {BaseHook} from "../forks/BaseHook.sol";

import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol";
import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol";
Expand All @@ -22,8 +22,8 @@ contract NoOpSwap is BaseHook {

constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}

function getHooksCalls() public pure override returns (Hooks.Calls memory) {
return Hooks.Calls({
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeInitialize: false,
afterInitialize: false,
beforeModifyPosition: false,
Expand All @@ -32,7 +32,8 @@ contract NoOpSwap is BaseHook {
afterSwap: true,
beforeDonate: false,
afterDonate: false,
noOp: true
noOp: true,
accessLock: false
});
}

Expand Down
130 changes: 130 additions & 0 deletions contracts/src/forks/BaseHook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// NOTE: ----------------------------------------------------------------------------------------------- //
// December 1, 2023: //
// Recent v4-core changes are uncompatible with an outdated v4-periphery/BaseHook.sol //
// This is a *temporary* fix until v4-periphery is updated. //
// ----------------------------------------------------------------------------------------------------- //

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol";
import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol";
import {IHooks} from "@uniswap/v4-core/contracts/interfaces/IHooks.sol";
import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol";
import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol";

abstract contract BaseHook is IHooks {
error NotPoolManager();
error NotSelf();
error InvalidPool();
error LockFailure();
error HookNotImplemented();

/// @notice The address of the pool manager
IPoolManager public immutable poolManager;

constructor(IPoolManager _poolManager) {
poolManager = _poolManager;
validateHookPermissions(this);
}

/// @dev Only the pool manager may call this function
modifier poolManagerOnly() {
if (msg.sender != address(poolManager)) revert NotPoolManager();
_;
}

/// @dev Only this address may call this function
modifier selfOnly() {
if (msg.sender != address(this)) revert NotSelf();
_;
}

/// @dev Only pools with hooks set to this contract may call this function
modifier onlyValidPools(IHooks hooks) {
if (hooks != this) revert InvalidPool();
_;
}

function getHookPermissions() public pure virtual returns (Hooks.Permissions memory);

// this function is virtual so that we can override it during testing,
// which allows us to deploy an implementation to any address
// and then etch the bytecode into the correct address
function validateHookPermissions(BaseHook _this) internal pure virtual {
Hooks.validateHookPermissions(_this, getHookPermissions());
}

function lockAcquired(bytes calldata data) external virtual poolManagerOnly returns (bytes memory) {
(bool success, bytes memory returnData) = address(this).call(data);
if (success) return returnData;
if (returnData.length == 0) revert LockFailure();
// if the call failed, bubble up the reason
/// @solidity memory-safe-assembly
assembly {
revert(add(returnData, 32), mload(returnData))
}
}

function beforeInitialize(address, PoolKey calldata, uint160, bytes calldata) external virtual returns (bytes4) {
revert HookNotImplemented();
}

function afterInitialize(address, PoolKey calldata, uint160, int24, bytes calldata)
external
virtual
returns (bytes4)
{
revert HookNotImplemented();
}

function beforeModifyPosition(address, PoolKey calldata, IPoolManager.ModifyPositionParams calldata, bytes calldata)
external
virtual
returns (bytes4)
{
revert HookNotImplemented();
}

function afterModifyPosition(
address,
PoolKey calldata,
IPoolManager.ModifyPositionParams calldata,
BalanceDelta,
bytes calldata
) external virtual returns (bytes4) {
revert HookNotImplemented();
}

function beforeSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, bytes calldata)
external
virtual
returns (bytes4)
{
revert HookNotImplemented();
}

function afterSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, BalanceDelta, bytes calldata)
external
virtual
returns (bytes4)
{
revert HookNotImplemented();
}

function beforeDonate(address, PoolKey calldata, uint256, uint256, bytes calldata)
external
virtual
returns (bytes4)
{
revert HookNotImplemented();
}

function afterDonate(address, PoolKey calldata, uint256, uint256, bytes calldata)
external
virtual
returns (bytes4)
{
revert HookNotImplemented();
}
}
71 changes: 71 additions & 0 deletions contracts/test/examples/FixedHookFee.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "forge-std/Test.sol";
import {IHooks} from "@uniswap/v4-core/contracts/interfaces/IHooks.sol";
import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol";
import {TickMath} from "@uniswap/v4-core/contracts/libraries/TickMath.sol";
import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol";
import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol";
import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol";
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol";
import {Constants} from "@uniswap/v4-core/contracts/../test/utils/Constants.sol";
import {CurrencyLibrary, Currency} from "@uniswap/v4-core/contracts/types/Currency.sol";
import {HookTest} from "../utils/HookTest.sol";
import {FixedHookFee} from "../../src/examples/FixedHookFee.sol";
import {HookMiner} from "../utils/HookMiner.sol";

contract FixedHookFeeTest is HookTest {
using PoolIdLibrary for PoolKey;
using CurrencyLibrary for Currency;

FixedHookFee hook;
PoolKey poolKey;
PoolId poolId;

address alice = makeAddr("alice");

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 | Hooks.ACCESS_LOCK_FLAG);
(address hookAddress, bytes32 salt) =
HookMiner.find(address(this), flags, type(FixedHookFee).creationCode, abi.encode(address(manager)));
hook = new FixedHookFee{salt: salt}(IPoolManager(address(manager)));
require(address(hook) == hookAddress, "FixedHookFeeTest: hook address mismatch");

// Create the pool
poolKey = PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 3000, 60, IHooks(hook));
poolId = poolKey.toId();
initializeRouter.initialize(poolKey, Constants.SQRT_RATIO_1_1, ZERO_BYTES);

// Provide liquidity to the pool
modifyPositionRouter.modifyPosition(poolKey, IPoolManager.ModifyPositionParams(-60, 60, 10 ether), ZERO_BYTES);
modifyPositionRouter.modifyPosition(poolKey, IPoolManager.ModifyPositionParams(-120, 120, 10 ether), ZERO_BYTES);
modifyPositionRouter.modifyPosition(
poolKey,
IPoolManager.ModifyPositionParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 10_000 ether),
ZERO_BYTES
);
}

function test_hookFee() public {
uint256 balanceBefore = token0.balanceOf(address(this));
// Perform a test swap //
int256 amount = 1e18;
bool zeroForOne = true;
swap(poolKey, amount, zeroForOne, ZERO_BYTES);
// ------------------- //
uint256 balanceAfter = token0.balanceOf(address(this));

// swapper paid for the fixed hook fee
assertEq(balanceBefore - balanceAfter, uint256(amount) + hook.FIXED_HOOK_FEE());

// collect the hook fees
assertEq(token0.balanceOf(alice), 0);
hook.collectFee(alice, Currency.wrap(address(token0)));
assertEq(token0.balanceOf(alice), hook.FIXED_HOOK_FEE());
}
}
7 changes: 7 additions & 0 deletions src/keywords.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@
"swap",
"skip swap"
],
"/fees/fixed-hook-fee": [
"hook",
"hooks",
"fee",
"static fee",
"hook fee"
],
"/create-liquidity": [
"liquidity",
"LP",
Expand Down
21 changes: 13 additions & 8 deletions src/nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ export const HOOK_ROUTES: Route[] = [
}
]

const HACK_ROUTES: Route[] = []
const FEE_ROUTES: Route[] = [
{
path: "fixed-hook-fee",
title: "Static Hook Fee"
}
]

export const TEST_ROUTES: Route[] = []

Expand All @@ -53,13 +58,13 @@ export const ROUTES_BY_CATEGORY = [
path: `/hooks/${route.path}`,
})),
},
// {
// title: "Hacks",
// routes: HACK_ROUTES.map((route) => ({
// ...route,
// path: `/hacks/${route.path}`,
// })),
// },
{
title: "Fees",
routes: FEE_ROUTES.map((route) => ({
...route,
path: `/fees/${route.path}`,
})),
},
// {
// title: "Tests",
// routes: TEST_ROUTES.map((route) => ({
Expand Down
Loading
Loading