From 753310189cb5836049efc1da8bc572265fdb879d Mon Sep 17 00:00:00 2001 From: Junion Date: Fri, 7 Jun 2024 13:48:48 -0400 Subject: [PATCH 01/25] add TakingFee hook --- contracts/hooks/examples/TakingFee.sol | 88 +++++++++++++++ test/TakingFee.t.sol | 101 ++++++++++++++++++ .../TakingFeeImplementation.sol | 16 +++ 3 files changed, 205 insertions(+) create mode 100644 contracts/hooks/examples/TakingFee.sol create mode 100644 test/TakingFee.t.sol create mode 100644 test/shared/implementation/TakingFeeImplementation.sol diff --git a/contracts/hooks/examples/TakingFee.sol b/contracts/hooks/examples/TakingFee.sol new file mode 100644 index 00000000..dbbe551a --- /dev/null +++ b/contracts/hooks/examples/TakingFee.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {BaseHook} from "../../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 {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; +import {Owned} from "solmate/auth/Owned.sol"; + +contract TakingFee is BaseHook, Owned { + using PoolIdLibrary for PoolKey; + using SafeCast for uint256; + + uint128 private constant TOTAL_BIPS = 10000; + uint128 private constant MAX_BIPS = 100; + uint128 public swapFeeBips; + address public treasury = msg.sender; + + constructor( + IPoolManager _poolManager, + uint128 _swapFeeBips, + address _treasury + ) BaseHook(_poolManager) Owned(msg.sender) { + swapFeeBips = _swapFeeBips; + treasury = _treasury; + } + + 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: true, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: true, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } + + function afterSwap( + address, + PoolKey calldata key, + IPoolManager.SwapParams calldata params, + BalanceDelta delta, + bytes calldata + ) external override returns (bytes4, int128) { + // fee will be in the unspecified token of the swap + bool specifiedTokenIs0 = (params.amountSpecified < 0 == + params.zeroForOne); + (Currency feeCurrency, int128 swapAmount) = (specifiedTokenIs0) + ? (key.currency1, delta.amount1()) + : (key.currency0, delta.amount0()); + // if fee is on output, get the absolute output amount + if (swapAmount < 0) swapAmount = -swapAmount; + + uint256 feeAmount = (uint128(swapAmount) * swapFeeBips) / TOTAL_BIPS; + poolManager.take(feeCurrency, treasury, feeAmount); + + return (BaseHook.afterSwap.selector, feeAmount.toInt128()); + } + + function setSwapFeeBips(uint128 _swapFeeBips) external onlyOwner { + require(_swapFeeBips <= MAX_BIPS); + swapFeeBips = _swapFeeBips; + } + + function setTreasury(address _treasury) external onlyOwner { + treasury = _treasury; + } +} diff --git a/test/TakingFee.t.sol b/test/TakingFee.t.sol new file mode 100644 index 00000000..18f48869 --- /dev/null +++ b/test/TakingFee.t.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {GetSender} from "./shared/GetSender.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {TakingFee} from "../contracts/hooks/examples/TakingFee.sol"; +import {TakingFeeImplementation} from "./shared/implementation/TakingFeeImplementation.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.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 {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {HookEnabledSwapRouter} from "./utils/HookEnabledSwapRouter.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; + +contract TakingFeeTest is Test, Deployers { + using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; + + uint160 constant SQRT_RATIO_10_1 = 250541448375047931186413801569; + + address constant TREASURY = address(0x1234567890123456789012345678901234567890); + uint128 private constant TOTAL_BIPS = 10000; + + HookEnabledSwapRouter router; + TestERC20 token0; + TestERC20 token1; + TakingFee takingFee = TakingFee(address(uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG))); + PoolId id; + + function setUp() public { + deployFreshManagerAndRouters(); + (currency0, currency1) = deployMintAndApprove2Currencies(); + + router = new HookEnabledSwapRouter(manager); + token0 = TestERC20(Currency.unwrap(currency0)); + token1 = TestERC20(Currency.unwrap(currency1)); + + vm.record(); + TakingFeeImplementation impl = new TakingFeeImplementation(manager, 25, TREASURY, takingFee); + (, bytes32[] memory writes) = vm.accesses(address(impl)); + vm.etch(address(takingFee), address(impl).code); + // for each storage key that was written during the hook implementation, copy the value over + unchecked { + for (uint256 i = 0; i < writes.length; i++) { + bytes32 slot = writes[i]; + vm.store(address(takingFee), slot, vm.load(address(impl), slot)); + } + } + + // key = PoolKey(currency0, currency1, 3000, 60, takingFee); + (key, id) = initPoolAndAddLiquidity(currency0, currency1, takingFee, 3000, SQRT_PRICE_1_1, ZERO_BYTES); + + token0.approve(address(takingFee), type(uint256).max); + token1.approve(address(takingFee), type(uint256).max); + token0.approve(address(router), type(uint256).max); + token1.approve(address(router), type(uint256).max); + } + + function testSwapHooks() public { + // rounding for tests + uint128 ROUND_FACTOR = 8; + + // positions were created in setup() + assertEq(currency0.balanceOf(TREASURY), 0); + assertEq(currency1.balanceOf(TREASURY), 0); + + // Perform a test swap // + bool zeroForOne = true; + int256 amountSpecified = -1e12; // negative number indicates exact input swap + BalanceDelta swapDelta = swap(key, zeroForOne, amountSpecified, ZERO_BYTES); + // ------------------- // + + uint128 output = uint128(swapDelta.amount1()); + assertFalse(output == 0); + + uint256 expectedFee = output * TOTAL_BIPS/(TOTAL_BIPS - takingFee.swapFeeBips()) - output; + + assertEq(currency0.balanceOf(TREASURY), 0); + assertEq(currency1.balanceOf(TREASURY) / ROUND_FACTOR, expectedFee / ROUND_FACTOR); + + // Perform a test swap // + bool zeroForOne2 = true; + int256 amountSpecified2 = 1e12; // positive number indicates exact output swap + BalanceDelta swapDelta2 = swap(key, zeroForOne2, amountSpecified2, ZERO_BYTES); + // ------------------- // + + uint128 input = uint128(-swapDelta2.amount0()); + assertFalse(input == 0); + + uint128 expectedFee2 = (input * takingFee.swapFeeBips()) / (TOTAL_BIPS + takingFee.swapFeeBips()); + + assertEq(currency0.balanceOf(TREASURY) / ROUND_FACTOR, expectedFee2 / ROUND_FACTOR); + assertEq(currency1.balanceOf(TREASURY) / ROUND_FACTOR, expectedFee / ROUND_FACTOR); + } +} diff --git a/test/shared/implementation/TakingFeeImplementation.sol b/test/shared/implementation/TakingFeeImplementation.sol new file mode 100644 index 00000000..e5e07237 --- /dev/null +++ b/test/shared/implementation/TakingFeeImplementation.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {BaseHook} from "../../../contracts/BaseHook.sol"; +import {TakingFee} from "../../../contracts/hooks/examples/TakingFee.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; + +contract TakingFeeImplementation is TakingFee { + constructor(IPoolManager _poolManager, uint128 _swapFeeBips, address _treasury, TakingFee addressToEtch) TakingFee(_poolManager, _swapFeeBips, _treasury) { + Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); + } + + // make this a no-op in testing + function validateHookAddress(BaseHook _this) internal pure override {} +} From c2c5e9df05f22267c343067636568ae2b3169602 Mon Sep 17 00:00:00 2001 From: Junion Date: Fri, 7 Jun 2024 17:17:52 -0400 Subject: [PATCH 02/25] update implementation to use ERC6909 --- contracts/hooks/examples/TakingFee.sol | 94 ++++++++++--------- test/TakingFee.t.sol | 87 ++++++++++++----- .../TakingFeeImplementation.sol | 4 +- 3 files changed, 121 insertions(+), 64 deletions(-) diff --git a/contracts/hooks/examples/TakingFee.sol b/contracts/hooks/examples/TakingFee.sol index dbbe551a..d3294e6c 100644 --- a/contracts/hooks/examples/TakingFee.sol +++ b/contracts/hooks/examples/TakingFee.sol @@ -5,54 +5,46 @@ import {BaseHook} from "../../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 {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; -import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {Owned} from "solmate/auth/Owned.sol"; +import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; -contract TakingFee is BaseHook, Owned { - using PoolIdLibrary for PoolKey; +contract TakingFee is BaseHook, IUnlockCallback, Owned { using SafeCast for uint256; uint128 private constant TOTAL_BIPS = 10000; uint128 private constant MAX_BIPS = 100; uint128 public swapFeeBips; - address public treasury = msg.sender; - constructor( - IPoolManager _poolManager, - uint128 _swapFeeBips, - address _treasury - ) BaseHook(_poolManager) Owned(msg.sender) { + struct CallbackData { + address to; + Currency[] currencies; + } + + constructor(IPoolManager _poolManager, uint128 _swapFeeBips) BaseHook(_poolManager) Owned(msg.sender) { swapFeeBips = _swapFeeBips; - treasury = _treasury; } - 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: true, - beforeDonate: false, - afterDonate: false, - beforeSwapReturnDelta: false, - afterSwapReturnDelta: true, - afterAddLiquidityReturnDelta: false, - afterRemoveLiquidityReturnDelta: false - }); + 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: true, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: true, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); } function afterSwap( @@ -63,16 +55,15 @@ contract TakingFee is BaseHook, Owned { bytes calldata ) external override returns (bytes4, int128) { // fee will be in the unspecified token of the swap - bool specifiedTokenIs0 = (params.amountSpecified < 0 == - params.zeroForOne); - (Currency feeCurrency, int128 swapAmount) = (specifiedTokenIs0) - ? (key.currency1, delta.amount1()) - : (key.currency0, delta.amount0()); + bool specifiedTokenIs0 = (params.amountSpecified < 0 == params.zeroForOne); + (Currency feeCurrency, int128 swapAmount) = + (specifiedTokenIs0) ? (key.currency1, delta.amount1()) : (key.currency0, delta.amount0()); // if fee is on output, get the absolute output amount if (swapAmount < 0) swapAmount = -swapAmount; uint256 feeAmount = (uint128(swapAmount) * swapFeeBips) / TOTAL_BIPS; - poolManager.take(feeCurrency, treasury, feeAmount); + // mint ERC6909 instead of take to avoid edge case where PM doesn't have enough balance + poolManager.mint(address(this), CurrencyLibrary.toId(feeCurrency), feeAmount); return (BaseHook.afterSwap.selector, feeAmount.toInt128()); } @@ -82,7 +73,26 @@ contract TakingFee is BaseHook, Owned { swapFeeBips = _swapFeeBips; } - function setTreasury(address _treasury) external onlyOwner { - treasury = _treasury; + function withdraw(address to, Currency[] calldata currencies) external onlyOwner { + poolManager.unlock(abi.encode(CallbackData(to, currencies))); + } + + function unlockCallback(bytes calldata rawData) + external + override(IUnlockCallback, BaseHook) + poolManagerOnly + returns (bytes memory) + { + CallbackData memory data = abi.decode(rawData, (CallbackData)); + uint256 length = data.currencies.length; + for (uint256 i = 0; i < length;) { + uint256 amount = poolManager.balanceOf(address(this), CurrencyLibrary.toId(data.currencies[i])); + poolManager.burn(address(this), CurrencyLibrary.toId(data.currencies[i]), amount); + poolManager.take(data.currencies[i], data.to, amount); + unchecked { + i++; + } + } + return ""; } } diff --git a/test/TakingFee.t.sol b/test/TakingFee.t.sol index 18f48869..7a1edd50 100644 --- a/test/TakingFee.t.sol +++ b/test/TakingFee.t.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; -import {GetSender} from "./shared/GetSender.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {TakingFee} from "../contracts/hooks/examples/TakingFee.sol"; import {TakingFeeImplementation} from "./shared/implementation/TakingFeeImplementation.sol"; @@ -12,7 +11,6 @@ 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 {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {HookEnabledSwapRouter} from "./utils/HookEnabledSwapRouter.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; @@ -27,6 +25,9 @@ contract TakingFeeTest is Test, Deployers { address constant TREASURY = address(0x1234567890123456789012345678901234567890); uint128 private constant TOTAL_BIPS = 10000; + // rounding for tests to avoid floating point errors + uint128 R = 10; + HookEnabledSwapRouter router; TestERC20 token0; TestERC20 token1; @@ -42,7 +43,7 @@ contract TakingFeeTest is Test, Deployers { token1 = TestERC20(Currency.unwrap(currency1)); vm.record(); - TakingFeeImplementation impl = new TakingFeeImplementation(manager, 25, TREASURY, takingFee); + TakingFeeImplementation impl = new TakingFeeImplementation(manager, 25, takingFee); (, bytes32[] memory writes) = vm.accesses(address(impl)); vm.etch(address(takingFee), address(impl).code); // for each storage key that was written during the hook implementation, copy the value over @@ -63,39 +64,83 @@ contract TakingFeeTest is Test, Deployers { } function testSwapHooks() public { - // rounding for tests - uint128 ROUND_FACTOR = 8; - - // positions were created in setup() assertEq(currency0.balanceOf(TREASURY), 0); assertEq(currency1.balanceOf(TREASURY), 0); - // Perform a test swap // + // Swap exact token0 for token1 // bool zeroForOne = true; - int256 amountSpecified = -1e12; // negative number indicates exact input swap + int256 amountSpecified = -1e12; BalanceDelta swapDelta = swap(key, zeroForOne, amountSpecified, ZERO_BYTES); - // ------------------- // + // ---------------------------- // uint128 output = uint128(swapDelta.amount1()); - assertFalse(output == 0); + assertTrue(output > 0); - uint256 expectedFee = output * TOTAL_BIPS/(TOTAL_BIPS - takingFee.swapFeeBips()) - output; + uint256 expectedFee = output * TOTAL_BIPS / (TOTAL_BIPS - takingFee.swapFeeBips()) - output; - assertEq(currency0.balanceOf(TREASURY), 0); - assertEq(currency1.balanceOf(TREASURY) / ROUND_FACTOR, expectedFee / ROUND_FACTOR); - - // Perform a test swap // + assertEq(manager.balanceOf(address(takingFee), CurrencyLibrary.toId(key.currency0)), 0); + assertEq(manager.balanceOf(address(takingFee), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); + + // Swap token0 for exact token1 // bool zeroForOne2 = true; int256 amountSpecified2 = 1e12; // positive number indicates exact output swap BalanceDelta swapDelta2 = swap(key, zeroForOne2, amountSpecified2, ZERO_BYTES); - // ------------------- // - + // ---------------------------- // + uint128 input = uint128(-swapDelta2.amount0()); - assertFalse(input == 0); + assertTrue(output > 0); + + uint128 expectedFee2 = (input * takingFee.swapFeeBips()) / (TOTAL_BIPS + takingFee.swapFeeBips()); + + assertEq(manager.balanceOf(address(takingFee), CurrencyLibrary.toId(key.currency0)) / R, expectedFee2 / R); + assertEq(manager.balanceOf(address(takingFee), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); + + // test withdrawing tokens // + Currency[] memory currencies = new Currency[](2); + currencies[0] = key.currency0; + currencies[1] = key.currency1; + takingFee.withdraw(TREASURY, currencies); + assertEq(manager.balanceOf(address(this), CurrencyLibrary.toId(key.currency0)), 0); + assertEq(manager.balanceOf(address(this), CurrencyLibrary.toId(key.currency1)), 0); + assertEq(currency0.balanceOf(TREASURY) / R, expectedFee2 / R); + assertEq(currency1.balanceOf(TREASURY) / R, expectedFee / R); + } + + function testEdgeCase() public { + // Swap exact token0 for token1 // + bool zeroForOne = true; + int256 amountSpecified = -1e18; + BalanceDelta swapDelta = swap(key, zeroForOne, amountSpecified, ZERO_BYTES); + // ---------------------------- // + + uint128 output = uint128(swapDelta.amount1()); + assertTrue(output > 0); + + uint256 expectedFee = output * TOTAL_BIPS / (TOTAL_BIPS - takingFee.swapFeeBips()) - output; + + assertEq(manager.balanceOf(address(takingFee), CurrencyLibrary.toId(key.currency0)), 0); + assertEq(manager.balanceOf(address(takingFee), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); + + // Swap token1 for exact token0 // + bool zeroForOne2 = false; + int256 amountSpecified2 = 1e18; // positive number indicates exact output swap + BalanceDelta swapDelta2 = swap(key, zeroForOne2, amountSpecified2, ZERO_BYTES); + // ---------------------------- // + + uint128 input = uint128(-swapDelta2.amount1()); + assertTrue(output > 0); uint128 expectedFee2 = (input * takingFee.swapFeeBips()) / (TOTAL_BIPS + takingFee.swapFeeBips()); - assertEq(currency0.balanceOf(TREASURY) / ROUND_FACTOR, expectedFee2 / ROUND_FACTOR); - assertEq(currency1.balanceOf(TREASURY) / ROUND_FACTOR, expectedFee / ROUND_FACTOR); + assertEq(manager.balanceOf(address(takingFee), CurrencyLibrary.toId(key.currency0)), 0); + assertEq(manager.balanceOf(address(takingFee), CurrencyLibrary.toId(key.currency1)) / R, (expectedFee + expectedFee2) / R); + + // test withdrawing tokens // + Currency[] memory currencies = new Currency[](2); + currencies[0] = key.currency0; + currencies[1] = key.currency1; + takingFee.withdraw(TREASURY, currencies); + assertEq(currency0.balanceOf(TREASURY) / R, 0); + assertEq(currency1.balanceOf(TREASURY) / R, (expectedFee + expectedFee2) / R); } } diff --git a/test/shared/implementation/TakingFeeImplementation.sol b/test/shared/implementation/TakingFeeImplementation.sol index e5e07237..8c1a0c11 100644 --- a/test/shared/implementation/TakingFeeImplementation.sol +++ b/test/shared/implementation/TakingFeeImplementation.sol @@ -7,7 +7,9 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; contract TakingFeeImplementation is TakingFee { - constructor(IPoolManager _poolManager, uint128 _swapFeeBips, address _treasury, TakingFee addressToEtch) TakingFee(_poolManager, _swapFeeBips, _treasury) { + constructor(IPoolManager _poolManager, uint128 _swapFeeBips, TakingFee addressToEtch) + TakingFee(_poolManager, _swapFeeBips) + { Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); } From 2db74673c47587b032ad1f93a31ff5ebd09b830b Mon Sep 17 00:00:00 2001 From: Junion Date: Fri, 7 Jun 2024 17:24:45 -0400 Subject: [PATCH 03/25] forge fmt --- test/TakingFee.t.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/TakingFee.t.sol b/test/TakingFee.t.sol index 7a1edd50..07af4faf 100644 --- a/test/TakingFee.t.sol +++ b/test/TakingFee.t.sol @@ -107,6 +107,7 @@ contract TakingFeeTest is Test, Deployers { } function testEdgeCase() public { + // first, deplete the pool of token1 // Swap exact token0 for token1 // bool zeroForOne = true; int256 amountSpecified = -1e18; @@ -133,7 +134,10 @@ contract TakingFeeTest is Test, Deployers { uint128 expectedFee2 = (input * takingFee.swapFeeBips()) / (TOTAL_BIPS + takingFee.swapFeeBips()); assertEq(manager.balanceOf(address(takingFee), CurrencyLibrary.toId(key.currency0)), 0); - assertEq(manager.balanceOf(address(takingFee), CurrencyLibrary.toId(key.currency1)) / R, (expectedFee + expectedFee2) / R); + assertEq( + manager.balanceOf(address(takingFee), CurrencyLibrary.toId(key.currency1)) / R, + (expectedFee + expectedFee2) / R + ); // test withdrawing tokens // Currency[] memory currencies = new Currency[](2); From 59da5a9bc4be5ce4f877c1b94b5d26ea205ed8f7 Mon Sep 17 00:00:00 2001 From: Junion Date: Thu, 13 Jun 2024 10:20:12 -0400 Subject: [PATCH 04/25] resolve nit --- contracts/hooks/examples/TakingFee.sol | 6 +-- test/TakingFee.t.sol | 50 +++++++++---------- .../TakingFeeImplementation.sol | 8 +-- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/contracts/hooks/examples/TakingFee.sol b/contracts/hooks/examples/TakingFee.sol index d3294e6c..07b04777 100644 --- a/contracts/hooks/examples/TakingFee.sol +++ b/contracts/hooks/examples/TakingFee.sol @@ -12,7 +12,7 @@ import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {Owned} from "solmate/auth/Owned.sol"; import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; -contract TakingFee is BaseHook, IUnlockCallback, Owned { +contract FeeTaking is BaseHook, IUnlockCallback, Owned { using SafeCast for uint256; uint128 private constant TOTAL_BIPS = 10000; @@ -55,9 +55,9 @@ contract TakingFee is BaseHook, IUnlockCallback, Owned { bytes calldata ) external override returns (bytes4, int128) { // fee will be in the unspecified token of the swap - bool specifiedTokenIs0 = (params.amountSpecified < 0 == params.zeroForOne); + bool currency0Specified = (params.amountSpecified < 0 == params.zeroForOne); (Currency feeCurrency, int128 swapAmount) = - (specifiedTokenIs0) ? (key.currency1, delta.amount1()) : (key.currency0, delta.amount0()); + (currency0Specified) ? (key.currency1, delta.amount1()) : (key.currency0, delta.amount0()); // if fee is on output, get the absolute output amount if (swapAmount < 0) swapAmount = -swapAmount; diff --git a/test/TakingFee.t.sol b/test/TakingFee.t.sol index 07af4faf..fb674031 100644 --- a/test/TakingFee.t.sol +++ b/test/TakingFee.t.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {TakingFee} from "../contracts/hooks/examples/TakingFee.sol"; -import {TakingFeeImplementation} from "./shared/implementation/TakingFeeImplementation.sol"; +import {FeeTaking} from "../contracts/hooks/examples/FeeTaking.sol"; +import {FeeTakingImplementation} from "./shared/implementation/FeeTakingImplementation.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; @@ -16,7 +16,7 @@ import {HookEnabledSwapRouter} from "./utils/HookEnabledSwapRouter.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -contract TakingFeeTest is Test, Deployers { +contract FeeTakingTest is Test, Deployers { using PoolIdLibrary for PoolKey; using StateLibrary for IPoolManager; @@ -31,7 +31,7 @@ contract TakingFeeTest is Test, Deployers { HookEnabledSwapRouter router; TestERC20 token0; TestERC20 token1; - TakingFee takingFee = TakingFee(address(uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG))); + FeeTaking FeeTaking = FeeTaking(address(uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG))); PoolId id; function setUp() public { @@ -43,22 +43,22 @@ contract TakingFeeTest is Test, Deployers { token1 = TestERC20(Currency.unwrap(currency1)); vm.record(); - TakingFeeImplementation impl = new TakingFeeImplementation(manager, 25, takingFee); + FeeTakingImplementation impl = new FeeTakingImplementation(manager, 25, FeeTaking); (, bytes32[] memory writes) = vm.accesses(address(impl)); - vm.etch(address(takingFee), address(impl).code); + vm.etch(address(FeeTaking), address(impl).code); // for each storage key that was written during the hook implementation, copy the value over unchecked { for (uint256 i = 0; i < writes.length; i++) { bytes32 slot = writes[i]; - vm.store(address(takingFee), slot, vm.load(address(impl), slot)); + vm.store(address(FeeTaking), slot, vm.load(address(impl), slot)); } } - // key = PoolKey(currency0, currency1, 3000, 60, takingFee); - (key, id) = initPoolAndAddLiquidity(currency0, currency1, takingFee, 3000, SQRT_PRICE_1_1, ZERO_BYTES); + // key = PoolKey(currency0, currency1, 3000, 60, FeeTaking); + (key, id) = initPoolAndAddLiquidity(currency0, currency1, FeeTaking, 3000, SQRT_PRICE_1_1, ZERO_BYTES); - token0.approve(address(takingFee), type(uint256).max); - token1.approve(address(takingFee), type(uint256).max); + token0.approve(address(FeeTaking), type(uint256).max); + token1.approve(address(FeeTaking), type(uint256).max); token0.approve(address(router), type(uint256).max); token1.approve(address(router), type(uint256).max); } @@ -76,10 +76,10 @@ contract TakingFeeTest is Test, Deployers { uint128 output = uint128(swapDelta.amount1()); assertTrue(output > 0); - uint256 expectedFee = output * TOTAL_BIPS / (TOTAL_BIPS - takingFee.swapFeeBips()) - output; + uint256 expectedFee = output * TOTAL_BIPS / (TOTAL_BIPS - FeeTaking.swapFeeBips()) - output; - assertEq(manager.balanceOf(address(takingFee), CurrencyLibrary.toId(key.currency0)), 0); - assertEq(manager.balanceOf(address(takingFee), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); + assertEq(manager.balanceOf(address(FeeTaking), CurrencyLibrary.toId(key.currency0)), 0); + assertEq(manager.balanceOf(address(FeeTaking), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); // Swap token0 for exact token1 // bool zeroForOne2 = true; @@ -90,16 +90,16 @@ contract TakingFeeTest is Test, Deployers { uint128 input = uint128(-swapDelta2.amount0()); assertTrue(output > 0); - uint128 expectedFee2 = (input * takingFee.swapFeeBips()) / (TOTAL_BIPS + takingFee.swapFeeBips()); + uint128 expectedFee2 = (input * FeeTaking.swapFeeBips()) / (TOTAL_BIPS + FeeTaking.swapFeeBips()); - assertEq(manager.balanceOf(address(takingFee), CurrencyLibrary.toId(key.currency0)) / R, expectedFee2 / R); - assertEq(manager.balanceOf(address(takingFee), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); + assertEq(manager.balanceOf(address(FeeTaking), CurrencyLibrary.toId(key.currency0)) / R, expectedFee2 / R); + assertEq(manager.balanceOf(address(FeeTaking), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); // test withdrawing tokens // Currency[] memory currencies = new Currency[](2); currencies[0] = key.currency0; currencies[1] = key.currency1; - takingFee.withdraw(TREASURY, currencies); + FeeTaking.withdraw(TREASURY, currencies); assertEq(manager.balanceOf(address(this), CurrencyLibrary.toId(key.currency0)), 0); assertEq(manager.balanceOf(address(this), CurrencyLibrary.toId(key.currency1)), 0); assertEq(currency0.balanceOf(TREASURY) / R, expectedFee2 / R); @@ -117,10 +117,10 @@ contract TakingFeeTest is Test, Deployers { uint128 output = uint128(swapDelta.amount1()); assertTrue(output > 0); - uint256 expectedFee = output * TOTAL_BIPS / (TOTAL_BIPS - takingFee.swapFeeBips()) - output; + uint256 expectedFee = output * TOTAL_BIPS / (TOTAL_BIPS - FeeTaking.swapFeeBips()) - output; - assertEq(manager.balanceOf(address(takingFee), CurrencyLibrary.toId(key.currency0)), 0); - assertEq(manager.balanceOf(address(takingFee), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); + assertEq(manager.balanceOf(address(FeeTaking), CurrencyLibrary.toId(key.currency0)), 0); + assertEq(manager.balanceOf(address(FeeTaking), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); // Swap token1 for exact token0 // bool zeroForOne2 = false; @@ -131,11 +131,11 @@ contract TakingFeeTest is Test, Deployers { uint128 input = uint128(-swapDelta2.amount1()); assertTrue(output > 0); - uint128 expectedFee2 = (input * takingFee.swapFeeBips()) / (TOTAL_BIPS + takingFee.swapFeeBips()); + uint128 expectedFee2 = (input * FeeTaking.swapFeeBips()) / (TOTAL_BIPS + FeeTaking.swapFeeBips()); - assertEq(manager.balanceOf(address(takingFee), CurrencyLibrary.toId(key.currency0)), 0); + assertEq(manager.balanceOf(address(FeeTaking), CurrencyLibrary.toId(key.currency0)), 0); assertEq( - manager.balanceOf(address(takingFee), CurrencyLibrary.toId(key.currency1)) / R, + manager.balanceOf(address(FeeTaking), CurrencyLibrary.toId(key.currency1)) / R, (expectedFee + expectedFee2) / R ); @@ -143,7 +143,7 @@ contract TakingFeeTest is Test, Deployers { Currency[] memory currencies = new Currency[](2); currencies[0] = key.currency0; currencies[1] = key.currency1; - takingFee.withdraw(TREASURY, currencies); + FeeTaking.withdraw(TREASURY, currencies); assertEq(currency0.balanceOf(TREASURY) / R, 0); assertEq(currency1.balanceOf(TREASURY) / R, (expectedFee + expectedFee2) / R); } diff --git a/test/shared/implementation/TakingFeeImplementation.sol b/test/shared/implementation/TakingFeeImplementation.sol index 8c1a0c11..1d3bc051 100644 --- a/test/shared/implementation/TakingFeeImplementation.sol +++ b/test/shared/implementation/TakingFeeImplementation.sol @@ -2,13 +2,13 @@ pragma solidity ^0.8.19; import {BaseHook} from "../../../contracts/BaseHook.sol"; -import {TakingFee} from "../../../contracts/hooks/examples/TakingFee.sol"; +import {FeeTaking} from "../../../contracts/hooks/examples/FeeTaking.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -contract TakingFeeImplementation is TakingFee { - constructor(IPoolManager _poolManager, uint128 _swapFeeBips, TakingFee addressToEtch) - TakingFee(_poolManager, _swapFeeBips) +contract FeeTakingImplementation is FeeTaking { + constructor(IPoolManager _poolManager, uint128 _swapFeeBips, FeeTaking addressToEtch) + FeeTaking(_poolManager, _swapFeeBips) { Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); } From 4628fe1a213dc622b55ec8bfdc710a0d3f1b4429 Mon Sep 17 00:00:00 2001 From: Junion Date: Thu, 13 Jun 2024 10:21:41 -0400 Subject: [PATCH 05/25] file name change --- contracts/hooks/examples/{TakingFee.sol => FeeTaking.sol} | 0 test/{TakingFee.t.sol => FeeTaking.t.sol} | 0 .../{TakingFeeImplementation.sol => FeeTakingImplementation.sol} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename contracts/hooks/examples/{TakingFee.sol => FeeTaking.sol} (100%) rename test/{TakingFee.t.sol => FeeTaking.t.sol} (100%) rename test/shared/implementation/{TakingFeeImplementation.sol => FeeTakingImplementation.sol} (100%) diff --git a/contracts/hooks/examples/TakingFee.sol b/contracts/hooks/examples/FeeTaking.sol similarity index 100% rename from contracts/hooks/examples/TakingFee.sol rename to contracts/hooks/examples/FeeTaking.sol diff --git a/test/TakingFee.t.sol b/test/FeeTaking.t.sol similarity index 100% rename from test/TakingFee.t.sol rename to test/FeeTaking.t.sol diff --git a/test/shared/implementation/TakingFeeImplementation.sol b/test/shared/implementation/FeeTakingImplementation.sol similarity index 100% rename from test/shared/implementation/TakingFeeImplementation.sol rename to test/shared/implementation/FeeTakingImplementation.sol From da0ce0825d668d82f92d026f51af70daa3c8ddd3 Mon Sep 17 00:00:00 2001 From: Junion Date: Thu, 13 Jun 2024 11:38:07 -0400 Subject: [PATCH 06/25] fix bug --- test/FeeTaking.t.sol | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/test/FeeTaking.t.sol b/test/FeeTaking.t.sol index fb674031..028c7ebe 100644 --- a/test/FeeTaking.t.sol +++ b/test/FeeTaking.t.sol @@ -31,7 +31,7 @@ contract FeeTakingTest is Test, Deployers { HookEnabledSwapRouter router; TestERC20 token0; TestERC20 token1; - FeeTaking FeeTaking = FeeTaking(address(uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG))); + FeeTaking feeTaking = FeeTaking(address(uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG))); PoolId id; function setUp() public { @@ -43,22 +43,22 @@ contract FeeTakingTest is Test, Deployers { token1 = TestERC20(Currency.unwrap(currency1)); vm.record(); - FeeTakingImplementation impl = new FeeTakingImplementation(manager, 25, FeeTaking); + FeeTakingImplementation impl = new FeeTakingImplementation(manager, 25, feeTaking); (, bytes32[] memory writes) = vm.accesses(address(impl)); - vm.etch(address(FeeTaking), address(impl).code); + vm.etch(address(feeTaking), address(impl).code); // for each storage key that was written during the hook implementation, copy the value over unchecked { for (uint256 i = 0; i < writes.length; i++) { bytes32 slot = writes[i]; - vm.store(address(FeeTaking), slot, vm.load(address(impl), slot)); + vm.store(address(feeTaking), slot, vm.load(address(impl), slot)); } } - // key = PoolKey(currency0, currency1, 3000, 60, FeeTaking); - (key, id) = initPoolAndAddLiquidity(currency0, currency1, FeeTaking, 3000, SQRT_PRICE_1_1, ZERO_BYTES); + // key = PoolKey(currency0, currency1, 3000, 60, feeTaking); + (key, id) = initPoolAndAddLiquidity(currency0, currency1, feeTaking, 3000, SQRT_PRICE_1_1, ZERO_BYTES); - token0.approve(address(FeeTaking), type(uint256).max); - token1.approve(address(FeeTaking), type(uint256).max); + token0.approve(address(feeTaking), type(uint256).max); + token1.approve(address(feeTaking), type(uint256).max); token0.approve(address(router), type(uint256).max); token1.approve(address(router), type(uint256).max); } @@ -76,10 +76,10 @@ contract FeeTakingTest is Test, Deployers { uint128 output = uint128(swapDelta.amount1()); assertTrue(output > 0); - uint256 expectedFee = output * TOTAL_BIPS / (TOTAL_BIPS - FeeTaking.swapFeeBips()) - output; + uint256 expectedFee = output * TOTAL_BIPS / (TOTAL_BIPS - feeTaking.swapFeeBips()) - output; - assertEq(manager.balanceOf(address(FeeTaking), CurrencyLibrary.toId(key.currency0)), 0); - assertEq(manager.balanceOf(address(FeeTaking), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); + assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency0)), 0); + assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); // Swap token0 for exact token1 // bool zeroForOne2 = true; @@ -90,16 +90,16 @@ contract FeeTakingTest is Test, Deployers { uint128 input = uint128(-swapDelta2.amount0()); assertTrue(output > 0); - uint128 expectedFee2 = (input * FeeTaking.swapFeeBips()) / (TOTAL_BIPS + FeeTaking.swapFeeBips()); + uint128 expectedFee2 = (input * feeTaking.swapFeeBips()) / (TOTAL_BIPS + feeTaking.swapFeeBips()); - assertEq(manager.balanceOf(address(FeeTaking), CurrencyLibrary.toId(key.currency0)) / R, expectedFee2 / R); - assertEq(manager.balanceOf(address(FeeTaking), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); + assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency0)) / R, expectedFee2 / R); + assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); // test withdrawing tokens // Currency[] memory currencies = new Currency[](2); currencies[0] = key.currency0; currencies[1] = key.currency1; - FeeTaking.withdraw(TREASURY, currencies); + feeTaking.withdraw(TREASURY, currencies); assertEq(manager.balanceOf(address(this), CurrencyLibrary.toId(key.currency0)), 0); assertEq(manager.balanceOf(address(this), CurrencyLibrary.toId(key.currency1)), 0); assertEq(currency0.balanceOf(TREASURY) / R, expectedFee2 / R); @@ -117,10 +117,10 @@ contract FeeTakingTest is Test, Deployers { uint128 output = uint128(swapDelta.amount1()); assertTrue(output > 0); - uint256 expectedFee = output * TOTAL_BIPS / (TOTAL_BIPS - FeeTaking.swapFeeBips()) - output; + uint256 expectedFee = output * TOTAL_BIPS / (TOTAL_BIPS - feeTaking.swapFeeBips()) - output; - assertEq(manager.balanceOf(address(FeeTaking), CurrencyLibrary.toId(key.currency0)), 0); - assertEq(manager.balanceOf(address(FeeTaking), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); + assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency0)), 0); + assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); // Swap token1 for exact token0 // bool zeroForOne2 = false; @@ -131,11 +131,11 @@ contract FeeTakingTest is Test, Deployers { uint128 input = uint128(-swapDelta2.amount1()); assertTrue(output > 0); - uint128 expectedFee2 = (input * FeeTaking.swapFeeBips()) / (TOTAL_BIPS + FeeTaking.swapFeeBips()); + uint128 expectedFee2 = (input * feeTaking.swapFeeBips()) / (TOTAL_BIPS + feeTaking.swapFeeBips()); - assertEq(manager.balanceOf(address(FeeTaking), CurrencyLibrary.toId(key.currency0)), 0); + assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency0)), 0); assertEq( - manager.balanceOf(address(FeeTaking), CurrencyLibrary.toId(key.currency1)) / R, + manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)) / R, (expectedFee + expectedFee2) / R ); @@ -143,7 +143,7 @@ contract FeeTakingTest is Test, Deployers { Currency[] memory currencies = new Currency[](2); currencies[0] = key.currency0; currencies[1] = key.currency1; - FeeTaking.withdraw(TREASURY, currencies); + feeTaking.withdraw(TREASURY, currencies); assertEq(currency0.balanceOf(TREASURY) / R, 0); assertEq(currency1.balanceOf(TREASURY) / R, (expectedFee + expectedFee2) / R); } From 0a61b061cdc20fce7e9ab854b9ffc427966be250 Mon Sep 17 00:00:00 2001 From: Junion Date: Thu, 13 Jun 2024 16:30:24 -0400 Subject: [PATCH 07/25] update constructor to take owner --- contracts/hooks/examples/FeeTaking.sol | 2 +- test/FeeTaking.t.sol | 10 +++++++--- test/shared/implementation/FeeTakingImplementation.sol | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/contracts/hooks/examples/FeeTaking.sol b/contracts/hooks/examples/FeeTaking.sol index 07b04777..183325dd 100644 --- a/contracts/hooks/examples/FeeTaking.sol +++ b/contracts/hooks/examples/FeeTaking.sol @@ -24,7 +24,7 @@ contract FeeTaking is BaseHook, IUnlockCallback, Owned { Currency[] currencies; } - constructor(IPoolManager _poolManager, uint128 _swapFeeBips) BaseHook(_poolManager) Owned(msg.sender) { + constructor(IPoolManager _poolManager, uint128 _swapFeeBips, address _owner) BaseHook(_poolManager) Owned(_owner) { swapFeeBips = _swapFeeBips; } diff --git a/test/FeeTaking.t.sol b/test/FeeTaking.t.sol index 028c7ebe..b301cfc9 100644 --- a/test/FeeTaking.t.sol +++ b/test/FeeTaking.t.sol @@ -43,7 +43,7 @@ contract FeeTakingTest is Test, Deployers { token1 = TestERC20(Currency.unwrap(currency1)); vm.record(); - FeeTakingImplementation impl = new FeeTakingImplementation(manager, 25, feeTaking); + FeeTakingImplementation impl = new FeeTakingImplementation(manager, 25, address(this), feeTaking); (, bytes32[] memory writes) = vm.accesses(address(impl)); vm.etch(address(feeTaking), address(impl).code); // for each storage key that was written during the hook implementation, copy the value over @@ -100,12 +100,13 @@ contract FeeTakingTest is Test, Deployers { currencies[0] = key.currency0; currencies[1] = key.currency1; feeTaking.withdraw(TREASURY, currencies); - assertEq(manager.balanceOf(address(this), CurrencyLibrary.toId(key.currency0)), 0); - assertEq(manager.balanceOf(address(this), CurrencyLibrary.toId(key.currency1)), 0); + assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency0)), 0); + assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)), 0); assertEq(currency0.balanceOf(TREASURY) / R, expectedFee2 / R); assertEq(currency1.balanceOf(TREASURY) / R, expectedFee / R); } + // this would error had the hook not used ERC6909 function testEdgeCase() public { // first, deplete the pool of token1 // Swap exact token0 for token1 // @@ -113,6 +114,9 @@ contract FeeTakingTest is Test, Deployers { int256 amountSpecified = -1e18; BalanceDelta swapDelta = swap(key, zeroForOne, amountSpecified, ZERO_BYTES); // ---------------------------- // + // now, pool only has 1 wei of token1 + uint256 poolToken1 = currency1.balanceOf(address(manager)) - manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)); + assertEq(poolToken1, 1); uint128 output = uint128(swapDelta.amount1()); assertTrue(output > 0); diff --git a/test/shared/implementation/FeeTakingImplementation.sol b/test/shared/implementation/FeeTakingImplementation.sol index 1d3bc051..697dbf41 100644 --- a/test/shared/implementation/FeeTakingImplementation.sol +++ b/test/shared/implementation/FeeTakingImplementation.sol @@ -7,8 +7,8 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; contract FeeTakingImplementation is FeeTaking { - constructor(IPoolManager _poolManager, uint128 _swapFeeBips, FeeTaking addressToEtch) - FeeTaking(_poolManager, _swapFeeBips) + constructor(IPoolManager _poolManager, uint128 _swapFeeBips, address _owner, FeeTaking addressToEtch) + FeeTaking(_poolManager, _swapFeeBips, _owner) { Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); } From a526b2d0cad05f4ae0ccbc2edc31e3c8d4ad7d1d Mon Sep 17 00:00:00 2001 From: Junion Date: Thu, 13 Jun 2024 17:13:21 -0400 Subject: [PATCH 08/25] add more tests --- contracts/hooks/examples/FeeTaking.sol | 3 +- test/FeeTaking.t.sol | 123 ++++++++++++++++++++++++- 2 files changed, 120 insertions(+), 6 deletions(-) diff --git a/contracts/hooks/examples/FeeTaking.sol b/contracts/hooks/examples/FeeTaking.sol index 183325dd..942f1f3e 100644 --- a/contracts/hooks/examples/FeeTaking.sol +++ b/contracts/hooks/examples/FeeTaking.sol @@ -15,6 +15,7 @@ import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockC contract FeeTaking is BaseHook, IUnlockCallback, Owned { using SafeCast for uint256; + bytes internal constant ZERO_BYTES = bytes(""); uint128 private constant TOTAL_BIPS = 10000; uint128 private constant MAX_BIPS = 100; uint128 public swapFeeBips; @@ -93,6 +94,6 @@ contract FeeTaking is BaseHook, IUnlockCallback, Owned { i++; } } - return ""; + return ZERO_BYTES; } } diff --git a/test/FeeTaking.t.sol b/test/FeeTaking.t.sol index b301cfc9..85de2605 100644 --- a/test/FeeTaking.t.sol +++ b/test/FeeTaking.t.sol @@ -76,7 +76,7 @@ contract FeeTakingTest is Test, Deployers { uint128 output = uint128(swapDelta.amount1()); assertTrue(output > 0); - uint256 expectedFee = output * TOTAL_BIPS / (TOTAL_BIPS - feeTaking.swapFeeBips()) - output; + uint256 expectedFee = calculateFeeForExactInput(output, feeTaking.swapFeeBips()); assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency0)), 0); assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); @@ -90,7 +90,7 @@ contract FeeTakingTest is Test, Deployers { uint128 input = uint128(-swapDelta2.amount0()); assertTrue(output > 0); - uint128 expectedFee2 = (input * feeTaking.swapFeeBips()) / (TOTAL_BIPS + feeTaking.swapFeeBips()); + uint256 expectedFee2 = calculateFeeForExactOutput(input, feeTaking.swapFeeBips()); assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency0)) / R, expectedFee2 / R); assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); @@ -115,13 +115,14 @@ contract FeeTakingTest is Test, Deployers { BalanceDelta swapDelta = swap(key, zeroForOne, amountSpecified, ZERO_BYTES); // ---------------------------- // // now, pool only has 1 wei of token1 - uint256 poolToken1 = currency1.balanceOf(address(manager)) - manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)); + uint256 poolToken1 = currency1.balanceOf(address(manager)) + - manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)); assertEq(poolToken1, 1); uint128 output = uint128(swapDelta.amount1()); assertTrue(output > 0); - uint256 expectedFee = output * TOTAL_BIPS / (TOTAL_BIPS - feeTaking.swapFeeBips()) - output; + uint256 expectedFee = calculateFeeForExactInput(output, feeTaking.swapFeeBips()); assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency0)), 0); assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); @@ -135,7 +136,7 @@ contract FeeTakingTest is Test, Deployers { uint128 input = uint128(-swapDelta2.amount1()); assertTrue(output > 0); - uint128 expectedFee2 = (input * feeTaking.swapFeeBips()) / (TOTAL_BIPS + feeTaking.swapFeeBips()); + uint256 expectedFee2 = calculateFeeForExactOutput(input, feeTaking.swapFeeBips()); assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency0)), 0); assertEq( @@ -151,4 +152,116 @@ contract FeeTakingTest is Test, Deployers { assertEq(currency0.balanceOf(TREASURY) / R, 0); assertEq(currency1.balanceOf(TREASURY) / R, (expectedFee + expectedFee2) / R); } + + function testSwapWithDifferentFees() public { + testSwapHooks(); + feeTaking.setSwapFeeBips(50); // Set fee to 0.50% + + // Swap exact token0 for token1 // + bool zeroForOne = true; + int256 amountSpecified = -1e12; + BalanceDelta swapDelta = swap(key, zeroForOne, amountSpecified, ZERO_BYTES); + // ---------------------------- // + + uint128 output = uint128(swapDelta.amount1()); + assertTrue(output > 0); + + uint256 expectedFee = calculateFeeForExactInput(output, 50); + + assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency0)), 0); + assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); + } + + function testZeroFeeSwap() public { + feeTaking.setSwapFeeBips(0); // Set fee to 0% + + // Swap exact token0 for token1 // + bool zeroForOne = true; + int256 amountSpecified = -1e12; + BalanceDelta swapDelta = swap(key, zeroForOne, amountSpecified, ZERO_BYTES); + // ---------------------------- // + + uint128 output = uint128(swapDelta.amount1()); + assertTrue(output > 0); + + // No fee should be taken + uint256 expectedFee = calculateFeeForExactInput(output, 0); + assertEq(expectedFee, 0); + assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency0)), 0); + assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)), 0); + } + + function testMaxFeeSwap() public { + feeTaking.setSwapFeeBips(100); // Set fee to 1% + + // Swap exact token0 for token1 // + bool zeroForOne = true; + int256 amountSpecified = -1e12; + BalanceDelta swapDelta = swap(key, zeroForOne, amountSpecified, ZERO_BYTES); + // ---------------------------- // + + uint128 output = uint128(swapDelta.amount1()); + assertTrue(output > 0); + + uint256 expectedFee = calculateFeeForExactInput(output, 100); + + assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency0)), 0); + assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); + } + + function testMultiTokenPoolSwap() public { + testSwapHooks(); + // Deploy additional tokens + (Currency currency2, Currency currency3) = deployMintAndApprove2Currencies(); + TestERC20 token2 = TestERC20(Currency.unwrap(currency2)); + TestERC20 token3 = TestERC20(Currency.unwrap(currency3)); + + // Create new pool with different tokens + (PoolKey memory key2, PoolId id2) = + initPoolAndAddLiquidity(currency2, currency3, feeTaking, 3000, SQRT_RATIO_10_1, ZERO_BYTES); + + // Approve tokens for the router + token2.approve(address(router), type(uint256).max); + token3.approve(address(router), type(uint256).max); + + // Swap exact token2 for token3 // + bool zeroForOne = true; + int256 amountSpecified = -1e12; + BalanceDelta swapDelta = swap(key2, zeroForOne, amountSpecified, ZERO_BYTES); + // ---------------------------- // + + uint128 output = uint128(swapDelta.amount1()); + assertTrue(output > 0); + + uint256 expectedFee = calculateFeeForExactInput(output, feeTaking.swapFeeBips()); + + assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key2.currency0)), 0); + assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key2.currency1)) / R, expectedFee / R); + + // Withdraw accumulated fees + Currency[] memory currencies = new Currency[](3); + currencies[0] = key.currency0; + currencies[1] = key.currency1; + currencies[2] = key2.currency1; + feeTaking.withdraw(TREASURY, currencies); + assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency0)), 0); + assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)), 0); + assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key2.currency1)), 0); + assertEq(currency0.balanceOf(TREASURY) / R, 0); + assertEq(currency1.balanceOf(TREASURY) / R, expectedFee / R); + assertEq(currency3.balanceOf(TREASURY) / R, expectedFee / R); + } + + function testTooHighFee() public { + vm.expectRevert(); + feeTaking.setSwapFeeBips(101); + } + + function calculateFeeForExactInput(uint256 outputAmount, uint128 feeBips) internal pure returns (uint256) { + return outputAmount * TOTAL_BIPS / (TOTAL_BIPS - feeBips) - outputAmount; + } + + function calculateFeeForExactOutput(uint256 inputAmount, uint128 feeBips) internal pure returns (uint256) { + return (inputAmount * feeBips) / (TOTAL_BIPS + feeBips); + } } From 99bb416481212580169541800790772495542cc9 Mon Sep 17 00:00:00 2001 From: Junion Date: Fri, 14 Jun 2024 15:44:10 -0400 Subject: [PATCH 09/25] primitive tests --- contracts/hooks/examples/BaseMiddleware.sol | 31 ++++ contracts/hooks/examples/FeeTakingLite.sol | 98 +++++++++++++ test/BaseMiddleware.t.sol | 138 ++++++++++++++++++ .../BaseMiddlewareImplementation.sol | 19 +++ .../FeeTakingLiteImplementation.sol | 16 ++ 5 files changed, 302 insertions(+) create mode 100644 contracts/hooks/examples/BaseMiddleware.sol create mode 100644 contracts/hooks/examples/FeeTakingLite.sol create mode 100644 test/BaseMiddleware.t.sol create mode 100644 test/shared/implementation/BaseMiddlewareImplementation.sol create mode 100644 test/shared/implementation/FeeTakingLiteImplementation.sol diff --git a/contracts/hooks/examples/BaseMiddleware.sol b/contracts/hooks/examples/BaseMiddleware.sol new file mode 100644 index 00000000..f5fc500c --- /dev/null +++ b/contracts/hooks/examples/BaseMiddleware.sol @@ -0,0 +1,31 @@ +// 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"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; +import {Proxy} from "@openzeppelin/contracts/proxy/Proxy.sol"; +import {console} from "../../../lib/forge-std/src/console.sol"; + +contract BaseMiddleware is Proxy { + /// @notice The address of the pool manager + IPoolManager public immutable poolManager; + address public immutable implementation; + + constructor(IPoolManager _poolManager, address _impl) { + poolManager = _poolManager; + implementation = _impl; + } + + function _implementation() internal view override returns (address) { + console.logAddress(implementation); + return implementation; + } + + receive() external payable { + // ?? + } +} diff --git a/contracts/hooks/examples/FeeTakingLite.sol b/contracts/hooks/examples/FeeTakingLite.sol new file mode 100644 index 00000000..05a83444 --- /dev/null +++ b/contracts/hooks/examples/FeeTakingLite.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {BaseHook} from "../../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 {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; +import {Owned} from "solmate/auth/Owned.sol"; +import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; +import {console} from "../../../lib/forge-std/src/console.sol"; + +contract FeeTakingLite is IUnlockCallback { + using SafeCast for uint256; + + bytes internal constant ZERO_BYTES = bytes(""); + uint128 private constant TOTAL_BIPS = 10000; + uint128 private constant MAX_BIPS = 100; + uint128 public constant swapFeeBips = 25; + IPoolManager public immutable poolManager; + + struct CallbackData { + address to; + Currency[] currencies; + } + + constructor(IPoolManager _poolManager) { + poolManager = _poolManager; + } + + function getHookPermissions() public pure returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + afterAddLiquidity: false, + beforeRemoveLiquidity: false, + afterRemoveLiquidity: false, + beforeSwap: false, + afterSwap: true, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: true, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } + + function afterSwap( + address, + PoolKey calldata key, + IPoolManager.SwapParams calldata params, + BalanceDelta delta, + bytes calldata + ) external returns (bytes4, int128) { + console.log("afterSwap"); + // fee will be in the unspecified token of the swap + bool currency0Specified = (params.amountSpecified < 0 == params.zeroForOne); + (Currency feeCurrency, int128 swapAmount) = + (currency0Specified) ? (key.currency1, delta.amount1()) : (key.currency0, delta.amount0()); + // if fee is on output, get the absolute output amount + if (swapAmount < 0) swapAmount = -swapAmount; + + uint256 feeAmount = (uint128(swapAmount) * swapFeeBips) / TOTAL_BIPS; + console.log(swapFeeBips); + // mint ERC6909 instead of take to avoid edge case where PM doesn't have enough balance + poolManager.mint(address(this), CurrencyLibrary.toId(feeCurrency), feeAmount); + + return (BaseHook.afterSwap.selector, feeAmount.toInt128()); + } + + function setSwapFeeBips(uint128 _swapFeeBips) external pure { + require(_swapFeeBips <= MAX_BIPS); + //swapFeeBips = _swapFeeBips; + } + + function withdraw(address to, Currency[] calldata currencies) external { + poolManager.unlock(abi.encode(CallbackData(to, currencies))); + } + + function unlockCallback(bytes calldata rawData) external override returns (bytes memory) { + CallbackData memory data = abi.decode(rawData, (CallbackData)); + uint256 length = data.currencies.length; + for (uint256 i = 0; i < length;) { + uint256 amount = poolManager.balanceOf(address(this), CurrencyLibrary.toId(data.currencies[i])); + poolManager.burn(address(this), CurrencyLibrary.toId(data.currencies[i]), amount); + poolManager.take(data.currencies[i], data.to, amount); + unchecked { + i++; + } + } + return ZERO_BYTES; + } +} diff --git a/test/BaseMiddleware.t.sol b/test/BaseMiddleware.t.sol new file mode 100644 index 00000000..08cc960f --- /dev/null +++ b/test/BaseMiddleware.t.sol @@ -0,0 +1,138 @@ +// 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 {FeeTakingLite} from "../contracts/hooks/examples/FeeTakingLite.sol"; +import {FeeTakingLiteImplementation} from "./shared/implementation/FeeTakingLiteImplementation.sol"; +import {BaseMiddleware} from "../contracts/hooks/examples/BaseMiddleware.sol"; +import {BaseMiddlewareImplementation} from "./shared/implementation/BaseMiddlewareImplementation.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.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 {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.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"; + +contract BaseMiddlewareTest is Test, Deployers { + using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; + + uint160 constant SQRT_RATIO_10_1 = 250541448375047931186413801569; + + address constant TREASURY = address(0x1234567890123456789012345678901234567890); + uint128 private constant TOTAL_BIPS = 10000; + + HookEnabledSwapRouter router; + TestERC20 token0; + TestERC20 token1; + FeeTakingLite feeTakingLite = + FeeTakingLite(address(uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG))); + BaseMiddleware baseMiddleware = BaseMiddleware( + payable(address(uint160(0x10000000 | Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG))) + ); + PoolId id; + + function setUp() public { + deployFreshManagerAndRouters(); + (currency0, currency1) = deployMintAndApprove2Currencies(); + + router = new HookEnabledSwapRouter(manager); + token0 = TestERC20(Currency.unwrap(currency0)); + token1 = TestERC20(Currency.unwrap(currency1)); + + console.logAddress(address(manager)); + + vm.record(); + FeeTakingLiteImplementation impl0 = new FeeTakingLiteImplementation(manager, feeTakingLite); + (, bytes32[] memory writes0) = vm.accesses(address(impl0)); + vm.etch(address(feeTakingLite), address(impl0).code); + // for each storage key that was written during the hook implementation, copy the value over + unchecked { + for (uint256 i = 0; i < writes0.length; i++) { + bytes32 slot = writes0[i]; + vm.store(address(feeTakingLite), slot, vm.load(address(impl0), slot)); + } + } + + vm.record(); + BaseMiddlewareImplementation impl = + new BaseMiddlewareImplementation(manager, address(feeTakingLite), baseMiddleware); + (, bytes32[] memory writes) = vm.accesses(address(impl)); + vm.etch(address(baseMiddleware), address(impl).code); + // for each storage key that was written during the hook implementation, copy the value over + unchecked { + for (uint256 i = 0; i < writes.length; i++) { + bytes32 slot = writes[i]; + vm.store(address(baseMiddleware), slot, vm.load(address(impl), slot)); + } + } + + // key = PoolKey(currency0, currency1, 3000, 60, baseMiddleware); + (key, id) = initPoolAndAddLiquidity( + currency0, currency1, IHooks(address(baseMiddleware)), 3000, SQRT_PRICE_1_1, ZERO_BYTES + ); + + token0.approve(address(baseMiddleware), type(uint256).max); + token1.approve(address(baseMiddleware), type(uint256).max); + token0.approve(address(router), type(uint256).max); + token1.approve(address(router), type(uint256).max); + } + + function testNormal() public { + console.log("before"); + // Swap exact token0 for token1 // + bool zeroForOne = true; + int256 amountSpecified = -1e12; + BalanceDelta swapDelta = swap(key, zeroForOne, amountSpecified, ZERO_BYTES); + // ---------------------------- // + + uint128 output = uint128(swapDelta.amount1()); + assertTrue(output > 0); + + console.log(output); + + uint256 expectedFee = calculateFeeForExactInput(output, 25); + + assertEq(manager.balanceOf(address(baseMiddleware), CurrencyLibrary.toId(key.currency0)), 0); + assertEq(manager.balanceOf(address(baseMiddleware), CurrencyLibrary.toId(key.currency1)), expectedFee); + + // // Swap token0 for exact token1 // + // bool zeroForOne2 = true; + // int256 amountSpecified2 = 1e12; // positive number indicates exact output swap + // BalanceDelta swapDelta2 = swap(key, zeroForOne2, amountSpecified2, ZERO_BYTES); + // // ---------------------------- // + + // uint128 input = uint128(-swapDelta2.amount0()); + // assertTrue(output > 0); + + // uint256 expectedFee2 = calculateFeeForExactOutput(input, 25); + + // assertEq(manager.balanceOf(address(baseMiddleware), CurrencyLibrary.toId(key.currency0)), expectedFee2); + // assertEq(manager.balanceOf(address(baseMiddleware), CurrencyLibrary.toId(key.currency1)), expectedFee); + + // // test withdrawing tokens // + // Currency[] memory currencies = new Currency[](2); + // currencies[0] = key.currency0; + // currencies[1] = key.currency1; + // feeTakingLite.withdraw(TREASURY, currencies); + // assertEq(manager.balanceOf(address(baseMiddleware), CurrencyLibrary.toId(key.currency0)), 0); + // assertEq(manager.balanceOf(address(baseMiddleware), CurrencyLibrary.toId(key.currency1)), 0); + // assertEq(currency0.balanceOf(TREASURY), expectedFee2); + // assertEq(currency1.balanceOf(TREASURY), expectedFee); + } + + function calculateFeeForExactInput(uint256 outputAmount, uint128 feeBips) internal pure returns (uint256) { + return outputAmount * TOTAL_BIPS / (TOTAL_BIPS - feeBips) - outputAmount; + } + + function calculateFeeForExactOutput(uint256 inputAmount, uint128 feeBips) internal pure returns (uint256) { + return (inputAmount * feeBips) / (TOTAL_BIPS + feeBips); + } +} diff --git a/test/shared/implementation/BaseMiddlewareImplementation.sol b/test/shared/implementation/BaseMiddlewareImplementation.sol new file mode 100644 index 00000000..0f8628bd --- /dev/null +++ b/test/shared/implementation/BaseMiddlewareImplementation.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {BaseHook} from "../../../contracts/BaseHook.sol"; +import {BaseMiddleware} from "../../../contracts/hooks/examples/BaseMiddleware.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; + +contract BaseMiddlewareImplementation is BaseMiddleware { + constructor(IPoolManager _poolManager, address _implementation, BaseMiddleware addressToEtch) + BaseMiddleware(_poolManager, _implementation) + { + //Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); + } + + // make this a no-op in testing + //function validateHookAddress(BaseHook _this) internal pure override {} +} diff --git a/test/shared/implementation/FeeTakingLiteImplementation.sol b/test/shared/implementation/FeeTakingLiteImplementation.sol new file mode 100644 index 00000000..b6048705 --- /dev/null +++ b/test/shared/implementation/FeeTakingLiteImplementation.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {BaseHook} from "../../../contracts/BaseHook.sol"; +import {FeeTakingLite} from "../../../contracts/hooks/examples/FeeTakingLite.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; + +contract FeeTakingLiteImplementation is FeeTakingLite { + constructor(IPoolManager _poolManager, FeeTakingLite addressToEtch) FeeTakingLite(_poolManager) { + //Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); + } + + // make this a no-op in testing + //function validateHookAddress(BaseHook _this) internal pure override {} +} From 608068dc5e72da38fc932d85c2d6a26721584f39 Mon Sep 17 00:00:00 2001 From: Junion Date: Mon, 17 Jun 2024 23:10:04 -0400 Subject: [PATCH 10/25] middleware for safely removing liquidity --- .../hooks/examples/BaseMiddlewareDefault.txt | 65 +++++++++ contracts/hooks/examples/RemoveMiddleware.sol | 45 ++++++ .../{examples => test}/FeeTakingLite.sol | 2 - contracts/hooks/test/HooksOutOfGas.sol | 129 +++++++++++++++++ contracts/hooks/test/HooksRevert.sol | 125 ++++++++++++++++ test/BaseMiddleware.t.sol | 27 +--- test/RemoveMiddleware.t.sol | 89 ++++++++++++ test/RemoveMiddleware.txt | 136 ++++++++++++++++++ .../FeeTakingLiteImplementation.sol | 2 +- .../RemoveMiddlewareImplementation.sol | 19 +++ 10 files changed, 613 insertions(+), 26 deletions(-) create mode 100644 contracts/hooks/examples/BaseMiddlewareDefault.txt create mode 100644 contracts/hooks/examples/RemoveMiddleware.sol rename contracts/hooks/{examples => test}/FeeTakingLite.sol (98%) create mode 100644 contracts/hooks/test/HooksOutOfGas.sol create mode 100644 contracts/hooks/test/HooksRevert.sol create mode 100644 test/RemoveMiddleware.t.sol create mode 100644 test/RemoveMiddleware.txt create mode 100644 test/shared/implementation/RemoveMiddlewareImplementation.sol diff --git a/contracts/hooks/examples/BaseMiddlewareDefault.txt b/contracts/hooks/examples/BaseMiddlewareDefault.txt new file mode 100644 index 00000000..046c1079 --- /dev/null +++ b/contracts/hooks/examples/BaseMiddlewareDefault.txt @@ -0,0 +1,65 @@ +// 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"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; +import {Proxy} from "@openzeppelin/contracts/proxy/Proxy.sol"; +import {console} from "../../../lib/forge-std/src/console.sol"; + +contract Middleware is IHooks{ + IPoolManager public immutable poolManager; + Hooks.Permissions private permissions; + IHooks public immutable implementation; + + constructor(IPoolManager _poolManager, address _implementation, uint160 _flags) { + poolManager = _poolManager; + permissions = IHooks(_implementation).getHookPermissions(); + implementation = IHooks(_implementation); + _flags = _flags; + } + + function getHookPermissions() public view returns (Hooks.Permissions memory) { + return permissions; + } + + modifier nonReentrantBefore() private { + if (_status == ENTERED) { + revert ActionBetweenHook(); + } + _status = ENTERED; + _; + } + + modifier nonReentrantAfter() private { + _; + _status = NOT_ENTERED; + } + + function beforeSwap(address sender, PoolKey calldata key, IPoolManager.SwapParams calldata params, bytes calldata hookData) + nonReentrantBefore + external + returns (bytes4, BeforeSwapDelta, uint24) + { + try implementation.beforeSwap(sender, key, params, hookData) returns ( + bytes4 selector, + BeforeSwapDelta memory beforeSwapDelta, + uint24 lpFeeOverride + ) { + + return (selector, beforeSwapDelta, lpFeeOverride); + } catch { + return (defaultSelector, defaultBeforeSwapDelta, defaultLpFeeOverride); + } + } + + function afterSwap(...) nonReentrantAfter { try catch... } + function beforeAddLiquidity(...) nonReentrantBefore { try catch... } + function afterAddLiquidity(...) nonReentrantAfter { try catch... } + function beforeRemoveLiquidity(...) nonReentrantBefore { try catch... } + function afterRemoveLiquidity(...) nonReentrantAfter { try catch... } + // who cares about donate lol +} diff --git a/contracts/hooks/examples/RemoveMiddleware.sol b/contracts/hooks/examples/RemoveMiddleware.sol new file mode 100644 index 00000000..3fd1ab09 --- /dev/null +++ b/contracts/hooks/examples/RemoveMiddleware.sol @@ -0,0 +1,45 @@ +// 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"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; +import {Proxy} from "@openzeppelin/contracts/proxy/Proxy.sol"; +import {BaseMiddleware} from "./BaseMiddleware.sol"; +import {BaseHook} from "../../BaseHook.sol"; +import {console} from "../../../lib/forge-std/src/console.sol"; + +contract RemoveMiddleware is BaseMiddleware { + bytes internal constant ZERO_BYTES = bytes(""); + uint256 public constant gasLimit = 100000; + + constructor(IPoolManager _poolManager, address _impl) BaseMiddleware(_poolManager, _impl) {} + + function beforeRemoveLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external returns (bytes4) { + console.log("beforeRemoveLiquidity middleware"); + (bool success, bytes memory returnData) = implementation.delegatecall{gas: gasLimit}(msg.data); + console.log(success); + return BaseHook.beforeRemoveLiquidity.selector; + } + + function afterRemoveLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + BalanceDelta, + bytes calldata + ) external returns (bytes4) { + console.log("afterRemoveLiquidity middleware"); + (bool success, bytes memory returnData) = implementation.delegatecall{gas: gasLimit}(msg.data); + console.log(success); + return BaseHook.afterRemoveLiquidity.selector; + } +} \ No newline at end of file diff --git a/contracts/hooks/examples/FeeTakingLite.sol b/contracts/hooks/test/FeeTakingLite.sol similarity index 98% rename from contracts/hooks/examples/FeeTakingLite.sol rename to contracts/hooks/test/FeeTakingLite.sol index 05a83444..e4195ff2 100644 --- a/contracts/hooks/examples/FeeTakingLite.sol +++ b/contracts/hooks/test/FeeTakingLite.sol @@ -57,7 +57,6 @@ contract FeeTakingLite is IUnlockCallback { BalanceDelta delta, bytes calldata ) external returns (bytes4, int128) { - console.log("afterSwap"); // fee will be in the unspecified token of the swap bool currency0Specified = (params.amountSpecified < 0 == params.zeroForOne); (Currency feeCurrency, int128 swapAmount) = @@ -66,7 +65,6 @@ contract FeeTakingLite is IUnlockCallback { if (swapAmount < 0) swapAmount = -swapAmount; uint256 feeAmount = (uint128(swapAmount) * swapFeeBips) / TOTAL_BIPS; - console.log(swapFeeBips); // mint ERC6909 instead of take to avoid edge case where PM doesn't have enough balance poolManager.mint(address(this), CurrencyLibrary.toId(feeCurrency), feeAmount); diff --git a/contracts/hooks/test/HooksOutOfGas.sol b/contracts/hooks/test/HooksOutOfGas.sol new file mode 100644 index 00000000..db441bfe --- /dev/null +++ b/contracts/hooks/test/HooksOutOfGas.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {BaseHook} from "../../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 {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; +import {Owned} from "solmate/auth/Owned.sol"; +import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; +import {console} from "../../../lib/forge-std/src/console.sol"; + +contract HooksOutOfGas { + IPoolManager public immutable poolManager; + + constructor(IPoolManager _poolManager) { + poolManager = _poolManager; + } + + function getHookPermissions() public pure 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, uint160, bytes calldata) external virtual returns (bytes4) { + consumeAllGas(); + } + + function afterInitialize(address, PoolKey calldata, uint160, int24, bytes calldata) + external + virtual + returns (bytes4) + { + consumeAllGas(); + } + + function beforeAddLiquidity(address, PoolKey calldata, IPoolManager.ModifyLiquidityParams calldata, bytes calldata) + external + virtual + returns (bytes4) + { + consumeAllGas(); + } + + function beforeRemoveLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external returns (bytes4) { + consumeAllGas(); + } + + function afterAddLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + BalanceDelta, + bytes calldata + ) external virtual returns (bytes4, BalanceDelta) { + consumeAllGas(); + } + + function afterRemoveLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + BalanceDelta, + bytes calldata + ) external returns (bytes4, BalanceDelta) { + consumeAllGas(); + } + + function beforeSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, bytes calldata) + external + virtual + returns (bytes4, BeforeSwapDelta, uint24) + { + consumeAllGas(); + } + + function afterSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, BalanceDelta, bytes calldata) + external + virtual + returns (bytes4, int128) + { + consumeAllGas(); + } + + function beforeDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) + external + virtual + returns (bytes4) + { + consumeAllGas(); + } + + function afterDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) + external + virtual + returns (bytes4) + { + consumeAllGas(); + } + + function consumeAllGas() internal view { + while (true) { + //console.log(gasleft()); + // This loop will run indefinitely and consume all available gas. + } + } +} diff --git a/contracts/hooks/test/HooksRevert.sol b/contracts/hooks/test/HooksRevert.sol new file mode 100644 index 00000000..bb125d3a --- /dev/null +++ b/contracts/hooks/test/HooksRevert.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {BaseHook} from "../../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 {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; +import {Owned} from "solmate/auth/Owned.sol"; +import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; +import {console} from "../../../lib/forge-std/src/console.sol"; + +contract HooksRevert { + error HookNotImplemented(); + + IPoolManager public immutable poolManager; + + constructor(IPoolManager _poolManager) { + poolManager = _poolManager; + } + + function getHookPermissions() public pure 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, 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 beforeAddLiquidity(address, PoolKey calldata, IPoolManager.ModifyLiquidityParams calldata, bytes calldata) + external + virtual + returns (bytes4) + { + revert HookNotImplemented(); + } + + function beforeRemoveLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external returns (bytes4) { + revert HookNotImplemented(); + } + + function afterAddLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + BalanceDelta, + bytes calldata + ) external virtual returns (bytes4, BalanceDelta) { + revert HookNotImplemented(); + } + + function afterRemoveLiquidity( + address sender, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + BalanceDelta, + bytes calldata + ) external returns (bytes4, BalanceDelta) { + require(sender == address(0), "nobody can remove"); + return (BaseHook.beforeRemoveLiquidity.selector, toBalanceDelta(0, 0)); + } + + function beforeSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, bytes calldata) + external + virtual + returns (bytes4, BeforeSwapDelta, uint24) + { + revert HookNotImplemented(); + } + + function afterSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, BalanceDelta, bytes calldata) + external + virtual + returns (bytes4, int128) + { + 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(); + } +} diff --git a/test/BaseMiddleware.t.sol b/test/BaseMiddleware.t.sol index 08cc960f..1ffb08d2 100644 --- a/test/BaseMiddleware.t.sol +++ b/test/BaseMiddleware.t.sol @@ -3,8 +3,7 @@ pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {FeeTakingLite} from "../contracts/hooks/examples/FeeTakingLite.sol"; -import {FeeTakingLiteImplementation} from "./shared/implementation/FeeTakingLiteImplementation.sol"; +import {FeeTakingLite} from "../contracts/hooks/test/FeeTakingLite.sol"; import {BaseMiddleware} from "../contracts/hooks/examples/BaseMiddleware.sol"; import {BaseMiddlewareImplementation} from "./shared/implementation/BaseMiddlewareImplementation.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; @@ -32,11 +31,8 @@ contract BaseMiddlewareTest is Test, Deployers { HookEnabledSwapRouter router; TestERC20 token0; TestERC20 token1; - FeeTakingLite feeTakingLite = - FeeTakingLite(address(uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG))); - BaseMiddleware baseMiddleware = BaseMiddleware( - payable(address(uint160(0x10000000 | Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG))) - ); + BaseMiddleware baseMiddleware = + BaseMiddleware(payable(address(uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG)))); PoolId id; function setUp() public { @@ -47,19 +43,7 @@ contract BaseMiddlewareTest is Test, Deployers { token0 = TestERC20(Currency.unwrap(currency0)); token1 = TestERC20(Currency.unwrap(currency1)); - console.logAddress(address(manager)); - - vm.record(); - FeeTakingLiteImplementation impl0 = new FeeTakingLiteImplementation(manager, feeTakingLite); - (, bytes32[] memory writes0) = vm.accesses(address(impl0)); - vm.etch(address(feeTakingLite), address(impl0).code); - // for each storage key that was written during the hook implementation, copy the value over - unchecked { - for (uint256 i = 0; i < writes0.length; i++) { - bytes32 slot = writes0[i]; - vm.store(address(feeTakingLite), slot, vm.load(address(impl0), slot)); - } - } + FeeTakingLite feeTakingLite = new FeeTakingLite(manager); vm.record(); BaseMiddlewareImplementation impl = @@ -86,7 +70,6 @@ contract BaseMiddlewareTest is Test, Deployers { } function testNormal() public { - console.log("before"); // Swap exact token0 for token1 // bool zeroForOne = true; int256 amountSpecified = -1e12; @@ -96,8 +79,6 @@ contract BaseMiddlewareTest is Test, Deployers { uint128 output = uint128(swapDelta.amount1()); assertTrue(output > 0); - console.log(output); - uint256 expectedFee = calculateFeeForExactInput(output, 25); assertEq(manager.balanceOf(address(baseMiddleware), CurrencyLibrary.toId(key.currency0)), 0); diff --git a/test/RemoveMiddleware.t.sol b/test/RemoveMiddleware.t.sol new file mode 100644 index 00000000..8fe94834 --- /dev/null +++ b/test/RemoveMiddleware.t.sol @@ -0,0 +1,89 @@ +// 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 {FeeTakingLite} from "../contracts/hooks/test/FeeTakingLite.sol"; +import {RemoveMiddleware} from "../contracts/hooks/examples/RemoveMiddleware.sol"; +import {RemoveMiddlewareImplementation} from "./shared/implementation/RemoveMiddlewareImplementation.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.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 {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.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 {HooksRevert} from "../contracts/hooks/test/HooksRevert.sol"; +import {HooksOutOfGas} from "../contracts/hooks/test/HooksOutOfGas.sol"; + +contract RemoveMiddlewareTest is Test, Deployers { + using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; + + uint160 constant SQRT_RATIO_10_1 = 250541448375047931186413801569; + + address constant TREASURY = address(0x1234567890123456789012345678901234567890); + uint128 private constant TOTAL_BIPS = 10000; + + HookEnabledSwapRouter router; + TestERC20 token0; + TestERC20 token1; + PoolId id; + + uint160 nonce = 0; + + function setUp() public { + deployFreshManagerAndRouters(); + (currency0, currency1) = deployMintAndApprove2Currencies(); + + router = new HookEnabledSwapRouter(manager); + token0 = TestERC20(Currency.unwrap(currency0)); + token1 = TestERC20(Currency.unwrap(currency1)); + + token0.approve(address(router), type(uint256).max); + token1.approve(address(router), type(uint256).max); + } + + function testVarious() public { + FeeTakingLite feeTakingLite = new FeeTakingLite(manager); + testOn( + address(feeTakingLite), + uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG) + ); + HooksRevert hooksRevert = new HooksRevert(manager); + testOn(address(hooksRevert), uint160(Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG)); + testOn(address(hooksRevert), uint160(Hooks.AFTER_REMOVE_LIQUIDITY_FLAG)); + testOn(address(hooksRevert), uint160(Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG)); + HooksOutOfGas hooksOutOfGas = new HooksOutOfGas(manager); + testOn(address(hooksOutOfGas), uint160(Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG)); + testOn(address(hooksOutOfGas), uint160(Hooks.AFTER_REMOVE_LIQUIDITY_FLAG)); + testOn(address(hooksOutOfGas), uint160(Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG)); + } + + // creates a middleware on an implementation + function testOn(address implementation, uint160 hooks) internal { + RemoveMiddleware removeMiddleware = RemoveMiddleware(payable(address(nonce << 20 | hooks))); + nonce++; + vm.record(); + RemoveMiddlewareImplementation impl = + new RemoveMiddlewareImplementation(manager, implementation, removeMiddleware); + (, bytes32[] memory writes) = vm.accesses(address(impl)); + vm.etch(address(removeMiddleware), address(impl).code); + unchecked { + for (uint256 i = 0; i < writes.length; i++) { + bytes32 slot = writes[i]; + vm.store(address(removeMiddleware), slot, vm.load(address(impl), slot)); + } + } + (key, id) = initPoolAndAddLiquidity( + currency0, currency1, IHooks(address(removeMiddleware)), 3000, SQRT_PRICE_1_1, ZERO_BYTES + ); + + removeLiquidity(currency0, currency1, IHooks(address(removeMiddleware)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + } +} diff --git a/test/RemoveMiddleware.txt b/test/RemoveMiddleware.txt new file mode 100644 index 00000000..f335176f --- /dev/null +++ b/test/RemoveMiddleware.txt @@ -0,0 +1,136 @@ +// 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 {FeeTakingLite} from "../contracts/hooks/test/FeeTakingLite.sol"; +import {HooksRevert} from "../contracts/hooks/test/HooksRevert.sol"; +import {RemoveMiddleware} from "../contracts/hooks/examples/RemoveMiddleware.sol"; +import {RemoveMiddlewareImplementation} from "./shared/implementation/RemoveMiddlewareImplementation.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.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 {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.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"; + +contract RemoveMiddlewareTest is Test, Deployers { + using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; + + uint160 constant SQRT_RATIO_10_1 = 250541448375047931186413801569; + + address constant TREASURY = address(0x1234567890123456789012345678901234567890); + uint128 private constant TOTAL_BIPS = 10000; + + HookEnabledSwapRouter router; + TestERC20 token0; + TestERC20 token1; + RemoveMiddleware removeMiddleware = + RemoveMiddleware(payable(address(uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG)))); + PoolId id; + + function setUp() public { + deployFreshManagerAndRouters(); + (currency0, currency1) = deployMintAndApprove2Currencies(); + + router = new HookEnabledSwapRouter(manager); + token0 = TestERC20(Currency.unwrap(currency0)); + token1 = TestERC20(Currency.unwrap(currency1)); + + FeeTakingLite feeTakingLite = new FeeTakingLite(manager); + HooksRevert hooksRevert = new HooksRevert(manager); + + vm.record(); + RemoveMiddlewareImplementation impl = + new RemoveMiddlewareImplementation(manager, address(feeTakingLite), removeMiddleware); + (, bytes32[] memory writes) = vm.accesses(address(impl)); + vm.etch(address(removeMiddleware), address(impl).code); + // for each storage key that was written during the hook implementation, copy the value over + unchecked { + for (uint256 i = 0; i < writes.length; i++) { + bytes32 slot = writes[i]; + vm.store(address(removeMiddleware), slot, vm.load(address(impl), slot)); + } + } + + // key = PoolKey(currency0, currency1, 3000, 60, removeMiddleware); + (key, id) = initPoolAndAddLiquidity( + currency0, currency1, IHooks(address(removeMiddleware)), 3000, SQRT_PRICE_1_1, ZERO_BYTES + ); + + token0.approve(address(router), type(uint256).max); + token1.approve(address(router), type(uint256).max); + } + + function testRemoveMiddleware() public { + console.log("testRemoveMiddleware"); + console.log("testRemoveMiddleware"); + console.log("testRemoveMiddleware"); + removeLiquidity(currency0, currency1, IHooks(address(removeMiddleware)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + } + + // test normal behavior, copied from FeeTaking.t.sol + function testNormal() public { + // Swap exact token0 for token1 // + bool zeroForOne = true; + int256 amountSpecified = -1e12; + BalanceDelta swapDelta = swap(key, zeroForOne, amountSpecified, ZERO_BYTES); + // ---------------------------- // + + uint128 output = uint128(swapDelta.amount1()); + assertTrue(output > 0); + + uint256 expectedFee = calculateFeeForExactInput(output, 25); + + assertEq(manager.balanceOf(address(removeMiddleware), CurrencyLibrary.toId(key.currency0)), 0); + assertEq(manager.balanceOf(address(removeMiddleware), CurrencyLibrary.toId(key.currency1)), expectedFee); + + // Swap token0 for exact token1 // + bool zeroForOne2 = true; + int256 amountSpecified2 = 1e12; // positive number indicates exact output swap + BalanceDelta swapDelta2 = swap(key, zeroForOne2, amountSpecified2, ZERO_BYTES); + // ---------------------------- // + + uint128 input = uint128(-swapDelta2.amount0()); + assertTrue(output > 0); + + uint256 expectedFee2 = calculateFeeForExactOutput(input, 25); + + assertEq(manager.balanceOf(address(removeMiddleware), CurrencyLibrary.toId(key.currency0)), expectedFee2); + assertEq(manager.balanceOf(address(removeMiddleware), CurrencyLibrary.toId(key.currency1)), expectedFee); + + // test withdrawing tokens // + Currency[] memory currencies = new Currency[](2); + currencies[0] = key.currency0; + currencies[1] = key.currency1; + FeeTakingLite impl = FeeTakingLite(address(removeMiddleware)); + impl.withdraw(TREASURY, currencies); + assertEq(manager.balanceOf(address(removeMiddleware), CurrencyLibrary.toId(key.currency0)), 0); + assertEq(manager.balanceOf(address(removeMiddleware), CurrencyLibrary.toId(key.currency1)), 0); + assertEq(currency0.balanceOf(TREASURY), expectedFee2); + assertEq(currency1.balanceOf(TREASURY), expectedFee); + } + + function calculateFeeForExactInput(uint256 outputAmount, uint128 feeBips) internal pure returns (uint256) { + return outputAmount * TOTAL_BIPS / (TOTAL_BIPS - feeBips) - outputAmount; + } + + function calculateFeeForExactOutput(uint256 inputAmount, uint128 feeBips) internal pure returns (uint256) { + return (inputAmount * feeBips) / (TOTAL_BIPS + feeBips); + } + + function testVarious() public { + + } + + function testOn(address impl) public { + + } +} diff --git a/test/shared/implementation/FeeTakingLiteImplementation.sol b/test/shared/implementation/FeeTakingLiteImplementation.sol index b6048705..63163d50 100644 --- a/test/shared/implementation/FeeTakingLiteImplementation.sol +++ b/test/shared/implementation/FeeTakingLiteImplementation.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.19; import {BaseHook} from "../../../contracts/BaseHook.sol"; -import {FeeTakingLite} from "../../../contracts/hooks/examples/FeeTakingLite.sol"; +import {FeeTakingLite} from "../../../contracts/hooks/test/FeeTakingLite.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; diff --git a/test/shared/implementation/RemoveMiddlewareImplementation.sol b/test/shared/implementation/RemoveMiddlewareImplementation.sol new file mode 100644 index 00000000..0e7f43e5 --- /dev/null +++ b/test/shared/implementation/RemoveMiddlewareImplementation.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {BaseHook} from "../../../contracts/BaseHook.sol"; +import {RemoveMiddleware} from "../../../contracts/hooks/examples/RemoveMiddleware.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; + +contract RemoveMiddlewareImplementation is RemoveMiddleware { + constructor(IPoolManager _poolManager, address _implementation, RemoveMiddleware addressToEtch) + RemoveMiddleware(_poolManager, _implementation) + { + //Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); + } + + // make this a no-op in testing + //function validateHookAddress(BaseHook _this) internal pure override {} +} From ba1ca963c0ca9bb4fad8330f5c3145889ec524a1 Mon Sep 17 00:00:00 2001 From: Junion Date: Mon, 17 Jun 2024 23:12:47 -0400 Subject: [PATCH 11/25] name change --- ...oveMiddleware.sol => MiddlewareRemove.sol} | 2 +- test/BaseMiddleware.t.sol | 48 ++++++++++--------- ...iddleware.t.sol => MiddlewareRemove.t.sol} | 20 ++++---- test/RemoveMiddleware.txt | 46 +++++++++--------- ...sol => MiddlewareRemoveImplementation.sol} | 8 ++-- 5 files changed, 63 insertions(+), 61 deletions(-) rename contracts/hooks/examples/{RemoveMiddleware.sol => MiddlewareRemove.sol} (97%) rename test/{RemoveMiddleware.t.sol => MiddlewareRemove.t.sol} (82%) rename test/shared/implementation/{RemoveMiddlewareImplementation.sol => MiddlewareRemoveImplementation.sol} (71%) diff --git a/contracts/hooks/examples/RemoveMiddleware.sol b/contracts/hooks/examples/MiddlewareRemove.sol similarity index 97% rename from contracts/hooks/examples/RemoveMiddleware.sol rename to contracts/hooks/examples/MiddlewareRemove.sol index 3fd1ab09..c8b8ebad 100644 --- a/contracts/hooks/examples/RemoveMiddleware.sol +++ b/contracts/hooks/examples/MiddlewareRemove.sol @@ -12,7 +12,7 @@ import {BaseMiddleware} from "./BaseMiddleware.sol"; import {BaseHook} from "../../BaseHook.sol"; import {console} from "../../../lib/forge-std/src/console.sol"; -contract RemoveMiddleware is BaseMiddleware { +contract MiddlewareRemove is BaseMiddleware { bytes internal constant ZERO_BYTES = bytes(""); uint256 public constant gasLimit = 100000; diff --git a/test/BaseMiddleware.t.sol b/test/BaseMiddleware.t.sol index 1ffb08d2..d0ab9db3 100644 --- a/test/BaseMiddleware.t.sol +++ b/test/BaseMiddleware.t.sol @@ -69,6 +69,7 @@ contract BaseMiddlewareTest is Test, Deployers { token1.approve(address(router), type(uint256).max); } + // test normal behavior, copied from FeeTaking.t.sol function testNormal() public { // Swap exact token0 for token1 // bool zeroForOne = true; @@ -84,29 +85,30 @@ contract BaseMiddlewareTest is Test, Deployers { assertEq(manager.balanceOf(address(baseMiddleware), CurrencyLibrary.toId(key.currency0)), 0); assertEq(manager.balanceOf(address(baseMiddleware), CurrencyLibrary.toId(key.currency1)), expectedFee); - // // Swap token0 for exact token1 // - // bool zeroForOne2 = true; - // int256 amountSpecified2 = 1e12; // positive number indicates exact output swap - // BalanceDelta swapDelta2 = swap(key, zeroForOne2, amountSpecified2, ZERO_BYTES); - // // ---------------------------- // - - // uint128 input = uint128(-swapDelta2.amount0()); - // assertTrue(output > 0); - - // uint256 expectedFee2 = calculateFeeForExactOutput(input, 25); - - // assertEq(manager.balanceOf(address(baseMiddleware), CurrencyLibrary.toId(key.currency0)), expectedFee2); - // assertEq(manager.balanceOf(address(baseMiddleware), CurrencyLibrary.toId(key.currency1)), expectedFee); - - // // test withdrawing tokens // - // Currency[] memory currencies = new Currency[](2); - // currencies[0] = key.currency0; - // currencies[1] = key.currency1; - // feeTakingLite.withdraw(TREASURY, currencies); - // assertEq(manager.balanceOf(address(baseMiddleware), CurrencyLibrary.toId(key.currency0)), 0); - // assertEq(manager.balanceOf(address(baseMiddleware), CurrencyLibrary.toId(key.currency1)), 0); - // assertEq(currency0.balanceOf(TREASURY), expectedFee2); - // assertEq(currency1.balanceOf(TREASURY), expectedFee); + // Swap token0 for exact token1 // + bool zeroForOne2 = true; + int256 amountSpecified2 = 1e12; // positive number indicates exact output swap + BalanceDelta swapDelta2 = swap(key, zeroForOne2, amountSpecified2, ZERO_BYTES); + // ---------------------------- // + + uint128 input = uint128(-swapDelta2.amount0()); + assertTrue(output > 0); + + uint256 expectedFee2 = calculateFeeForExactOutput(input, 25); + + assertEq(manager.balanceOf(address(baseMiddleware), CurrencyLibrary.toId(key.currency0)), expectedFee2); + assertEq(manager.balanceOf(address(baseMiddleware), CurrencyLibrary.toId(key.currency1)), expectedFee); + + // test withdrawing tokens // + Currency[] memory currencies = new Currency[](2); + currencies[0] = key.currency0; + currencies[1] = key.currency1; + FeeTakingLite impl = FeeTakingLite(address(baseMiddleware)); + impl.withdraw(TREASURY, currencies); + assertEq(manager.balanceOf(address(baseMiddleware), CurrencyLibrary.toId(key.currency0)), 0); + assertEq(manager.balanceOf(address(baseMiddleware), CurrencyLibrary.toId(key.currency1)), 0); + assertEq(currency0.balanceOf(TREASURY), expectedFee2); + assertEq(currency1.balanceOf(TREASURY), expectedFee); } function calculateFeeForExactInput(uint256 outputAmount, uint128 feeBips) internal pure returns (uint256) { diff --git a/test/RemoveMiddleware.t.sol b/test/MiddlewareRemove.t.sol similarity index 82% rename from test/RemoveMiddleware.t.sol rename to test/MiddlewareRemove.t.sol index 8fe94834..13ba403b 100644 --- a/test/RemoveMiddleware.t.sol +++ b/test/MiddlewareRemove.t.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {FeeTakingLite} from "../contracts/hooks/test/FeeTakingLite.sol"; -import {RemoveMiddleware} from "../contracts/hooks/examples/RemoveMiddleware.sol"; -import {RemoveMiddlewareImplementation} from "./shared/implementation/RemoveMiddlewareImplementation.sol"; +import {MiddlewareRemove} from "../contracts/hooks/examples/MiddlewareRemove.sol"; +import {MiddlewareRemoveImplementation} from "./shared/implementation/MiddlewareRemoveImplementation.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; @@ -21,7 +21,7 @@ import {console} from "../../../lib/forge-std/src/console.sol"; import {HooksRevert} from "../contracts/hooks/test/HooksRevert.sol"; import {HooksOutOfGas} from "../contracts/hooks/test/HooksOutOfGas.sol"; -contract RemoveMiddlewareTest is Test, Deployers { +contract MiddlewareRemoveTest is Test, Deployers { using PoolIdLibrary for PoolKey; using StateLibrary for IPoolManager; @@ -67,23 +67,23 @@ contract RemoveMiddlewareTest is Test, Deployers { // creates a middleware on an implementation function testOn(address implementation, uint160 hooks) internal { - RemoveMiddleware removeMiddleware = RemoveMiddleware(payable(address(nonce << 20 | hooks))); + MiddlewareRemove middlewareRemove = MiddlewareRemove(payable(address(nonce << 20 | hooks))); nonce++; vm.record(); - RemoveMiddlewareImplementation impl = - new RemoveMiddlewareImplementation(manager, implementation, removeMiddleware); + MiddlewareRemoveImplementation impl = + new MiddlewareRemoveImplementation(manager, implementation, middlewareRemove); (, bytes32[] memory writes) = vm.accesses(address(impl)); - vm.etch(address(removeMiddleware), address(impl).code); + vm.etch(address(middlewareRemove), address(impl).code); unchecked { for (uint256 i = 0; i < writes.length; i++) { bytes32 slot = writes[i]; - vm.store(address(removeMiddleware), slot, vm.load(address(impl), slot)); + vm.store(address(middlewareRemove), slot, vm.load(address(impl), slot)); } } (key, id) = initPoolAndAddLiquidity( - currency0, currency1, IHooks(address(removeMiddleware)), 3000, SQRT_PRICE_1_1, ZERO_BYTES + currency0, currency1, IHooks(address(middlewareRemove)), 3000, SQRT_PRICE_1_1, ZERO_BYTES ); - removeLiquidity(currency0, currency1, IHooks(address(removeMiddleware)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + removeLiquidity(currency0, currency1, IHooks(address(middlewareRemove)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); } } diff --git a/test/RemoveMiddleware.txt b/test/RemoveMiddleware.txt index f335176f..99d34afa 100644 --- a/test/RemoveMiddleware.txt +++ b/test/RemoveMiddleware.txt @@ -5,8 +5,8 @@ import {Test} from "forge-std/Test.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {FeeTakingLite} from "../contracts/hooks/test/FeeTakingLite.sol"; import {HooksRevert} from "../contracts/hooks/test/HooksRevert.sol"; -import {RemoveMiddleware} from "../contracts/hooks/examples/RemoveMiddleware.sol"; -import {RemoveMiddlewareImplementation} from "./shared/implementation/RemoveMiddlewareImplementation.sol"; +import {MiddlewareRemove} from "../contracts/hooks/examples/MiddlewareRemove.sol"; +import {MiddlewareRemoveImplementation} from "./shared/implementation/MiddlewareRemoveImplementation.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; @@ -20,7 +20,7 @@ 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"; -contract RemoveMiddlewareTest is Test, Deployers { +contract MiddlewareRemoveTest is Test, Deployers { using PoolIdLibrary for PoolKey; using StateLibrary for IPoolManager; @@ -32,8 +32,8 @@ contract RemoveMiddlewareTest is Test, Deployers { HookEnabledSwapRouter router; TestERC20 token0; TestERC20 token1; - RemoveMiddleware removeMiddleware = - RemoveMiddleware(payable(address(uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG)))); + MiddlewareRemove middlewareRemove = + MiddlewareRemove(payable(address(uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG)))); PoolId id; function setUp() public { @@ -48,32 +48,32 @@ contract RemoveMiddlewareTest is Test, Deployers { HooksRevert hooksRevert = new HooksRevert(manager); vm.record(); - RemoveMiddlewareImplementation impl = - new RemoveMiddlewareImplementation(manager, address(feeTakingLite), removeMiddleware); + MiddlewareRemoveImplementation impl = + new MiddlewareRemoveImplementation(manager, address(feeTakingLite), middlewareRemove); (, bytes32[] memory writes) = vm.accesses(address(impl)); - vm.etch(address(removeMiddleware), address(impl).code); + vm.etch(address(middlewareRemove), address(impl).code); // for each storage key that was written during the hook implementation, copy the value over unchecked { for (uint256 i = 0; i < writes.length; i++) { bytes32 slot = writes[i]; - vm.store(address(removeMiddleware), slot, vm.load(address(impl), slot)); + vm.store(address(middlewareRemove), slot, vm.load(address(impl), slot)); } } - // key = PoolKey(currency0, currency1, 3000, 60, removeMiddleware); + // key = PoolKey(currency0, currency1, 3000, 60, middlewareRemove); (key, id) = initPoolAndAddLiquidity( - currency0, currency1, IHooks(address(removeMiddleware)), 3000, SQRT_PRICE_1_1, ZERO_BYTES + currency0, currency1, IHooks(address(middlewareRemove)), 3000, SQRT_PRICE_1_1, ZERO_BYTES ); token0.approve(address(router), type(uint256).max); token1.approve(address(router), type(uint256).max); } - function testRemoveMiddleware() public { - console.log("testRemoveMiddleware"); - console.log("testRemoveMiddleware"); - console.log("testRemoveMiddleware"); - removeLiquidity(currency0, currency1, IHooks(address(removeMiddleware)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + function testMiddlewareRemove() public { + console.log("testMiddlewareRemove"); + console.log("testMiddlewareRemove"); + console.log("testMiddlewareRemove"); + removeLiquidity(currency0, currency1, IHooks(address(middlewareRemove)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); } // test normal behavior, copied from FeeTaking.t.sol @@ -89,8 +89,8 @@ contract RemoveMiddlewareTest is Test, Deployers { uint256 expectedFee = calculateFeeForExactInput(output, 25); - assertEq(manager.balanceOf(address(removeMiddleware), CurrencyLibrary.toId(key.currency0)), 0); - assertEq(manager.balanceOf(address(removeMiddleware), CurrencyLibrary.toId(key.currency1)), expectedFee); + assertEq(manager.balanceOf(address(middlewareRemove), CurrencyLibrary.toId(key.currency0)), 0); + assertEq(manager.balanceOf(address(middlewareRemove), CurrencyLibrary.toId(key.currency1)), expectedFee); // Swap token0 for exact token1 // bool zeroForOne2 = true; @@ -103,17 +103,17 @@ contract RemoveMiddlewareTest is Test, Deployers { uint256 expectedFee2 = calculateFeeForExactOutput(input, 25); - assertEq(manager.balanceOf(address(removeMiddleware), CurrencyLibrary.toId(key.currency0)), expectedFee2); - assertEq(manager.balanceOf(address(removeMiddleware), CurrencyLibrary.toId(key.currency1)), expectedFee); + assertEq(manager.balanceOf(address(middlewareRemove), CurrencyLibrary.toId(key.currency0)), expectedFee2); + assertEq(manager.balanceOf(address(middlewareRemove), CurrencyLibrary.toId(key.currency1)), expectedFee); // test withdrawing tokens // Currency[] memory currencies = new Currency[](2); currencies[0] = key.currency0; currencies[1] = key.currency1; - FeeTakingLite impl = FeeTakingLite(address(removeMiddleware)); + FeeTakingLite impl = FeeTakingLite(address(middlewareRemove)); impl.withdraw(TREASURY, currencies); - assertEq(manager.balanceOf(address(removeMiddleware), CurrencyLibrary.toId(key.currency0)), 0); - assertEq(manager.balanceOf(address(removeMiddleware), CurrencyLibrary.toId(key.currency1)), 0); + assertEq(manager.balanceOf(address(middlewareRemove), CurrencyLibrary.toId(key.currency0)), 0); + assertEq(manager.balanceOf(address(middlewareRemove), CurrencyLibrary.toId(key.currency1)), 0); assertEq(currency0.balanceOf(TREASURY), expectedFee2); assertEq(currency1.balanceOf(TREASURY), expectedFee); } diff --git a/test/shared/implementation/RemoveMiddlewareImplementation.sol b/test/shared/implementation/MiddlewareRemoveImplementation.sol similarity index 71% rename from test/shared/implementation/RemoveMiddlewareImplementation.sol rename to test/shared/implementation/MiddlewareRemoveImplementation.sol index 0e7f43e5..d3473768 100644 --- a/test/shared/implementation/RemoveMiddlewareImplementation.sol +++ b/test/shared/implementation/MiddlewareRemoveImplementation.sol @@ -2,14 +2,14 @@ pragma solidity ^0.8.19; import {BaseHook} from "../../../contracts/BaseHook.sol"; -import {RemoveMiddleware} from "../../../contracts/hooks/examples/RemoveMiddleware.sol"; +import {MiddlewareRemove} from "../../../contracts/hooks/examples/MiddlewareRemove.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -contract RemoveMiddlewareImplementation is RemoveMiddleware { - constructor(IPoolManager _poolManager, address _implementation, RemoveMiddleware addressToEtch) - RemoveMiddleware(_poolManager, _implementation) +contract MiddlewareRemoveImplementation is MiddlewareRemove { + constructor(IPoolManager _poolManager, address _implementation, MiddlewareRemove addressToEtch) + MiddlewareRemove(_poolManager, _implementation) { //Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); } From b56523610e24e2c46d0cc45f62ee8fa00ff754f4 Mon Sep 17 00:00:00 2001 From: Junion Date: Tue, 18 Jun 2024 12:34:49 -0400 Subject: [PATCH 12/25] rename files --- .../BaseMiddleware.sol | 0 .../BaseMiddlewareDefault.txt | 0 .../MiddlewareRemove.sol | 0 .../{ => middleware}/test/FeeTakingLite.sol | 4 +- .../{ => middleware}/test/HooksOutOfGas.sol | 4 +- .../{ => middleware}/test/HooksRevert.sol | 4 +- contracts/interfaces/IMiddlewareFactory.sol | 62 +++++++++++++++++++ test/BaseMiddleware.t.sol | 4 +- test/MiddlewareRemove.t.sol | 8 +-- test/RemoveMiddleware.txt | 6 +- .../BaseMiddlewareImplementation.sol | 2 +- .../FeeTakingLiteImplementation.sol | 2 +- .../MiddlewareRemoveImplementation.sol | 2 +- 13 files changed, 80 insertions(+), 18 deletions(-) rename contracts/hooks/{examples => middleware}/BaseMiddleware.sol (100%) rename contracts/hooks/{examples => middleware}/BaseMiddlewareDefault.txt (100%) rename contracts/hooks/{examples => middleware}/MiddlewareRemove.sol (100%) rename contracts/hooks/{ => middleware}/test/FeeTakingLite.sol (97%) rename contracts/hooks/{ => middleware}/test/HooksOutOfGas.sol (97%) rename contracts/hooks/{ => middleware}/test/HooksRevert.sol (97%) create mode 100644 contracts/interfaces/IMiddlewareFactory.sol diff --git a/contracts/hooks/examples/BaseMiddleware.sol b/contracts/hooks/middleware/BaseMiddleware.sol similarity index 100% rename from contracts/hooks/examples/BaseMiddleware.sol rename to contracts/hooks/middleware/BaseMiddleware.sol diff --git a/contracts/hooks/examples/BaseMiddlewareDefault.txt b/contracts/hooks/middleware/BaseMiddlewareDefault.txt similarity index 100% rename from contracts/hooks/examples/BaseMiddlewareDefault.txt rename to contracts/hooks/middleware/BaseMiddlewareDefault.txt diff --git a/contracts/hooks/examples/MiddlewareRemove.sol b/contracts/hooks/middleware/MiddlewareRemove.sol similarity index 100% rename from contracts/hooks/examples/MiddlewareRemove.sol rename to contracts/hooks/middleware/MiddlewareRemove.sol diff --git a/contracts/hooks/test/FeeTakingLite.sol b/contracts/hooks/middleware/test/FeeTakingLite.sol similarity index 97% rename from contracts/hooks/test/FeeTakingLite.sol rename to contracts/hooks/middleware/test/FeeTakingLite.sol index e4195ff2..d926b1a5 100644 --- a/contracts/hooks/test/FeeTakingLite.sol +++ b/contracts/hooks/middleware/test/FeeTakingLite.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {BaseHook} from "../../BaseHook.sol"; +import {BaseHook} from "../../../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"; @@ -11,7 +11,7 @@ import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {Owned} from "solmate/auth/Owned.sol"; import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; -import {console} from "../../../lib/forge-std/src/console.sol"; +import {console} from "../../../../lib/forge-std/src/console.sol"; contract FeeTakingLite is IUnlockCallback { using SafeCast for uint256; diff --git a/contracts/hooks/test/HooksOutOfGas.sol b/contracts/hooks/middleware/test/HooksOutOfGas.sol similarity index 97% rename from contracts/hooks/test/HooksOutOfGas.sol rename to contracts/hooks/middleware/test/HooksOutOfGas.sol index db441bfe..6550032f 100644 --- a/contracts/hooks/test/HooksOutOfGas.sol +++ b/contracts/hooks/middleware/test/HooksOutOfGas.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {BaseHook} from "../../BaseHook.sol"; +import {BaseHook} from "../../../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"; @@ -11,7 +11,7 @@ import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {Owned} from "solmate/auth/Owned.sol"; import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; -import {console} from "../../../lib/forge-std/src/console.sol"; +import {console} from "../../../../lib/forge-std/src/console.sol"; contract HooksOutOfGas { IPoolManager public immutable poolManager; diff --git a/contracts/hooks/test/HooksRevert.sol b/contracts/hooks/middleware/test/HooksRevert.sol similarity index 97% rename from contracts/hooks/test/HooksRevert.sol rename to contracts/hooks/middleware/test/HooksRevert.sol index bb125d3a..98efdf5a 100644 --- a/contracts/hooks/test/HooksRevert.sol +++ b/contracts/hooks/middleware/test/HooksRevert.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {BaseHook} from "../../BaseHook.sol"; +import {BaseHook} from "../../../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"; @@ -11,7 +11,7 @@ import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {Owned} from "solmate/auth/Owned.sol"; import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; -import {console} from "../../../lib/forge-std/src/console.sol"; +import {console} from "../../../../lib/forge-std/src/console.sol"; contract HooksRevert { error HookNotImplemented(); diff --git a/contracts/interfaces/IMiddlewareFactory.sol b/contracts/interfaces/IMiddlewareFactory.sol new file mode 100644 index 00000000..dfe4c61b --- /dev/null +++ b/contracts/interfaces/IMiddlewareFactory.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +interface IUniswapV3Factory { + event MiddlewareCreated( + address implementation, + address middleware + ); + + /// @notice Emitted when a new fee amount is enabled for middleware creation via the factory + /// @param fee The enabled fee, denominated in hundredths of a bip + /// @param tickSpacing The minimum number of ticks between initialized ticks for middlewares created with the given fee + event FeeAmountEnabled(uint24 indexed fee, int24 indexed tickSpacing); + + /// @notice Returns the current owner of the factory + /// @dev Can be changed by the current owner via setOwner + /// @return The address of the factory owner + function owner() external view returns (address); + + /// @notice Returns the tick spacing for a given fee amount, if enabled, or 0 if not enabled + /// @dev A fee amount can never be removed, so this value should be hard coded or cached in the calling context + /// @param fee The enabled fee, denominated in hundredths of a bip. Returns 0 in case of unenabled fee + /// @return The tick spacing + function feeAmountTickSpacing(uint24 fee) external view returns (int24); + + /// @notice Returns the middleware address for a given pair of tokens and a fee, or address 0 if it does not exist + /// @dev tokenA and tokenB may be passed in either token0/token1 or token1/token0 order + /// @param tokenA The contract address of either token0 or token1 + /// @param tokenB The contract address of the other token + /// @param fee The fee collected upon every swap in the middleware, denominated in hundredths of a bip + /// @return middleware The middleware address + function getMiddleware( + address tokenA, + address tokenB, + uint24 fee + ) external view returns (address middleware); + + /// @notice Creates a middleware for the given two tokens and fee + /// @param tokenA One of the two tokens in the desired middleware + /// @param tokenB The other of the two tokens in the desired middleware + /// @param fee The desired fee for the middleware + /// @dev tokenA and tokenB may be passed in either order: token0/token1 or token1/token0. tickSpacing is retrieved + /// from the fee. The call will revert if the middleware already exists, the fee is invalid, or the token arguments + /// are invalid. + /// @return middleware The address of the newly created middleware + function createMiddleware( + address tokenA, + address tokenB, + uint24 fee + ) external returns (address middleware); + + /// @notice Updates the owner of the factory + /// @dev Must be called by the current owner + /// @param _owner The new owner of the factory + function setOwner(address _owner) external; + + /// @notice Enables a fee amount with the given tickSpacing + /// @dev Fee amounts may never be removed once enabled + /// @param fee The fee amount to enable, denominated in hundredths of a bip (i.e. 1e-6) + /// @param tickSpacing The spacing between ticks to be enforced for all middlewares created with the given fee amount + function enableFeeAmount(uint24 fee, int24 tickSpacing) external; +} \ No newline at end of file diff --git a/test/BaseMiddleware.t.sol b/test/BaseMiddleware.t.sol index d0ab9db3..58863bfd 100644 --- a/test/BaseMiddleware.t.sol +++ b/test/BaseMiddleware.t.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {FeeTakingLite} from "../contracts/hooks/test/FeeTakingLite.sol"; -import {BaseMiddleware} from "../contracts/hooks/examples/BaseMiddleware.sol"; +import {FeeTakingLite} from "../contracts/hooks/middleware/test/FeeTakingLite.sol"; +import {BaseMiddleware} from "../contracts/hooks/middleware/BaseMiddleware.sol"; import {BaseMiddlewareImplementation} from "./shared/implementation/BaseMiddlewareImplementation.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; diff --git a/test/MiddlewareRemove.t.sol b/test/MiddlewareRemove.t.sol index 13ba403b..2dde78e8 100644 --- a/test/MiddlewareRemove.t.sol +++ b/test/MiddlewareRemove.t.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {FeeTakingLite} from "../contracts/hooks/test/FeeTakingLite.sol"; -import {MiddlewareRemove} from "../contracts/hooks/examples/MiddlewareRemove.sol"; +import {FeeTakingLite} from "../contracts/hooks/middleware/test/FeeTakingLite.sol"; +import {MiddlewareRemove} from "../contracts/hooks/middleware/MiddlewareRemove.sol"; import {MiddlewareRemoveImplementation} from "./shared/implementation/MiddlewareRemoveImplementation.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; @@ -18,8 +18,8 @@ import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.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 {HooksRevert} from "../contracts/hooks/test/HooksRevert.sol"; -import {HooksOutOfGas} from "../contracts/hooks/test/HooksOutOfGas.sol"; +import {HooksRevert} from "../contracts/hooks/middleware/test/HooksRevert.sol"; +import {HooksOutOfGas} from "../contracts/hooks/middleware/test/HooksOutOfGas.sol"; contract MiddlewareRemoveTest is Test, Deployers { using PoolIdLibrary for PoolKey; diff --git a/test/RemoveMiddleware.txt b/test/RemoveMiddleware.txt index 99d34afa..bfaaf25f 100644 --- a/test/RemoveMiddleware.txt +++ b/test/RemoveMiddleware.txt @@ -3,9 +3,9 @@ pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {FeeTakingLite} from "../contracts/hooks/test/FeeTakingLite.sol"; -import {HooksRevert} from "../contracts/hooks/test/HooksRevert.sol"; -import {MiddlewareRemove} from "../contracts/hooks/examples/MiddlewareRemove.sol"; +import {FeeTakingLite} from "../contracts/hooks/middleware/test/FeeTakingLite.sol"; +import {HooksRevert} from "../contracts/hooks/middleware/test/HooksRevert.sol"; +import {MiddlewareRemove} from "../contracts/hooks/middleware/MiddlewareRemove.sol"; import {MiddlewareRemoveImplementation} from "./shared/implementation/MiddlewareRemoveImplementation.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; diff --git a/test/shared/implementation/BaseMiddlewareImplementation.sol b/test/shared/implementation/BaseMiddlewareImplementation.sol index 0f8628bd..9e598630 100644 --- a/test/shared/implementation/BaseMiddlewareImplementation.sol +++ b/test/shared/implementation/BaseMiddlewareImplementation.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.19; import {BaseHook} from "../../../contracts/BaseHook.sol"; -import {BaseMiddleware} from "../../../contracts/hooks/examples/BaseMiddleware.sol"; +import {BaseMiddleware} from "../../../contracts/hooks/middleware/BaseMiddleware.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; diff --git a/test/shared/implementation/FeeTakingLiteImplementation.sol b/test/shared/implementation/FeeTakingLiteImplementation.sol index 63163d50..39742eb5 100644 --- a/test/shared/implementation/FeeTakingLiteImplementation.sol +++ b/test/shared/implementation/FeeTakingLiteImplementation.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.19; import {BaseHook} from "../../../contracts/BaseHook.sol"; -import {FeeTakingLite} from "../../../contracts/hooks/test/FeeTakingLite.sol"; +import {FeeTakingLite} from "../../../contracts/hooks/middleware/test/FeeTakingLite.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; diff --git a/test/shared/implementation/MiddlewareRemoveImplementation.sol b/test/shared/implementation/MiddlewareRemoveImplementation.sol index d3473768..38ab6dcf 100644 --- a/test/shared/implementation/MiddlewareRemoveImplementation.sol +++ b/test/shared/implementation/MiddlewareRemoveImplementation.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.19; import {BaseHook} from "../../../contracts/BaseHook.sol"; -import {MiddlewareRemove} from "../../../contracts/hooks/examples/MiddlewareRemove.sol"; +import {MiddlewareRemove} from "../../../contracts/hooks/middleware/MiddlewareRemove.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; From 12114cba70df55e11e4af0a2578f2e07dd5c3733 Mon Sep 17 00:00:00 2001 From: Junion Date: Tue, 18 Jun 2024 18:18:34 -0400 Subject: [PATCH 13/25] add MiddlewareProtect --- .../hooks/middleware/MiddlewareProtect.sol | 94 +++++++++++++++++++ .../hooks/middleware/MiddlewareRemove.sol | 4 +- 2 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 contracts/hooks/middleware/MiddlewareProtect.sol diff --git a/contracts/hooks/middleware/MiddlewareProtect.sol b/contracts/hooks/middleware/MiddlewareProtect.sol new file mode 100644 index 00000000..688fe536 --- /dev/null +++ b/contracts/hooks/middleware/MiddlewareProtect.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BaseMiddleware} from "./BaseMiddleware.sol"; +import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {BeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; +import {console} from "../../../lib/forge-std/src/console.sol"; +import {BaseHook} from "./../../BaseHook.sol"; + +contract MiddlewareProtect is BaseMiddleware { + bool private swapBlocked; + bool private removeBlocked; + + uint256 public constant gasLimit = 100000; + + error ActionBetweenHook(); + + constructor(IPoolManager _poolManager, address _impl) BaseMiddleware(_poolManager, _impl) {} + + modifier swapNotBlocked() { + if (swapBlocked) { + revert ActionBetweenHook(); + } + _; + } + + modifier removeNotBlocked() { + if (removeBlocked) { + revert ActionBetweenHook(); + } + _; + } + + // block swaps and removes + function beforeSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, bytes calldata) + external + swapNotBlocked + returns (bytes4, BeforeSwapDelta, uint24) + { + swapBlocked = true; + removeBlocked = true; + console.log("beforeSwap middleware"); + (bool success, bytes memory returnData) = implementation.delegatecall{gas: gasLimit}(msg.data); + require(success); + swapBlocked = false; + removeBlocked = false; + return abi.decode(returnData, (bytes4, BeforeSwapDelta, uint24)); + } + + // afterSwap - no protections + + // block swaps + function beforeAddLiquidity(address, PoolKey calldata, IPoolManager.ModifyLiquidityParams calldata, bytes calldata) + external + returns (bytes4) + { + swapBlocked = true; + console.log("beforeAddLiquidity middleware"); + (bool success, bytes memory returnData) = implementation.delegatecall{gas: gasLimit}(msg.data); + require(success); + swapBlocked = false; + return abi.decode(returnData, (bytes4)); + } + + // afterAddLiquidity - no protections + + // block swaps and reverts + function beforeRemoveLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external removeNotBlocked returns (bytes4) { + swapBlocked = true; + console.log("beforeRemoveLiquidity middleware"); + implementation.delegatecall{gas: gasLimit}(msg.data); + swapBlocked = false; + return BaseHook.beforeRemoveLiquidity.selector; + } + + // block reverts + function afterRemoveLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external returns (bytes4, BalanceDelta) { + console.log("afterRemoveLiquidity middleware"); + implementation.delegatecall{gas: gasLimit}(msg.data); + return (BaseHook.afterRemoveLiquidity.selector, BalanceDeltaLibrary.ZERO_DELTA); + } +} diff --git a/contracts/hooks/middleware/MiddlewareRemove.sol b/contracts/hooks/middleware/MiddlewareRemove.sol index c8b8ebad..f8b2bad6 100644 --- a/contracts/hooks/middleware/MiddlewareRemove.sol +++ b/contracts/hooks/middleware/MiddlewareRemove.sol @@ -36,10 +36,10 @@ contract MiddlewareRemove is BaseMiddleware { IPoolManager.ModifyLiquidityParams calldata, BalanceDelta, bytes calldata - ) external returns (bytes4) { + ) external returns (bytes4, BalanceDelta) { console.log("afterRemoveLiquidity middleware"); (bool success, bytes memory returnData) = implementation.delegatecall{gas: gasLimit}(msg.data); console.log(success); return BaseHook.afterRemoveLiquidity.selector; } -} \ No newline at end of file +} From 9fb20a32c4c0c5d0d8e831e51af0309034cf7834 Mon Sep 17 00:00:00 2001 From: Junion Date: Thu, 20 Jun 2024 12:46:58 -0400 Subject: [PATCH 14/25] middleware factory interface --- .../hooks/middleware/MiddlewareRemove.sol | 4 +- contracts/interfaces/IMiddlewareFactory.sol | 66 +++---------------- 2 files changed, 13 insertions(+), 57 deletions(-) diff --git a/contracts/hooks/middleware/MiddlewareRemove.sol b/contracts/hooks/middleware/MiddlewareRemove.sol index f8b2bad6..2a90f89a 100644 --- a/contracts/hooks/middleware/MiddlewareRemove.sol +++ b/contracts/hooks/middleware/MiddlewareRemove.sol @@ -11,6 +11,7 @@ import {Proxy} from "@openzeppelin/contracts/proxy/Proxy.sol"; import {BaseMiddleware} from "./BaseMiddleware.sol"; import {BaseHook} from "../../BaseHook.sol"; import {console} from "../../../lib/forge-std/src/console.sol"; +import {BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; contract MiddlewareRemove is BaseMiddleware { bytes internal constant ZERO_BYTES = bytes(""); @@ -40,6 +41,7 @@ contract MiddlewareRemove is BaseMiddleware { console.log("afterRemoveLiquidity middleware"); (bool success, bytes memory returnData) = implementation.delegatecall{gas: gasLimit}(msg.data); console.log(success); - return BaseHook.afterRemoveLiquidity.selector; + // hook cannot return delta + return (BaseHook.afterRemoveLiquidity.selector, BalanceDeltaLibrary.ZERO_DELTA); } } diff --git a/contracts/interfaces/IMiddlewareFactory.sol b/contracts/interfaces/IMiddlewareFactory.sol index dfe4c61b..afc10a68 100644 --- a/contracts/interfaces/IMiddlewareFactory.sol +++ b/contracts/interfaces/IMiddlewareFactory.sol @@ -1,62 +1,16 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; -interface IUniswapV3Factory { - event MiddlewareCreated( - address implementation, - address middleware - ); +interface IMiddlewareFactory { + event MiddlewareCreated(address implementation, address middleware); - /// @notice Emitted when a new fee amount is enabled for middleware creation via the factory - /// @param fee The enabled fee, denominated in hundredths of a bip - /// @param tickSpacing The minimum number of ticks between initialized ticks for middlewares created with the given fee - event FeeAmountEnabled(uint24 indexed fee, int24 indexed tickSpacing); + /// @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 Returns the current owner of the factory - /// @dev Can be changed by the current owner via setOwner - /// @return The address of the factory owner - function owner() external view returns (address); - - /// @notice Returns the tick spacing for a given fee amount, if enabled, or 0 if not enabled - /// @dev A fee amount can never be removed, so this value should be hard coded or cached in the calling context - /// @param fee The enabled fee, denominated in hundredths of a bip. Returns 0 in case of unenabled fee - /// @return The tick spacing - function feeAmountTickSpacing(uint24 fee) external view returns (int24); - - /// @notice Returns the middleware address for a given pair of tokens and a fee, or address 0 if it does not exist - /// @dev tokenA and tokenB may be passed in either token0/token1 or token1/token0 order - /// @param tokenA The contract address of either token0 or token1 - /// @param tokenB The contract address of the other token - /// @param fee The fee collected upon every swap in the middleware, denominated in hundredths of a bip - /// @return middleware The middleware address - function getMiddleware( - address tokenA, - address tokenB, - uint24 fee - ) external view returns (address middleware); - - /// @notice Creates a middleware for the given two tokens and fee - /// @param tokenA One of the two tokens in the desired middleware - /// @param tokenB The other of the two tokens in the desired middleware - /// @param fee The desired fee for the middleware - /// @dev tokenA and tokenB may be passed in either order: token0/token1 or token1/token0. tickSpacing is retrieved - /// from the fee. The call will revert if the middleware already exists, the fee is invalid, or the token arguments - /// are invalid. + /// @notice Creates a middleware for the given implementation + /// @param implementation The implementation address /// @return middleware The address of the newly created middleware - function createMiddleware( - address tokenA, - address tokenB, - uint24 fee - ) external returns (address middleware); - - /// @notice Updates the owner of the factory - /// @dev Must be called by the current owner - /// @param _owner The new owner of the factory - function setOwner(address _owner) external; - - /// @notice Enables a fee amount with the given tickSpacing - /// @dev Fee amounts may never be removed once enabled - /// @param fee The fee amount to enable, denominated in hundredths of a bip (i.e. 1e-6) - /// @param tickSpacing The spacing between ticks to be enforced for all middlewares created with the given fee amount - function enableFeeAmount(uint24 fee, int24 tickSpacing) external; -} \ No newline at end of file + function createMiddleware(address implementation) external returns (address middleware); +} From 7fb28eae04acfcebe20136faf36da6ec04f0ce05 Mon Sep 17 00:00:00 2001 From: Junion Date: Thu, 20 Jun 2024 16:39:34 -0400 Subject: [PATCH 15/25] add factory --- contracts/interfaces/IMiddlewareFactory.sol | 3 +- .../{hooks => }/middleware/BaseMiddleware.sol | 2 +- .../middleware/BaseMiddlewareDefault.txt | 0 .../middleware/MiddlewareProtect.sol | 4 +- .../middleware/MiddlewareRemove.sol | 4 +- .../middleware/MiddlewareRemoveFactory.sol | 26 +++++ .../middleware/test/FeeTakingLite.sol | 4 +- .../middleware/test/HooksOutOfGas.sol | 4 +- .../middleware/test/HooksRevert.sol | 4 +- test/BaseMiddleware.t.sol | 4 +- test/MiddlewareRemove.t.sol | 12 +-- test/MiddlewareRemoveFactory.t.sol | 98 +++++++++++++++++++ test/RemoveMiddleware.txt | 6 +- .../BaseMiddlewareImplementation.sol | 2 +- .../FeeTakingLiteImplementation.sol | 2 +- .../MiddlewareRemoveImplementation.sol | 2 +- test/utils/HookMiner.sol | 52 ++++++++++ 17 files changed, 203 insertions(+), 26 deletions(-) rename contracts/{hooks => }/middleware/BaseMiddleware.sol (94%) rename contracts/{hooks => }/middleware/BaseMiddlewareDefault.txt (100%) rename contracts/{hooks => }/middleware/MiddlewareProtect.sol (96%) rename contracts/{hooks => }/middleware/MiddlewareRemove.sol (94%) create mode 100644 contracts/middleware/MiddlewareRemoveFactory.sol rename contracts/{hooks => }/middleware/test/FeeTakingLite.sol (97%) rename contracts/{hooks => }/middleware/test/HooksOutOfGas.sol (97%) rename contracts/{hooks => }/middleware/test/HooksRevert.sol (97%) create mode 100644 test/MiddlewareRemoveFactory.t.sol create mode 100644 test/utils/HookMiner.sol diff --git a/contracts/interfaces/IMiddlewareFactory.sol b/contracts/interfaces/IMiddlewareFactory.sol index afc10a68..4af554fe 100644 --- a/contracts/interfaces/IMiddlewareFactory.sol +++ b/contracts/interfaces/IMiddlewareFactory.sol @@ -11,6 +11,7 @@ interface IMiddlewareFactory { /// @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) external returns (address middleware); + function createMiddleware(address implementation, bytes32 salt) external returns (address middleware); } diff --git a/contracts/hooks/middleware/BaseMiddleware.sol b/contracts/middleware/BaseMiddleware.sol similarity index 94% rename from contracts/hooks/middleware/BaseMiddleware.sol rename to contracts/middleware/BaseMiddleware.sol index f5fc500c..c9f6334d 100644 --- a/contracts/hooks/middleware/BaseMiddleware.sol +++ b/contracts/middleware/BaseMiddleware.sol @@ -8,7 +8,7 @@ import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; import {Proxy} from "@openzeppelin/contracts/proxy/Proxy.sol"; -import {console} from "../../../lib/forge-std/src/console.sol"; +import {console} from "../../lib/forge-std/src/console.sol"; contract BaseMiddleware is Proxy { /// @notice The address of the pool manager diff --git a/contracts/hooks/middleware/BaseMiddlewareDefault.txt b/contracts/middleware/BaseMiddlewareDefault.txt similarity index 100% rename from contracts/hooks/middleware/BaseMiddlewareDefault.txt rename to contracts/middleware/BaseMiddlewareDefault.txt diff --git a/contracts/hooks/middleware/MiddlewareProtect.sol b/contracts/middleware/MiddlewareProtect.sol similarity index 96% rename from contracts/hooks/middleware/MiddlewareProtect.sol rename to contracts/middleware/MiddlewareProtect.sol index 688fe536..faeb21ae 100644 --- a/contracts/hooks/middleware/MiddlewareProtect.sol +++ b/contracts/middleware/MiddlewareProtect.sol @@ -6,8 +6,8 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BaseMiddleware} from "./BaseMiddleware.sol"; import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {BeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; -import {console} from "../../../lib/forge-std/src/console.sol"; -import {BaseHook} from "./../../BaseHook.sol"; +import {console} from "../../lib/forge-std/src/console.sol"; +import {BaseHook} from "./../BaseHook.sol"; contract MiddlewareProtect is BaseMiddleware { bool private swapBlocked; diff --git a/contracts/hooks/middleware/MiddlewareRemove.sol b/contracts/middleware/MiddlewareRemove.sol similarity index 94% rename from contracts/hooks/middleware/MiddlewareRemove.sol rename to contracts/middleware/MiddlewareRemove.sol index 2a90f89a..1cb08e90 100644 --- a/contracts/hooks/middleware/MiddlewareRemove.sol +++ b/contracts/middleware/MiddlewareRemove.sol @@ -9,8 +9,8 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; import {Proxy} from "@openzeppelin/contracts/proxy/Proxy.sol"; import {BaseMiddleware} from "./BaseMiddleware.sol"; -import {BaseHook} from "../../BaseHook.sol"; -import {console} from "../../../lib/forge-std/src/console.sol"; +import {BaseHook} from "../BaseHook.sol"; +import {console} from "../../lib/forge-std/src/console.sol"; import {BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; contract MiddlewareRemove is BaseMiddleware { diff --git a/contracts/middleware/MiddlewareRemoveFactory.sol b/contracts/middleware/MiddlewareRemoveFactory.sol new file mode 100644 index 00000000..72f4368a --- /dev/null +++ b/contracts/middleware/MiddlewareRemoveFactory.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {IMiddlewareFactory} from "../interfaces/IMiddlewareFactory.sol"; +import {MiddlewareRemove} from "./MiddlewareRemove.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; + +contract MiddlewareRemoveFactory is IMiddlewareFactory { + mapping(address => address) private _implementations; + + IPoolManager public immutable poolManager; + + constructor(IPoolManager _poolManager) { + poolManager = _poolManager; + } + + 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 = address(new MiddlewareRemove{salt: salt}(poolManager, implementation)); + _implementations[middleware] = implementation; + emit MiddlewareCreated(implementation, middleware); + } +} diff --git a/contracts/hooks/middleware/test/FeeTakingLite.sol b/contracts/middleware/test/FeeTakingLite.sol similarity index 97% rename from contracts/hooks/middleware/test/FeeTakingLite.sol rename to contracts/middleware/test/FeeTakingLite.sol index d926b1a5..e4195ff2 100644 --- a/contracts/hooks/middleware/test/FeeTakingLite.sol +++ b/contracts/middleware/test/FeeTakingLite.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {BaseHook} from "../../../BaseHook.sol"; +import {BaseHook} from "../../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"; @@ -11,7 +11,7 @@ import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {Owned} from "solmate/auth/Owned.sol"; import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; -import {console} from "../../../../lib/forge-std/src/console.sol"; +import {console} from "../../../lib/forge-std/src/console.sol"; contract FeeTakingLite is IUnlockCallback { using SafeCast for uint256; diff --git a/contracts/hooks/middleware/test/HooksOutOfGas.sol b/contracts/middleware/test/HooksOutOfGas.sol similarity index 97% rename from contracts/hooks/middleware/test/HooksOutOfGas.sol rename to contracts/middleware/test/HooksOutOfGas.sol index 6550032f..db441bfe 100644 --- a/contracts/hooks/middleware/test/HooksOutOfGas.sol +++ b/contracts/middleware/test/HooksOutOfGas.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {BaseHook} from "../../../BaseHook.sol"; +import {BaseHook} from "../../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"; @@ -11,7 +11,7 @@ import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {Owned} from "solmate/auth/Owned.sol"; import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; -import {console} from "../../../../lib/forge-std/src/console.sol"; +import {console} from "../../../lib/forge-std/src/console.sol"; contract HooksOutOfGas { IPoolManager public immutable poolManager; diff --git a/contracts/hooks/middleware/test/HooksRevert.sol b/contracts/middleware/test/HooksRevert.sol similarity index 97% rename from contracts/hooks/middleware/test/HooksRevert.sol rename to contracts/middleware/test/HooksRevert.sol index 98efdf5a..bb125d3a 100644 --- a/contracts/hooks/middleware/test/HooksRevert.sol +++ b/contracts/middleware/test/HooksRevert.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {BaseHook} from "../../../BaseHook.sol"; +import {BaseHook} from "../../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"; @@ -11,7 +11,7 @@ import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {Owned} from "solmate/auth/Owned.sol"; import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; -import {console} from "../../../../lib/forge-std/src/console.sol"; +import {console} from "../../../lib/forge-std/src/console.sol"; contract HooksRevert { error HookNotImplemented(); diff --git a/test/BaseMiddleware.t.sol b/test/BaseMiddleware.t.sol index 58863bfd..56093d31 100644 --- a/test/BaseMiddleware.t.sol +++ b/test/BaseMiddleware.t.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {FeeTakingLite} from "../contracts/hooks/middleware/test/FeeTakingLite.sol"; -import {BaseMiddleware} from "../contracts/hooks/middleware/BaseMiddleware.sol"; +import {FeeTakingLite} from "../contracts/middleware/test/FeeTakingLite.sol"; +import {BaseMiddleware} from "../contracts/middleware/BaseMiddleware.sol"; import {BaseMiddlewareImplementation} from "./shared/implementation/BaseMiddlewareImplementation.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; diff --git a/test/MiddlewareRemove.t.sol b/test/MiddlewareRemove.t.sol index 2dde78e8..fb198463 100644 --- a/test/MiddlewareRemove.t.sol +++ b/test/MiddlewareRemove.t.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {FeeTakingLite} from "../contracts/hooks/middleware/test/FeeTakingLite.sol"; -import {MiddlewareRemove} from "../contracts/hooks/middleware/MiddlewareRemove.sol"; +import {FeeTakingLite} from "../contracts/middleware/test/FeeTakingLite.sol"; +import {MiddlewareRemove} from "../contracts/middleware/MiddlewareRemove.sol"; import {MiddlewareRemoveImplementation} from "./shared/implementation/MiddlewareRemoveImplementation.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; @@ -18,8 +18,8 @@ import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.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 {HooksRevert} from "../contracts/hooks/middleware/test/HooksRevert.sol"; -import {HooksOutOfGas} from "../contracts/hooks/middleware/test/HooksOutOfGas.sol"; +import {HooksRevert} from "../contracts/middleware/test/HooksRevert.sol"; +import {HooksOutOfGas} from "../contracts/middleware/test/HooksOutOfGas.sol"; contract MiddlewareRemoveTest is Test, Deployers { using PoolIdLibrary for PoolKey; @@ -66,8 +66,8 @@ contract MiddlewareRemoveTest is Test, Deployers { } // creates a middleware on an implementation - function testOn(address implementation, uint160 hooks) internal { - MiddlewareRemove middlewareRemove = MiddlewareRemove(payable(address(nonce << 20 | hooks))); + function testOn(address implementation, uint160 flags) internal { + MiddlewareRemove middlewareRemove = MiddlewareRemove(payable(address(nonce << 20 | flags))); nonce++; vm.record(); MiddlewareRemoveImplementation impl = diff --git a/test/MiddlewareRemoveFactory.t.sol b/test/MiddlewareRemoveFactory.t.sol new file mode 100644 index 00000000..29e40d83 --- /dev/null +++ b/test/MiddlewareRemoveFactory.t.sol @@ -0,0 +1,98 @@ +// 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 {FeeTakingLite} from "../contracts/middleware/test/FeeTakingLite.sol"; +import {MiddlewareRemove} from "../contracts/middleware/MiddlewareRemove.sol"; +import {MiddlewareRemoveImplementation} from "./shared/implementation/MiddlewareRemoveImplementation.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.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 {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.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 {HooksRevert} from "../contracts/middleware/test/HooksRevert.sol"; +import {HooksOutOfGas} from "../contracts/middleware/test/HooksOutOfGas.sol"; +import {MiddlewareRemoveFactory} from "./../contracts/middleware/MiddlewareRemoveFactory.sol"; +import {HookMiner} from "./utils/HookMiner.sol"; + +contract MiddlewareRemoveFactoryTest is Test, Deployers { + using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; + + uint160 constant SQRT_RATIO_10_1 = 250541448375047931186413801569; + + address constant TREASURY = address(0x1234567890123456789012345678901234567890); + uint128 private constant TOTAL_BIPS = 10000; + + HookEnabledSwapRouter router; + TestERC20 token0; + TestERC20 token1; + PoolId id; + + MiddlewareRemoveFactory factory; + + function setUp() public { + deployFreshManagerAndRouters(); + (currency0, currency1) = deployMintAndApprove2Currencies(); + + router = new HookEnabledSwapRouter(manager); + token0 = TestERC20(Currency.unwrap(currency0)); + token1 = TestERC20(Currency.unwrap(currency1)); + + token0.approve(address(router), type(uint256).max); + token1.approve(address(router), type(uint256).max); + + factory = new MiddlewareRemoveFactory(manager); + } + + function testVariousE() public { + FeeTakingLite feeTakingLite = new FeeTakingLite(manager); + uint160 flags = + uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG); + (address hookAddress, bytes32 salt) = HookMiner.find( + address(factory), + flags, + type(MiddlewareRemove).creationCode, + abi.encode(address(manager), address(feeTakingLite)) + ); + testOn(address(feeTakingLite), salt); + HooksRevert hooksRevert = new HooksRevert(manager); + flags = uint160(Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG); + (hookAddress, salt) = HookMiner.find( + address(factory), + flags, + type(MiddlewareRemove).creationCode, + abi.encode(address(manager), address(hooksRevert)) + ); + testOn(address(hooksRevert), salt); + HooksOutOfGas hooksOutOfGas = new HooksOutOfGas(manager); + flags = uint160(Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG); + (hookAddress, salt) = HookMiner.find( + address(factory), + flags, + type(MiddlewareRemove).creationCode, + abi.encode(address(manager), address(hooksOutOfGas)) + ); + testOn(address(hooksOutOfGas), salt); + } + + // creates a middleware on an implementation + function testOn(address implementation, bytes32 salt) internal { + address hookAddress = factory.createMiddleware(implementation, salt); + MiddlewareRemove middlewareRemove = MiddlewareRemove(payable(hookAddress)); + + (key, id) = initPoolAndAddLiquidity( + currency0, currency1, IHooks(address(middlewareRemove)), 3000, SQRT_PRICE_1_1, ZERO_BYTES + ); + + removeLiquidity(currency0, currency1, IHooks(address(middlewareRemove)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + } +} diff --git a/test/RemoveMiddleware.txt b/test/RemoveMiddleware.txt index bfaaf25f..eb5db684 100644 --- a/test/RemoveMiddleware.txt +++ b/test/RemoveMiddleware.txt @@ -3,9 +3,9 @@ pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {FeeTakingLite} from "../contracts/hooks/middleware/test/FeeTakingLite.sol"; -import {HooksRevert} from "../contracts/hooks/middleware/test/HooksRevert.sol"; -import {MiddlewareRemove} from "../contracts/hooks/middleware/MiddlewareRemove.sol"; +import {FeeTakingLite} from "../contracts/middleware/test/FeeTakingLite.sol"; +import {HooksRevert} from "../contracts/middleware/test/HooksRevert.sol"; +import {MiddlewareRemove} from "../contracts/middleware/MiddlewareRemove.sol"; import {MiddlewareRemoveImplementation} from "./shared/implementation/MiddlewareRemoveImplementation.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; diff --git a/test/shared/implementation/BaseMiddlewareImplementation.sol b/test/shared/implementation/BaseMiddlewareImplementation.sol index 9e598630..db954fc6 100644 --- a/test/shared/implementation/BaseMiddlewareImplementation.sol +++ b/test/shared/implementation/BaseMiddlewareImplementation.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.19; import {BaseHook} from "../../../contracts/BaseHook.sol"; -import {BaseMiddleware} from "../../../contracts/hooks/middleware/BaseMiddleware.sol"; +import {BaseMiddleware} from "../../../contracts/middleware/BaseMiddleware.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; diff --git a/test/shared/implementation/FeeTakingLiteImplementation.sol b/test/shared/implementation/FeeTakingLiteImplementation.sol index 39742eb5..51d2cd0f 100644 --- a/test/shared/implementation/FeeTakingLiteImplementation.sol +++ b/test/shared/implementation/FeeTakingLiteImplementation.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.19; import {BaseHook} from "../../../contracts/BaseHook.sol"; -import {FeeTakingLite} from "../../../contracts/hooks/middleware/test/FeeTakingLite.sol"; +import {FeeTakingLite} from "../../../contracts/middleware/test/FeeTakingLite.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; diff --git a/test/shared/implementation/MiddlewareRemoveImplementation.sol b/test/shared/implementation/MiddlewareRemoveImplementation.sol index 38ab6dcf..33a190ac 100644 --- a/test/shared/implementation/MiddlewareRemoveImplementation.sol +++ b/test/shared/implementation/MiddlewareRemoveImplementation.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.19; import {BaseHook} from "../../../contracts/BaseHook.sol"; -import {MiddlewareRemove} from "../../../contracts/hooks/middleware/MiddlewareRemove.sol"; +import {MiddlewareRemove} from "../../../contracts/middleware/MiddlewareRemove.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; diff --git a/test/utils/HookMiner.sol b/test/utils/HookMiner.sol new file mode 100644 index 00000000..d6b30c40 --- /dev/null +++ b/test/utils/HookMiner.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.21; + +/// @title HookMiner - a library for mining hook addresses +/// @dev This library is intended for `forge test` environments. There may be gotchas when using salts in `forge script` or `forge create` +library HookMiner { + // mask to slice out the bottom 14 bit of the address + uint160 constant FLAG_MASK = 0x3FFF; + + // Maximum number of iterations to find a salt, avoid infinite loops + uint256 constant MAX_LOOP = 100_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 + /// In `forge script`, this should be `0x4e59b44847b379578588920cA78FbF26c0B4956C` (CREATE2 Deployer Proxy) + /// @param flags The desired flags for the hook address + /// @param creationCode The creation code of a hook contract. Example: `type(Counter).creationCode` + /// @param constructorArgs The encoded constructor arguments of a hook contract. Example: `abi.encode(address(manager))` + /// @return hookAddress salt and corresponding address that was found. The salt can be used in `new Hook{salt: salt}()` + function find(address deployer, uint160 flags, bytes memory creationCode, bytes memory constructorArgs) + internal + view + returns (address, bytes32) + { + address hookAddress; + bytes memory creationCodeWithArgs = abi.encodePacked(creationCode, constructorArgs); + + uint256 salt; + for (salt; salt < MAX_LOOP; salt++) { + hookAddress = computeAddress(deployer, salt, creationCodeWithArgs); + if (uint160(hookAddress) & FLAG_MASK == flags && hookAddress.code.length == 0) { + return (hookAddress, bytes32(salt)); + } + } + revert("HookMiner: could not find salt"); + } + + /// @notice Precompute a contract address deployed via CREATE2 + /// @param deployer The address that will deploy the hook. In `forge test`, this will be the test contract `address(this)` or the pranking address + /// In `forge script`, this should be `0x4e59b44847b379578588920cA78FbF26c0B4956C` (CREATE2 Deployer Proxy) + /// @param salt The salt used to deploy the hook + /// @param creationCode The creation code of a hook contract + function computeAddress(address deployer, uint256 salt, bytes memory creationCode) + internal + pure + returns (address hookAddress) + { + return address( + uint160(uint256(keccak256(abi.encodePacked(bytes1(0xFF), deployer, salt, keccak256(creationCode))))) + ); + } +} From cc1e3e78c58ff4e3bf77104f6bcd59ec215ad3bd Mon Sep 17 00:00:00 2001 From: Junion Date: Fri, 21 Jun 2024 11:00:04 -0400 Subject: [PATCH 16/25] fix error --- contracts/middleware/MiddlewareRemove.sol | 5 +---- test/FeeTaking.t.sol | 1 - test/MiddlewareRemoveFactory.t.sol | 3 ++- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/contracts/middleware/MiddlewareRemove.sol b/contracts/middleware/MiddlewareRemove.sol index 1cb08e90..9dc2b531 100644 --- a/contracts/middleware/MiddlewareRemove.sol +++ b/contracts/middleware/MiddlewareRemove.sol @@ -1,12 +1,9 @@ // 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"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {BeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; import {Proxy} from "@openzeppelin/contracts/proxy/Proxy.sol"; import {BaseMiddleware} from "./BaseMiddleware.sol"; import {BaseHook} from "../BaseHook.sol"; @@ -15,7 +12,7 @@ import {BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; contract MiddlewareRemove is BaseMiddleware { bytes internal constant ZERO_BYTES = bytes(""); - uint256 public constant gasLimit = 100000; + uint256 public constant gasLimit = 1000000; constructor(IPoolManager _poolManager, address _impl) BaseMiddleware(_poolManager, _impl) {} diff --git a/test/FeeTaking.t.sol b/test/FeeTaking.t.sol index 85de2605..c6793e9c 100644 --- a/test/FeeTaking.t.sol +++ b/test/FeeTaking.t.sol @@ -5,7 +5,6 @@ import {Test} from "forge-std/Test.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {FeeTaking} from "../contracts/hooks/examples/FeeTaking.sol"; import {FeeTakingImplementation} from "./shared/implementation/FeeTakingImplementation.sol"; -import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; import {TestERC20} from "@uniswap/v4-core/src/test/TestERC20.sol"; diff --git a/test/MiddlewareRemoveFactory.t.sol b/test/MiddlewareRemoveFactory.t.sol index 29e40d83..3e4a5ce8 100644 --- a/test/MiddlewareRemoveFactory.t.sol +++ b/test/MiddlewareRemoveFactory.t.sol @@ -53,7 +53,7 @@ contract MiddlewareRemoveFactoryTest is Test, Deployers { factory = new MiddlewareRemoveFactory(manager); } - function testVariousE() public { + function testVariousFactory() public { FeeTakingLite feeTakingLite = new FeeTakingLite(manager); uint160 flags = uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG); @@ -94,5 +94,6 @@ contract MiddlewareRemoveFactoryTest is Test, Deployers { ); removeLiquidity(currency0, currency1, IHooks(address(middlewareRemove)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + assertEq(factory.getImplementation(hookAddress), implementation); } } From 832024ec6d5a01b4a10033018bce1acb6a44830d Mon Sep 17 00:00:00 2001 From: Junion Date: Fri, 21 Jun 2024 11:05:52 -0400 Subject: [PATCH 17/25] remove FeeTaking --- contracts/hooks/examples/FeeTaking.sol | 99 ------- test/FeeTaking.t.sol | 266 ------------------ .../FeeTakingImplementation.sol | 18 -- 3 files changed, 383 deletions(-) delete mode 100644 contracts/hooks/examples/FeeTaking.sol delete mode 100644 test/FeeTaking.t.sol delete mode 100644 test/shared/implementation/FeeTakingImplementation.sol diff --git a/contracts/hooks/examples/FeeTaking.sol b/contracts/hooks/examples/FeeTaking.sol deleted file mode 100644 index 942f1f3e..00000000 --- a/contracts/hooks/examples/FeeTaking.sol +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import {BaseHook} from "../../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 {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; -import {Owned} from "solmate/auth/Owned.sol"; -import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; - -contract FeeTaking is BaseHook, IUnlockCallback, Owned { - using SafeCast for uint256; - - bytes internal constant ZERO_BYTES = bytes(""); - uint128 private constant TOTAL_BIPS = 10000; - uint128 private constant MAX_BIPS = 100; - uint128 public swapFeeBips; - - struct CallbackData { - address to; - Currency[] currencies; - } - - constructor(IPoolManager _poolManager, uint128 _swapFeeBips, address _owner) BaseHook(_poolManager) Owned(_owner) { - swapFeeBips = _swapFeeBips; - } - - 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: true, - beforeDonate: false, - afterDonate: false, - beforeSwapReturnDelta: false, - afterSwapReturnDelta: true, - afterAddLiquidityReturnDelta: false, - afterRemoveLiquidityReturnDelta: false - }); - } - - function afterSwap( - address, - PoolKey calldata key, - IPoolManager.SwapParams calldata params, - BalanceDelta delta, - bytes calldata - ) external override returns (bytes4, int128) { - // fee will be in the unspecified token of the swap - bool currency0Specified = (params.amountSpecified < 0 == params.zeroForOne); - (Currency feeCurrency, int128 swapAmount) = - (currency0Specified) ? (key.currency1, delta.amount1()) : (key.currency0, delta.amount0()); - // if fee is on output, get the absolute output amount - if (swapAmount < 0) swapAmount = -swapAmount; - - uint256 feeAmount = (uint128(swapAmount) * swapFeeBips) / TOTAL_BIPS; - // mint ERC6909 instead of take to avoid edge case where PM doesn't have enough balance - poolManager.mint(address(this), CurrencyLibrary.toId(feeCurrency), feeAmount); - - return (BaseHook.afterSwap.selector, feeAmount.toInt128()); - } - - function setSwapFeeBips(uint128 _swapFeeBips) external onlyOwner { - require(_swapFeeBips <= MAX_BIPS); - swapFeeBips = _swapFeeBips; - } - - function withdraw(address to, Currency[] calldata currencies) external onlyOwner { - poolManager.unlock(abi.encode(CallbackData(to, currencies))); - } - - function unlockCallback(bytes calldata rawData) - external - override(IUnlockCallback, BaseHook) - poolManagerOnly - returns (bytes memory) - { - CallbackData memory data = abi.decode(rawData, (CallbackData)); - uint256 length = data.currencies.length; - for (uint256 i = 0; i < length;) { - uint256 amount = poolManager.balanceOf(address(this), CurrencyLibrary.toId(data.currencies[i])); - poolManager.burn(address(this), CurrencyLibrary.toId(data.currencies[i]), amount); - poolManager.take(data.currencies[i], data.to, amount); - unchecked { - i++; - } - } - return ZERO_BYTES; - } -} diff --git a/test/FeeTaking.t.sol b/test/FeeTaking.t.sol deleted file mode 100644 index c6793e9c..00000000 --- a/test/FeeTaking.t.sol +++ /dev/null @@ -1,266 +0,0 @@ -// 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 {FeeTaking} from "../contracts/hooks/examples/FeeTaking.sol"; -import {FeeTakingImplementation} from "./shared/implementation/FeeTakingImplementation.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.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 {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; - -contract FeeTakingTest is Test, Deployers { - using PoolIdLibrary for PoolKey; - using StateLibrary for IPoolManager; - - uint160 constant SQRT_RATIO_10_1 = 250541448375047931186413801569; - - address constant TREASURY = address(0x1234567890123456789012345678901234567890); - uint128 private constant TOTAL_BIPS = 10000; - - // rounding for tests to avoid floating point errors - uint128 R = 10; - - HookEnabledSwapRouter router; - TestERC20 token0; - TestERC20 token1; - FeeTaking feeTaking = FeeTaking(address(uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG))); - PoolId id; - - function setUp() public { - deployFreshManagerAndRouters(); - (currency0, currency1) = deployMintAndApprove2Currencies(); - - router = new HookEnabledSwapRouter(manager); - token0 = TestERC20(Currency.unwrap(currency0)); - token1 = TestERC20(Currency.unwrap(currency1)); - - vm.record(); - FeeTakingImplementation impl = new FeeTakingImplementation(manager, 25, address(this), feeTaking); - (, bytes32[] memory writes) = vm.accesses(address(impl)); - vm.etch(address(feeTaking), address(impl).code); - // for each storage key that was written during the hook implementation, copy the value over - unchecked { - for (uint256 i = 0; i < writes.length; i++) { - bytes32 slot = writes[i]; - vm.store(address(feeTaking), slot, vm.load(address(impl), slot)); - } - } - - // key = PoolKey(currency0, currency1, 3000, 60, feeTaking); - (key, id) = initPoolAndAddLiquidity(currency0, currency1, feeTaking, 3000, SQRT_PRICE_1_1, ZERO_BYTES); - - token0.approve(address(feeTaking), type(uint256).max); - token1.approve(address(feeTaking), type(uint256).max); - token0.approve(address(router), type(uint256).max); - token1.approve(address(router), type(uint256).max); - } - - function testSwapHooks() public { - assertEq(currency0.balanceOf(TREASURY), 0); - assertEq(currency1.balanceOf(TREASURY), 0); - - // Swap exact token0 for token1 // - bool zeroForOne = true; - int256 amountSpecified = -1e12; - BalanceDelta swapDelta = swap(key, zeroForOne, amountSpecified, ZERO_BYTES); - // ---------------------------- // - - uint128 output = uint128(swapDelta.amount1()); - assertTrue(output > 0); - - uint256 expectedFee = calculateFeeForExactInput(output, feeTaking.swapFeeBips()); - - assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency0)), 0); - assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); - - // Swap token0 for exact token1 // - bool zeroForOne2 = true; - int256 amountSpecified2 = 1e12; // positive number indicates exact output swap - BalanceDelta swapDelta2 = swap(key, zeroForOne2, amountSpecified2, ZERO_BYTES); - // ---------------------------- // - - uint128 input = uint128(-swapDelta2.amount0()); - assertTrue(output > 0); - - uint256 expectedFee2 = calculateFeeForExactOutput(input, feeTaking.swapFeeBips()); - - assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency0)) / R, expectedFee2 / R); - assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); - - // test withdrawing tokens // - Currency[] memory currencies = new Currency[](2); - currencies[0] = key.currency0; - currencies[1] = key.currency1; - feeTaking.withdraw(TREASURY, currencies); - assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency0)), 0); - assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)), 0); - assertEq(currency0.balanceOf(TREASURY) / R, expectedFee2 / R); - assertEq(currency1.balanceOf(TREASURY) / R, expectedFee / R); - } - - // this would error had the hook not used ERC6909 - function testEdgeCase() public { - // first, deplete the pool of token1 - // Swap exact token0 for token1 // - bool zeroForOne = true; - int256 amountSpecified = -1e18; - BalanceDelta swapDelta = swap(key, zeroForOne, amountSpecified, ZERO_BYTES); - // ---------------------------- // - // now, pool only has 1 wei of token1 - uint256 poolToken1 = currency1.balanceOf(address(manager)) - - manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)); - assertEq(poolToken1, 1); - - uint128 output = uint128(swapDelta.amount1()); - assertTrue(output > 0); - - uint256 expectedFee = calculateFeeForExactInput(output, feeTaking.swapFeeBips()); - - assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency0)), 0); - assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); - - // Swap token1 for exact token0 // - bool zeroForOne2 = false; - int256 amountSpecified2 = 1e18; // positive number indicates exact output swap - BalanceDelta swapDelta2 = swap(key, zeroForOne2, amountSpecified2, ZERO_BYTES); - // ---------------------------- // - - uint128 input = uint128(-swapDelta2.amount1()); - assertTrue(output > 0); - - uint256 expectedFee2 = calculateFeeForExactOutput(input, feeTaking.swapFeeBips()); - - assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency0)), 0); - assertEq( - manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)) / R, - (expectedFee + expectedFee2) / R - ); - - // test withdrawing tokens // - Currency[] memory currencies = new Currency[](2); - currencies[0] = key.currency0; - currencies[1] = key.currency1; - feeTaking.withdraw(TREASURY, currencies); - assertEq(currency0.balanceOf(TREASURY) / R, 0); - assertEq(currency1.balanceOf(TREASURY) / R, (expectedFee + expectedFee2) / R); - } - - function testSwapWithDifferentFees() public { - testSwapHooks(); - feeTaking.setSwapFeeBips(50); // Set fee to 0.50% - - // Swap exact token0 for token1 // - bool zeroForOne = true; - int256 amountSpecified = -1e12; - BalanceDelta swapDelta = swap(key, zeroForOne, amountSpecified, ZERO_BYTES); - // ---------------------------- // - - uint128 output = uint128(swapDelta.amount1()); - assertTrue(output > 0); - - uint256 expectedFee = calculateFeeForExactInput(output, 50); - - assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency0)), 0); - assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); - } - - function testZeroFeeSwap() public { - feeTaking.setSwapFeeBips(0); // Set fee to 0% - - // Swap exact token0 for token1 // - bool zeroForOne = true; - int256 amountSpecified = -1e12; - BalanceDelta swapDelta = swap(key, zeroForOne, amountSpecified, ZERO_BYTES); - // ---------------------------- // - - uint128 output = uint128(swapDelta.amount1()); - assertTrue(output > 0); - - // No fee should be taken - uint256 expectedFee = calculateFeeForExactInput(output, 0); - assertEq(expectedFee, 0); - assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency0)), 0); - assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)), 0); - } - - function testMaxFeeSwap() public { - feeTaking.setSwapFeeBips(100); // Set fee to 1% - - // Swap exact token0 for token1 // - bool zeroForOne = true; - int256 amountSpecified = -1e12; - BalanceDelta swapDelta = swap(key, zeroForOne, amountSpecified, ZERO_BYTES); - // ---------------------------- // - - uint128 output = uint128(swapDelta.amount1()); - assertTrue(output > 0); - - uint256 expectedFee = calculateFeeForExactInput(output, 100); - - assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency0)), 0); - assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)) / R, expectedFee / R); - } - - function testMultiTokenPoolSwap() public { - testSwapHooks(); - // Deploy additional tokens - (Currency currency2, Currency currency3) = deployMintAndApprove2Currencies(); - TestERC20 token2 = TestERC20(Currency.unwrap(currency2)); - TestERC20 token3 = TestERC20(Currency.unwrap(currency3)); - - // Create new pool with different tokens - (PoolKey memory key2, PoolId id2) = - initPoolAndAddLiquidity(currency2, currency3, feeTaking, 3000, SQRT_RATIO_10_1, ZERO_BYTES); - - // Approve tokens for the router - token2.approve(address(router), type(uint256).max); - token3.approve(address(router), type(uint256).max); - - // Swap exact token2 for token3 // - bool zeroForOne = true; - int256 amountSpecified = -1e12; - BalanceDelta swapDelta = swap(key2, zeroForOne, amountSpecified, ZERO_BYTES); - // ---------------------------- // - - uint128 output = uint128(swapDelta.amount1()); - assertTrue(output > 0); - - uint256 expectedFee = calculateFeeForExactInput(output, feeTaking.swapFeeBips()); - - assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key2.currency0)), 0); - assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key2.currency1)) / R, expectedFee / R); - - // Withdraw accumulated fees - Currency[] memory currencies = new Currency[](3); - currencies[0] = key.currency0; - currencies[1] = key.currency1; - currencies[2] = key2.currency1; - feeTaking.withdraw(TREASURY, currencies); - assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency0)), 0); - assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key.currency1)), 0); - assertEq(manager.balanceOf(address(feeTaking), CurrencyLibrary.toId(key2.currency1)), 0); - assertEq(currency0.balanceOf(TREASURY) / R, 0); - assertEq(currency1.balanceOf(TREASURY) / R, expectedFee / R); - assertEq(currency3.balanceOf(TREASURY) / R, expectedFee / R); - } - - function testTooHighFee() public { - vm.expectRevert(); - feeTaking.setSwapFeeBips(101); - } - - function calculateFeeForExactInput(uint256 outputAmount, uint128 feeBips) internal pure returns (uint256) { - return outputAmount * TOTAL_BIPS / (TOTAL_BIPS - feeBips) - outputAmount; - } - - function calculateFeeForExactOutput(uint256 inputAmount, uint128 feeBips) internal pure returns (uint256) { - return (inputAmount * feeBips) / (TOTAL_BIPS + feeBips); - } -} diff --git a/test/shared/implementation/FeeTakingImplementation.sol b/test/shared/implementation/FeeTakingImplementation.sol deleted file mode 100644 index 697dbf41..00000000 --- a/test/shared/implementation/FeeTakingImplementation.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {BaseHook} from "../../../contracts/BaseHook.sol"; -import {FeeTaking} from "../../../contracts/hooks/examples/FeeTaking.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; - -contract FeeTakingImplementation is FeeTaking { - constructor(IPoolManager _poolManager, uint128 _swapFeeBips, address _owner, FeeTaking addressToEtch) - FeeTaking(_poolManager, _swapFeeBips, _owner) - { - Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); - } - - // make this a no-op in testing - function validateHookAddress(BaseHook _this) internal pure override {} -} From 532f60ce3ddf4f384a21a205cf6ea72b00cd445a Mon Sep 17 00:00:00 2001 From: Junion <69495294+Jun1on@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:14:55 -0400 Subject: [PATCH 18/25] move files --- contracts/middleware/BaseMiddleware.sol | 6 +- lib/openzeppelin-contracts | 2 +- test/BaseMiddleware.t.sol | 2 +- test/MiddlewareRemove.t.sol | 6 +- test/MiddlewareRemoveFactory.t.sol | 8 +- test/middleware/CouterPayable.sol | 88 +++++++++++++++++++ .../middleware}/FeeTakingLite.sol | 2 +- .../middleware}/HooksOutOfGas.sol | 2 +- .../test => test/middleware}/HooksRevert.sol | 2 +- .../FeeTakingLiteImplementation.sol | 2 +- 10 files changed, 105 insertions(+), 15 deletions(-) create mode 100644 test/middleware/CouterPayable.sol rename {contracts/middleware/test => test/middleware}/FeeTakingLite.sol (98%) rename {contracts/middleware/test => test/middleware}/HooksOutOfGas.sol (98%) rename {contracts/middleware/test => test/middleware}/HooksRevert.sol (98%) diff --git a/contracts/middleware/BaseMiddleware.sol b/contracts/middleware/BaseMiddleware.sol index c9f6334d..ce7bbe68 100644 --- a/contracts/middleware/BaseMiddleware.sol +++ b/contracts/middleware/BaseMiddleware.sol @@ -25,7 +25,7 @@ contract BaseMiddleware is Proxy { return implementation; } - receive() external payable { - // ?? - } + // yo how do i remove this warning + // receive() external payable { + // } } diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index 5ae63068..337bfd5e 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 5ae630684a0f57de400ef69499addab4c32ac8fb +Subproject commit 337bfd5ea4df9f7ebc755cd3cb4ecb3bd3d33fc7 diff --git a/test/BaseMiddleware.t.sol b/test/BaseMiddleware.t.sol index 56093d31..e2c7bdf0 100644 --- a/test/BaseMiddleware.t.sol +++ b/test/BaseMiddleware.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {FeeTakingLite} from "../contracts/middleware/test/FeeTakingLite.sol"; +import {FeeTakingLite} from "./middleware/FeeTakingLite.sol"; import {BaseMiddleware} from "../contracts/middleware/BaseMiddleware.sol"; import {BaseMiddlewareImplementation} from "./shared/implementation/BaseMiddlewareImplementation.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; diff --git a/test/MiddlewareRemove.t.sol b/test/MiddlewareRemove.t.sol index fb198463..6bafa59b 100644 --- a/test/MiddlewareRemove.t.sol +++ b/test/MiddlewareRemove.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {FeeTakingLite} from "../contracts/middleware/test/FeeTakingLite.sol"; +import {FeeTakingLite} from "./middleware/FeeTakingLite.sol"; import {MiddlewareRemove} from "../contracts/middleware/MiddlewareRemove.sol"; import {MiddlewareRemoveImplementation} from "./shared/implementation/MiddlewareRemoveImplementation.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; @@ -18,8 +18,8 @@ import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.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 {HooksRevert} from "../contracts/middleware/test/HooksRevert.sol"; -import {HooksOutOfGas} from "../contracts/middleware/test/HooksOutOfGas.sol"; +import {HooksRevert} from "./middleware/HooksRevert.sol"; +import {HooksOutOfGas} from "./middleware/HooksOutOfGas.sol"; contract MiddlewareRemoveTest is Test, Deployers { using PoolIdLibrary for PoolKey; diff --git a/test/MiddlewareRemoveFactory.t.sol b/test/MiddlewareRemoveFactory.t.sol index 3e4a5ce8..d769e077 100644 --- a/test/MiddlewareRemoveFactory.t.sol +++ b/test/MiddlewareRemoveFactory.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {FeeTakingLite} from "../contracts/middleware/test/FeeTakingLite.sol"; +import {FeeTakingLite} from "./middleware/FeeTakingLite.sol"; import {MiddlewareRemove} from "../contracts/middleware/MiddlewareRemove.sol"; import {MiddlewareRemoveImplementation} from "./shared/implementation/MiddlewareRemoveImplementation.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; @@ -18,8 +18,8 @@ import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.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 {HooksRevert} from "../contracts/middleware/test/HooksRevert.sol"; -import {HooksOutOfGas} from "../contracts/middleware/test/HooksOutOfGas.sol"; +import {HooksRevert} from "./middleware/HooksRevert.sol"; +import {HooksOutOfGas} from "./middleware/HooksOutOfGas.sol"; import {MiddlewareRemoveFactory} from "./../contracts/middleware/MiddlewareRemoveFactory.sol"; import {HookMiner} from "./utils/HookMiner.sol"; @@ -64,6 +64,7 @@ contract MiddlewareRemoveFactoryTest is Test, Deployers { abi.encode(address(manager), address(feeTakingLite)) ); testOn(address(feeTakingLite), salt); + HooksRevert hooksRevert = new HooksRevert(manager); flags = uint160(Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG); (hookAddress, salt) = HookMiner.find( @@ -73,6 +74,7 @@ contract MiddlewareRemoveFactoryTest is Test, Deployers { abi.encode(address(manager), address(hooksRevert)) ); testOn(address(hooksRevert), salt); + HooksOutOfGas hooksOutOfGas = new HooksOutOfGas(manager); flags = uint160(Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG); (hookAddress, salt) = HookMiner.find( diff --git a/test/middleware/CouterPayable.sol b/test/middleware/CouterPayable.sol new file mode 100644 index 00000000..13bcc521 --- /dev/null +++ b/test/middleware/CouterPayable.sol @@ -0,0 +1,88 @@ +// 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 {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; + +contract Counter is BaseHook { + using PoolIdLibrary for PoolKey; + + // NOTE: --------------------------------------------------------- + // state variables should typically be unique to a pool + // a single hook contract should be able to service multiple pools + // --------------------------------------------------------------- + + mapping(PoolId => uint256 count) public beforeSwapCount; + mapping(PoolId => uint256 count) public afterSwapCount; + + mapping(PoolId => uint256 count) public beforeAddLiquidityCount; + mapping(PoolId => uint256 count) public beforeRemoveLiquidityCount; + + constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: true, + afterAddLiquidity: false, + beforeRemoveLiquidity: true, + afterRemoveLiquidity: false, + beforeSwap: true, + afterSwap: true, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } + + // ----------------------------------------------- + // NOTE: see IHooks.sol for function documentation + // ----------------------------------------------- + + function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) + external + override + returns (bytes4, BeforeSwapDelta, uint24) + { + beforeSwapCount[key.toId()]++; + return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); + } + + function afterSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, BalanceDelta, bytes calldata) + external + override + returns (bytes4, int128) + { + afterSwapCount[key.toId()]++; + return (BaseHook.afterSwap.selector, 0); + } + + function beforeAddLiquidity( + address, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external override returns (bytes4) { + beforeAddLiquidityCount[key.toId()]++; + return BaseHook.beforeAddLiquidity.selector; + } + + function beforeRemoveLiquidity( + address, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external override returns (bytes4) { + beforeRemoveLiquidityCount[key.toId()]++; + return BaseHook.beforeRemoveLiquidity.selector; + } +} diff --git a/contracts/middleware/test/FeeTakingLite.sol b/test/middleware/FeeTakingLite.sol similarity index 98% rename from contracts/middleware/test/FeeTakingLite.sol rename to test/middleware/FeeTakingLite.sol index e4195ff2..3781ece6 100644 --- a/contracts/middleware/test/FeeTakingLite.sol +++ b/test/middleware/FeeTakingLite.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {BaseHook} from "../../BaseHook.sol"; +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"; diff --git a/contracts/middleware/test/HooksOutOfGas.sol b/test/middleware/HooksOutOfGas.sol similarity index 98% rename from contracts/middleware/test/HooksOutOfGas.sol rename to test/middleware/HooksOutOfGas.sol index db441bfe..d7b35a37 100644 --- a/contracts/middleware/test/HooksOutOfGas.sol +++ b/test/middleware/HooksOutOfGas.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {BaseHook} from "../../BaseHook.sol"; +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"; diff --git a/contracts/middleware/test/HooksRevert.sol b/test/middleware/HooksRevert.sol similarity index 98% rename from contracts/middleware/test/HooksRevert.sol rename to test/middleware/HooksRevert.sol index bb125d3a..f3f31401 100644 --- a/contracts/middleware/test/HooksRevert.sol +++ b/test/middleware/HooksRevert.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {BaseHook} from "../../BaseHook.sol"; +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"; diff --git a/test/shared/implementation/FeeTakingLiteImplementation.sol b/test/shared/implementation/FeeTakingLiteImplementation.sol index 51d2cd0f..346b3099 100644 --- a/test/shared/implementation/FeeTakingLiteImplementation.sol +++ b/test/shared/implementation/FeeTakingLiteImplementation.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.19; import {BaseHook} from "../../../contracts/BaseHook.sol"; -import {FeeTakingLite} from "../../../contracts/middleware/test/FeeTakingLite.sol"; +import {FeeTakingLite} from "../../middleware/FeeTakingLite.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; From eaabe37bf0834323931cae384f5f87fc9347f6c2 Mon Sep 17 00:00:00 2001 From: Junion <69495294+Jun1on@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:33:36 -0400 Subject: [PATCH 19/25] update implementations --- contracts/middleware/BaseMiddleware.sol | 5 +++-- .../implementation/BaseMiddlewareImplementation.sol | 5 +---- .../shared/implementation/FeeTakingLiteImplementation.sol | 8 ++------ 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/contracts/middleware/BaseMiddleware.sol b/contracts/middleware/BaseMiddleware.sol index ce7bbe68..ce4a7004 100644 --- a/contracts/middleware/BaseMiddleware.sol +++ b/contracts/middleware/BaseMiddleware.sol @@ -26,6 +26,7 @@ contract BaseMiddleware is Proxy { } // yo how do i remove this warning - // receive() external payable { - // } + receive() external payable { + _delegate(_implementation()); + } } diff --git a/test/shared/implementation/BaseMiddlewareImplementation.sol b/test/shared/implementation/BaseMiddlewareImplementation.sol index db954fc6..fcabbd48 100644 --- a/test/shared/implementation/BaseMiddlewareImplementation.sol +++ b/test/shared/implementation/BaseMiddlewareImplementation.sol @@ -11,9 +11,6 @@ contract BaseMiddlewareImplementation is BaseMiddleware { constructor(IPoolManager _poolManager, address _implementation, BaseMiddleware addressToEtch) BaseMiddleware(_poolManager, _implementation) { - //Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); + Hooks.validateHookPermissions(IHooks(address(addressToEtch)), BaseHook(_implementation).getHookPermissions()); } - - // make this a no-op in testing - //function validateHookAddress(BaseHook _this) internal pure override {} } diff --git a/test/shared/implementation/FeeTakingLiteImplementation.sol b/test/shared/implementation/FeeTakingLiteImplementation.sol index 346b3099..a9145bc7 100644 --- a/test/shared/implementation/FeeTakingLiteImplementation.sol +++ b/test/shared/implementation/FeeTakingLiteImplementation.sol @@ -5,12 +5,8 @@ import {BaseHook} from "../../../contracts/BaseHook.sol"; import {FeeTakingLite} from "../../middleware/FeeTakingLite.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; contract FeeTakingLiteImplementation is FeeTakingLite { - constructor(IPoolManager _poolManager, FeeTakingLite addressToEtch) FeeTakingLite(_poolManager) { - //Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); - } - - // make this a no-op in testing - //function validateHookAddress(BaseHook _this) internal pure override {} + constructor(IPoolManager _poolManager, FeeTakingLite addressToEtch) FeeTakingLite(_poolManager) {} } From 97a3627e089c9171b776d5d0db347b835700fa51 Mon Sep 17 00:00:00 2001 From: Junion <69495294+Jun1on@users.noreply.github.com> Date: Mon, 1 Jul 2024 11:29:29 -0400 Subject: [PATCH 20/25] update MiddlewareProtect to use tstore --- contracts/libraries/ReentrancyState.sol | 39 ++++++++++++++++++++++ contracts/middleware/MiddlewareProtect.sol | 32 ++++++++---------- lib/forge-gas-snapshot | 2 +- lib/forge-std | 2 +- lib/openzeppelin-contracts | 2 +- 5 files changed, 56 insertions(+), 21 deletions(-) create mode 100644 contracts/libraries/ReentrancyState.sol diff --git a/contracts/libraries/ReentrancyState.sol b/contracts/libraries/ReentrancyState.sol new file mode 100644 index 00000000..966d1269 --- /dev/null +++ b/contracts/libraries/ReentrancyState.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +library ReentrancyState { + // bytes32(uint256(keccak256("ReentrancyState")) - 1) + bytes32 constant REENTRANCY_STATE_SLOT = 0xbedc9a60a226d4ae7b727cbc828f66c94c4eead57777428ceab2f04b0efca3a5; + + function unlock() internal { + assembly { + tstore(REENTRANCY_STATE_SLOT, 0) + } + } + + function lockSwap() internal { + assembly { + tstore(REENTRANCY_STATE_SLOT, 1) + } + } + + function lockSwapRemove() internal { + assembly { + tstore(REENTRANCY_STATE_SLOT, 2) + } + } + + function read() internal view returns (uint256 state) { + assembly { + state := tload(REENTRANCY_STATE_SLOT) + } + } + + function swapLocked() internal view returns (bool) { + return read() == 1 || read() == 2; + } + + function removeLocked() internal view returns (bool) { + return read() == 2; + } +} diff --git a/contracts/middleware/MiddlewareProtect.sol b/contracts/middleware/MiddlewareProtect.sol index faeb21ae..0ee8a20d 100644 --- a/contracts/middleware/MiddlewareProtect.sol +++ b/contracts/middleware/MiddlewareProtect.sol @@ -8,26 +8,24 @@ import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/Bala import {BeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; import {console} from "../../lib/forge-std/src/console.sol"; import {BaseHook} from "./../BaseHook.sol"; +import {ReentrancyState} from "./../libraries/ReentrancyState.sol"; contract MiddlewareProtect is BaseMiddleware { - bool private swapBlocked; - bool private removeBlocked; - uint256 public constant gasLimit = 100000; error ActionBetweenHook(); constructor(IPoolManager _poolManager, address _impl) BaseMiddleware(_poolManager, _impl) {} - modifier swapNotBlocked() { - if (swapBlocked) { + modifier swapNotLocked() { + if (ReentrancyState.swapLocked()) { revert ActionBetweenHook(); } _; } - modifier removeNotBlocked() { - if (removeBlocked) { + modifier removeNotLocked() { + if (ReentrancyState.removeLocked()) { revert ActionBetweenHook(); } _; @@ -36,16 +34,14 @@ contract MiddlewareProtect is BaseMiddleware { // block swaps and removes function beforeSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, bytes calldata) external - swapNotBlocked + swapNotLocked returns (bytes4, BeforeSwapDelta, uint24) { - swapBlocked = true; - removeBlocked = true; + ReentrancyState.lockSwapRemove(); console.log("beforeSwap middleware"); (bool success, bytes memory returnData) = implementation.delegatecall{gas: gasLimit}(msg.data); require(success); - swapBlocked = false; - removeBlocked = false; + ReentrancyState.unlock(); return abi.decode(returnData, (bytes4, BeforeSwapDelta, uint24)); } @@ -56,11 +52,11 @@ contract MiddlewareProtect is BaseMiddleware { external returns (bytes4) { - swapBlocked = true; + ReentrancyState.lockSwap(); console.log("beforeAddLiquidity middleware"); (bool success, bytes memory returnData) = implementation.delegatecall{gas: gasLimit}(msg.data); require(success); - swapBlocked = false; + ReentrancyState.unlock(); return abi.decode(returnData, (bytes4)); } @@ -72,11 +68,11 @@ contract MiddlewareProtect is BaseMiddleware { PoolKey calldata, IPoolManager.ModifyLiquidityParams calldata, bytes calldata - ) external removeNotBlocked returns (bytes4) { - swapBlocked = true; + ) external removeNotLocked returns (bytes4) { + ReentrancyState.lockSwap(); console.log("beforeRemoveLiquidity middleware"); implementation.delegatecall{gas: gasLimit}(msg.data); - swapBlocked = false; + ReentrancyState.unlock(); return BaseHook.beforeRemoveLiquidity.selector; } @@ -91,4 +87,4 @@ contract MiddlewareProtect is BaseMiddleware { implementation.delegatecall{gas: gasLimit}(msg.data); return (BaseHook.afterRemoveLiquidity.selector, BalanceDeltaLibrary.ZERO_DELTA); } -} +} \ No newline at end of file diff --git a/lib/forge-gas-snapshot b/lib/forge-gas-snapshot index 2f884282..9161f7c0 160000 --- a/lib/forge-gas-snapshot +++ b/lib/forge-gas-snapshot @@ -1 +1 @@ -Subproject commit 2f884282b4cd067298e798974f5b534288b13bc2 +Subproject commit 9161f7c0b6c6788a89081e2b3b9c67592b71e689 diff --git a/lib/forge-std b/lib/forge-std index 2b58ecbc..75b3fcf0 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 2b58ecbcf3dfde7a75959dc7b4eb3d0670278de6 +Subproject commit 75b3fcf052cc7886327e4c2eac3d1a1f36942b41 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index 337bfd5e..5ae63068 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 337bfd5ea4df9f7ebc755cd3cb4ecb3bd3d33fc7 +Subproject commit 5ae630684a0f57de400ef69499addab4c32ac8fb From 5f77dcadfdde993fce2100491bd4bc0600cae434 Mon Sep 17 00:00:00 2001 From: Junion <69495294+Jun1on@users.noreply.github.com> Date: Tue, 2 Jul 2024 01:31:58 -0400 Subject: [PATCH 21/25] start frontrun test hook --- contracts/middleware/BaseMiddleware.sol | 4 +- contracts/middleware/MiddlewareProtect.sol | 3 +- .../middleware/MiddlewareProtectFactory.sol | 26 +++++ test/MiddlewareProtectFactory.t.sol | 101 ++++++++++++++++++ test/MiddlewareRemove.t.sol | 4 +- test/MiddlewareRemoveFactory.t.sol | 1 - test/middleware/HookFrontrun.sol | 61 +++++++++++ .../MiddlewareRemoveImplementation.sol | 19 ---- 8 files changed, 192 insertions(+), 27 deletions(-) create mode 100644 contracts/middleware/MiddlewareProtectFactory.sol create mode 100644 test/MiddlewareProtectFactory.t.sol create mode 100644 test/middleware/HookFrontrun.sol delete mode 100644 test/shared/implementation/MiddlewareRemoveImplementation.sol diff --git a/contracts/middleware/BaseMiddleware.sol b/contracts/middleware/BaseMiddleware.sol index ce4a7004..ebe5c609 100644 --- a/contracts/middleware/BaseMiddleware.sol +++ b/contracts/middleware/BaseMiddleware.sol @@ -8,7 +8,6 @@ import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; import {Proxy} from "@openzeppelin/contracts/proxy/Proxy.sol"; -import {console} from "../../lib/forge-std/src/console.sol"; contract BaseMiddleware is Proxy { /// @notice The address of the pool manager @@ -21,11 +20,10 @@ contract BaseMiddleware is Proxy { } function _implementation() internal view override returns (address) { - console.logAddress(implementation); return implementation; } - // yo how do i remove this warning + // yo i wanna delete this function but how do i remove this warning receive() external payable { _delegate(_implementation()); } diff --git a/contracts/middleware/MiddlewareProtect.sol b/contracts/middleware/MiddlewareProtect.sol index 0ee8a20d..1e800878 100644 --- a/contracts/middleware/MiddlewareProtect.sol +++ b/contracts/middleware/MiddlewareProtect.sol @@ -81,10 +81,11 @@ contract MiddlewareProtect is BaseMiddleware { address, PoolKey calldata, IPoolManager.ModifyLiquidityParams calldata, + BalanceDelta, bytes calldata ) external returns (bytes4, BalanceDelta) { console.log("afterRemoveLiquidity middleware"); implementation.delegatecall{gas: gasLimit}(msg.data); return (BaseHook.afterRemoveLiquidity.selector, BalanceDeltaLibrary.ZERO_DELTA); } -} \ No newline at end of file +} diff --git a/contracts/middleware/MiddlewareProtectFactory.sol b/contracts/middleware/MiddlewareProtectFactory.sol new file mode 100644 index 00000000..3287a629 --- /dev/null +++ b/contracts/middleware/MiddlewareProtectFactory.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {IMiddlewareFactory} from "../interfaces/IMiddlewareFactory.sol"; +import {MiddlewareProtect} from "./MiddlewareProtect.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; + +contract MiddlewareProtectFactory is IMiddlewareFactory { + mapping(address => address) private _implementations; + + IPoolManager public immutable poolManager; + + constructor(IPoolManager _poolManager) { + poolManager = _poolManager; + } + + 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 = address(new MiddlewareProtect{salt: salt}(poolManager, implementation)); + _implementations[middleware] = implementation; + emit MiddlewareCreated(implementation, middleware); + } +} diff --git a/test/MiddlewareProtectFactory.t.sol b/test/MiddlewareProtectFactory.t.sol new file mode 100644 index 00000000..b5a472ac --- /dev/null +++ b/test/MiddlewareProtectFactory.t.sol @@ -0,0 +1,101 @@ +// 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 {FeeTakingLite} from "./middleware/FeeTakingLite.sol"; +import {MiddlewareProtect} from "../contracts/middleware/MiddlewareProtect.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.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 {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.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 {HooksRevert} from "./middleware/HooksRevert.sol"; +import {HooksOutOfGas} from "./middleware/HooksOutOfGas.sol"; +import {MiddlewareProtectFactory} from "./../contracts/middleware/MiddlewareProtectFactory.sol"; +import {HookMiner} from "./utils/HookMiner.sol"; + +contract MiddlewareProtectFactoryTest is Test, Deployers { + using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; + + uint160 constant SQRT_RATIO_10_1 = 250541448375047931186413801569; + + address constant TREASURY = address(0x1234567890123456789012345678901234567890); + uint128 private constant TOTAL_BIPS = 10000; + + HookEnabledSwapRouter router; + TestERC20 token0; + TestERC20 token1; + PoolId id; + + MiddlewareProtectFactory factory; + + function setUp() public { + deployFreshManagerAndRouters(); + (currency0, currency1) = deployMintAndApprove2Currencies(); + + router = new HookEnabledSwapRouter(manager); + token0 = TestERC20(Currency.unwrap(currency0)); + token1 = TestERC20(Currency.unwrap(currency1)); + + token0.approve(address(router), type(uint256).max); + token1.approve(address(router), type(uint256).max); + + factory = new MiddlewareProtectFactory(manager); + } + + function testVariousProtectFactory() public { + FeeTakingLite feeTakingLite = new FeeTakingLite(manager); + uint160 flags = + uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG); + (address hookAddress, bytes32 salt) = HookMiner.find( + address(factory), + flags, + type(MiddlewareProtect).creationCode, + abi.encode(address(manager), address(feeTakingLite)) + ); + testOn(address(feeTakingLite), salt); + + HooksRevert hooksRevert = new HooksRevert(manager); + flags = uint160(Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG); + (hookAddress, salt) = HookMiner.find( + address(factory), + flags, + type(MiddlewareProtect).creationCode, + abi.encode(address(manager), address(hooksRevert)) + ); + testOn(address(hooksRevert), salt); + + HooksOutOfGas hooksOutOfGas = new HooksOutOfGas(manager); + flags = uint160(Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG); + (hookAddress, salt) = HookMiner.find( + address(factory), + flags, + type(MiddlewareProtect).creationCode, + abi.encode(address(manager), address(hooksOutOfGas)) + ); + testOn(address(hooksOutOfGas), salt); + } + + // creates a middleware on an implementation + function testOn(address implementation, bytes32 salt) internal { + address hookAddress = factory.createMiddleware(implementation, salt); + MiddlewareProtect middlewareProtect = MiddlewareProtect(payable(hookAddress)); + + (key, id) = initPoolAndAddLiquidity( + currency0, currency1, IHooks(address(middlewareProtect)), 3000, SQRT_PRICE_1_1, ZERO_BYTES + ); + + removeLiquidity(currency0, currency1, IHooks(address(middlewareProtect)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + + assertEq(factory.getImplementation(hookAddress), implementation); + } +} diff --git a/test/MiddlewareRemove.t.sol b/test/MiddlewareRemove.t.sol index 6bafa59b..0ecae3f5 100644 --- a/test/MiddlewareRemove.t.sol +++ b/test/MiddlewareRemove.t.sol @@ -5,7 +5,6 @@ import {Test} from "forge-std/Test.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {FeeTakingLite} from "./middleware/FeeTakingLite.sol"; import {MiddlewareRemove} from "../contracts/middleware/MiddlewareRemove.sol"; -import {MiddlewareRemoveImplementation} from "./shared/implementation/MiddlewareRemoveImplementation.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; @@ -70,8 +69,7 @@ contract MiddlewareRemoveTest is Test, Deployers { MiddlewareRemove middlewareRemove = MiddlewareRemove(payable(address(nonce << 20 | flags))); nonce++; vm.record(); - MiddlewareRemoveImplementation impl = - new MiddlewareRemoveImplementation(manager, implementation, middlewareRemove); + MiddlewareRemove impl = new MiddlewareRemove(manager, implementation); (, bytes32[] memory writes) = vm.accesses(address(impl)); vm.etch(address(middlewareRemove), address(impl).code); unchecked { diff --git a/test/MiddlewareRemoveFactory.t.sol b/test/MiddlewareRemoveFactory.t.sol index d769e077..dea6b7ae 100644 --- a/test/MiddlewareRemoveFactory.t.sol +++ b/test/MiddlewareRemoveFactory.t.sol @@ -5,7 +5,6 @@ import {Test} from "forge-std/Test.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {FeeTakingLite} from "./middleware/FeeTakingLite.sol"; import {MiddlewareRemove} from "../contracts/middleware/MiddlewareRemove.sol"; -import {MiddlewareRemoveImplementation} from "./shared/implementation/MiddlewareRemoveImplementation.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; diff --git a/test/middleware/HookFrontrun.sol b/test/middleware/HookFrontrun.sol new file mode 100644 index 00000000..615dfb8b --- /dev/null +++ b/test/middleware/HookFrontrun.sol @@ -0,0 +1,61 @@ +// 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 {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; + +abstract contract FeeTaker is BaseHook { + using SafeCast for uint256; + + bytes internal constant ZERO_BYTES = bytes(""); + + constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + + function getHookPermissions() public pure virtual override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + afterAddLiquidity: false, + beforeRemoveLiquidity: false, + afterRemoveLiquidity: false, + beforeSwap: true, + afterSwap: true, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } + + function afterSwap( + address sender, + PoolKey calldata key, + IPoolManager.SwapParams calldata params, + BalanceDelta delta, + bytes calldata hookData + ) external override onlyByManager returns (bytes4, int128) { + //(Currency currencyUnspecified, amountUnspecified) = key.getUnspecified(params); + + // fee will be in the unspecified token of the swap + bool currency0Specified = (params.amountSpecified < 0 == params.zeroForOne); + (Currency currencyUnspecified, int128 amountUnspecified) = + (currency0Specified) ? (key.currency1, delta.amount1()) : (key.currency0, delta.amount0()); + // if exactOutput swap, get the absolute output amount + if (amountUnspecified < 0) amountUnspecified = -amountUnspecified; + + uint256 feeAmount = _feeAmount(amountUnspecified); + // mint ERC6909 instead of take to avoid edge case where PM doesn't have enough balance + manager.mint(address(this), CurrencyLibrary.toId(currencyUnspecified), feeAmount); + + (bytes4 selector, int128 amount) = _afterSwap(sender, key, params, delta, hookData); + return (selector, feeAmount.toInt128() + amount); + } +} diff --git a/test/shared/implementation/MiddlewareRemoveImplementation.sol b/test/shared/implementation/MiddlewareRemoveImplementation.sol deleted file mode 100644 index 33a190ac..00000000 --- a/test/shared/implementation/MiddlewareRemoveImplementation.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {BaseHook} from "../../../contracts/BaseHook.sol"; -import {MiddlewareRemove} from "../../../contracts/middleware/MiddlewareRemove.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; - -contract MiddlewareRemoveImplementation is MiddlewareRemove { - constructor(IPoolManager _poolManager, address _implementation, MiddlewareRemove addressToEtch) - MiddlewareRemove(_poolManager, _implementation) - { - //Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); - } - - // make this a no-op in testing - //function validateHookAddress(BaseHook _this) internal pure override {} -} From fed88573768fcf48a1ada224db1687ac57375d76 Mon Sep 17 00:00:00 2001 From: Sara Reynolds <30504811+snreynolds@users.noreply.github.com> Date: Tue, 18 Jun 2024 11:53:46 -0400 Subject: [PATCH 22/25] add safecallback (#123) * add safecallback * use manager --- .../FullRangeAddInitialLiquidity.snap | 2 +- .forge-snapshots/FullRangeAddLiquidity.snap | 2 +- .forge-snapshots/FullRangeFirstSwap.snap | 2 +- .forge-snapshots/FullRangeInitialize.snap | 2 +- .../FullRangeRemoveLiquidity.snap | 2 +- .../FullRangeRemoveLiquidityAndRebalance.snap | 2 +- .forge-snapshots/FullRangeSecondSwap.snap | 2 +- .forge-snapshots/FullRangeSwap.snap | 2 +- .forge-snapshots/TWAMMSubmitOrder.snap | 2 +- contracts/BaseHook.sol | 19 ++---- contracts/base/ImmutableState.sol | 12 ++++ contracts/base/SafeCallback.sol | 22 +++++++ contracts/hooks/examples/FullRange.sol | 45 ++++++-------- contracts/hooks/examples/GeomeanOracle.sol | 24 ++++---- contracts/hooks/examples/LimitOrder.sol | 50 +++++++-------- contracts/hooks/examples/TWAMM.sol | 61 +++++++++---------- contracts/hooks/examples/VolatilityOracle.sol | 4 +- 17 files changed, 136 insertions(+), 119 deletions(-) create mode 100644 contracts/base/ImmutableState.sol create mode 100644 contracts/base/SafeCallback.sol diff --git a/.forge-snapshots/FullRangeAddInitialLiquidity.snap b/.forge-snapshots/FullRangeAddInitialLiquidity.snap index cd1e3c37..404cf12a 100644 --- a/.forge-snapshots/FullRangeAddInitialLiquidity.snap +++ b/.forge-snapshots/FullRangeAddInitialLiquidity.snap @@ -1 +1 @@ -311073 \ No newline at end of file +311181 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeAddLiquidity.snap b/.forge-snapshots/FullRangeAddLiquidity.snap index b3de2b4e..a4a14676 100644 --- a/.forge-snapshots/FullRangeAddLiquidity.snap +++ b/.forge-snapshots/FullRangeAddLiquidity.snap @@ -1 +1 @@ -122882 \ No newline at end of file +122990 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeFirstSwap.snap b/.forge-snapshots/FullRangeFirstSwap.snap index 54d0d097..da120795 100644 --- a/.forge-snapshots/FullRangeFirstSwap.snap +++ b/.forge-snapshots/FullRangeFirstSwap.snap @@ -1 +1 @@ -80283 \ No newline at end of file +80220 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeInitialize.snap b/.forge-snapshots/FullRangeInitialize.snap index f81651f8..7a0170eb 100644 --- a/.forge-snapshots/FullRangeInitialize.snap +++ b/.forge-snapshots/FullRangeInitialize.snap @@ -1 +1 @@ -1015169 \ No newline at end of file +1015181 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeRemoveLiquidity.snap b/.forge-snapshots/FullRangeRemoveLiquidity.snap index 265c1dec..feea4936 100644 --- a/.forge-snapshots/FullRangeRemoveLiquidity.snap +++ b/.forge-snapshots/FullRangeRemoveLiquidity.snap @@ -1 +1 @@ -110476 \ No newline at end of file +110566 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap b/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap index dcb62527..e0df7eb7 100644 --- a/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap +++ b/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap @@ -1 +1 @@ -239954 \ No newline at end of file +240044 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeSecondSwap.snap b/.forge-snapshots/FullRangeSecondSwap.snap index 1bf183ef..e68df8d3 100644 --- a/.forge-snapshots/FullRangeSecondSwap.snap +++ b/.forge-snapshots/FullRangeSecondSwap.snap @@ -1 +1 @@ -45993 \ No newline at end of file +45930 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeSwap.snap b/.forge-snapshots/FullRangeSwap.snap index 5630ac05..b50d0ea2 100644 --- a/.forge-snapshots/FullRangeSwap.snap +++ b/.forge-snapshots/FullRangeSwap.snap @@ -1 +1 @@ -79414 \ No newline at end of file +79351 \ No newline at end of file diff --git a/.forge-snapshots/TWAMMSubmitOrder.snap b/.forge-snapshots/TWAMMSubmitOrder.snap index b2759d7f..eb3b0f6b 100644 --- a/.forge-snapshots/TWAMMSubmitOrder.snap +++ b/.forge-snapshots/TWAMMSubmitOrder.snap @@ -1 +1 @@ -122355 \ No newline at end of file +122336 \ No newline at end of file diff --git a/contracts/BaseHook.sol b/contracts/BaseHook.sol index eb75502c..01fc4954 100644 --- a/contracts/BaseHook.sol +++ b/contracts/BaseHook.sol @@ -7,28 +7,19 @@ import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; +import {SafeCallback} from "./base/SafeCallback.sol"; +import {ImmutableState} from "./base/ImmutableState.sol"; -abstract contract BaseHook is IHooks { - error NotPoolManager(); +abstract contract BaseHook is IHooks, SafeCallback { error NotSelf(); error InvalidPool(); error LockFailure(); error HookNotImplemented(); - /// @notice The address of the pool manager - IPoolManager public immutable poolManager; - - constructor(IPoolManager _poolManager) { - poolManager = _poolManager; + constructor(IPoolManager _manager) ImmutableState(_manager) { validateHookAddress(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(); @@ -50,7 +41,7 @@ abstract contract BaseHook is IHooks { Hooks.validateHookPermissions(_this, getHookPermissions()); } - function unlockCallback(bytes calldata data) external virtual poolManagerOnly returns (bytes memory) { + function _unlockCallback(bytes calldata data) internal virtual override returns (bytes memory) { (bool success, bytes memory returnData) = address(this).call(data); if (success) return returnData; if (returnData.length == 0) revert LockFailure(); diff --git a/contracts/base/ImmutableState.sol b/contracts/base/ImmutableState.sol new file mode 100644 index 00000000..cce37514 --- /dev/null +++ b/contracts/base/ImmutableState.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.19; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; + +contract ImmutableState { + IPoolManager public immutable manager; + + constructor(IPoolManager _manager) { + manager = _manager; + } +} diff --git a/contracts/base/SafeCallback.sol b/contracts/base/SafeCallback.sol new file mode 100644 index 00000000..f985e67c --- /dev/null +++ b/contracts/base/SafeCallback.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {ImmutableState} from "./ImmutableState.sol"; + +abstract contract SafeCallback is ImmutableState, IUnlockCallback { + error NotManager(); + + modifier onlyByManager() { + if (msg.sender != address(manager)) revert NotManager(); + _; + } + + /// @dev We force the onlyByManager modifier by exposing a virtual function after the onlyByManager check. + function unlockCallback(bytes calldata data) external onlyByManager returns (bytes memory) { + return _unlockCallback(data); + } + + function _unlockCallback(bytes calldata data) internal virtual returns (bytes memory); +} diff --git a/contracts/hooks/examples/FullRange.sol b/contracts/hooks/examples/FullRange.sol index 194be803..191593b8 100644 --- a/contracts/hooks/examples/FullRange.sol +++ b/contracts/hooks/examples/FullRange.sol @@ -26,7 +26,7 @@ import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/type import "../../libraries/LiquidityAmounts.sol"; -contract FullRange is BaseHook, IUnlockCallback { +contract FullRange is BaseHook { using CurrencyLibrary for Currency; using CurrencySettler for Currency; using PoolIdLibrary for PoolKey; @@ -85,7 +85,7 @@ contract FullRange is BaseHook, IUnlockCallback { mapping(PoolId => PoolInfo) public poolInfo; - constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + constructor(IPoolManager _manager) BaseHook(_manager) {} modifier ensure(uint256 deadline) { if (deadline < block.timestamp) revert ExpiredPastDeadline(); @@ -126,13 +126,13 @@ contract FullRange is BaseHook, IUnlockCallback { PoolId poolId = key.toId(); - (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(poolId); + (uint160 sqrtPriceX96,,,) = manager.getSlot0(poolId); if (sqrtPriceX96 == 0) revert PoolNotInitialized(); PoolInfo storage pool = poolInfo[poolId]; - uint128 poolLiquidity = poolManager.getLiquidity(poolId); + uint128 poolLiquidity = manager.getLiquidity(poolId); liquidity = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, @@ -184,7 +184,7 @@ contract FullRange is BaseHook, IUnlockCallback { PoolId poolId = key.toId(); - (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(poolId); + (uint160 sqrtPriceX96,,,) = manager.getSlot0(poolId); if (sqrtPriceX96 == 0) revert PoolNotInitialized(); @@ -260,17 +260,17 @@ contract FullRange is BaseHook, IUnlockCallback { internal returns (BalanceDelta delta) { - delta = abi.decode(poolManager.unlock(abi.encode(CallbackData(msg.sender, key, params))), (BalanceDelta)); + delta = abi.decode(manager.unlock(abi.encode(CallbackData(msg.sender, key, params))), (BalanceDelta)); } function _settleDeltas(address sender, PoolKey memory key, BalanceDelta delta) internal { - key.currency0.settle(poolManager, sender, uint256(int256(-delta.amount0())), false); - key.currency1.settle(poolManager, sender, uint256(int256(-delta.amount1())), false); + key.currency0.settle(manager, sender, uint256(int256(-delta.amount0())), false); + key.currency1.settle(manager, sender, uint256(int256(-delta.amount1())), false); } function _takeDeltas(address sender, PoolKey memory key, BalanceDelta delta) internal { - poolManager.take(key.currency0, sender, uint256(uint128(delta.amount0()))); - poolManager.take(key.currency1, sender, uint256(uint128(delta.amount1()))); + manager.take(key.currency0, sender, uint256(uint128(delta.amount0()))); + manager.take(key.currency1, sender, uint256(uint128(delta.amount1()))); } function _removeLiquidity(PoolKey memory key, IPoolManager.ModifyLiquidityParams memory params) @@ -286,21 +286,16 @@ contract FullRange is BaseHook, IUnlockCallback { uint256 liquidityToRemove = FullMath.mulDiv( uint256(-params.liquidityDelta), - poolManager.getLiquidity(poolId), + manager.getLiquidity(poolId), UniswapV4ERC20(pool.liquidityToken).totalSupply() ); params.liquidityDelta = -(liquidityToRemove.toInt256()); - (delta,) = poolManager.modifyLiquidity(key, params, ZERO_BYTES); + (delta,) = manager.modifyLiquidity(key, params, ZERO_BYTES); pool.hasAccruedFees = false; } - function unlockCallback(bytes calldata rawData) - external - override(IUnlockCallback, BaseHook) - poolManagerOnly - returns (bytes memory) - { + function _unlockCallback(bytes calldata rawData) internal override returns (bytes memory) { CallbackData memory data = abi.decode(rawData, (CallbackData)); BalanceDelta delta; @@ -308,7 +303,7 @@ contract FullRange is BaseHook, IUnlockCallback { delta = _removeLiquidity(data.key, data.params); _takeDeltas(data.sender, data.key, delta); } else { - (delta,) = poolManager.modifyLiquidity(data.key, data.params, ZERO_BYTES); + (delta,) = manager.modifyLiquidity(data.key, data.params, ZERO_BYTES); _settleDeltas(data.sender, data.key, delta); } return abi.encode(delta); @@ -316,12 +311,12 @@ contract FullRange is BaseHook, IUnlockCallback { function _rebalance(PoolKey memory key) public { PoolId poolId = key.toId(); - (BalanceDelta balanceDelta,) = poolManager.modifyLiquidity( + (BalanceDelta balanceDelta,) = manager.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams({ tickLower: MIN_TICK, tickUpper: MAX_TICK, - liquidityDelta: -(poolManager.getLiquidity(poolId).toInt256()), + liquidityDelta: -(manager.getLiquidity(poolId).toInt256()), salt: 0 }), ZERO_BYTES @@ -333,9 +328,9 @@ contract FullRange is BaseHook, IUnlockCallback { ) * FixedPointMathLib.sqrt(FixedPoint96.Q96) ).toUint160(); - (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(poolId); + (uint160 sqrtPriceX96,,,) = manager.getSlot0(poolId); - poolManager.swap( + manager.swap( key, IPoolManager.SwapParams({ zeroForOne: newSqrtPriceX96 < sqrtPriceX96, @@ -353,7 +348,7 @@ contract FullRange is BaseHook, IUnlockCallback { uint256(uint128(balanceDelta.amount1())) ); - (BalanceDelta balanceDeltaAfter,) = poolManager.modifyLiquidity( + (BalanceDelta balanceDeltaAfter,) = manager.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams({ tickLower: MIN_TICK, @@ -368,6 +363,6 @@ contract FullRange is BaseHook, IUnlockCallback { uint128 donateAmount0 = uint128(balanceDelta.amount0() + balanceDeltaAfter.amount0()); uint128 donateAmount1 = uint128(balanceDelta.amount1() + balanceDeltaAfter.amount1()); - poolManager.donate(key, donateAmount0, donateAmount1, ZERO_BYTES); + manager.donate(key, donateAmount0, donateAmount1, ZERO_BYTES); } } diff --git a/contracts/hooks/examples/GeomeanOracle.sol b/contracts/hooks/examples/GeomeanOracle.sol index ec8301a5..df5a9ad1 100644 --- a/contracts/hooks/examples/GeomeanOracle.sol +++ b/contracts/hooks/examples/GeomeanOracle.sol @@ -61,7 +61,7 @@ contract GeomeanOracle is BaseHook { return uint32(block.timestamp); } - constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + constructor(IPoolManager _manager) BaseHook(_manager) {} function getHookPermissions() public pure override returns (Hooks.Permissions memory) { return Hooks.Permissions({ @@ -86,20 +86,20 @@ contract GeomeanOracle is BaseHook { external view override - poolManagerOnly + onlyByManager returns (bytes4) { // This is to limit the fragmentation of pools using this oracle hook. In other words, // there may only be one pool per pair of tokens that use this hook. The tick spacing is set to the maximum // because we only allow max range liquidity in this pool. - if (key.fee != 0 || key.tickSpacing != poolManager.MAX_TICK_SPACING()) revert OnlyOneOraclePoolAllowed(); + if (key.fee != 0 || key.tickSpacing != manager.MAX_TICK_SPACING()) revert OnlyOneOraclePoolAllowed(); return GeomeanOracle.beforeInitialize.selector; } function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata) external override - poolManagerOnly + onlyByManager returns (bytes4) { PoolId id = key.toId(); @@ -110,9 +110,9 @@ contract GeomeanOracle is BaseHook { /// @dev Called before any action that potentially modifies pool price or liquidity, such as swap or modify position function _updatePool(PoolKey calldata key) private { PoolId id = key.toId(); - (, int24 tick,,) = poolManager.getSlot0(id); + (, int24 tick,,) = manager.getSlot0(id); - uint128 liquidity = poolManager.getLiquidity(id); + uint128 liquidity = manager.getLiquidity(id); (states[id].index, states[id].cardinality) = observations[id].write( states[id].index, _blockTimestamp(), tick, liquidity, states[id].cardinality, states[id].cardinalityNext @@ -124,8 +124,8 @@ contract GeomeanOracle is BaseHook { PoolKey calldata key, IPoolManager.ModifyLiquidityParams calldata params, bytes calldata - ) external override poolManagerOnly returns (bytes4) { - int24 maxTickSpacing = poolManager.MAX_TICK_SPACING(); + ) external override onlyByManager returns (bytes4) { + int24 maxTickSpacing = manager.MAX_TICK_SPACING(); if ( params.tickLower != TickMath.minUsableTick(maxTickSpacing) || params.tickUpper != TickMath.maxUsableTick(maxTickSpacing) @@ -139,14 +139,14 @@ contract GeomeanOracle is BaseHook { PoolKey calldata, IPoolManager.ModifyLiquidityParams calldata, bytes calldata - ) external view override poolManagerOnly returns (bytes4) { + ) external view override onlyByManager returns (bytes4) { revert OraclePoolMustLockLiquidity(); } function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) external override - poolManagerOnly + onlyByManager returns (bytes4, BeforeSwapDelta, uint24) { _updatePool(key); @@ -163,9 +163,9 @@ contract GeomeanOracle is BaseHook { ObservationState memory state = states[id]; - (, int24 tick,,) = poolManager.getSlot0(id); + (, int24 tick,,) = manager.getSlot0(id); - uint128 liquidity = poolManager.getLiquidity(id); + uint128 liquidity = manager.getLiquidity(id); return observations[id].observe(_blockTimestamp(), secondsAgos, tick, state.index, liquidity, state.cardinality); } diff --git a/contracts/hooks/examples/LimitOrder.sol b/contracts/hooks/examples/LimitOrder.sol index 9ee7a33c..2a8ca909 100644 --- a/contracts/hooks/examples/LimitOrder.sol +++ b/contracts/hooks/examples/LimitOrder.sol @@ -75,7 +75,7 @@ contract LimitOrder is BaseHook { mapping(bytes32 => Epoch) public epochs; mapping(Epoch => EpochInfo) public epochInfos; - constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + constructor(IPoolManager _manager) BaseHook(_manager) {} function getHookPermissions() public pure override returns (Hooks.Permissions memory) { return Hooks.Permissions({ @@ -117,7 +117,7 @@ contract LimitOrder is BaseHook { } function getTick(PoolId poolId) private view returns (int24 tick) { - (, tick,,) = poolManager.getSlot0(poolId); + (, tick,,) = manager.getSlot0(poolId); } function getTickLower(int24 tick, int24 tickSpacing) private pure returns (int24) { @@ -129,7 +129,7 @@ contract LimitOrder is BaseHook { function afterInitialize(address, PoolKey calldata key, uint160, int24 tick, bytes calldata) external override - poolManagerOnly + onlyByManager returns (bytes4) { setTickLowerLast(key.toId(), getTickLower(tick, key.tickSpacing)); @@ -142,7 +142,7 @@ contract LimitOrder is BaseHook { IPoolManager.SwapParams calldata params, BalanceDelta, bytes calldata - ) external override poolManagerOnly returns (bytes4, int128) { + ) external override onlyByManager returns (bytes4, int128) { (int24 tickLower, int24 lower, int24 upper) = _getCrossedTicks(key.toId(), key.tickSpacing); if (lower > upper) return (LimitOrder.afterSwap.selector, 0); @@ -197,10 +197,10 @@ contract LimitOrder is BaseHook { function _unlockCallbackFill(PoolKey calldata key, int24 tickLower, int256 liquidityDelta) private - poolManagerOnly + onlyByManager returns (uint128 amount0, uint128 amount1) { - (BalanceDelta delta,) = poolManager.modifyLiquidity( + (BalanceDelta delta,) = manager.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams({ tickLower: tickLower, @@ -212,10 +212,10 @@ contract LimitOrder is BaseHook { ); if (delta.amount0() > 0) { - poolManager.mint(address(this), key.currency0.toId(), amount0 = uint128(delta.amount0())); + manager.mint(address(this), key.currency0.toId(), amount0 = uint128(delta.amount0())); } if (delta.amount1() > 0) { - poolManager.mint(address(this), key.currency1.toId(), amount1 = uint128(delta.amount1())); + manager.mint(address(this), key.currency1.toId(), amount1 = uint128(delta.amount1())); } } @@ -225,7 +225,7 @@ contract LimitOrder is BaseHook { { if (liquidity == 0) revert ZeroLiquidity(); - poolManager.unlock( + manager.unlock( abi.encodeCall( this.unlockCallbackPlace, (key, tickLower, zeroForOne, int256(uint256(liquidity)), msg.sender) ) @@ -263,7 +263,7 @@ contract LimitOrder is BaseHook { int256 liquidityDelta, address owner ) external selfOnly { - (BalanceDelta delta,) = poolManager.modifyLiquidity( + (BalanceDelta delta,) = manager.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams({ tickLower: tickLower, @@ -277,11 +277,11 @@ contract LimitOrder is BaseHook { if (delta.amount0() < 0) { if (delta.amount1() != 0) revert InRange(); if (!zeroForOne) revert CrossedRange(); - key.currency0.settle(poolManager, owner, uint256(uint128(-delta.amount0())), false); + key.currency0.settle(manager, owner, uint256(uint128(-delta.amount0())), false); } else { if (delta.amount0() != 0) revert InRange(); if (zeroForOne) revert CrossedRange(); - key.currency1.settle(poolManager, owner, uint256(uint128(-delta.amount1())), false); + key.currency1.settle(manager, owner, uint256(uint128(-delta.amount1())), false); } } @@ -298,7 +298,7 @@ contract LimitOrder is BaseHook { uint256 amount0Fee; uint256 amount1Fee; (amount0Fee, amount1Fee) = abi.decode( - poolManager.unlock( + manager.unlock( abi.encodeCall( this.unlockCallbackKill, (key, tickLower, -int256(uint256(liquidity)), to, liquidity == epochInfo.liquidityTotal) @@ -329,7 +329,7 @@ contract LimitOrder is BaseHook { // could be unfairly diluted by a user sychronously placing then killing a limit order to skim off fees. // to prevent this, we allocate all fee revenue to remaining limit order placers, unless this is the last order. if (!removingAllLiquidity) { - (, BalanceDelta deltaFee) = poolManager.modifyLiquidity( + (, BalanceDelta deltaFee) = manager.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams({ tickLower: tickLower, @@ -341,14 +341,14 @@ contract LimitOrder is BaseHook { ); if (deltaFee.amount0() > 0) { - poolManager.mint(address(this), key.currency0.toId(), amount0Fee = uint128(deltaFee.amount0())); + manager.mint(address(this), key.currency0.toId(), amount0Fee = uint128(deltaFee.amount0())); } if (deltaFee.amount1() > 0) { - poolManager.mint(address(this), key.currency1.toId(), amount1Fee = uint128(deltaFee.amount1())); + manager.mint(address(this), key.currency1.toId(), amount1Fee = uint128(deltaFee.amount1())); } } - (BalanceDelta delta,) = poolManager.modifyLiquidity( + (BalanceDelta delta,) = manager.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams({ tickLower: tickLower, @@ -360,10 +360,10 @@ contract LimitOrder is BaseHook { ); if (delta.amount0() > 0) { - key.currency0.take(poolManager, to, uint256(uint128(delta.amount0())), false); + key.currency0.take(manager, to, uint256(uint128(delta.amount0())), false); } if (delta.amount1() > 0) { - key.currency1.take(poolManager, to, uint256(uint128(delta.amount1())), false); + key.currency1.take(manager, to, uint256(uint128(delta.amount1())), false); } } @@ -385,7 +385,7 @@ contract LimitOrder is BaseHook { epochInfo.token1Total -= amount1; epochInfo.liquidityTotal = liquidityTotal - liquidity; - poolManager.unlock( + manager.unlock( abi.encodeCall( this.unlockCallbackWithdraw, (epochInfo.currency0, epochInfo.currency1, amount0, amount1, to) ) @@ -402,17 +402,17 @@ contract LimitOrder is BaseHook { address to ) external selfOnly { if (token0Amount > 0) { - poolManager.burn(address(this), currency0.toId(), token0Amount); - poolManager.take(currency0, to, token0Amount); + manager.burn(address(this), currency0.toId(), token0Amount); + manager.take(currency0, to, token0Amount); } if (token1Amount > 0) { - poolManager.burn(address(this), currency1.toId(), token1Amount); - poolManager.take(currency1, to, token1Amount); + manager.burn(address(this), currency1.toId(), token1Amount); + manager.take(currency1, to, token1Amount); } } function onERC1155Received(address, address, uint256, uint256, bytes calldata) external view returns (bytes4) { - if (msg.sender != address(poolManager)) revert NotPoolManagerToken(); + if (msg.sender != address(manager)) revert NotPoolManagerToken(); return IERC1155Receiver.onERC1155Received.selector; } } diff --git a/contracts/hooks/examples/TWAMM.sol b/contracts/hooks/examples/TWAMM.sol index 5704d765..dc1f3b00 100644 --- a/contracts/hooks/examples/TWAMM.sol +++ b/contracts/hooks/examples/TWAMM.sol @@ -61,7 +61,7 @@ contract TWAMM is BaseHook, ITWAMM { // tokensOwed[token][owner] => amountOwed mapping(Currency => mapping(address => uint256)) public tokensOwed; - constructor(IPoolManager _poolManager, uint256 _expirationInterval) BaseHook(_poolManager) { + constructor(IPoolManager _manager, uint256 _expirationInterval) BaseHook(_manager) { expirationInterval = _expirationInterval; } @@ -88,7 +88,7 @@ contract TWAMM is BaseHook, ITWAMM { external virtual override - poolManagerOnly + onlyByManager returns (bytes4) { // one-time initialization enforced in PoolManager @@ -101,7 +101,7 @@ contract TWAMM is BaseHook, ITWAMM { PoolKey calldata key, IPoolManager.ModifyLiquidityParams calldata, bytes calldata - ) external override poolManagerOnly returns (bytes4) { + ) external override onlyByManager returns (bytes4) { executeTWAMMOrders(key); return BaseHook.beforeAddLiquidity.selector; } @@ -109,7 +109,7 @@ contract TWAMM is BaseHook, ITWAMM { function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) external override - poolManagerOnly + onlyByManager returns (bytes4, BeforeSwapDelta, uint24) { executeTWAMMOrders(key); @@ -143,17 +143,14 @@ contract TWAMM is BaseHook, ITWAMM { /// @inheritdoc ITWAMM function executeTWAMMOrders(PoolKey memory key) public { PoolId poolId = key.toId(); - (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(poolId); + (uint160 sqrtPriceX96,,,) = manager.getSlot0(poolId); State storage twamm = twammStates[poolId]; - (bool zeroForOne, uint160 sqrtPriceLimitX96) = _executeTWAMMOrders( - twamm, poolManager, key, PoolParamsOnExecute(sqrtPriceX96, poolManager.getLiquidity(poolId)) - ); + (bool zeroForOne, uint160 sqrtPriceLimitX96) = + _executeTWAMMOrders(twamm, manager, key, PoolParamsOnExecute(sqrtPriceX96, manager.getLiquidity(poolId))); if (sqrtPriceLimitX96 != 0 && sqrtPriceLimitX96 != sqrtPriceX96) { - poolManager.unlock( - abi.encode(key, IPoolManager.SwapParams(zeroForOne, type(int256).max, sqrtPriceLimitX96)) - ); + manager.unlock(abi.encode(key, IPoolManager.SwapParams(zeroForOne, type(int256).max, sqrtPriceLimitX96))); } } @@ -309,25 +306,25 @@ contract TWAMM is BaseHook, ITWAMM { IERC20Minimal(Currency.unwrap(token)).safeTransfer(to, amountTransferred); } - function unlockCallback(bytes calldata rawData) external override poolManagerOnly returns (bytes memory) { + function _unlockCallback(bytes calldata rawData) internal override returns (bytes memory) { (PoolKey memory key, IPoolManager.SwapParams memory swapParams) = abi.decode(rawData, (PoolKey, IPoolManager.SwapParams)); - BalanceDelta delta = poolManager.swap(key, swapParams, ZERO_BYTES); + BalanceDelta delta = manager.swap(key, swapParams, ZERO_BYTES); if (swapParams.zeroForOne) { if (delta.amount0() < 0) { - key.currency0.settle(poolManager, address(this), uint256(uint128(-delta.amount0())), false); + key.currency0.settle(manager, address(this), uint256(uint128(-delta.amount0())), false); } if (delta.amount1() > 0) { - key.currency1.take(poolManager, address(this), uint256(uint128(delta.amount1())), false); + key.currency1.take(manager, address(this), uint256(uint128(delta.amount1())), false); } } else { if (delta.amount1() < 0) { - key.currency1.settle(poolManager, address(this), uint256(uint128(-delta.amount1())), false); + key.currency1.settle(manager, address(this), uint256(uint128(-delta.amount1())), false); } if (delta.amount0() > 0) { - key.currency0.take(poolManager, address(this), uint256(uint128(delta.amount0())), false); + key.currency0.take(manager, address(this), uint256(uint128(delta.amount0())), false); } } return bytes(""); @@ -346,7 +343,7 @@ contract TWAMM is BaseHook, ITWAMM { /// @param pool The relevant state of the pool function _executeTWAMMOrders( State storage self, - IPoolManager poolManager, + IPoolManager manager, PoolKey memory key, PoolParamsOnExecute memory pool ) internal returns (bool zeroForOne, uint160 newSqrtPriceX96) { @@ -371,7 +368,7 @@ contract TWAMM is BaseHook, ITWAMM { if (orderPool0For1.sellRateCurrent != 0 && orderPool1For0.sellRateCurrent != 0) { pool = _advanceToNewTimestamp( self, - poolManager, + manager, key, AdvanceParams( expirationInterval, @@ -383,7 +380,7 @@ contract TWAMM is BaseHook, ITWAMM { } else { pool = _advanceTimestampForSinglePoolSell( self, - poolManager, + manager, key, AdvanceSingleParams( expirationInterval, @@ -405,14 +402,14 @@ contract TWAMM is BaseHook, ITWAMM { if (orderPool0For1.sellRateCurrent != 0 && orderPool1For0.sellRateCurrent != 0) { pool = _advanceToNewTimestamp( self, - poolManager, + manager, key, AdvanceParams(expirationInterval, block.timestamp, block.timestamp - prevTimestamp, pool) ); } else { pool = _advanceTimestampForSinglePoolSell( self, - poolManager, + manager, key, AdvanceSingleParams( expirationInterval, @@ -440,7 +437,7 @@ contract TWAMM is BaseHook, ITWAMM { function _advanceToNewTimestamp( State storage self, - IPoolManager poolManager, + IPoolManager manager, PoolKey memory poolKey, AdvanceParams memory params ) private returns (PoolParamsOnExecute memory) { @@ -462,13 +459,13 @@ contract TWAMM is BaseHook, ITWAMM { finalSqrtPriceX96 = TwammMath.getNewSqrtPriceX96(executionParams); (bool crossingInitializedTick, int24 tick) = - _isCrossingInitializedTick(params.pool, poolManager, poolKey, finalSqrtPriceX96); + _isCrossingInitializedTick(params.pool, manager, poolKey, finalSqrtPriceX96); unchecked { if (crossingInitializedTick) { uint256 secondsUntilCrossingX96; (params.pool, secondsUntilCrossingX96) = _advanceTimeThroughTickCrossing( self, - poolManager, + manager, poolKey, TickCrossingParams(tick, params.nextTimestamp, secondsElapsedX96, params.pool) ); @@ -503,7 +500,7 @@ contract TWAMM is BaseHook, ITWAMM { function _advanceTimestampForSinglePoolSell( State storage self, - IPoolManager poolManager, + IPoolManager manager, PoolKey memory poolKey, AdvanceSingleParams memory params ) private returns (PoolParamsOnExecute memory) { @@ -518,10 +515,10 @@ contract TWAMM is BaseHook, ITWAMM { ); (bool crossingInitializedTick, int24 tick) = - _isCrossingInitializedTick(params.pool, poolManager, poolKey, finalSqrtPriceX96); + _isCrossingInitializedTick(params.pool, manager, poolKey, finalSqrtPriceX96); if (crossingInitializedTick) { - (, int128 liquidityNetAtTick) = poolManager.getTickLiquidity(poolKey.toId(), tick); + (, int128 liquidityNetAtTick) = manager.getTickLiquidity(poolKey.toId(), tick); uint160 initializedSqrtPrice = TickMath.getSqrtPriceAtTick(tick); uint256 swapDelta0 = SqrtPriceMath.getAmount0Delta( @@ -575,7 +572,7 @@ contract TWAMM is BaseHook, ITWAMM { function _advanceTimeThroughTickCrossing( State storage self, - IPoolManager poolManager, + IPoolManager manager, PoolKey memory poolKey, TickCrossingParams memory params ) private returns (PoolParamsOnExecute memory, uint256) { @@ -605,7 +602,7 @@ contract TWAMM is BaseHook, ITWAMM { unchecked { // update pool - (, int128 liquidityNet) = poolManager.getTickLiquidity(poolKey.toId(), params.initializedTick); + (, int128 liquidityNet) = manager.getTickLiquidity(poolKey.toId(), params.initializedTick); if (initializedSqrtPrice < params.pool.sqrtPriceX96) liquidityNet = -liquidityNet; params.pool.liquidity = liquidityNet < 0 ? params.pool.liquidity - uint128(-liquidityNet) @@ -618,7 +615,7 @@ contract TWAMM is BaseHook, ITWAMM { function _isCrossingInitializedTick( PoolParamsOnExecute memory pool, - IPoolManager poolManager, + IPoolManager manager, PoolKey memory poolKey, uint160 nextSqrtPriceX96 ) internal view returns (bool crossingInitializedTick, int24 nextTickInit) { @@ -634,7 +631,7 @@ contract TWAMM is BaseHook, ITWAMM { unchecked { if (searchingLeft) nextTickInit -= 1; } - (nextTickInit, crossingInitializedTick) = poolManager.getNextInitializedTickWithinOneWord( + (nextTickInit, crossingInitializedTick) = manager.getNextInitializedTickWithinOneWord( poolKey.toId(), nextTickInit, poolKey.tickSpacing, searchingLeft ); nextTickInitFurtherThanTarget = searchingLeft ? nextTickInit <= targetTick : nextTickInit > targetTick; diff --git a/contracts/hooks/examples/VolatilityOracle.sol b/contracts/hooks/examples/VolatilityOracle.sol index ede61bf5..1a42e121 100644 --- a/contracts/hooks/examples/VolatilityOracle.sol +++ b/contracts/hooks/examples/VolatilityOracle.sol @@ -19,7 +19,7 @@ contract VolatilityOracle is BaseHook { return uint32(block.timestamp); } - constructor(IPoolManager _poolManager) BaseHook(_poolManager) { + constructor(IPoolManager _manager) BaseHook(_manager) { deployTimestamp = _blockTimestamp(); } @@ -56,7 +56,7 @@ contract VolatilityOracle is BaseHook { uint24 startingFee = 3000; uint32 lapsed = _blockTimestamp() - deployTimestamp; uint24 fee = startingFee + (uint24(lapsed) * 100) / 60; // 100 bps a minute - poolManager.updateDynamicLPFee(key, fee); // initial fee 0.30% + manager.updateDynamicLPFee(key, fee); // initial fee 0.30% } function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata) From 01e29c5fd5478604807d3dbb5523adf70fee8b3a Mon Sep 17 00:00:00 2001 From: Junion <69495294+Jun1on@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:29:09 -0400 Subject: [PATCH 23/25] make deployTimestamp immutable (#125) --- contracts/hooks/examples/VolatilityOracle.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/hooks/examples/VolatilityOracle.sol b/contracts/hooks/examples/VolatilityOracle.sol index 1a42e121..2900632f 100644 --- a/contracts/hooks/examples/VolatilityOracle.sol +++ b/contracts/hooks/examples/VolatilityOracle.sol @@ -12,7 +12,7 @@ contract VolatilityOracle is BaseHook { error MustUseDynamicFee(); - uint32 deployTimestamp; + uint32 immutable deployTimestamp; /// @dev For mocking function _blockTimestamp() internal view virtual returns (uint32) { From 357ef472312c39c8569ba916f7077e4a7884bf57 Mon Sep 17 00:00:00 2001 From: Junion <69495294+Jun1on@users.noreply.github.com> Date: Tue, 2 Jul 2024 17:21:35 -0400 Subject: [PATCH 24/25] finish frontrun hook --- .../hooks/examples/FeeTaker.sol | 49 +++++++++- contracts/hooks/examples/FeeTaking.sol | 35 +++++++ contracts/middleware/MiddlewareProtect.sol | 2 +- contracts/middleware/MiddlewareRemove.sol | 2 +- test/MiddlewareProtectFactory.t.sol | 71 ++++++++------ test/middleware/HooksFrontrun.sol | 96 +++++++++++++++++++ .../HooksFrontrunImplementation.sol | 16 ++++ 7 files changed, 236 insertions(+), 35 deletions(-) rename test/middleware/HookFrontrun.sol => contracts/hooks/examples/FeeTaker.sol (59%) create mode 100644 contracts/hooks/examples/FeeTaking.sol create mode 100644 test/middleware/HooksFrontrun.sol create mode 100644 test/shared/implementation/HooksFrontrunImplementation.sol diff --git a/test/middleware/HookFrontrun.sol b/contracts/hooks/examples/FeeTaker.sol similarity index 59% rename from test/middleware/HookFrontrun.sol rename to contracts/hooks/examples/FeeTaker.sol index 615dfb8b..91d1fb7d 100644 --- a/test/middleware/HookFrontrun.sol +++ b/contracts/hooks/examples/FeeTaker.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {BaseHook} from "./../../contracts/BaseHook.sol"; +import {BaseHook} from "../../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"; @@ -16,6 +16,10 @@ abstract contract FeeTaker is BaseHook { constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + /** + * @notice This hook takes a fee from the unspecified token after a swap. + * @dev This can be overridden if more permissions are needed. + */ function getHookPermissions() public pure virtual override returns (Hooks.Permissions memory) { return Hooks.Permissions({ beforeInitialize: false, @@ -24,12 +28,12 @@ abstract contract FeeTaker is BaseHook { afterAddLiquidity: false, beforeRemoveLiquidity: false, afterRemoveLiquidity: false, - beforeSwap: true, + beforeSwap: false, afterSwap: true, beforeDonate: false, afterDonate: false, beforeSwapReturnDelta: false, - afterSwapReturnDelta: false, + afterSwapReturnDelta: true, afterAddLiquidityReturnDelta: false, afterRemoveLiquidityReturnDelta: false }); @@ -58,4 +62,43 @@ abstract contract FeeTaker is BaseHook { (bytes4 selector, int128 amount) = _afterSwap(sender, key, params, delta, hookData); return (selector, feeAmount.toInt128() + amount); } + + function withdraw(Currency[] calldata currencies) external { + manager.unlock(abi.encode(currencies)); + } + + function _unlockCallback(bytes calldata rawData) internal override returns (bytes memory) { + Currency[] memory currencies = abi.decode(rawData, (Currency[])); + uint256 length = currencies.length; + for (uint256 i = 0; i < length;) { + uint256 amount = manager.balanceOf(address(this), CurrencyLibrary.toId(currencies[i])); + manager.burn(address(this), CurrencyLibrary.toId(currencies[i]), amount); + manager.take(currencies[i], _recipient(), amount); + unchecked { + ++i; + } + } + return ZERO_BYTES; + } + + /** + * @dev This is a virtual function that should be overridden so it returns the fee charged for a given amount. + */ + function _feeAmount(int128 amountUnspecified) internal view virtual returns (uint256); + + /** + * @dev This is a virtual function that should be overridden so it returns the address to receive the fee. + */ + function _recipient() internal view virtual returns (address); + + /** + * @dev This can be overridden to add logic after a swap. + */ + function _afterSwap(address, PoolKey memory, IPoolManager.SwapParams memory, BalanceDelta, bytes calldata) + internal + virtual + returns (bytes4, int128) + { + return (BaseHook.afterSwap.selector, 0); + } } diff --git a/contracts/hooks/examples/FeeTaking.sol b/contracts/hooks/examples/FeeTaking.sol new file mode 100644 index 00000000..c0099149 --- /dev/null +++ b/contracts/hooks/examples/FeeTaking.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; +import {Owned} from "solmate/auth/Owned.sol"; +import {FeeTaker} from "./FeeTaker.sol"; + +contract FeeTaking is FeeTaker, Owned { + using SafeCast for uint256; + + uint128 private constant TOTAL_BIPS = 10000; + uint128 public immutable swapFeeBips; + address public treasury; + + constructor(IPoolManager _poolManager, uint128 _swapFeeBips, address _owner, address _treasury) + FeeTaker(_poolManager) + Owned(_owner) + { + swapFeeBips = _swapFeeBips; + treasury = _treasury; + } + + function setTreasury(address _treasury) external onlyOwner { + treasury = _treasury; + } + + function _feeAmount(int128 amountUnspecified) internal view override returns (uint256) { + return uint128(amountUnspecified) * swapFeeBips / TOTAL_BIPS; + } + + function _recipient() internal view override returns (address) { + return treasury; + } +} diff --git a/contracts/middleware/MiddlewareProtect.sol b/contracts/middleware/MiddlewareProtect.sol index 1e800878..ee9e2c73 100644 --- a/contracts/middleware/MiddlewareProtect.sol +++ b/contracts/middleware/MiddlewareProtect.sol @@ -11,7 +11,7 @@ import {BaseHook} from "./../BaseHook.sol"; import {ReentrancyState} from "./../libraries/ReentrancyState.sol"; contract MiddlewareProtect is BaseMiddleware { - uint256 public constant gasLimit = 100000; + uint256 public constant gasLimit = 1000000; error ActionBetweenHook(); diff --git a/contracts/middleware/MiddlewareRemove.sol b/contracts/middleware/MiddlewareRemove.sol index 9dc2b531..5df5fc43 100644 --- a/contracts/middleware/MiddlewareRemove.sol +++ b/contracts/middleware/MiddlewareRemove.sol @@ -12,7 +12,7 @@ import {BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; contract MiddlewareRemove is BaseMiddleware { bytes internal constant ZERO_BYTES = bytes(""); - uint256 public constant gasLimit = 1000000; + uint256 public constant gasLimit = 10000000; constructor(IPoolManager _poolManager, address _impl) BaseMiddleware(_poolManager, _impl) {} diff --git a/test/MiddlewareProtectFactory.t.sol b/test/MiddlewareProtectFactory.t.sol index b5a472ac..52fed845 100644 --- a/test/MiddlewareProtectFactory.t.sol +++ b/test/MiddlewareProtectFactory.t.sol @@ -3,7 +3,8 @@ pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {FeeTakingLite} from "./middleware/FeeTakingLite.sol"; +import {HooksFrontrun} from "./middleware/HooksFrontrun.sol"; +import {HooksFrontrunImplementation} from "./shared/implementation/HooksFrontrunImplementation.sol"; import {MiddlewareProtect} from "../contracts/middleware/MiddlewareProtect.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; @@ -37,6 +38,7 @@ contract MiddlewareProtectFactoryTest is Test, Deployers { PoolId id; MiddlewareProtectFactory factory; + HooksFrontrun hooksFrontrun; function setUp() public { deployFreshManagerAndRouters(); @@ -46,43 +48,50 @@ contract MiddlewareProtectFactoryTest is Test, Deployers { token0 = TestERC20(Currency.unwrap(currency0)); token1 = TestERC20(Currency.unwrap(currency1)); + hooksFrontrun = HooksFrontrun(address(uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG))); + vm.record(); + HooksFrontrunImplementation impl = new HooksFrontrunImplementation(manager, hooksFrontrun); + (, bytes32[] memory writes) = vm.accesses(address(impl)); + vm.etch(address(hooksFrontrun), address(impl).code); + unchecked { + for (uint256 i = 0; i < writes.length; i++) { + bytes32 slot = writes[i]; + vm.store(address(hooksFrontrun), slot, vm.load(address(impl), slot)); + } + } + token0.approve(address(router), type(uint256).max); token1.approve(address(router), type(uint256).max); factory = new MiddlewareProtectFactory(manager); } - function testVariousProtectFactory() public { - FeeTakingLite feeTakingLite = new FeeTakingLite(manager); - uint160 flags = - uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG); - (address hookAddress, bytes32 salt) = HookMiner.find( - address(factory), - flags, - type(MiddlewareProtect).creationCode, - abi.encode(address(manager), address(feeTakingLite)) - ); - testOn(address(feeTakingLite), salt); + function testFrontrun() public { + (PoolKey memory key,) = + initPoolAndAddLiquidity(currency0, currency1, IHooks(address(0)), 100, SQRT_PRICE_1_1, ZERO_BYTES); + BalanceDelta swapDelta = swap(key, true, 0.001 ether, ZERO_BYTES); - HooksRevert hooksRevert = new HooksRevert(manager); - flags = uint160(Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG); - (hookAddress, salt) = HookMiner.find( - address(factory), - flags, - type(MiddlewareProtect).creationCode, - abi.encode(address(manager), address(hooksRevert)) + (key,) = initPoolAndAddLiquidity( + currency0, currency1, IHooks(address(hooksFrontrun)), 100, SQRT_PRICE_1_1, ZERO_BYTES ); - testOn(address(hooksRevert), salt); + BalanceDelta swapDelta2 = swap(key, true, 0.001 ether, ZERO_BYTES); + + // while both swaps are in the same pool, the second swap is more expensive + assertEq(swapDelta.amount1(), swapDelta2.amount1()); + assertTrue(abs(swapDelta.amount0()) < abs(swapDelta2.amount0())); + assertTrue(manager.balanceOf(address(hooksFrontrun), CurrencyLibrary.toId(key.currency0)) > 0); + } + + function testVariousProtectFactory() public { + uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG); - HooksOutOfGas hooksOutOfGas = new HooksOutOfGas(manager); - flags = uint160(Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG); - (hookAddress, salt) = HookMiner.find( + (address hookAddress, bytes32 salt) = HookMiner.find( address(factory), flags, type(MiddlewareProtect).creationCode, - abi.encode(address(manager), address(hooksOutOfGas)) + abi.encode(address(manager), address(hooksFrontrun)) ); - testOn(address(hooksOutOfGas), salt); + testOn(address(hooksFrontrun), salt); } // creates a middleware on an implementation @@ -90,12 +99,14 @@ contract MiddlewareProtectFactoryTest is Test, Deployers { address hookAddress = factory.createMiddleware(implementation, salt); MiddlewareProtect middlewareProtect = MiddlewareProtect(payable(hookAddress)); - (key, id) = initPoolAndAddLiquidity( - currency0, currency1, IHooks(address(middlewareProtect)), 3000, SQRT_PRICE_1_1, ZERO_BYTES + (key,) = initPoolAndAddLiquidity( + currency0, currency1, IHooks(address(middlewareProtect)), 100, SQRT_PRICE_1_1, ZERO_BYTES ); + swap(key, true, 0.001 ether, ZERO_BYTES); + //vm.expectRevert(); + } - removeLiquidity(currency0, currency1, IHooks(address(middlewareProtect)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); - - assertEq(factory.getImplementation(hookAddress), implementation); + function abs(int256 x) internal pure returns (uint256) { + return x >= 0 ? uint256(x) : uint256(-x); } } diff --git a/test/middleware/HooksFrontrun.sol b/test/middleware/HooksFrontrun.sol new file mode 100644 index 00000000..c0d23a8f --- /dev/null +++ b/test/middleware/HooksFrontrun.sol @@ -0,0 +1,96 @@ +// 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 {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; +import {console} from "forge-std/console.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; + +contract HooksFrontrun is BaseHook { + using SafeCast for uint256; + + bytes internal constant ZERO_BYTES = bytes(""); + uint160 public constant MIN_PRICE_LIMIT = TickMath.MIN_SQRT_PRICE + 1; + uint160 public constant MAX_PRICE_LIMIT = TickMath.MAX_SQRT_PRICE - 1; + + BalanceDelta swapDelta; + IPoolManager.SwapParams swapParams; + + constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + + function getHookPermissions() public pure virtual override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + afterAddLiquidity: false, + beforeRemoveLiquidity: false, + afterRemoveLiquidity: false, + beforeSwap: true, + afterSwap: true, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } + + function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata params, bytes calldata) + external + override + onlyByManager + returns (bytes4, BeforeSwapDelta, uint24) + { + swapParams = params; + console.log(params.zeroForOne); + console.logInt(params.amountSpecified); + swapDelta = manager.swap(key, params, ZERO_BYTES); + console.log("beforeDelta"); + console.logInt(swapDelta.amount0()); + console.logInt(swapDelta.amount1()); + return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); + } + + function afterSwap( + address sender, + PoolKey calldata key, + IPoolManager.SwapParams calldata params, + BalanceDelta delta, + bytes calldata hookData + ) external override onlyByManager returns (bytes4, int128) { + BalanceDelta afterDelta = manager.swap( + key, + IPoolManager.SwapParams( + !swapParams.zeroForOne, + -swapParams.amountSpecified, + swapParams.zeroForOne ? MAX_PRICE_LIMIT : MIN_PRICE_LIMIT + ), + ZERO_BYTES + ); + if (swapParams.zeroForOne) { + int256 profit = afterDelta.amount0() + swapDelta.amount0(); + if (profit > 0) { + // else hook reverts + manager.mint(address(this), key.currency0.toId(), uint256(profit)); + } + } else { + int256 profit = afterDelta.amount1() + swapDelta.amount1(); + if (profit > 0) { + // else hook reverts + manager.mint(address(this), key.currency1.toId(), uint256(profit)); + } + } + console.log("afterDelta"); + console.logInt(afterDelta.amount0()); + console.logInt(afterDelta.amount1()); + return (BaseHook.afterSwap.selector, 0); + } +} diff --git a/test/shared/implementation/HooksFrontrunImplementation.sol b/test/shared/implementation/HooksFrontrunImplementation.sol new file mode 100644 index 00000000..104a07b7 --- /dev/null +++ b/test/shared/implementation/HooksFrontrunImplementation.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {BaseHook} from "../../../contracts/BaseHook.sol"; +import {HooksFrontrun} from "../../middleware/HooksFrontrun.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; + +contract HooksFrontrunImplementation is HooksFrontrun { + constructor(IPoolManager _poolManager, HooksFrontrun addressToEtch) HooksFrontrun(_poolManager) { + Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); + } + + // make this a no-op in testing + function validateHookAddress(BaseHook _this) internal pure override {} +} From 6021c05581b298a76f6deee2d57ad6e0f8732372 Mon Sep 17 00:00:00 2001 From: Junion <69495294+Jun1on@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:36:05 -0400 Subject: [PATCH 25/25] update MiddlewareProtect --- contracts/middleware/MiddlewareProtect.sol | 42 +++++++++++++--------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/contracts/middleware/MiddlewareProtect.sol b/contracts/middleware/MiddlewareProtect.sol index ee9e2c73..204679b2 100644 --- a/contracts/middleware/MiddlewareProtect.sol +++ b/contracts/middleware/MiddlewareProtect.sol @@ -5,30 +5,35 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BaseMiddleware} from "./BaseMiddleware.sol"; import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {BeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; import {console} from "../../lib/forge-std/src/console.sol"; import {BaseHook} from "./../BaseHook.sol"; import {ReentrancyState} from "./../libraries/ReentrancyState.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; contract MiddlewareProtect is BaseMiddleware { - uint256 public constant gasLimit = 1000000; - - error ActionBetweenHook(); + using StateLibrary for IPoolManager; + using Hooks for IHooks; - constructor(IPoolManager _poolManager, address _impl) BaseMiddleware(_poolManager, _impl) {} + uint256 public constant gasLimit = 1000000; - modifier swapNotLocked() { - if (ReentrancyState.swapLocked()) { - revert ActionBetweenHook(); - } - _; - } + /// @notice Thrown if the address will lead to forbidden flags being set + /// @param hooks The address of the hooks contract + error HookPermissionForbidden(address hooks); + error ForbiddenReturn(); - modifier removeNotLocked() { - if (ReentrancyState.removeLocked()) { - revert ActionBetweenHook(); + constructor(IPoolManager _poolManager, address _impl) BaseMiddleware(_poolManager, _impl) { + // deny any hooks that return deltas + if ( + this.hasPermission(Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG) + || this.hasPermission(Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG) + || this.hasPermission(Hooks.AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG) + || this.hasPermission(Hooks.AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG) + ) { + HookPermissionForbidden.selector.revertWith(address(this)); } - _; } // block swaps and removes @@ -42,7 +47,12 @@ contract MiddlewareProtect is BaseMiddleware { (bool success, bytes memory returnData) = implementation.delegatecall{gas: gasLimit}(msg.data); require(success); ReentrancyState.unlock(); - return abi.decode(returnData, (bytes4, BeforeSwapDelta, uint24)); + (bytes4 selector, BeforeSwapDelta beforeSwapDelta, uint24 lpFeeOverride) = + abi.decode(returnData, (bytes4, BeforeSwapDelta, uint24)); + if (lpFeeOverride != 0) { + revert ForbiddenReturn(); + } + return (selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); } // afterSwap - no protections