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

Base Hook Middleware #144

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 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
9 changes: 9 additions & 0 deletions contracts/interfaces/IBaseHook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";

interface IBaseHook is IHooks {
function getHookPermissions() external pure returns (Hooks.Permissions memory);
}
17 changes: 17 additions & 0 deletions contracts/interfaces/IMiddlewareFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

interface IMiddlewareFactory {
event MiddlewareCreated(address implementation, address middleware);

/// @notice Returns the implementation address for a given middleware
/// @param middleware The middleware address
/// @return implementation The implementation address
function getImplementation(address middleware) external view returns (address implementation);

/// @notice Creates a middleware for the given implementation
/// @param implementation The implementation address
/// @param salt The salt to use to deploy the middleware
/// @return middleware The address of the newly created middleware
function createMiddleware(address implementation, bytes32 salt) external returns (address middleware);
}
30 changes: 30 additions & 0 deletions contracts/middleware/BaseMiddleware.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
Jun1on marked this conversation as resolved.
Show resolved Hide resolved
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
Jun1on marked this conversation as resolved.
Show resolved Hide resolved
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
Jun1on marked this conversation as resolved.
Show resolved Hide resolved
import {BeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol";
Jun1on marked this conversation as resolved.
Show resolved Hide resolved
import {Proxy} from "@openzeppelin/contracts/proxy/Proxy.sol";

contract BaseMiddleware is Proxy {
Jun1on marked this conversation as resolved.
Show resolved Hide resolved
/// @notice The address of the pool manager
IPoolManager public immutable manager;
Copy link
Contributor

Choose a reason for hiding this comment

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

you're never using the manager immutable as far as I can tell - is this just to be referenced by the underlying impl? if so please elaborate in the comment

Copy link
Member

Choose a reason for hiding this comment

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

If you do need the manager var, there will soon be merged an ImmutableState contract you can use to initialize maanger

address public immutable implementation;
Jun1on marked this conversation as resolved.
Show resolved Hide resolved

constructor(IPoolManager _manager, address _impl) {
Copy link
Contributor

Choose a reason for hiding this comment

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

what is the purpose of this BaseMiddleware? It seems to be mostly just a proxy wrapper. Are there any useful things that we can add for downstream implementers? Some ideas -

  • Standard modifiers i.e. onlyPoolManager at least?
  • Standard implementations of some hook functions, with virtual functions that downstream implementers can override with their custom checks?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's pretty much just oz proxy yeah.

  • i think it's up to the implementation to add the onlyPoolManager modifier. most hooks already use it, so adding onlyPoolManager to the middleware itself would result in a redundant check.
  • like a beforeBeforeSwap, afterBeforeSwap pattern? this sounds a bit complicated and also creates deployment size bloat for middlewares that don't implement the hook.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah onlyPoolManager is already defined in contracts we've written upstream like SafeCallback

manager = _manager;
implementation = _impl;
}

function _implementation() internal view override returns (address) {
return implementation;
}

// yo i wanna delete this function but how do i remove this warning
Jun1on marked this conversation as resolved.
Show resolved Hide resolved
receive() external payable {
_delegate(_implementation());
}
}
34 changes: 34 additions & 0 deletions contracts/middleware/BaseMiddlewareFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

import {IMiddlewareFactory} from "../interfaces/IMiddlewareFactory.sol";
import {BaseMiddleware} from "./BaseMiddleware.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {IBaseHook} from "../interfaces/IBaseHook.sol";

contract BaseMiddlewareFactory is IMiddlewareFactory {
mapping(address => address) private _implementations;

IPoolManager public immutable manager;

constructor(IPoolManager _manager) {
manager = _manager;
}

function getImplementation(address middleware) external view override returns (address implementation) {
return _implementations[middleware];
}

function createMiddleware(address implementation, bytes32 salt) external override returns (address middleware) {
middleware = _deployMiddleware(implementation, salt);
Hooks.validateHookPermissions(IHooks(middleware), IBaseHook(implementation).getHookPermissions());
_implementations[middleware] = implementation;
emit MiddlewareCreated(implementation, middleware);
}

function _deployMiddleware(address implementation, bytes32 salt) internal virtual returns (address middleware) {
return address(new BaseMiddleware{salt: salt}(manager, implementation));
}
}
124 changes: 124 additions & 0 deletions test/BaseMiddlewareFactory.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

import {Test} from "forge-std/Test.sol";
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol";
import {TestERC20} from "@uniswap/v4-core/src/test/TestERC20.sol";
import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {HookEnabledSwapRouter} from "./utils/HookEnabledSwapRouter.sol";
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {console} from "../../../lib/forge-std/src/console.sol";
import {BaseMiddleware} from "../contracts/middleware/BaseMiddleware.sol";
import {BaseMiddlewareFactory} from "./../contracts/middleware/BaseMiddlewareFactory.sol";
import {HookMiner} from "./utils/HookMiner.sol";
import {Counter} from "./middleware/Counter.sol";
import {SafeCallback} from "./../contracts/base/SafeCallback.sol";

contract BaseMiddlewareFactoryTest is Test, Deployers {
HookEnabledSwapRouter router;
TestERC20 token0;
TestERC20 token1;

BaseMiddlewareFactory factory;
Counter counter;

address middleware;

function setUp() public {
deployFreshManagerAndRouters();
(currency0, currency1) = deployMintAndApprove2Currencies();

router = new HookEnabledSwapRouter(manager);
token0 = TestERC20(Currency.unwrap(currency0));
token1 = TestERC20(Currency.unwrap(currency1));

factory = new BaseMiddlewareFactory(manager);
counter = new Counter(manager);

token0.approve(address(router), type(uint256).max);
token1.approve(address(router), type(uint256).max);

uint160 flags = uint160(
Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG
| Hooks.BEFORE_ADD_LIQUIDITY_FLAG | Hooks.AFTER_ADD_LIQUIDITY_FLAG | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG
| Hooks.AFTER_REMOVE_LIQUIDITY_FLAG | Hooks.BEFORE_DONATE_FLAG | Hooks.AFTER_DONATE_FLAG
);

(address hookAddress, bytes32 salt) = HookMiner.find(
address(factory), flags, type(BaseMiddleware).creationCode, abi.encode(address(manager), address(counter))
);
middleware = factory.createMiddleware(address(counter), salt);
assertEq(hookAddress, middleware);
}

function testRevertOnSameDeployment() public {
uint160 flags = uint160(
Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG
| Hooks.BEFORE_ADD_LIQUIDITY_FLAG | Hooks.AFTER_ADD_LIQUIDITY_FLAG | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG
| Hooks.AFTER_REMOVE_LIQUIDITY_FLAG | Hooks.BEFORE_DONATE_FLAG | Hooks.AFTER_DONATE_FLAG
);
(address hookAddress, bytes32 salt) = HookMiner.find(
address(factory), flags, type(BaseMiddleware).creationCode, abi.encode(address(manager), address(counter))
);
factory.createMiddleware(address(counter), salt);
// second deployment should revert
vm.expectRevert(bytes(""));
factory.createMiddleware(address(counter), salt);
}

function testRevertOnIncorrectFlags() public {
Counter counter2 = new Counter(manager);
uint160 flags = uint160(Hooks.BEFORE_INITIALIZE_FLAG);

(address hookAddress, bytes32 salt) = HookMiner.find(
address(factory), flags, type(BaseMiddleware).creationCode, abi.encode(address(manager), address(counter2))
);
address implementation = address(counter2);
vm.expectRevert(abi.encodePacked(bytes16(Hooks.HookAddressNotValid.selector), hookAddress));
factory.createMiddleware(implementation, salt);
}

function testRevertOnIncorrectFlagsMined() public {
Counter counter2 = new Counter(manager);
address implementation = address(counter2);
vm.expectRevert(); // HookAddressNotValid
factory.createMiddleware(implementation, bytes32("who needs to mine a salt?"));
}

function testRevertOnIncorrectCaller() public {
vm.expectRevert(SafeCallback.NotManager.selector);
counter.afterDonate(address(this), key, 0, 0, ZERO_BYTES);
}

function testCounters() public {
(PoolKey memory key, PoolId id) =
initPoolAndAddLiquidity(currency0, currency1, IHooks(middleware), 3000, SQRT_PRICE_1_1, ZERO_BYTES);

Counter counterProxy = Counter(middleware);
assertEq(counterProxy.beforeInitializeCount(id), 1);
assertEq(counterProxy.afterInitializeCount(id), 1);
assertEq(counterProxy.beforeSwapCount(id), 0);
assertEq(counterProxy.afterSwapCount(id), 0);
assertEq(counterProxy.beforeAddLiquidityCount(id), 1);
assertEq(counterProxy.afterAddLiquidityCount(id), 1);
assertEq(counterProxy.beforeRemoveLiquidityCount(id), 0);
assertEq(counterProxy.afterRemoveLiquidityCount(id), 0);
assertEq(counterProxy.beforeDonateCount(id), 0);
assertEq(counterProxy.afterDonateCount(id), 0);

assertEq(counterProxy.lastHookData(), ZERO_BYTES);
swap(key, true, 1, bytes("hi"));
assertEq(counterProxy.lastHookData(), bytes("hi"));
assertEq(counterProxy.beforeSwapCount(id), 1);
assertEq(counterProxy.afterSwapCount(id), 1);

// counter does not store data itself
assertEq(counter.lastHookData(), bytes(""));
assertEq(counter.beforeSwapCount(id), 0);
assertEq(counter.afterSwapCount(id), 0);
}
}
167 changes: 167 additions & 0 deletions test/middleware/Counter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {BaseHook} from "./../../contracts/BaseHook.sol";
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol";
import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";

contract Counter is BaseHook {
Jun1on marked this conversation as resolved.
Show resolved Hide resolved
using PoolIdLibrary for PoolKey;

mapping(PoolId => uint256) public beforeInitializeCount;
mapping(PoolId => uint256) public afterInitializeCount;

mapping(PoolId => uint256) public beforeSwapCount;
mapping(PoolId => uint256) public afterSwapCount;

mapping(PoolId => uint256) public beforeAddLiquidityCount;
mapping(PoolId => uint256) public afterAddLiquidityCount;
mapping(PoolId => uint256) public beforeRemoveLiquidityCount;
mapping(PoolId => uint256) public afterRemoveLiquidityCount;

mapping(PoolId => uint256) public beforeDonateCount;
mapping(PoolId => uint256) public afterDonateCount;

bytes public lastHookData;

Choose a reason for hiding this comment

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

What's the use of this? And does it make sense to make it a mapping from selectors to lastHookData instead

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's just for testing to make sure hookData is passed through correctly


constructor(IPoolManager _manager) BaseHook(_manager) {}

// middleware implementations do not need to be mined
function validateHookAddress(BaseHook _this) internal pure override {}

function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeInitialize: true,
afterInitialize: true,
beforeAddLiquidity: true,
afterAddLiquidity: true,
beforeRemoveLiquidity: true,
afterRemoveLiquidity: true,
beforeSwap: true,
afterSwap: true,
beforeDonate: true,
afterDonate: true,
beforeSwapReturnDelta: false,
afterSwapReturnDelta: false,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}

function beforeInitialize(address, PoolKey calldata key, uint160, bytes calldata hookData)
external
override
onlyByManager
returns (bytes4)
{
beforeInitializeCount[key.toId()]++;
lastHookData = hookData;
return BaseHook.beforeInitialize.selector;
}

function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata hookData)
external
override
onlyByManager
returns (bytes4)
{
afterInitializeCount[key.toId()]++;
lastHookData = hookData;
return BaseHook.afterInitialize.selector;
}

