Skip to content

Commit

Permalink
Merge pull request #9 from uniswapfoundation/hook-fees-access-lock
Browse files Browse the repository at this point in the history
Static Hook Fees
  • Loading branch information
saucepoint authored Dec 3, 2023
2 parents 713f789 + abca37f commit 8e266f2
Show file tree
Hide file tree
Showing 18 changed files with 619 additions and 24 deletions.
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

0 comments on commit 8e266f2

Please sign in to comment.