function beforeAddLiquidity(
address,
PoolKey calldata key,
IPoolManager.ModifyLiquidityParams calldata,
bytes calldata hookData
) external override onlyByManager returns (bytes4) {
beforeAddLiquidityCount[key.toId()]++;
lastHookData = hookData;
return BaseHook.beforeAddLiquidity.selector;
}

function afterAddLiquidity(
address,
PoolKey calldata key,
IPoolManager.ModifyLiquidityParams calldata,
BalanceDelta,
bytes calldata hookData
) external override onlyByManager returns (bytes4, BalanceDelta) {
afterAddLiquidityCount[key.toId()]++;
lastHookData = hookData;
return (BaseHook.afterAddLiquidity.selector, BalanceDeltaLibrary.ZERO_DELTA);
}

function beforeRemoveLiquidity(
address,
PoolKey calldata key,
IPoolManager.ModifyLiquidityParams calldata,
bytes calldata hookData
) external override onlyByManager returns (bytes4) {
beforeRemoveLiquidityCount[key.toId()]++;
lastHookData = hookData;
return BaseHook.beforeRemoveLiquidity.selector;
}

function afterRemoveLiquidity(
address,
PoolKey calldata key,
IPoolManager.ModifyLiquidityParams calldata,
BalanceDelta,
bytes calldata hookData
) external override onlyByManager returns (bytes4, BalanceDelta) {
afterRemoveLiquidityCount[key.toId()]++;
lastHookData = hookData;
return (BaseHook.afterRemoveLiquidity.selector, BalanceDeltaLibrary.ZERO_DELTA);
}

function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata hookData)
external
override
onlyByManager
returns (bytes4, BeforeSwapDelta, uint24)
{
beforeSwapCount[key.toId()]++;
lastHookData = hookData;
return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
}

function afterSwap(
address,
PoolKey calldata key,
IPoolManager.SwapParams calldata,
BalanceDelta,
bytes calldata hookData
) external override onlyByManager returns (bytes4, int128) {
afterSwapCount[key.toId()]++;
lastHookData = hookData;
return (BaseHook.afterSwap.selector, 0);
}

function beforeDonate(address, PoolKey calldata key, uint256, uint256, bytes calldata hookData)
external
override
onlyByManager
returns (bytes4)
{
beforeDonateCount[key.toId()]++;
lastHookData = hookData;
return BaseHook.beforeDonate.selector;
}

function afterDonate(address, PoolKey calldata key, uint256, uint256, bytes calldata hookData)
external
override
onlyByManager
returns (bytes4)
{
afterDonateCount[key.toId()]++;
lastHookData = hookData;
return BaseHook.afterDonate.selector;
}
}
Loading
Loading