diff --git a/.forge-snapshots/autodynamic fee.snap b/.forge-snapshots/autodynamic fee.snap index 58d87c97..301965ac 100644 --- a/.forge-snapshots/autodynamic fee.snap +++ b/.forge-snapshots/autodynamic fee.snap @@ -1 +1 @@ -242952 \ No newline at end of file +141494 \ No newline at end of file diff --git a/.forge-snapshots/hookFee.snap b/.forge-snapshots/hookFee.snap index 16c3b790..68591885 100644 --- a/.forge-snapshots/hookFee.snap +++ b/.forge-snapshots/hookFee.snap @@ -1 +1 @@ -265804 \ No newline at end of file +150527 \ No newline at end of file diff --git a/.forge-snapshots/manual dynamic fee.snap b/.forge-snapshots/manual dynamic fee.snap index 8b180eb5..3f9779f0 100644 --- a/.forge-snapshots/manual dynamic fee.snap +++ b/.forge-snapshots/manual dynamic fee.snap @@ -1 +1 @@ -219217 \ No newline at end of file +122293 \ No newline at end of file diff --git a/forge-lib/v4-core b/forge-lib/v4-core index f5674e46..3351c80e 160000 --- a/forge-lib/v4-core +++ b/forge-lib/v4-core @@ -1 +1 @@ -Subproject commit f5674e46720c0fc4606b287cccc583d56245e724 +Subproject commit 3351c80e58e6300cb263d33a4efe75b88ad7b9b2 diff --git a/forge-lib/v4-periphery b/forge-lib/v4-periphery index 6616b12d..ad7976af 160000 --- a/forge-lib/v4-periphery +++ b/forge-lib/v4-periphery @@ -1 +1 @@ -Subproject commit 6616b12db25257ffb3c562f131612ebb2fd89082 +Subproject commit ad7976af2181a80af0d381f4a8e9e234b880cc8f diff --git a/forge-test/Counter.sol b/forge-test/Counter.sol index b998efbf..69136866 100644 --- a/forge-test/Counter.sol +++ b/forge-test/Counter.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +pragma solidity ^0.8.24; -// import {BaseHook} from "v4-periphery/BaseHook.sol"; -import {BaseHook} from "@v4-by-example/utils/BaseHook.sol"; +import {BaseHook} from "v4-periphery/BaseHook.sol"; import {Hooks} from "v4-core/src/libraries/Hooks.sol"; import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "v4-core/src/types/PoolKey.sol"; import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol"; import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "v4-core/src/types/BeforeSwapDelta.sol"; contract Counter is BaseHook { using PoolIdLibrary for PoolKey; @@ -37,7 +37,11 @@ contract Counter is BaseHook { beforeSwap: true, afterSwap: true, beforeDonate: false, - afterDonate: false + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false }); } @@ -48,19 +52,19 @@ contract Counter is BaseHook { function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) external override - returns (bytes4) + returns (bytes4, BeforeSwapDelta, uint24) { beforeSwapCount[key.toId()]++; - return BaseHook.beforeSwap.selector; + return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); } function afterSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, BalanceDelta, bytes calldata) external override - returns (bytes4) + returns (bytes4, int128) { afterSwapCount[key.toId()]++; - return BaseHook.afterSwap.selector; + return (BaseHook.afterSwap.selector, 0); } function beforeAddLiquidity( diff --git a/forge-test/Counter.t.sol b/forge-test/Counter.t.sol index 6b2a2525..20859e28 100644 --- a/forge-test/Counter.t.sol +++ b/forge-test/Counter.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +pragma solidity ^0.8.24; import "forge-std/Test.sol"; import {IHooks} from "v4-core/src/interfaces/IHooks.sol"; @@ -10,22 +10,20 @@ import {PoolKey} from "v4-core/src/types/PoolKey.sol"; import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol"; import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol"; import {CurrencyLibrary, Currency} from "v4-core/src/types/Currency.sol"; +import {PoolSwapTest} from "v4-core/src/test/PoolSwapTest.sol"; import {Deployers} from "v4-core/test/utils/Deployers.sol"; import {Counter} from "./Counter.sol"; import {HookMiner} from "./utils/HookMiner.sol"; -import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; -import {PoolSwapTest} from "v4-core/src/test/PoolSwapTest.sol"; -contract CounterTest is Test, Deployers, GasSnapshot { +contract CounterTest is Test, Deployers { using PoolIdLibrary for PoolKey; using CurrencyLibrary for Currency; Counter counter; - PoolKey poolKey; PoolId poolId; function setUp() public { - // creates the pool manager, test tokens, and other utility routers + // creates the pool manager, utility routers, and test tokens Deployers.deployFreshManagerAndRouters(); Deployers.deployMintAndApprove2Currencies(); @@ -40,20 +38,18 @@ contract CounterTest is Test, Deployers, GasSnapshot { require(address(counter) == hookAddress, "CounterTest: hook address mismatch"); // Create the pool - poolKey = PoolKey(currency0, currency1, 3000, 60, IHooks(address(counter))); - poolId = poolKey.toId(); - manager.initialize(poolKey, SQRT_RATIO_1_1, ZERO_BYTES); + key = PoolKey(currency0, currency1, 3000, 60, IHooks(address(counter))); + poolId = key.toId(); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); // Provide liquidity to the pool + modifyLiquidityRouter.modifyLiquidity(key, IPoolManager.ModifyLiquidityParams(-60, 60, 10 ether, 0), ZERO_BYTES); modifyLiquidityRouter.modifyLiquidity( - poolKey, IPoolManager.ModifyLiquidityParams(-60, 60, 10 ether), ZERO_BYTES - ); - modifyLiquidityRouter.modifyLiquidity( - poolKey, IPoolManager.ModifyLiquidityParams(-120, 120, 10 ether), ZERO_BYTES + key, IPoolManager.ModifyLiquidityParams(-120, 120, 10 ether, 0), ZERO_BYTES ); modifyLiquidityRouter.modifyLiquidity( - poolKey, - IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 10 ether), + key, + IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 10 ether, 0), ZERO_BYTES ); } @@ -67,31 +63,29 @@ contract CounterTest is Test, Deployers, GasSnapshot { assertEq(counter.afterSwapCount(poolId), 0); // Perform a test swap // - int256 amount = -100; bool zeroForOne = true; - BalanceDelta swapDelta = swap(poolKey, zeroForOne, amount, ZERO_BYTES); + int256 amountSpecified = -1e18; // negative number indicates exact input swap! + BalanceDelta swapDelta = swap(key, zeroForOne, amountSpecified, ZERO_BYTES); // ------------------- // - assertEq(int256(swapDelta.amount0()), amount); + assertEq(int256(swapDelta.amount0()), amountSpecified); assertEq(counter.beforeSwapCount(poolId), 1); assertEq(counter.afterSwapCount(poolId), 1); } - function test_counter_snapshot() public { - int256 amount = 1e18; - bool zeroForOne = true; - IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ - zeroForOne: zeroForOne, - amountSpecified: amount, - sqrtPriceLimitX96: zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT // unlimited impact - }); + function testLiquidityHooks() public { + // positions were created in setup() + assertEq(counter.beforeAddLiquidityCount(poolId), 3); + assertEq(counter.beforeRemoveLiquidityCount(poolId), 0); - PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true, currencyAlreadySent: false}); + // remove liquidity + int256 liquidityDelta = -1e18; + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-60, 60, liquidityDelta, 0), ZERO_BYTES + ); - snapStart("counter"); - swapRouter.swap(poolKey, params, testSettings, ZERO_BYTES); - snapEnd(); + assertEq(counter.beforeAddLiquidityCount(poolId), 3); + assertEq(counter.beforeRemoveLiquidityCount(poolId), 1); } } diff --git a/forge-test/CustomCurve.t.sol b/forge-test/CustomCurve.t.sol index 6f739b33..69d580ac 100644 --- a/forge-test/CustomCurve.t.sol +++ b/forge-test/CustomCurve.t.sol @@ -11,7 +11,7 @@ import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol"; import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol"; import {CurrencyLibrary, Currency} from "v4-core/src/types/Currency.sol"; import {Deployers} from "v4-core/test/utils/Deployers.sol"; -import {CustomCurve} from "@v4-by-example/pages/hooks/custom-curve/CustomCurve.sol"; +import {ConstantSumCurve} from "@v4-by-example/pages/hooks/custom-curve/CustomCurve.sol"; import {HookMiner} from "./utils/HookMiner.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; @@ -19,7 +19,7 @@ contract CustomCurveTest is Test, Deployers { using PoolIdLibrary for PoolKey; using CurrencyLibrary for Currency; - CustomCurve hook; + ConstantSumCurve hook; PoolKey poolKey; PoolId poolId; @@ -29,41 +29,42 @@ contract CustomCurveTest is Test, Deployers { Deployers.deployMintAndApprove2Currencies(); // Deploy the hook to an address with the correct flags - uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG); + uint160 flags = + uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG); (address hookAddress, bytes32 salt) = - HookMiner.find(address(this), flags, type(CustomCurve).creationCode, abi.encode(address(manager))); - hook = new CustomCurve{salt: salt}(IPoolManager(address(manager))); + HookMiner.find(address(this), flags, type(ConstantSumCurve).creationCode, abi.encode(address(manager))); + hook = new ConstantSumCurve{salt: salt}(IPoolManager(address(manager))); require(address(hook) == hookAddress, "CustomCurveTest: hook address mismatch"); // Create the pool poolKey = PoolKey(currency0, currency1, 3000, 60, IHooks(hook)); poolId = poolKey.toId(); - manager.initialize(poolKey, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(poolKey, SQRT_PRICE_1_1, ZERO_BYTES); - PoolKey memory hookless = PoolKey(currency0, currency1, 3000, 60, IHooks(address(0x0))); - manager.initialize(hookless, SQRT_RATIO_1_1, ZERO_BYTES); - - // add liquidity so theres tokens to take - modifyLiquidityRouter.modifyLiquidity( - hookless, IPoolManager.ModifyLiquidityParams(-60, 60, 10000 ether), ZERO_BYTES - ); - - // Provide liquidity to the pool - IERC20(Currency.unwrap(currency0)).transfer(address(hook), 10_000 ether); - IERC20(Currency.unwrap(currency1)).transfer(address(hook), 10_000 ether); + // Add liquidity + IERC20(Currency.unwrap(currency0)).approve(address(hook), type(uint256).max); + IERC20(Currency.unwrap(currency1)).approve(address(hook), type(uint256).max); + hook.addLiquidity(poolKey, 100 ether, 100 ether); } - function test_swap() public { - uint256 token1Before = currency1.balanceOfSelf(); - - // Perform a test swap // - int256 amount = 10e18; - bool zeroForOne = true; - swap(poolKey, zeroForOne, amount, ZERO_BYTES); - // ------------------- // + function test_swap(bool zeroForOne, int256 amountSpecified) public { + amountSpecified = bound(amountSpecified, -100 ether, 100 ether); + vm.assume(amountSpecified != 0); + uint256 token0Before = currency0.balanceOfSelf(); + uint256 token1Before = currency1.balanceOfSelf(); + swap(poolKey, zeroForOne, amountSpecified, ZERO_BYTES); + uint256 token0After = currency0.balanceOfSelf(); uint256 token1After = currency1.balanceOfSelf(); - assertEq(token1After - token1Before, 1e18); + bool exactInput = amountSpecified < 0; + uint256 amountSwapped = exactInput ? uint256(-amountSpecified) : uint256(amountSpecified); + if (zeroForOne) { + assertEq(token0Before - token0After, amountSwapped); + assertEq(token1After - token1Before, amountSwapped); + } else { + assertEq(token0After - token0Before, amountSwapped); + assertEq(token1Before - token1After, amountSwapped); + } } } diff --git a/forge-test/DynamicFees.t.sol b/forge-test/DynamicFees.t.sol index c9ce36ed..066d70f4 100644 --- a/forge-test/DynamicFees.t.sol +++ b/forge-test/DynamicFees.t.sol @@ -10,7 +10,7 @@ import {PoolKey} from "v4-core/src/types/PoolKey.sol"; import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol"; import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol"; import {CurrencyLibrary, Currency} from "v4-core/src/types/Currency.sol"; -import {SwapFeeLibrary} from "v4-core/src/libraries/SwapFeeLibrary.sol"; +import {LPFeeLibrary} from "v4-core/src/libraries/LPFeeLibrary.sol"; import {Deployers} from "v4-core/test/utils/Deployers.sol"; import {HookMiner} from "./utils/HookMiner.sol"; import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; @@ -18,14 +18,14 @@ import {PoolSwapTest} from "v4-core/src/test/PoolSwapTest.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {ManualDynamicFee} from "@v4-by-example/pages/fees/dynamic-fee/ManualDynamicFee.sol"; -import {AutoDynamicFee} from "@v4-by-example/pages/fees/dynamic-fee/AutoDynamicFee.sol"; +import {DynamicFeeOverride} from "@v4-by-example/pages/fees/dynamic-fee/DynamicFeeOverride.sol"; contract DynamicFeesTest is Test, Deployers, GasSnapshot { using FixedPointMathLib for uint256; using PoolIdLibrary for PoolKey; using CurrencyLibrary for Currency; - AutoDynamicFee autoDynamicFee; + DynamicFeeOverride autoDynamicFee; ManualDynamicFee manualDynamicFee; PoolKey autoDynamicFeePoolKey; @@ -37,51 +37,52 @@ contract DynamicFeesTest is Test, Deployers, GasSnapshot { Deployers.deployMintAndApprove2Currencies(); // Deploy the hook to an address with the correct flags - uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG); + uint160 flags = uint160(Hooks.AFTER_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG); (address hookAddress, bytes32 salt) = - HookMiner.find(address(this), flags, type(AutoDynamicFee).creationCode, abi.encode(address(manager))); - autoDynamicFee = new AutoDynamicFee{salt: salt}(IPoolManager(address(manager))); + HookMiner.find(address(this), flags, type(DynamicFeeOverride).creationCode, abi.encode(address(manager))); + autoDynamicFee = new DynamicFeeOverride{salt: salt}(IPoolManager(address(manager))); require(address(autoDynamicFee) == hookAddress, "hook address mismatch"); + flags = uint160(Hooks.AFTER_INITIALIZE_FLAG); (hookAddress, salt) = - HookMiner.find(address(this), uint160(0), type(ManualDynamicFee).creationCode, abi.encode(address(manager))); + HookMiner.find(address(this), flags, type(ManualDynamicFee).creationCode, abi.encode(address(manager))); manualDynamicFee = new ManualDynamicFee{salt: salt}(IPoolManager(address(manager))); require(address(manualDynamicFee) == hookAddress, "hook address mismatch"); // Create the pools - autoDynamicFeePoolKey = PoolKey(currency0, currency1, SwapFeeLibrary.DYNAMIC_FEE_FLAG, 60, IHooks(autoDynamicFee)); - manager.initialize(autoDynamicFeePoolKey, SQRT_RATIO_1_1, ZERO_BYTES); + autoDynamicFeePoolKey = PoolKey(currency0, currency1, LPFeeLibrary.DYNAMIC_FEE_FLAG, 60, IHooks(autoDynamicFee)); + manager.initialize(autoDynamicFeePoolKey, SQRT_PRICE_1_1, ZERO_BYTES); manualDynamicFeePoolKey = - PoolKey(currency0, currency1, SwapFeeLibrary.DYNAMIC_FEE_FLAG, 60, IHooks(manualDynamicFee)); - manager.initialize(manualDynamicFeePoolKey, SQRT_RATIO_1_1, ZERO_BYTES); + PoolKey(currency0, currency1, LPFeeLibrary.DYNAMIC_FEE_FLAG, 60, IHooks(manualDynamicFee)); + manager.initialize(manualDynamicFeePoolKey, SQRT_PRICE_1_1, ZERO_BYTES); // Provide liquidity to the pool modifyLiquidityRouter.modifyLiquidity( autoDynamicFeePoolKey, - IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 100000 ether), + IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 100000 ether, 0), ZERO_BYTES ); modifyLiquidityRouter.modifyLiquidity( manualDynamicFeePoolKey, - IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 100000 ether), + IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 100000 ether, 0), ZERO_BYTES ); } function test_start_autoFee() public { // Perform a test swap // - int256 amount = 1e18; + int256 amount = -1e18; bool zeroForOne = true; BalanceDelta swapDelta = swap(autoDynamicFeePoolKey, zeroForOne, amount, ZERO_BYTES); // ------------------- // // fee on output token, so expect ~0.95e18 output - assertLt(uint256(-int256(swapDelta.amount1())), 0.95e18); - assertGt(uint256(-int256(swapDelta.amount1())), 0.94e18); + assertLt(uint256(int256(swapDelta.amount1())), 0.95e18); + assertGt(uint256(int256(swapDelta.amount1())), 0.94e18); assertApproxEqAbs( - uint256(-int256(swapDelta.amount1())), uint256(amount), uint256(amount).mulWadDown(0.05001e18) + uint256(int256(swapDelta.amount1())), uint256(-amount), uint256(-amount).mulWadDown(0.05001e18) ); } @@ -90,30 +91,30 @@ contract DynamicFeesTest is Test, Deployers, GasSnapshot { skip(496000); // Perform a test swap // - int256 amount = 1e18; + int256 amount = -1e18; bool zeroForOne = true; BalanceDelta swapDelta = swap(autoDynamicFeePoolKey, zeroForOne, amount, ZERO_BYTES); // ------------------- // - assertLt(uint256(-int256(swapDelta.amount1())), 0.9995e18); - assertGt(uint256(-int256(swapDelta.amount1())), 0.9994e18); + assertLt(uint256(int256(swapDelta.amount1())), 0.9995e18); + assertGt(uint256(int256(swapDelta.amount1())), 0.9994e18); assertApproxEqAbs( - uint256(-int256(swapDelta.amount1())), uint256(amount), uint256(amount).mulWadDown(0.00051e18) + uint256(int256(swapDelta.amount1())), uint256(-amount), uint256(-amount).mulWadDown(0.00051e18) ); } function test_start_manualFee() public { // Perform a test swap // - int256 amount = 1e18; + int256 amount = -1e18; bool zeroForOne = true; BalanceDelta swapDelta = swap(manualDynamicFeePoolKey, zeroForOne, amount, ZERO_BYTES); // ------------------- // // fee on output token, so expect ~0.95e18 output - assertLt(uint256(-int256(swapDelta.amount1())), 0.95e18); - assertGt(uint256(-int256(swapDelta.amount1())), 0.94e18); + assertLt(uint256(int256(swapDelta.amount1())), 0.95e18); + assertGt(uint256(int256(swapDelta.amount1())), 0.94e18); assertApproxEqAbs( - uint256(-int256(swapDelta.amount1())), uint256(amount), uint256(amount).mulWadDown(0.05001e18) + uint256(int256(swapDelta.amount1())), uint256(-amount), uint256(-amount).mulWadDown(0.05001e18) ); } @@ -125,15 +126,15 @@ contract DynamicFeesTest is Test, Deployers, GasSnapshot { manualDynamicFee.setFee(manualDynamicFeePoolKey); // Perform a test swap // - int256 amount = 1e18; + int256 amount = -1e18; bool zeroForOne = true; BalanceDelta swapDelta = swap(manualDynamicFeePoolKey, zeroForOne, amount, ZERO_BYTES); // ------------------- // - assertLt(uint256(-int256(swapDelta.amount1())), 0.9995e18); - assertGt(uint256(-int256(swapDelta.amount1())), 0.9994e18); + assertLt(uint256(int256(swapDelta.amount1())), 0.9995e18); + assertGt(uint256(int256(swapDelta.amount1())), 0.9994e18); assertApproxEqAbs( - uint256(-int256(swapDelta.amount1())), uint256(amount), uint256(amount).mulWadDown(0.00051e18) + uint256(int256(swapDelta.amount1())), uint256(-amount), uint256(-amount).mulWadDown(0.00051e18) ); } @@ -142,22 +143,22 @@ contract DynamicFeesTest is Test, Deployers, GasSnapshot { skip(496000); // Perform a test swap // - int256 amount = 1e18; + int256 amount = -1e18; bool zeroForOne = true; BalanceDelta swapDelta = swap(manualDynamicFeePoolKey, zeroForOne, amount, ZERO_BYTES); // ------------------- // // fee on output token, so expect ~0.95e18 output - assertLt(uint256(-int256(swapDelta.amount1())), 0.95e18); - assertGt(uint256(-int256(swapDelta.amount1())), 0.94e18); + assertLt(uint256(int256(swapDelta.amount1())), 0.95e18); + assertGt(uint256(int256(swapDelta.amount1())), 0.94e18); assertApproxEqAbs( - uint256(-int256(swapDelta.amount1())), uint256(amount), uint256(amount).mulWadDown(0.05001e18) + uint256(int256(swapDelta.amount1())), uint256(-amount), uint256(-amount).mulWadDown(0.05001e18) ); } function test_snapshot_autoFee() public { skip(100_000); - int256 amount = 1e18; + int256 amount = -1e18; bool zeroForOne = true; IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ zeroForOne: zeroForOne, @@ -166,7 +167,7 @@ contract DynamicFeesTest is Test, Deployers, GasSnapshot { }); PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true, currencyAlreadySent: false}); + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); snapStart("autodynamic fee"); swapRouter.swap(autoDynamicFeePoolKey, params, testSettings, ZERO_BYTES); @@ -178,7 +179,7 @@ contract DynamicFeesTest is Test, Deployers, GasSnapshot { // update the fee manualDynamicFee.setFee(manualDynamicFeePoolKey); - int256 amount = 1e18; + int256 amount = -1e18; bool zeroForOne = true; IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ zeroForOne: zeroForOne, @@ -187,7 +188,7 @@ contract DynamicFeesTest is Test, Deployers, GasSnapshot { }); PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true, currencyAlreadySent: false}); + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); snapStart("manual dynamic fee"); swapRouter.swap(manualDynamicFeePoolKey, params, testSettings, ZERO_BYTES); diff --git a/forge-test/FixedHookFee.t.sol b/forge-test/FixedHookFee.t.sol index c22951c0..b2e3796b 100644 --- a/forge-test/FixedHookFee.t.sol +++ b/forge-test/FixedHookFee.t.sol @@ -24,6 +24,8 @@ contract FixedHookFeeTest is Test, Deployers, GasSnapshot { PoolKey poolKey; PoolId poolId; + PoolKey hooklessKey; + address alice = makeAddr("alice"); function setUp() public { @@ -32,51 +34,104 @@ contract FixedHookFeeTest is Test, Deployers, GasSnapshot { Deployers.deployMintAndApprove2Currencies(); // Deploy the hook to an address with the correct flags - uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG); + uint160 flags = uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG); (address hookAddress, bytes32 salt) = HookMiner.find(address(this), flags, type(FixedHookFee).creationCode, abi.encode(address(manager))); hook = new FixedHookFee{salt: salt}(IPoolManager(address(manager))); require(address(hook) == hookAddress, "FixedHookFeeTest: hook address mismatch"); - // Create the pool - poolKey = PoolKey(currency0, currency1, 3000, 60, IHooks(hook)); + // Create the pool with 0% fee + poolKey = PoolKey(currency0, currency1, 0, 60, IHooks(hook)); poolId = poolKey.toId(); - manager.initialize(poolKey, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(poolKey, SQRT_PRICE_1_1, ZERO_BYTES); // Provide liquidity to the pool modifyLiquidityRouter.modifyLiquidity( - poolKey, IPoolManager.ModifyLiquidityParams(-60, 60, 10 ether), ZERO_BYTES - ); - modifyLiquidityRouter.modifyLiquidity( - poolKey, IPoolManager.ModifyLiquidityParams(-120, 120, 10 ether), ZERO_BYTES + poolKey, + IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 100_000 ether, 0), + ZERO_BYTES ); + + // create a hookless pool + hooklessKey = PoolKey(currency0, currency1, 0, 60, IHooks(address(0x0))); + manager.initialize(hooklessKey, SQRT_PRICE_1_1, ZERO_BYTES); + + // Provide liquidity to the pool modifyLiquidityRouter.modifyLiquidity( - poolKey, - IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 10_000 ether), + hooklessKey, + IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 100_000 ether, 0), ZERO_BYTES ); } - function test_hookFee() public { - uint256 balanceBefore = currency0.balanceOfSelf(); - // Perform a test swap // - int256 amount = 1e18; - bool zeroForOne = true; - swap(poolKey, zeroForOne, amount, ZERO_BYTES); - // ------------------- // - uint256 balanceAfter = currency0.balanceOfSelf(); + function test_hookFee(bool zeroForOne, int256 amountSpecified) public { + amountSpecified = bound(amountSpecified, -100e18, 100e18); + // assume the swap amount is material + uint256 swapAmount = amountSpecified < 0 ? uint256(-amountSpecified) : uint256(amountSpecified); + vm.assume(swapAmount > 1e18); + + bool exactInput = amountSpecified < 0; + bool zeroIsSpecified = zeroForOne == exactInput; + Currency specifiedCurrency = zeroIsSpecified ? currency0 : currency1; + Currency unspecifiedCurrency = specifiedCurrency == currency0 ? currency1 : currency0; + + BalanceDelta withoutHookFee = swap(hooklessKey, zeroForOne, amountSpecified, ZERO_BYTES); + + uint256 specifiedAmountBefore = specifiedCurrency.balanceOfSelf(); + uint256 unspecifiedAmountBefore = unspecifiedCurrency.balanceOfSelf(); + BalanceDelta result = swap(poolKey, zeroForOne, amountSpecified, ZERO_BYTES); + uint256 specifiedAmountAfter = specifiedCurrency.balanceOfSelf(); + uint256 unspecifiedAmountAfter = unspecifiedCurrency.balanceOfSelf(); + + if (exactInput) { + assertEq(specifiedAmountBefore - specifiedAmountAfter, uint256(-amountSpecified)); + if (zeroIsSpecified) { + assertEq(uint256(int256(-result.amount0())), specifiedAmountBefore - specifiedAmountAfter); + + assertEq(unspecifiedAmountAfter - unspecifiedAmountBefore, uint256(int256(result.amount1()))); + assertEq( + unspecifiedAmountAfter - unspecifiedAmountBefore, + uint256(int256(withoutHookFee.amount1())) - hook.FIXED_HOOK_FEE() + ); + } else { + // token1 is specified + assertEq(uint256(int256(-result.amount1())), specifiedAmountBefore - specifiedAmountAfter); + + assertEq(unspecifiedAmountAfter - unspecifiedAmountBefore, uint256(int256(result.amount0()))); + assertEq( + unspecifiedAmountAfter - unspecifiedAmountBefore, + uint256(int256(withoutHookFee.amount0())) - hook.FIXED_HOOK_FEE() + ); + } + } else { + assertEq(specifiedAmountAfter - specifiedAmountBefore, uint256(amountSpecified)); + if (zeroIsSpecified) { + // token0 (exactOut) is specified + assertEq(uint256(int256(result.amount0())), specifiedAmountAfter - specifiedAmountBefore); + + assertEq(unspecifiedAmountBefore - unspecifiedAmountAfter, uint256(int256(-result.amount1()))); + assertEq( + unspecifiedAmountBefore - unspecifiedAmountAfter, + uint256(int256(-withoutHookFee.amount1())) + hook.FIXED_HOOK_FEE() + ); + } else { + // token1 is specified + assertEq(uint256(int256(result.amount1())), specifiedAmountAfter - specifiedAmountBefore); - // swapper paid for the fixed hook fee - assertEq(balanceBefore - balanceAfter, uint256(amount) + hook.FIXED_HOOK_FEE()); + assertEq(unspecifiedAmountBefore - unspecifiedAmountAfter, uint256(int256(-result.amount0()))); + assertEq( + unspecifiedAmountBefore - unspecifiedAmountAfter, + uint256(int256(-withoutHookFee.amount0())) + hook.FIXED_HOOK_FEE() + ); + } + } - // collect the hook fees - assertEq(currency0.balanceOf(alice), 0); - hook.collectFee(alice, currency0); - assertEq(currency0.balanceOf(alice), hook.FIXED_HOOK_FEE()); + // hook collected fees + assertEq(manager.balanceOf(address(hook), unspecifiedCurrency.toId()), hook.FIXED_HOOK_FEE()); } function test_snap_hookFee() public { - int256 amount = 1e18; + int256 amount = -1e18; bool zeroForOne = true; IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ zeroForOne: zeroForOne, @@ -85,7 +140,7 @@ contract FixedHookFeeTest is Test, Deployers, GasSnapshot { }); PoolSwapTest.TestSettings memory testSettings = - PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true, currencyAlreadySent: false}); + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); snapStart("hookFee"); swapRouter.swap(poolKey, params, testSettings, ZERO_BYTES); diff --git a/forge-test/MsgSenderHookData.t.sol b/forge-test/MsgSenderHookData.t.sol index 8ff15ad1..1c23a1d6 100644 --- a/forge-test/MsgSenderHookData.t.sol +++ b/forge-test/MsgSenderHookData.t.sol @@ -40,18 +40,18 @@ contract MsgSenderHookDataTest is Test, Deployers { // Create the pool poolKey = PoolKey(currency0, currency1, 3000, 60, IHooks(counter)); poolId = poolKey.toId(); - manager.initialize(poolKey, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(poolKey, SQRT_PRICE_1_1, ZERO_BYTES); // Provide liquidity to the pool modifyLiquidityRouter.modifyLiquidity( - poolKey, IPoolManager.ModifyLiquidityParams(-60, 60, 10 ether), ZERO_BYTES + poolKey, IPoolManager.ModifyLiquidityParams(-60, 60, 10 ether, 0), ZERO_BYTES ); modifyLiquidityRouter.modifyLiquidity( - poolKey, IPoolManager.ModifyLiquidityParams(-120, 120, 10 ether), ZERO_BYTES + poolKey, IPoolManager.ModifyLiquidityParams(-120, 120, 10 ether, 0), ZERO_BYTES ); modifyLiquidityRouter.modifyLiquidity( poolKey, - IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 10 ether), + IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 10 ether, 0), ZERO_BYTES ); } diff --git a/forge-test/NoOpSwap.t.sol b/forge-test/NoOpSwap.t.sol index 9341d10a..5eea6132 100644 --- a/forge-test/NoOpSwap.t.sol +++ b/forge-test/NoOpSwap.t.sol @@ -28,7 +28,7 @@ contract NoOpSwapTest is Test, Deployers { Deployers.deployMintAndApprove2Currencies(); // Deploy the hook to an address with the correct flags - uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG); + uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG); (address hookAddress, bytes32 salt) = HookMiner.find(address(this), flags, type(NoOpSwap).creationCode, abi.encode(address(manager))); hook = new NoOpSwap{salt: salt}(IPoolManager(address(manager))); @@ -37,18 +37,18 @@ contract NoOpSwapTest is Test, Deployers { // Create the pool poolKey = PoolKey(currency0, currency1, 3000, 60, IHooks(hook)); poolId = poolKey.toId(); - manager.initialize(poolKey, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(poolKey, SQRT_PRICE_1_1, ZERO_BYTES); // Provide liquidity to the pool modifyLiquidityRouter.modifyLiquidity( - poolKey, IPoolManager.ModifyLiquidityParams(-60, 60, 10 ether), ZERO_BYTES + poolKey, IPoolManager.ModifyLiquidityParams(-60, 60, 10 ether, 0), ZERO_BYTES ); modifyLiquidityRouter.modifyLiquidity( - poolKey, IPoolManager.ModifyLiquidityParams(-120, 120, 10 ether), ZERO_BYTES + poolKey, IPoolManager.ModifyLiquidityParams(-120, 120, 10 ether, 0), ZERO_BYTES ); modifyLiquidityRouter.modifyLiquidity( poolKey, - IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 10_000 ether), + IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 10_000 ether, 0), ZERO_BYTES ); } @@ -57,13 +57,13 @@ contract NoOpSwapTest is Test, Deployers { assertEq(hook.beforeSwapCount(poolId), 0); // Perform a test swap // - int256 amount = 69e18; + int256 amount = -69e18; bool zeroForOne = true; BalanceDelta swapDelta = swap(poolKey, zeroForOne, amount, ZERO_BYTES); // ------------------- // - // no-op will return an indicator that the swap was skipped - assertEq(int256(swapDelta.amount0()), -1); + // no-op means the user does not receive any output + assertEq(int256(swapDelta.amount1()), 0); // Swapping with 69e18 will skip the swap logic! assertEq(hook.beforeSwapCount(poolId), 0); @@ -73,7 +73,7 @@ contract NoOpSwapTest is Test, Deployers { assertEq(hook.beforeSwapCount(poolId), 0); // Perform a test swap // - int256 amount = 1e18; + int256 amount = -1e18; bool zeroForOne = true; BalanceDelta swapDelta = swap(poolKey, zeroForOne, amount, ZERO_BYTES); // ------------------- // diff --git a/forge-test/SwapFee.t.sol b/forge-test/SwapFee.t.sol index c5f3b9d0..46b2aec2 100644 --- a/forge-test/SwapFee.t.sol +++ b/forge-test/SwapFee.t.sol @@ -30,18 +30,18 @@ contract SwapFeeTest is Test, Deployers, GasSnapshot { // Create the pool poolKey = PoolKey(currency0, currency1, 3000, 60, IHooks(address(0x0))); poolId = poolKey.toId(); - manager.initialize(poolKey, SQRT_RATIO_1_1, ZERO_BYTES); + manager.initialize(poolKey, SQRT_PRICE_1_1, ZERO_BYTES); // Provide liquidity to the pool modifyLiquidityRouter.modifyLiquidity( - poolKey, IPoolManager.ModifyLiquidityParams(-60, 60, 10 ether), ZERO_BYTES + poolKey, IPoolManager.ModifyLiquidityParams(-60, 60, 10 ether, 0), ZERO_BYTES ); modifyLiquidityRouter.modifyLiquidity( - poolKey, IPoolManager.ModifyLiquidityParams(-120, 120, 10 ether), ZERO_BYTES + poolKey, IPoolManager.ModifyLiquidityParams(-120, 120, 10 ether, 0), ZERO_BYTES ); modifyLiquidityRouter.modifyLiquidity( poolKey, - IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 10 ether), + IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 10 ether, 0), ZERO_BYTES ); } diff --git a/forge-test/utils/HookMiner.sol b/forge-test/utils/HookMiner.sol index ca421b5b..d6b30c40 100644 --- a/forge-test/utils/HookMiner.sol +++ b/forge-test/utils/HookMiner.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.8.20; +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 top 12 bit of the address - uint160 constant FLAG_MASK = 0xFFF << 148; + // 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 = 20_000; + 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 @@ -18,7 +18,7 @@ library HookMiner { /// @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) - external + internal view returns (address, bytes32) { @@ -41,7 +41,7 @@ library HookMiner { /// @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) - public + internal pure returns (address hookAddress) { diff --git a/src/nav.ts b/src/nav.ts index 7a766eaf..5fb1a623 100644 --- a/src/nav.ts +++ b/src/nav.ts @@ -32,7 +32,7 @@ export const SOL_ROUTES: Route[] = [ export const HOOK_ROUTES: Route[] = [ { path: "no-op", - title: "No Op" + title: "NoOp Swap" }, { path: "custom-curve", diff --git a/src/pages/create-liquidity/CreateLiquidity.sol b/src/pages/create-liquidity/CreateLiquidity.sol index 7158f847..5f193477 100644 --- a/src/pages/create-liquidity/CreateLiquidity.sol +++ b/src/pages/create-liquidity/CreateLiquidity.sol @@ -19,7 +19,12 @@ contract CreateLiquidity { // if 0 < liquidity: add liquidity -- otherwise remove liquidity lpRouter.modifyLiquidity( poolKey, - IPoolManager.ModifyLiquidityParams({tickLower: tickLower, tickUpper: tickUpper, liquidityDelta: liquidity}), + IPoolManager.ModifyLiquidityParams({ + tickLower: tickLower, + tickUpper: tickUpper, + liquidityDelta: liquidity, + salt: 0 + }), hookData ); } diff --git a/src/pages/create-liquidity/CreateLiquidityExampleInputs.solsnippet b/src/pages/create-liquidity/CreateLiquidityExampleInputs.solsnippet index 3b0424a5..1ae06c15 100644 --- a/src/pages/create-liquidity/CreateLiquidityExampleInputs.solsnippet +++ b/src/pages/create-liquidity/CreateLiquidityExampleInputs.solsnippet @@ -24,6 +24,6 @@ int24 tickUpper = 600; int256 liquidity = 10e18; lpRouter.modifyLiquidity( poolKey, - IPoolManager.ModifyLiquidityParams({tickLower: tickLower, tickUpper: tickUpper, liquidityDelta: liquidity}), + IPoolManager.ModifyLiquidityParams({tickLower: tickLower, tickUpper: tickUpper, liquidityDelta: liquidity, salt: 0}), new bytes(0) ); diff --git a/src/pages/create-liquidity/index.html.ts b/src/pages/create-liquidity/index.html.ts index 05a0b9e9..8767e56a 100644 --- a/src/pages/create-liquidity/index.html.ts +++ b/src/pages/create-liquidity/index.html.ts @@ -15,11 +15,11 @@ export const keywords = [ export const codes = [ { fileName: "CreateLiquidity.sol", - code: "Ly8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IE1JVApwcmFnbWEgc29saWRpdHkgXjAuOC4yMDsKCmltcG9ydCB7SVBvb2xNYW5hZ2VyfSBmcm9tICJ2NC1jb3JlL3NyYy9pbnRlcmZhY2VzL0lQb29sTWFuYWdlci5zb2wiOwppbXBvcnQge1Bvb2xLZXl9IGZyb20gInY0LWNvcmUvc3JjL3R5cGVzL1Bvb2xLZXkuc29sIjsKaW1wb3J0IHtQb29sTW9kaWZ5TGlxdWlkaXR5VGVzdH0gZnJvbSAidjQtY29yZS9zcmMvdGVzdC9Qb29sTW9kaWZ5TGlxdWlkaXR5VGVzdC5zb2wiOwoKY29udHJhY3QgQ3JlYXRlTGlxdWlkaXR5IHsKICAgIC8vIHNldCB0aGUgcm91dGVyIGFkZHJlc3MKICAgIFBvb2xNb2RpZnlMaXF1aWRpdHlUZXN0IGxwUm91dGVyID0gUG9vbE1vZGlmeUxpcXVpZGl0eVRlc3QoYWRkcmVzcygweDAxKSk7CgogICAgZnVuY3Rpb24gY3JlYXRlTGlxdWlkaXR5KAogICAgICAgIFBvb2xLZXkgbWVtb3J5IHBvb2xLZXksCiAgICAgICAgaW50MjQgdGlja0xvd2VyLAogICAgICAgIGludDI0IHRpY2tVcHBlciwKICAgICAgICBpbnQyNTYgbGlxdWlkaXR5LAogICAgICAgIGJ5dGVzIGNhbGxkYXRhIGhvb2tEYXRhCiAgICApIGV4dGVybmFsIHsKICAgICAgICAvLyBpZiAwIDwgbGlxdWlkaXR5OiBhZGQgbGlxdWlkaXR5IC0tIG90aGVyd2lzZSByZW1vdmUgbGlxdWlkaXR5CiAgICAgICAgbHBSb3V0ZXIubW9kaWZ5TGlxdWlkaXR5KAogICAgICAgICAgICBwb29sS2V5LAogICAgICAgICAgICBJUG9vbE1hbmFnZXIuTW9kaWZ5TGlxdWlkaXR5UGFyYW1zKHt0aWNrTG93ZXI6IHRpY2tMb3dlciwgdGlja1VwcGVyOiB0aWNrVXBwZXIsIGxpcXVpZGl0eURlbHRhOiBsaXF1aWRpdHl9KSwKICAgICAgICAgICAgaG9va0RhdGEKICAgICAgICApOwogICAgfQp9Cg==", + code: "Ly8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IE1JVApwcmFnbWEgc29saWRpdHkgXjAuOC4yMDsKCmltcG9ydCB7SVBvb2xNYW5hZ2VyfSBmcm9tICJ2NC1jb3JlL3NyYy9pbnRlcmZhY2VzL0lQb29sTWFuYWdlci5zb2wiOwppbXBvcnQge1Bvb2xLZXl9IGZyb20gInY0LWNvcmUvc3JjL3R5cGVzL1Bvb2xLZXkuc29sIjsKaW1wb3J0IHtQb29sTW9kaWZ5TGlxdWlkaXR5VGVzdH0gZnJvbSAidjQtY29yZS9zcmMvdGVzdC9Qb29sTW9kaWZ5TGlxdWlkaXR5VGVzdC5zb2wiOwoKY29udHJhY3QgQ3JlYXRlTGlxdWlkaXR5IHsKICAgIC8vIHNldCB0aGUgcm91dGVyIGFkZHJlc3MKICAgIFBvb2xNb2RpZnlMaXF1aWRpdHlUZXN0IGxwUm91dGVyID0gUG9vbE1vZGlmeUxpcXVpZGl0eVRlc3QoYWRkcmVzcygweDAxKSk7CgogICAgZnVuY3Rpb24gY3JlYXRlTGlxdWlkaXR5KAogICAgICAgIFBvb2xLZXkgbWVtb3J5IHBvb2xLZXksCiAgICAgICAgaW50MjQgdGlja0xvd2VyLAogICAgICAgIGludDI0IHRpY2tVcHBlciwKICAgICAgICBpbnQyNTYgbGlxdWlkaXR5LAogICAgICAgIGJ5dGVzIGNhbGxkYXRhIGhvb2tEYXRhCiAgICApIGV4dGVybmFsIHsKICAgICAgICAvLyBpZiAwIDwgbGlxdWlkaXR5OiBhZGQgbGlxdWlkaXR5IC0tIG90aGVyd2lzZSByZW1vdmUgbGlxdWlkaXR5CiAgICAgICAgbHBSb3V0ZXIubW9kaWZ5TGlxdWlkaXR5KAogICAgICAgICAgICBwb29sS2V5LAogICAgICAgICAgICBJUG9vbE1hbmFnZXIuTW9kaWZ5TGlxdWlkaXR5UGFyYW1zKHsKICAgICAgICAgICAgICAgIHRpY2tMb3dlcjogdGlja0xvd2VyLAogICAgICAgICAgICAgICAgdGlja1VwcGVyOiB0aWNrVXBwZXIsCiAgICAgICAgICAgICAgICBsaXF1aWRpdHlEZWx0YTogbGlxdWlkaXR5LAogICAgICAgICAgICAgICAgc2FsdDogMAogICAgICAgICAgICB9KSwKICAgICAgICAgICAgaG9va0RhdGEKICAgICAgICApOwogICAgfQp9Cg==", }, { fileName: "CreateLiquidityExampleInputs.sol", - code: "aW1wb3J0IHtQb29sTW9kaWZ5TGlxdWlkaXR5VGVzdH0gZnJvbSAidjQtY29yZS9zcmMvdGVzdC9Qb29sTW9kaWZ5TGlxdWlkaXR5VGVzdC5zb2wiOwoKUG9vbE1vZGlmeUxpcXVpZGl0eVRlc3QgbHBSb3V0ZXIgPSBQb29sTW9kaWZ5TGlxdWlkaXR5VGVzdCgweDAxKTsKYWRkcmVzcyB0b2tlbjAgPSBhZGRyZXNzKDB4MTEpOwphZGRyZXNzIHRva2VuMSA9IGFkZHJlc3MoMHgyMik7CmFkZHJlc3MgaG9va0FkZHJlc3MgPSBhZGRyZXNzKDB4ODApOwoKLy8gUG9vbCB0aGF0IHdpbGwgcmVjZWlldmUgbGlxdWlkaXR5ClBvb2xLZXkgbWVtb3J5IHBvb2wgPSBQb29sS2V5KHsKICAgIGN1cnJlbmN5MDogQ3VycmVuY3kud3JhcCh0b2tlbjApLAogICAgY3VycmVuY3kxOiBDdXJyZW5jeS53cmFwKHRva2VuMSksCiAgICBmZWU6IDMwMDAsCiAgICB0aWNrU3BhY2luZzogNjAsCiAgICBob29rczogSUhvb2tzKGhvb2tBZGRyZXNzKQp9KTsKCi8vIGFwcHJvdmUgdG9rZW5zIHRvIHRoZSBMUCBSb3V0ZXIKSUVSQzIwKHRva2VuMCkuYXBwcm92ZShhZGRyZXNzKGxwUm91dGVyKSwgdHlwZSh1aW50MjU2KS5tYXgpOwpJRVJDMjAodG9rZW4xKS5hcHByb3ZlKGFkZHJlc3MobHBSb3V0ZXIpLCB0eXBlKHVpbnQyNTYpLm1heCk7CgovLyBQcm92aWRlIDEwZTE4IHdvcnRoIG9mIGxpcXVpZGl0eSBvbiB0aGUgcmFuZ2Ugb2YgWy02MDAsIDYwMF0KaW50MjQgdGlja0xvd2VyID0gLTYwMDsKaW50MjQgdGlja1VwcGVyID0gNjAwOwppbnQyNTYgbGlxdWlkaXR5ID0gMTBlMTg7CmxwUm91dGVyLm1vZGlmeUxpcXVpZGl0eSgKICAgIHBvb2xLZXksCiAgICBJUG9vbE1hbmFnZXIuTW9kaWZ5TGlxdWlkaXR5UGFyYW1zKHt0aWNrTG93ZXI6IHRpY2tMb3dlciwgdGlja1VwcGVyOiB0aWNrVXBwZXIsIGxpcXVpZGl0eURlbHRhOiBsaXF1aWRpdHl9KSwKICAgIG5ldyBieXRlcygwKQopOwo=", + code: "aW1wb3J0IHtQb29sTW9kaWZ5TGlxdWlkaXR5VGVzdH0gZnJvbSAidjQtY29yZS9zcmMvdGVzdC9Qb29sTW9kaWZ5TGlxdWlkaXR5VGVzdC5zb2wiOwoKUG9vbE1vZGlmeUxpcXVpZGl0eVRlc3QgbHBSb3V0ZXIgPSBQb29sTW9kaWZ5TGlxdWlkaXR5VGVzdCgweDAxKTsKYWRkcmVzcyB0b2tlbjAgPSBhZGRyZXNzKDB4MTEpOwphZGRyZXNzIHRva2VuMSA9IGFkZHJlc3MoMHgyMik7CmFkZHJlc3MgaG9va0FkZHJlc3MgPSBhZGRyZXNzKDB4ODApOwoKLy8gUG9vbCB0aGF0IHdpbGwgcmVjZWlldmUgbGlxdWlkaXR5ClBvb2xLZXkgbWVtb3J5IHBvb2wgPSBQb29sS2V5KHsKICAgIGN1cnJlbmN5MDogQ3VycmVuY3kud3JhcCh0b2tlbjApLAogICAgY3VycmVuY3kxOiBDdXJyZW5jeS53cmFwKHRva2VuMSksCiAgICBmZWU6IDMwMDAsCiAgICB0aWNrU3BhY2luZzogNjAsCiAgICBob29rczogSUhvb2tzKGhvb2tBZGRyZXNzKQp9KTsKCi8vIGFwcHJvdmUgdG9rZW5zIHRvIHRoZSBMUCBSb3V0ZXIKSUVSQzIwKHRva2VuMCkuYXBwcm92ZShhZGRyZXNzKGxwUm91dGVyKSwgdHlwZSh1aW50MjU2KS5tYXgpOwpJRVJDMjAodG9rZW4xKS5hcHByb3ZlKGFkZHJlc3MobHBSb3V0ZXIpLCB0eXBlKHVpbnQyNTYpLm1heCk7CgovLyBQcm92aWRlIDEwZTE4IHdvcnRoIG9mIGxpcXVpZGl0eSBvbiB0aGUgcmFuZ2Ugb2YgWy02MDAsIDYwMF0KaW50MjQgdGlja0xvd2VyID0gLTYwMDsKaW50MjQgdGlja1VwcGVyID0gNjAwOwppbnQyNTYgbGlxdWlkaXR5ID0gMTBlMTg7CmxwUm91dGVyLm1vZGlmeUxpcXVpZGl0eSgKICAgIHBvb2xLZXksCiAgICBJUG9vbE1hbmFnZXIuTW9kaWZ5TGlxdWlkaXR5UGFyYW1zKHt0aWNrTG93ZXI6IHRpY2tMb3dlciwgdGlja1VwcGVyOiB0aWNrVXBwZXIsIGxpcXVpZGl0eURlbHRhOiBsaXF1aWRpdHksIHNhbHQ6IDB9KSwKICAgIG5ldyBieXRlcygwKQopOwo=", }, ] @@ -60,7 +60,12 @@ const html = ` -
    -
  1. Implement setFee()
  2. -
  3. Poke hook.setFee() to change the fee
  4. -
+

An external party must call hook.setFee() to update the dynamic fee

// SPDX-License-Identifier: MIT
 pragma solidity ^0.8.19;
 
-import {BaseHook} from "@v4-by-example/utils/BaseHook.sol";
+import {BaseHook} from "v4-periphery/BaseHook.sol";
 
 import {Hooks} from "v4-core/src/libraries/Hooks.sol";
 import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
@@ -86,7 +93,9 @@ manager.initialize(poolKey, startingPrice, hookData);
             uint256 timeElapsed = block.timestamp - startTimestamp;
             _currentFee = timeElapsed > 495000 ? uint24(MIN_FEE) : uint24((START_FEE - (timeElapsed * decayRate)) / 10);
         }
-        poolManager.updateDynamicSwapFee(key, _currentFee);
+
+        // Pushes/updates the swap fee for the pool
+        poolManager.updateDynamicLPFee(key, _currentFee);
     }
 
     function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata)
@@ -94,6 +103,7 @@ manager.initialize(poolKey, startingPrice, hookData);
         override
         returns (bytes4)
     {
+        // after pool is initialized, set the initial fee
         setFee(key);
         return BaseHook.afterInitialize.selector;
     }
@@ -110,25 +120,31 @@ manager.initialize(poolKey, startingPrice, hookData);
             beforeSwap: false,
             afterSwap: false,
             beforeDonate: false,
-            afterDonate: false
+            afterDonate: false,
+            beforeSwapReturnDelta: false,
+            afterSwapReturnDelta: false,
+            afterAddLiquidityReturnDelta: false,
+            afterRemoveLiquidityReturnDelta: false
         });
     }
 }
-

Example: Automatic Dynamic Fee

+

Example: Overriding Dynamic Fee

Implements an automatically-updated, time-decaying dynamic fee

-

The hook uses beforeSwap to automatically call the PoolManager, ensuring the fee is always up-to-date

-

incurs +23,000 gas overhead

+

The hook uses beforeSwap to return a valid override fee, which is always up-to-date

+

a few thousand gas cheaper than calling updateDynamicLPFee

// SPDX-License-Identifier: MIT
 pragma solidity ^0.8.19;
 
-import {BaseHook} from "@v4-by-example/utils/BaseHook.sol";
+import {BaseHook} from "v4-periphery/BaseHook.sol";
 
 import {Hooks} from "v4-core/src/libraries/Hooks.sol";
 import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
 import {PoolKey} from "v4-core/src/types/PoolKey.sol";
+import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "v4-core/src/types/BeforeSwapDelta.sol";
+import {LPFeeLibrary} from "v4-core/src/libraries/LPFeeLibrary.sol";
 
 /// @notice A time-decaying dynamically fee, updated automatically with beforeSwap()
-contract AutoDynamicFee is BaseHook {
+contract DynamicFeeOverride is BaseHook {
     uint256 public immutable startTimestamp;
 
     // Start at 5% fee, decaying at rate of 0.00001% per second
@@ -143,16 +159,32 @@ manager.initialize(poolKey, startingPrice, hookData);
         startTimestamp = block.timestamp;
     }
 
-    /// @dev Deteremines a Pool's swap fee
-    function setFee(PoolKey calldata key) public {
+    function beforeSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, bytes calldata)
+        external
+        override
+        returns (bytes4, BeforeSwapDelta, uint24)
+    {
         // Linearly decaying fee, y = mx + b
         // After 495,000 seconds (5.72 days), fee will be a minimum of 0.05%
-        uint24 _currentFee;
+        uint256 _currentFee;
         unchecked {
             uint256 timeElapsed = block.timestamp - startTimestamp;
-            _currentFee = timeElapsed > 495000 ? uint24(MIN_FEE) : uint24((START_FEE - (timeElapsed * decayRate)) / 10);
+            _currentFee =
+                timeElapsed > 495000 ? uint256(MIN_FEE) : (uint256(START_FEE) - (timeElapsed * decayRate)) / 10;
         }
-        poolManager.updateDynamicSwapFee(key, _currentFee);
+
+        // to override the LP fee, its 2nd bit must be set for the override to apply
+        uint256 overrideFee = _currentFee | uint256(LPFeeLibrary.OVERRIDE_FEE_FLAG);
+        return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, uint24(overrideFee));
+    }
+
+    function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata)
+        external
+        override
+        returns (bytes4)
+    {
+        poolManager.updateDynamicLPFee(key, uint24(START_FEE));
+        return BaseHook.afterInitialize.selector;
     }
 
     /// @dev this example hook contract does not implement any hooks
@@ -167,29 +199,13 @@ manager.initialize(poolKey, startingPrice, hookData);
             beforeSwap: true,
             afterSwap: false,
             beforeDonate: false,
-            afterDonate: false
+            afterDonate: false,
+            beforeSwapReturnDelta: false,
+            afterSwapReturnDelta: false,
+            afterAddLiquidityReturnDelta: false,
+            afterRemoveLiquidityReturnDelta: false
         });
     }
-
-    function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata)
-        external
-        override
-        returns (bytes4)
-    {
-        // update the fee on every swap
-        // optimization: only call for top-of-block swap
-        setFee(key);
-        return BaseHook.beforeSwap.selector;
-    }
-
-    function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata)
-        external
-        override
-        returns (bytes4)
-    {
-        setFee(key);
-        return BaseHook.afterInitialize.selector;
-    }
 }
 
` diff --git a/src/pages/fees/dynamic-fee/index.md b/src/pages/fees/dynamic-fee/index.md index 1748f010..d2d21306 100644 --- a/src/pages/fees/dynamic-fee/index.md +++ b/src/pages/fees/dynamic-fee/index.md @@ -7,24 +7,32 @@ keywords: [fee, fees, dynamic fee, dynamic, poke] - Design a v4 pool with a dynamic fee -Uniswap v4 pools can support dynamic swap fees, and do not need to adhere to a static fee (0.05% / 0.30% / 1.0%). The hook needs to use `SwapFeeLibrary.DYNAMIC_FEE_FLAG` in its `PoolKey.fee`. +Uniswap v4 pools can support dynamic swap fees, and do not need to adhere to a static fee (0.05% / 0.30% / 1.0%). The hook needs to use `LPFeeLibrary.DYNAMIC_FEE_FLAG` as its `PoolKey.fee`. **By default, dynamic-fee-pools initialize with a 0% fee** -Despite its name, the fee is *cached* by the `PoolManager` and *the hook* must call `PoolManager.updateDynamicSwapFee()` to change the swap fee. +> Use `afterInitialize` to set the initial fee of a dynamic-fee-pool -**Note: dynamic fees can be computed every swap, but incurs a gas overhead** +--- + +There are two ways to update the dynamic fee: + +1) The hook contract calls `IPoolManager.updateDynamicLPFee(PoolKey memory key, uint24 newDynamicLPFee)` + +2) Use `beforeSwap` and return a valid fee with its 2nd bit set to 1 (i.e. `fee | LPFeeLibrary.OVERRIDE_FEE_FLAG`) + +Using `beforeSwap` is useful for dynamic fees that may change on *every* swap. It's more gas efficient than calling `updateDynamicLPFee` in every call. **Note: the fee returned by beforeSwap is not saved to the PoolManager** --- ### Initialize a Dynamic Fee Pool ```solidity -import {SwapFeeLibrary} from "v4-core/src/libraries/SwapFeeLibrary.sol"; +import {LPFeeLibrary} from "v4-core/src/libraries/LPFeeLibrary.sol"; poolKey = PoolKey( currency0, currency1, - SwapFeeLibrary.DYNAMIC_FEE_FLAG, // signal that the pool has a dynamic fee + LPFeeLibrary.DYNAMIC_FEE_FLAG, // signal that the pool has a dynamic fee 60, IHooks(hook) ); @@ -39,20 +47,18 @@ manager.initialize(poolKey, startingPrice, hookData); * The fee decays 0.00001% every second * After 495,000 seconds, the minimum fee is set to 0.05% - -2) Implement `setFee()` -3) Poke `hook.setFee()` to change the fee +An external party must call `hook.setFee()` to update the dynamic fee ```solidity {{{ManualDynamicFee}}} ``` -## Example: Automatic Dynamic Fee +## Example: Overriding Dynamic Fee *Implements an automatically-updated, time-decaying dynamic fee* -The hook uses `beforeSwap` to automatically call the PoolManager, ensuring the fee is always up-to-date +The hook uses `beforeSwap` to return a valid *override* fee, which is always up-to-date -*incurs +23,000 gas overhead* +*a few thousand gas cheaper than calling `updateDynamicLPFee`* ```solidity -{{{AutoDynamicFee}}} +{{{DynamicFeeOverride}}} ``` diff --git a/src/pages/fees/fixed-hook-fee/FixedHookFee.sol b/src/pages/fees/fixed-hook-fee/FixedHookFee.sol index 2b45c0fe..385fa0ce 100644 --- a/src/pages/fees/fixed-hook-fee/FixedHookFee.sol +++ b/src/pages/fees/fixed-hook-fee/FixedHookFee.sol @@ -1,23 +1,51 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {BaseHook} from "@v4-by-example/utils/BaseHook.sol"; +import {BaseHook} from "v4-periphery/BaseHook.sol"; import {Hooks} from "v4-core/src/libraries/Hooks.sol"; import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "v4-core/src/types/PoolKey.sol"; -import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol"; import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol"; import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "v4-core/src/types/BeforeSwapDelta.sol"; +import {SafeCast} from "v4-core/src/libraries/SafeCast.sol"; contract FixedHookFee is BaseHook { - using PoolIdLibrary for PoolKey; using CurrencyLibrary for Currency; + using SafeCast for uint256; uint256 public constant FIXED_HOOK_FEE = 0.0001e18; constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + function afterSwap( + address, + PoolKey calldata key, + IPoolManager.SwapParams calldata params, + BalanceDelta, + bytes calldata + ) external override returns (bytes4, int128) { + // take a fixed fee of 0.0001 of the unspecified token + + bool exactInput = params.amountSpecified < 0; + bool specifiedIsZero = params.zeroForOne == exactInput; + + // taking a hook fee on the unspecified token + if (specifiedIsZero) { + poolManager.mint(address(this), key.currency1.toId(), FIXED_HOOK_FEE); + } else { + poolManager.mint(address(this), key.currency0.toId(), FIXED_HOOK_FEE); + } + + // by returning the amount the amount the hook has taken, PoolManager will apply the hook's delta to the swapper's delta + return (BaseHook.afterSwap.selector, FIXED_HOOK_FEE.toInt128()); + } + + /// @dev Because the fee is taking as an ERC6909 claim, you'll want to implement logic to collect + /// fee as ERC20 OR ERC6909 + /// ... + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { return Hooks.Permissions({ beforeInitialize: false, @@ -26,38 +54,14 @@ contract FixedHookFee is BaseHook { beforeRemoveLiquidity: false, afterAddLiquidity: false, afterRemoveLiquidity: false, - beforeSwap: true, - afterSwap: false, + beforeSwap: false, + afterSwap: true, beforeDonate: false, - afterDonate: false + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: true, // -- Fee charged on unspecified after swap -- // + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false }); } - - function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata params, bytes calldata) - external - override - returns (bytes4) - { - // take a fixed fee of 0.0001 of the input token - params.zeroForOne - ? poolManager.mint(address(this), key.currency0.toId(), FIXED_HOOK_FEE) - : poolManager.mint(address(this), key.currency1.toId(), FIXED_HOOK_FEE); - - return BaseHook.beforeSwap.selector; - } - - /// @dev Hook fees are kept as PoolManager claims, so collecting ERC20s will require locking - function collectFee(address recipient, Currency currency) external returns (uint256 amount) { - amount = abi.decode(poolManager.lock(abi.encodeCall(this.handleCollectFee, (recipient, currency))), (uint256)); - } - - /// @dev requires the lock pattern in order to call poolManager.burn - function handleCollectFee(address recipient, Currency currency) external returns (uint256 amount) { - // convert the fee (Claims) into ERC20 tokens - amount = poolManager.balanceOf(address(this), currency.toId()); - poolManager.burn(address(this), currency.toId(), amount); - - // direct claims (the tokens) to the recipient - poolManager.take(currency, recipient, amount); - } } diff --git a/src/pages/fees/fixed-hook-fee/index.html.ts b/src/pages/fees/fixed-hook-fee/index.html.ts index 7bac15a0..61743d1c 100644 --- a/src/pages/fees/fixed-hook-fee/index.html.ts +++ b/src/pages/fees/fixed-hook-fee/index.html.ts @@ -18,7 +18,7 @@ export const codes = [ }, { fileName: "FixedHookFee.sol", - code: "Ly8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IE1JVApwcmFnbWEgc29saWRpdHkgXjAuOC4xOTsKCmltcG9ydCB7QmFzZUhvb2t9IGZyb20gIkB2NC1ieS1leGFtcGxlL3V0aWxzL0Jhc2VIb29rLnNvbCI7CgppbXBvcnQge0hvb2tzfSBmcm9tICJ2NC1jb3JlL3NyYy9saWJyYXJpZXMvSG9va3Muc29sIjsKaW1wb3J0IHtJUG9vbE1hbmFnZXJ9IGZyb20gInY0LWNvcmUvc3JjL2ludGVyZmFjZXMvSVBvb2xNYW5hZ2VyLnNvbCI7CmltcG9ydCB7UG9vbEtleX0gZnJvbSAidjQtY29yZS9zcmMvdHlwZXMvUG9vbEtleS5zb2wiOwppbXBvcnQge1Bvb2xJZCwgUG9vbElkTGlicmFyeX0gZnJvbSAidjQtY29yZS9zcmMvdHlwZXMvUG9vbElkLnNvbCI7CmltcG9ydCB7QmFsYW5jZURlbHRhfSBmcm9tICJ2NC1jb3JlL3NyYy90eXBlcy9CYWxhbmNlRGVsdGEuc29sIjsKaW1wb3J0IHtDdXJyZW5jeSwgQ3VycmVuY3lMaWJyYXJ5fSBmcm9tICJ2NC1jb3JlL3NyYy90eXBlcy9DdXJyZW5jeS5zb2wiOwoKY29udHJhY3QgRml4ZWRIb29rRmVlIGlzIEJhc2VIb29rIHsKICAgIHVzaW5nIFBvb2xJZExpYnJhcnkgZm9yIFBvb2xLZXk7CiAgICB1c2luZyBDdXJyZW5jeUxpYnJhcnkgZm9yIEN1cnJlbmN5OwoKICAgIHVpbnQyNTYgcHVibGljIGNvbnN0YW50IEZJWEVEX0hPT0tfRkVFID0gMC4wMDAxZTE4OwoKICAgIGNvbnN0cnVjdG9yKElQb29sTWFuYWdlciBfcG9vbE1hbmFnZXIpIEJhc2VIb29rKF9wb29sTWFuYWdlcikge30KCiAgICBmdW5jdGlvbiBnZXRIb29rUGVybWlzc2lvbnMoKSBwdWJsaWMgcHVyZSBvdmVycmlkZSByZXR1cm5zIChIb29rcy5QZXJtaXNzaW9ucyBtZW1vcnkpIHsKICAgICAgICByZXR1cm4gSG9va3MuUGVybWlzc2lvbnMoewogICAgICAgICAgICBiZWZvcmVJbml0aWFsaXplOiBmYWxzZSwKICAgICAgICAgICAgYWZ0ZXJJbml0aWFsaXplOiBmYWxzZSwKICAgICAgICAgICAgYmVmb3JlQWRkTGlxdWlkaXR5OiBmYWxzZSwKICAgICAgICAgICAgYmVmb3JlUmVtb3ZlTGlxdWlkaXR5OiBmYWxzZSwKICAgICAgICAgICAgYWZ0ZXJBZGRMaXF1aWRpdHk6IGZhbHNlLAogICAgICAgICAgICBhZnRlclJlbW92ZUxpcXVpZGl0eTogZmFsc2UsCiAgICAgICAgICAgIGJlZm9yZVN3YXA6IHRydWUsCiAgICAgICAgICAgIGFmdGVyU3dhcDogZmFsc2UsCiAgICAgICAgICAgIGJlZm9yZURvbmF0ZTogZmFsc2UsCiAgICAgICAgICAgIGFmdGVyRG9uYXRlOiBmYWxzZQogICAgICAgIH0pOwogICAgfQoKICAgIGZ1bmN0aW9uIGJlZm9yZVN3YXAoYWRkcmVzcywgUG9vbEtleSBjYWxsZGF0YSBrZXksIElQb29sTWFuYWdlci5Td2FwUGFyYW1zIGNhbGxkYXRhIHBhcmFtcywgYnl0ZXMgY2FsbGRhdGEpCiAgICAgICAgZXh0ZXJuYWwKICAgICAgICBvdmVycmlkZQogICAgICAgIHJldHVybnMgKGJ5dGVzNCkKICAgIHsKICAgICAgICAvLyB0YWtlIGEgZml4ZWQgZmVlIG9mIDAuMDAwMSBvZiB0aGUgaW5wdXQgdG9rZW4KICAgICAgICBwYXJhbXMuemVyb0Zvck9uZQogICAgICAgICAgICA/IHBvb2xNYW5hZ2VyLm1pbnQoYWRkcmVzcyh0aGlzKSwga2V5LmN1cnJlbmN5MC50b0lkKCksIEZJWEVEX0hPT0tfRkVFKQogICAgICAgICAgICA6IHBvb2xNYW5hZ2VyLm1pbnQoYWRkcmVzcyh0aGlzKSwga2V5LmN1cnJlbmN5MS50b0lkKCksIEZJWEVEX0hPT0tfRkVFKTsKCiAgICAgICAgcmV0dXJuIEJhc2VIb29rLmJlZm9yZVN3YXAuc2VsZWN0b3I7CiAgICB9CgogICAgLy8vIEBkZXYgSG9vayBmZWVzIGFyZSBrZXB0IGFzIFBvb2xNYW5hZ2VyIGNsYWltcywgc28gY29sbGVjdGluZyBFUkMyMHMgd2lsbCByZXF1aXJlIGxvY2tpbmcKICAgIGZ1bmN0aW9uIGNvbGxlY3RGZWUoYWRkcmVzcyByZWNpcGllbnQsIEN1cnJlbmN5IGN1cnJlbmN5KSBleHRlcm5hbCByZXR1cm5zICh1aW50MjU2IGFtb3VudCkgewogICAgICAgIGFtb3VudCA9IGFiaS5kZWNvZGUocG9vbE1hbmFnZXIubG9jayhhYmkuZW5jb2RlQ2FsbCh0aGlzLmhhbmRsZUNvbGxlY3RGZWUsIChyZWNpcGllbnQsIGN1cnJlbmN5KSkpLCAodWludDI1NikpOwogICAgfQoKICAgIC8vLyBAZGV2IHJlcXVpcmVzIHRoZSBsb2NrIHBhdHRlcm4gaW4gb3JkZXIgdG8gY2FsbCBwb29sTWFuYWdlci5idXJuCiAgICBmdW5jdGlvbiBoYW5kbGVDb2xsZWN0RmVlKGFkZHJlc3MgcmVjaXBpZW50LCBDdXJyZW5jeSBjdXJyZW5jeSkgZXh0ZXJuYWwgcmV0dXJucyAodWludDI1NiBhbW91bnQpIHsKICAgICAgICAvLyBjb252ZXJ0IHRoZSBmZWUgKENsYWltcykgaW50byBFUkMyMCB0b2tlbnMKICAgICAgICBhbW91bnQgPSBwb29sTWFuYWdlci5iYWxhbmNlT2YoYWRkcmVzcyh0aGlzKSwgY3VycmVuY3kudG9JZCgpKTsKICAgICAgICBwb29sTWFuYWdlci5idXJuKGFkZHJlc3ModGhpcyksIGN1cnJlbmN5LnRvSWQoKSwgYW1vdW50KTsKCiAgICAgICAgLy8gZGlyZWN0IGNsYWltcyAodGhlIHRva2VucykgdG8gdGhlIHJlY2lwaWVudAogICAgICAgIHBvb2xNYW5hZ2VyLnRha2UoY3VycmVuY3ksIHJlY2lwaWVudCwgYW1vdW50KTsKICAgIH0KfQo=", + code: "Ly8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IE1JVApwcmFnbWEgc29saWRpdHkgXjAuOC4xOTsKCmltcG9ydCB7QmFzZUhvb2t9IGZyb20gInY0LXBlcmlwaGVyeS9CYXNlSG9vay5zb2wiOwoKaW1wb3J0IHtIb29rc30gZnJvbSAidjQtY29yZS9zcmMvbGlicmFyaWVzL0hvb2tzLnNvbCI7CmltcG9ydCB7SVBvb2xNYW5hZ2VyfSBmcm9tICJ2NC1jb3JlL3NyYy9pbnRlcmZhY2VzL0lQb29sTWFuYWdlci5zb2wiOwppbXBvcnQge1Bvb2xLZXl9IGZyb20gInY0LWNvcmUvc3JjL3R5cGVzL1Bvb2xLZXkuc29sIjsKaW1wb3J0IHtCYWxhbmNlRGVsdGF9IGZyb20gInY0LWNvcmUvc3JjL3R5cGVzL0JhbGFuY2VEZWx0YS5zb2wiOwppbXBvcnQge0N1cnJlbmN5LCBDdXJyZW5jeUxpYnJhcnl9IGZyb20gInY0LWNvcmUvc3JjL3R5cGVzL0N1cnJlbmN5LnNvbCI7CmltcG9ydCB7QmVmb3JlU3dhcERlbHRhLCBCZWZvcmVTd2FwRGVsdGFMaWJyYXJ5fSBmcm9tICJ2NC1jb3JlL3NyYy90eXBlcy9CZWZvcmVTd2FwRGVsdGEuc29sIjsKaW1wb3J0IHtTYWZlQ2FzdH0gZnJvbSAidjQtY29yZS9zcmMvbGlicmFyaWVzL1NhZmVDYXN0LnNvbCI7Cgpjb250cmFjdCBGaXhlZEhvb2tGZWUgaXMgQmFzZUhvb2sgewogICAgdXNpbmcgQ3VycmVuY3lMaWJyYXJ5IGZvciBDdXJyZW5jeTsKICAgIHVzaW5nIFNhZmVDYXN0IGZvciB1aW50MjU2OwoKICAgIHVpbnQyNTYgcHVibGljIGNvbnN0YW50IEZJWEVEX0hPT0tfRkVFID0gMC4wMDAxZTE4OwoKICAgIGNvbnN0cnVjdG9yKElQb29sTWFuYWdlciBfcG9vbE1hbmFnZXIpIEJhc2VIb29rKF9wb29sTWFuYWdlcikge30KCiAgICBmdW5jdGlvbiBhZnRlclN3YXAoCiAgICAgICAgYWRkcmVzcywKICAgICAgICBQb29sS2V5IGNhbGxkYXRhIGtleSwKICAgICAgICBJUG9vbE1hbmFnZXIuU3dhcFBhcmFtcyBjYWxsZGF0YSBwYXJhbXMsCiAgICAgICAgQmFsYW5jZURlbHRhLAogICAgICAgIGJ5dGVzIGNhbGxkYXRhCiAgICApIGV4dGVybmFsIG92ZXJyaWRlIHJldHVybnMgKGJ5dGVzNCwgaW50MTI4KSB7CiAgICAgICAgLy8gdGFrZSBhIGZpeGVkIGZlZSBvZiAwLjAwMDEgb2YgdGhlIHVuc3BlY2lmaWVkIHRva2VuCgogICAgICAgIGJvb2wgZXhhY3RJbnB1dCA9IHBhcmFtcy5hbW91bnRTcGVjaWZpZWQgPCAwOwogICAgICAgIGJvb2wgc3BlY2lmaWVkSXNaZXJvID0gcGFyYW1zLnplcm9Gb3JPbmUgPT0gZXhhY3RJbnB1dDsKCiAgICAgICAgLy8gdGFraW5nIGEgaG9vayBmZWUgb24gdGhlIHVuc3BlY2lmaWVkIHRva2VuCiAgICAgICAgaWYgKHNwZWNpZmllZElzWmVybykgewogICAgICAgICAgICBwb29sTWFuYWdlci5taW50KGFkZHJlc3ModGhpcyksIGtleS5jdXJyZW5jeTEudG9JZCgpLCBGSVhFRF9IT09LX0ZFRSk7CiAgICAgICAgfSBlbHNlIHsKICAgICAgICAgICAgcG9vbE1hbmFnZXIubWludChhZGRyZXNzKHRoaXMpLCBrZXkuY3VycmVuY3kwLnRvSWQoKSwgRklYRURfSE9PS19GRUUpOwogICAgICAgIH0KCiAgICAgICAgLy8gYnkgcmV0dXJuaW5nIHRoZSBhbW91bnQgdGhlIGFtb3VudCB0aGUgaG9vayBoYXMgdGFrZW4sIFBvb2xNYW5hZ2VyIHdpbGwgYXBwbHkgdGhlIGhvb2sncyBkZWx0YSB0byB0aGUgc3dhcHBlcidzIGRlbHRhCiAgICAgICAgcmV0dXJuIChCYXNlSG9vay5hZnRlclN3YXAuc2VsZWN0b3IsIEZJWEVEX0hPT0tfRkVFLnRvSW50MTI4KCkpOwogICAgfQoKICAgIC8vLyBAZGV2IEJlY2F1c2UgdGhlIGZlZSBpcyB0YWtpbmcgYXMgYW4gRVJDNjkwOSBjbGFpbSwgeW91J2xsIHdhbnQgdG8gaW1wbGVtZW50IGxvZ2ljIHRvIGNvbGxlY3QKICAgIC8vLyBmZWUgYXMgRVJDMjAgT1IgRVJDNjkwOQogICAgLy8vIC4uLgoKICAgIGZ1bmN0aW9uIGdldEhvb2tQZXJtaXNzaW9ucygpIHB1YmxpYyBwdXJlIG92ZXJyaWRlIHJldHVybnMgKEhvb2tzLlBlcm1pc3Npb25zIG1lbW9yeSkgewogICAgICAgIHJldHVybiBIb29rcy5QZXJtaXNzaW9ucyh7CiAgICAgICAgICAgIGJlZm9yZUluaXRpYWxpemU6IGZhbHNlLAogICAgICAgICAgICBhZnRlckluaXRpYWxpemU6IGZhbHNlLAogICAgICAgICAgICBiZWZvcmVBZGRMaXF1aWRpdHk6IGZhbHNlLAogICAgICAgICAgICBiZWZvcmVSZW1vdmVMaXF1aWRpdHk6IGZhbHNlLAogICAgICAgICAgICBhZnRlckFkZExpcXVpZGl0eTogZmFsc2UsCiAgICAgICAgICAgIGFmdGVyUmVtb3ZlTGlxdWlkaXR5OiBmYWxzZSwKICAgICAgICAgICAgYmVmb3JlU3dhcDogZmFsc2UsCiAgICAgICAgICAgIGFmdGVyU3dhcDogdHJ1ZSwKICAgICAgICAgICAgYmVmb3JlRG9uYXRlOiBmYWxzZSwKICAgICAgICAgICAgYWZ0ZXJEb25hdGU6IGZhbHNlLAogICAgICAgICAgICBiZWZvcmVTd2FwUmV0dXJuRGVsdGE6IGZhbHNlLAogICAgICAgICAgICBhZnRlclN3YXBSZXR1cm5EZWx0YTogdHJ1ZSwgLy8gLS0gRmVlIGNoYXJnZWQgb24gdW5zcGVjaWZpZWQgYWZ0ZXIgc3dhcCAtLSAvLwogICAgICAgICAgICBhZnRlckFkZExpcXVpZGl0eVJldHVybkRlbHRhOiBmYWxzZSwKICAgICAgICAgICAgYWZ0ZXJSZW1vdmVMaXF1aWRpdHlSZXR1cm5EZWx0YTogZmFsc2UKICAgICAgICB9KTsKICAgIH0KfQo=", }, { fileName: "SetAccessLockPermission.sol", @@ -26,14 +26,89 @@ export const codes = [ }, ] -const html = `

UNDER CONSTRUCTION

-

PROCEED IF YOU ARE BRAVE

-

requires using a bleeding edge PR

-

Hook Fees

+const html = `

Hook Fees

-

Optional hook fees are taken (from swappers) via the hook. Hook fees can be dynamically calculated, or simply set to a fixed amount.

-` +

Optional hook fees are taken (from swappers) via the hook. Hook fees can be dynamically calculated, or simply set to a fixed amount. Hooks can charge fees in any currency, however charging USDC on the ETH/DAI pair may pose routing-compatibility issues.

+

Hook fees are achieved using the return-delta-flags, i.e. BEFORE_SWAP_RETURNS_DELTA_FLAG and/or AFTER_SWAP_RETURNS_DELTA_FLAG. In beforeSwap or afterSwap, the hook uses .mint or .take to charge fees. The additional deltas are then applied to the Swapper.

+

While more investigations are required, charging fees on the unspecified currency is the recommended practice.

+ +
+

Example: Static Hook Fee

+

This example hook charges a fixed-fee of 0.0001e18 tokens

+
// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.19;
+
+import {BaseHook} from "v4-periphery/BaseHook.sol";
+
+import {Hooks} from "v4-core/src/libraries/Hooks.sol";
+import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
+import {PoolKey} from "v4-core/src/types/PoolKey.sol";
+import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
+import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol";
+import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "v4-core/src/types/BeforeSwapDelta.sol";
+import {SafeCast} from "v4-core/src/libraries/SafeCast.sol";
+
+contract FixedHookFee is BaseHook {
+    using CurrencyLibrary for Currency;
+    using SafeCast for uint256;
+
+    uint256 public constant FIXED_HOOK_FEE = 0.0001e18;
+
+    constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}
+
+    function afterSwap(
+        address,
+        PoolKey calldata key,
+        IPoolManager.SwapParams calldata params,
+        BalanceDelta,
+        bytes calldata
+    ) external override returns (bytes4, int128) {
+        // take a fixed fee of 0.0001 of the unspecified token
+
+        bool exactInput = params.amountSpecified < 0;
+        bool specifiedIsZero = params.zeroForOne == exactInput;
+
+        // taking a hook fee on the unspecified token
+        if (specifiedIsZero) {
+            poolManager.mint(address(this), key.currency1.toId(), FIXED_HOOK_FEE);
+        } else {
+            poolManager.mint(address(this), key.currency0.toId(), FIXED_HOOK_FEE);
+        }
+
+        // by returning the amount the amount the hook has taken, PoolManager will apply the hook's delta to the swapper's delta
+        return (BaseHook.afterSwap.selector, FIXED_HOOK_FEE.toInt128());
+    }
+
+    /// @dev Because the fee is taking as an ERC6909 claim, you'll want to implement logic to collect
+    /// fee as ERC20 OR ERC6909
+    /// ...
+
+    function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
+        return Hooks.Permissions({
+            beforeInitialize: false,
+            afterInitialize: false,
+            beforeAddLiquidity: false,
+            beforeRemoveLiquidity: false,
+            afterAddLiquidity: false,
+            afterRemoveLiquidity: false,
+            beforeSwap: false,
+            afterSwap: true,
+            beforeDonate: false,
+            afterDonate: false,
+            beforeSwapReturnDelta: false,
+            afterSwapReturnDelta: true, // -- Fee charged on unspecified after swap -- //
+            afterAddLiquidityReturnDelta: false,
+            afterRemoveLiquidityReturnDelta: false
+        });
+    }
+}
+
` export default html diff --git a/src/pages/fees/fixed-hook-fee/index.md b/src/pages/fees/fixed-hook-fee/index.md index 1187d7a8..e1db94f7 100644 --- a/src/pages/fees/fixed-hook-fee/index.md +++ b/src/pages/fees/fixed-hook-fee/index.md @@ -5,14 +5,25 @@ description: Charge a static hook fee keywords: [hook, hooks, fee, static fee, hook fee] --- -# UNDER CONSTRUCTION +Hook Fees +- Charge a hook fee -# PROCEED IF YOU ARE BRAVE +Optional hook fees are taken (from swappers) via the hook. Hook fees can be dynamically calculated, or simply set to a fixed amount. Hooks can charge fees in any currency, however charging USDC on the ETH/DAI pair may pose routing-compatibility issues. -## requires using a [bleeding edge PR](https://github.com/Uniswap/v4-core/pull/482) +Hook fees are achieved using the return-delta-flags, i.e. `BEFORE_SWAP_RETURNS_DELTA_FLAG` and/or `AFTER_SWAP_RETURNS_DELTA_FLAG`. In beforeSwap or afterSwap, the hook uses `.mint` or `.take` to charge fees. The additional deltas are then applied to the Swapper. +While more investigations are required, charging fees on the `unspecified` currency is the recommended practice. -Hook Fees -- Charge a hook fee +* For exact-input swaps, the fee on *unspecified* is the *output* token + +* For exact-outpout swaps, the fee on *unspecified* is the *input* token + +--- + +## Example: Static Hook Fee + +This example hook charges a fixed-fee of 0.0001e18 tokens -Optional hook fees are taken (from swappers) via the hook. Hook fees can be dynamically calculated, or simply set to a fixed amount. +```solidity +{{{FixedHookFee}}} +``` \ No newline at end of file diff --git a/src/pages/fees/swap-fee/index.html.ts b/src/pages/fees/swap-fee/index.html.ts index 2db1c233..7525c1d0 100644 --- a/src/pages/fees/swap-fee/index.html.ts +++ b/src/pages/fees/swap-fee/index.html.ts @@ -32,14 +32,7 @@ const html = `

Swap fees are accrued to liquidity providers and paid by swappe

  • Exact Output: User is willing to pay USDC for 0.01 ETH: fee is taken from the USDC input
  • Note on Protocol Fee

    -

    The protocol fee is not currently enabled. However, it is expressed as a percentage of the swap fee and taken from the swap fee

    -

    Example:

    - +

    The protocol fee is not currently enabled. However, it is an additive fee on top of the Swap Fee and is expressed as a percentage


    Example: Setting a Swap Fee

    The swap fee is set during pool creation, as defined in its PoolKey

    diff --git a/src/pages/fees/swap-fee/index.md b/src/pages/fees/swap-fee/index.md index d51e0084..7374fa06 100644 --- a/src/pages/fees/swap-fee/index.md +++ b/src/pages/fees/swap-fee/index.md @@ -23,14 +23,7 @@ Example: ### Note on Protocol Fee -The protocol fee is **not** currently enabled. However, it is expressed as a percentage of the swap fee and _taken_ from the swap fee - -Example: - -- swap fee 0.30%, protocol fee 0.10% -- A swapper pays 1e18 in fees (0.30% of their swap size) -- 0.001e18 token (0.10% of 1e18) is taken for the protocol -- 0.999e18 token (99.9% of 1e18) is given the liquidity providers +The protocol fee is **not** currently enabled. However, it is an additive fee on top of the Swap Fee and is expressed as a percentage --- diff --git a/src/pages/hooks/custom-curve/CustomCurve.sol b/src/pages/hooks/custom-curve/CustomCurve.sol index c7fcb445..864bca10 100644 --- a/src/pages/hooks/custom-curve/CustomCurve.sol +++ b/src/pages/hooks/custom-curve/CustomCurve.sol @@ -1,23 +1,93 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -// TODO: replace with v4-periphery/BaseHook.sol when compatibility is fixed -import {BaseHook} from "@v4-by-example/utils/BaseHook.sol"; - +import {BaseHook} from "v4-periphery/BaseHook.sol"; import {Hooks} from "v4-core/src/libraries/Hooks.sol"; import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "v4-core/src/types/PoolKey.sol"; -import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol"; import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol"; +import {toBeforeSwapDelta, BeforeSwapDelta} from "v4-core/src/types/BeforeSwapDelta.sol"; +import {CurrencySettleTake} from "v4-core/src/libraries/CurrencySettleTake.sol"; +import {SafeCast} from "v4-core/src/libraries/SafeCast.sol"; -import {IERC20} from "forge-std/interfaces/IERC20.sol"; - -contract CustomCurve is BaseHook { - using PoolIdLibrary for PoolKey; +abstract contract CustomCurveBase is BaseHook { using CurrencyLibrary for Currency; + using CurrencySettleTake for Currency; + using SafeCast for uint256; constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + /// NOTE: In the inheriting contract, define a function to add liquidity... + + /// @notice Returns the amount of output tokens for an exact-input swap + /// @param amountIn the amount of input tokens + /// @param input the input token + /// @param output the output token + /// @param zeroForOne true if the input token is token0 + /// @return amountOut the amount of output tokens + function getAmountOutFromExactInput(uint256 amountIn, Currency input, Currency output, bool zeroForOne) + internal + virtual + returns (uint256 amountOut); + + /// @notice Returns the amount of input tokens for an exact-output swap + /// @param amountOut the amount of output tokens the user expects to receive + /// @param input the input token + /// @param output the output token + /// @param zeroForOne true if the input token is token0 + /// @return amountIn the amount of input tokens required to produce amountOut + function getAmountInForExactOutput(uint256 amountOut, Currency input, Currency output, bool zeroForOne) + internal + virtual + returns (uint256 amountIn); + + /// @dev Facilitate a custom curve via beforeSwap + return delta + /// @dev input tokens are taken from the PoolManager, creating a debt paid by the swapper + /// @dev output takens are transferred from the hook to the PoolManager, creating a credit claimed by the swapper + function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata params, bytes calldata) + external + override + returns (bytes4, BeforeSwapDelta, uint24) + { + bool exactInput = params.amountSpecified < 0; + (Currency specified, Currency unspecified) = + (params.zeroForOne == exactInput) ? (key.currency0, key.currency1) : (key.currency1, key.currency0); + + uint256 specifiedAmount = exactInput ? uint256(-params.amountSpecified) : uint256(params.amountSpecified); + uint256 unspecifiedAmount; + BeforeSwapDelta returnDelta; + if (exactInput) { + // in exact-input swaps, the specified token is a debt that gets paid down by the swapper + // the unspecified token is credited to the PoolManager, that is claimed by the swapper + unspecifiedAmount = getAmountOutFromExactInput(specifiedAmount, specified, unspecified, params.zeroForOne); + specified.take(poolManager, address(this), specifiedAmount, true); + unspecified.settle(poolManager, address(this), unspecifiedAmount, true); + + returnDelta = toBeforeSwapDelta(specifiedAmount.toInt128(), -unspecifiedAmount.toInt128()); + } else { + // exactOutput + // in exact-output swaps, the unspecified token is a debt that gets paid down by the swapper + // the specified token is credited to the PoolManager, that is claimed by the swapper + unspecifiedAmount = getAmountInForExactOutput(specifiedAmount, unspecified, specified, params.zeroForOne); + unspecified.take(poolManager, address(this), unspecifiedAmount, true); + specified.settle(poolManager, address(this), specifiedAmount, true); + + returnDelta = toBeforeSwapDelta(-specifiedAmount.toInt128(), unspecifiedAmount.toInt128()); + } + + return (BaseHook.beforeSwap.selector, returnDelta, 0); + } + + /// @notice No liquidity will be managed by v4 PoolManager + function beforeAddLiquidity(address, PoolKey calldata, IPoolManager.ModifyLiquidityParams calldata, bytes calldata) + external + pure + override + returns (bytes4) + { + revert("No v4 Liquidity allowed"); + } + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { return Hooks.Permissions({ beforeInitialize: false, @@ -26,77 +96,65 @@ contract CustomCurve is BaseHook { beforeRemoveLiquidity: false, afterAddLiquidity: false, afterRemoveLiquidity: false, - beforeSwap: true, // -- No-op'ing the swap -- // + beforeSwap: true, // -- Custom Curve Handler -- // afterSwap: false, beforeDonate: false, - afterDonate: false + afterDonate: false, + beforeSwapReturnDelta: true, // -- Enables Custom Curves -- // + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false }); } +} - // ------------------------------------------ // - // Liquidity Functions (not production ready) // - // ------------------------------------------ // - /// @notice Add liquidity for the custom curve - /// @param key PoolKey of the pool to add liquidity to - /// @param liquidityDelta Amount of liquidity to add - function addLiquidity(PoolKey calldata key, uint256 liquidityDelta) external { - // @dev: Update this - // Given spot price and the custom curve, calculate the ratio of tokens to add - uint256 token0In; - uint256 token1In; - - // transfer tokens to hook, to act as liquidity for swaps - IERC20(Currency.unwrap(key.currency0)).transferFrom(msg.sender, address(this), token0In); - IERC20(Currency.unwrap(key.currency1)).transferFrom(msg.sender, address(this), token1In); - - // TODO: production-ready requires minting a receipt token etc - } +contract ConstantSumCurve is CustomCurveBase { + using CurrencySettleTake for Currency; - /// @notice Calculate the amount of tokens paid by the swapper - /// @param params SwapParams passed to the swap function - /// @return The amount of tokens paid by the swapper - function getTokenInAmount(IPoolManager.SwapParams calldata params) public pure returns (uint256) { - return 1e18; - } + constructor(IPoolManager _manager) CustomCurveBase(_manager) {} - /// @notice Calculate the amount of tokens sent to the swapper - /// @param params SwapParams passed to the swap function - /// @return The amount of tokens sent to the swapper - function getTokenOutAmount(IPoolManager.SwapParams calldata params) public pure returns (uint256) { - return 1e18; + function getAmountOutFromExactInput(uint256 amountIn, Currency, Currency, bool) + internal + pure + override + returns (uint256 amountOut) + { + // in constant-sum curve, tokens trade exactly 1:1 + amountOut = amountIn; } - function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata params, bytes calldata) - external + function getAmountInForExactOutput(uint256 amountOut, Currency, Currency, bool) + internal + pure override - returns (bytes4) + returns (uint256 amountIn) { - // calculate the amount of tokens, based on a custom curve - uint256 tokenInAmount = getTokenInAmount(params); // amount of tokens paid by the swapper - uint256 tokenOutAmount = getTokenOutAmount(params); // amount of tokens sent to the swapper - - // determine inbound/outbound token based on 0->1 or 1->0 swap - (Currency inbound, Currency outbound) = - params.zeroForOne ? (key.currency0, key.currency1) : (key.currency1, key.currency0); - - // inbound token is added to hook's reserves, debt paid by the swapper - poolManager.take(inbound, address(this), tokenInAmount); - - // outbound token is removed from hook's reserves, and sent to the swapper - outbound.transfer(address(poolManager), tokenOutAmount); - poolManager.settle(outbound); + // in constant-sum curve, tokens trade exactly 1:1 + amountIn = amountOut; + } - // prevent normal v4 swap logic from executing - return BaseHook.beforeSwap.selector; + /// @notice Add liquidity through the hook + /// @dev Not production-ready, only serves an example of hook-owned liquidity + function addLiquidity(PoolKey calldata key, uint256 amount0, uint256 amount1) external { + poolManager.unlock( + abi.encodeCall(this.handleAddLiquidity, (key.currency0, key.currency1, amount0, amount1, msg.sender)) + ); } - /// @notice No liquidity will be managed by v4 PoolManager - function beforeAddLiquidity( - address, - PoolKey calldata key, - IPoolManager.ModifyLiquidityParams calldata, - bytes calldata - ) external override returns (bytes4) { - revert("No v4 Liquidity allowed"); + /// @dev Handle liquidity addition by taking tokens from the sender and claiming ERC6909 to the hook address + function handleAddLiquidity( + Currency currency0, + Currency currency1, + uint256 amount0, + uint256 amount1, + address sender + ) external selfOnly returns (bytes memory) { + currency0.settle(poolManager, sender, amount0, false); + currency0.take(poolManager, address(this), amount0, true); + + currency1.settle(poolManager, sender, amount1, false); + currency1.take(poolManager, address(this), amount1, true); + + return abi.encode(amount0, amount1); } } diff --git a/src/pages/hooks/custom-curve/index.html.ts b/src/pages/hooks/custom-curve/index.html.ts index fc485016..7a318981 100644 --- a/src/pages/hooks/custom-curve/index.html.ts +++ b/src/pages/hooks/custom-curve/index.html.ts @@ -15,24 +15,189 @@ export const keywords = [ export const codes = [ { fileName: "CustomCurve.sol", - code: "Ly8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IE1JVApwcmFnbWEgc29saWRpdHkgXjAuOC4yMDsKCi8vIFRPRE86IHJlcGxhY2Ugd2l0aCB2NC1wZXJpcGhlcnkvQmFzZUhvb2suc29sIHdoZW4gY29tcGF0aWJpbGl0eSBpcyBmaXhlZAppbXBvcnQge0Jhc2VIb29rfSBmcm9tICJAdjQtYnktZXhhbXBsZS91dGlscy9CYXNlSG9vay5zb2wiOwoKaW1wb3J0IHtIb29rc30gZnJvbSAidjQtY29yZS9zcmMvbGlicmFyaWVzL0hvb2tzLnNvbCI7CmltcG9ydCB7SVBvb2xNYW5hZ2VyfSBmcm9tICJ2NC1jb3JlL3NyYy9pbnRlcmZhY2VzL0lQb29sTWFuYWdlci5zb2wiOwppbXBvcnQge1Bvb2xLZXl9IGZyb20gInY0LWNvcmUvc3JjL3R5cGVzL1Bvb2xLZXkuc29sIjsKaW1wb3J0IHtQb29sSWQsIFBvb2xJZExpYnJhcnl9IGZyb20gInY0LWNvcmUvc3JjL3R5cGVzL1Bvb2xJZC5zb2wiOwppbXBvcnQge0N1cnJlbmN5LCBDdXJyZW5jeUxpYnJhcnl9IGZyb20gInY0LWNvcmUvc3JjL3R5cGVzL0N1cnJlbmN5LnNvbCI7CgppbXBvcnQge0lFUkMyMH0gZnJvbSAiZm9yZ2Utc3RkL2ludGVyZmFjZXMvSUVSQzIwLnNvbCI7Cgpjb250cmFjdCBDdXN0b21DdXJ2ZSBpcyBCYXNlSG9vayB7CiAgICB1c2luZyBQb29sSWRMaWJyYXJ5IGZvciBQb29sS2V5OwogICAgdXNpbmcgQ3VycmVuY3lMaWJyYXJ5IGZvciBDdXJyZW5jeTsKCiAgICBjb25zdHJ1Y3RvcihJUG9vbE1hbmFnZXIgX3Bvb2xNYW5hZ2VyKSBCYXNlSG9vayhfcG9vbE1hbmFnZXIpIHt9CgogICAgZnVuY3Rpb24gZ2V0SG9va1Blcm1pc3Npb25zKCkgcHVibGljIHB1cmUgb3ZlcnJpZGUgcmV0dXJucyAoSG9va3MuUGVybWlzc2lvbnMgbWVtb3J5KSB7CiAgICAgICAgcmV0dXJuIEhvb2tzLlBlcm1pc3Npb25zKHsKICAgICAgICAgICAgYmVmb3JlSW5pdGlhbGl6ZTogZmFsc2UsCiAgICAgICAgICAgIGFmdGVySW5pdGlhbGl6ZTogZmFsc2UsCiAgICAgICAgICAgIGJlZm9yZUFkZExpcXVpZGl0eTogdHJ1ZSwgLy8gLS0gZGlzYWJsZSB2NCBsaXF1aWRpdHkgd2l0aCBhIHJldmVydCAtLSAvLwogICAgICAgICAgICBiZWZvcmVSZW1vdmVMaXF1aWRpdHk6IGZhbHNlLAogICAgICAgICAgICBhZnRlckFkZExpcXVpZGl0eTogZmFsc2UsCiAgICAgICAgICAgIGFmdGVyUmVtb3ZlTGlxdWlkaXR5OiBmYWxzZSwKICAgICAgICAgICAgYmVmb3JlU3dhcDogdHJ1ZSwgLy8gLS0gTm8tb3AnaW5nIHRoZSBzd2FwIC0tICAvLwogICAgICAgICAgICBhZnRlclN3YXA6IGZhbHNlLAogICAgICAgICAgICBiZWZvcmVEb25hdGU6IGZhbHNlLAogICAgICAgICAgICBhZnRlckRvbmF0ZTogZmFsc2UKICAgICAgICB9KTsKICAgIH0KCiAgICAvLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gLy8KICAgIC8vIExpcXVpZGl0eSBGdW5jdGlvbnMgKG5vdCBwcm9kdWN0aW9uIHJlYWR5KSAvLwogICAgLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tIC8vCiAgICAvLy8gQG5vdGljZSBBZGQgbGlxdWlkaXR5IGZvciB0aGUgY3VzdG9tIGN1cnZlCiAgICAvLy8gQHBhcmFtIGtleSBQb29sS2V5IG9mIHRoZSBwb29sIHRvIGFkZCBsaXF1aWRpdHkgdG8KICAgIC8vLyBAcGFyYW0gbGlxdWlkaXR5RGVsdGEgQW1vdW50IG9mIGxpcXVpZGl0eSB0byBhZGQKICAgIGZ1bmN0aW9uIGFkZExpcXVpZGl0eShQb29sS2V5IGNhbGxkYXRhIGtleSwgdWludDI1NiBsaXF1aWRpdHlEZWx0YSkgZXh0ZXJuYWwgewogICAgICAgIC8vIEBkZXY6IFVwZGF0ZSB0aGlzCiAgICAgICAgLy8gR2l2ZW4gc3BvdCBwcmljZSBhbmQgdGhlIGN1c3RvbSBjdXJ2ZSwgY2FsY3VsYXRlIHRoZSByYXRpbyBvZiB0b2tlbnMgdG8gYWRkCiAgICAgICAgdWludDI1NiB0b2tlbjBJbjsKICAgICAgICB1aW50MjU2IHRva2VuMUluOwoKICAgICAgICAvLyB0cmFuc2ZlciB0b2tlbnMgdG8gaG9vaywgdG8gYWN0IGFzIGxpcXVpZGl0eSBmb3Igc3dhcHMKICAgICAgICBJRVJDMjAoQ3VycmVuY3kudW53cmFwKGtleS5jdXJyZW5jeTApKS50cmFuc2ZlckZyb20obXNnLnNlbmRlciwgYWRkcmVzcyh0aGlzKSwgdG9rZW4wSW4pOwogICAgICAgIElFUkMyMChDdXJyZW5jeS51bndyYXAoa2V5LmN1cnJlbmN5MSkpLnRyYW5zZmVyRnJvbShtc2cuc2VuZGVyLCBhZGRyZXNzKHRoaXMpLCB0b2tlbjFJbik7CgogICAgICAgIC8vIFRPRE86IHByb2R1Y3Rpb24tcmVhZHkgcmVxdWlyZXMgbWludGluZyBhIHJlY2VpcHQgdG9rZW4gZXRjCiAgICB9CgogICAgLy8vIEBub3RpY2UgQ2FsY3VsYXRlIHRoZSBhbW91bnQgb2YgdG9rZW5zIHBhaWQgYnkgdGhlIHN3YXBwZXIKICAgIC8vLyBAcGFyYW0gcGFyYW1zIFN3YXBQYXJhbXMgcGFzc2VkIHRvIHRoZSBzd2FwIGZ1bmN0aW9uCiAgICAvLy8gQHJldHVybiBUaGUgYW1vdW50IG9mIHRva2VucyBwYWlkIGJ5IHRoZSBzd2FwcGVyCiAgICBmdW5jdGlvbiBnZXRUb2tlbkluQW1vdW50KElQb29sTWFuYWdlci5Td2FwUGFyYW1zIGNhbGxkYXRhIHBhcmFtcykgcHVibGljIHB1cmUgcmV0dXJucyAodWludDI1NikgewogICAgICAgIHJldHVybiAxZTE4OwogICAgfQoKICAgIC8vLyBAbm90aWNlIENhbGN1bGF0ZSB0aGUgYW1vdW50IG9mIHRva2VucyBzZW50IHRvIHRoZSBzd2FwcGVyCiAgICAvLy8gQHBhcmFtIHBhcmFtcyBTd2FwUGFyYW1zIHBhc3NlZCB0byB0aGUgc3dhcCBmdW5jdGlvbgogICAgLy8vIEByZXR1cm4gVGhlIGFtb3VudCBvZiB0b2tlbnMgc2VudCB0byB0aGUgc3dhcHBlcgogICAgZnVuY3Rpb24gZ2V0VG9rZW5PdXRBbW91bnQoSVBvb2xNYW5hZ2VyLlN3YXBQYXJhbXMgY2FsbGRhdGEgcGFyYW1zKSBwdWJsaWMgcHVyZSByZXR1cm5zICh1aW50MjU2KSB7CiAgICAgICAgcmV0dXJuIDFlMTg7CiAgICB9CgogICAgZnVuY3Rpb24gYmVmb3JlU3dhcChhZGRyZXNzLCBQb29sS2V5IGNhbGxkYXRhIGtleSwgSVBvb2xNYW5hZ2VyLlN3YXBQYXJhbXMgY2FsbGRhdGEgcGFyYW1zLCBieXRlcyBjYWxsZGF0YSkKICAgICAgICBleHRlcm5hbAogICAgICAgIG92ZXJyaWRlCiAgICAgICAgcmV0dXJucyAoYnl0ZXM0KQogICAgewogICAgICAgIC8vIGNhbGN1bGF0ZSB0aGUgYW1vdW50IG9mIHRva2VucywgYmFzZWQgb24gYSBjdXN0b20gY3VydmUKICAgICAgICB1aW50MjU2IHRva2VuSW5BbW91bnQgPSBnZXRUb2tlbkluQW1vdW50KHBhcmFtcyk7IC8vIGFtb3VudCBvZiB0b2tlbnMgcGFpZCBieSB0aGUgc3dhcHBlcgogICAgICAgIHVpbnQyNTYgdG9rZW5PdXRBbW91bnQgPSBnZXRUb2tlbk91dEFtb3VudChwYXJhbXMpOyAvLyBhbW91bnQgb2YgdG9rZW5zIHNlbnQgdG8gdGhlIHN3YXBwZXIKCiAgICAgICAgLy8gZGV0ZXJtaW5lIGluYm91bmQvb3V0Ym91bmQgdG9rZW4gYmFzZWQgb24gMC0+MSBvciAxLT4wIHN3YXAKICAgICAgICAoQ3VycmVuY3kgaW5ib3VuZCwgQ3VycmVuY3kgb3V0Ym91bmQpID0KICAgICAgICAgICAgcGFyYW1zLnplcm9Gb3JPbmUgPyAoa2V5LmN1cnJlbmN5MCwga2V5LmN1cnJlbmN5MSkgOiAoa2V5LmN1cnJlbmN5MSwga2V5LmN1cnJlbmN5MCk7CgogICAgICAgIC8vIGluYm91bmQgdG9rZW4gaXMgYWRkZWQgdG8gaG9vaydzIHJlc2VydmVzLCBkZWJ0IHBhaWQgYnkgdGhlIHN3YXBwZXIKICAgICAgICBwb29sTWFuYWdlci50YWtlKGluYm91bmQsIGFkZHJlc3ModGhpcyksIHRva2VuSW5BbW91bnQpOwoKICAgICAgICAvLyBvdXRib3VuZCB0b2tlbiBpcyByZW1vdmVkIGZyb20gaG9vaydzIHJlc2VydmVzLCBhbmQgc2VudCB0byB0aGUgc3dhcHBlcgogICAgICAgIG91dGJvdW5kLnRyYW5zZmVyKGFkZHJlc3MocG9vbE1hbmFnZXIpLCB0b2tlbk91dEFtb3VudCk7CiAgICAgICAgcG9vbE1hbmFnZXIuc2V0dGxlKG91dGJvdW5kKTsKCiAgICAgICAgLy8gcHJldmVudCBub3JtYWwgdjQgc3dhcCBsb2dpYyBmcm9tIGV4ZWN1dGluZwogICAgICAgIHJldHVybiBCYXNlSG9vay5iZWZvcmVTd2FwLnNlbGVjdG9yOwogICAgfQoKICAgIC8vLyBAbm90aWNlIE5vIGxpcXVpZGl0eSB3aWxsIGJlIG1hbmFnZWQgYnkgdjQgUG9vbE1hbmFnZXIKICAgIGZ1bmN0aW9uIGJlZm9yZUFkZExpcXVpZGl0eSgKICAgICAgICBhZGRyZXNzLAogICAgICAgIFBvb2xLZXkgY2FsbGRhdGEga2V5LAogICAgICAgIElQb29sTWFuYWdlci5Nb2RpZnlMaXF1aWRpdHlQYXJhbXMgY2FsbGRhdGEsCiAgICAgICAgYnl0ZXMgY2FsbGRhdGEKICAgICkgZXh0ZXJuYWwgb3ZlcnJpZGUgcmV0dXJucyAoYnl0ZXM0KSB7CiAgICAgICAgcmV2ZXJ0KCJObyB2NCBMaXF1aWRpdHkgYWxsb3dlZCIpOwogICAgfQp9Cg==", + code: "Ly8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IE1JVApwcmFnbWEgc29saWRpdHkgXjAuOC4yMDsKCmltcG9ydCB7QmFzZUhvb2t9IGZyb20gInY0LXBlcmlwaGVyeS9CYXNlSG9vay5zb2wiOwppbXBvcnQge0hvb2tzfSBmcm9tICJ2NC1jb3JlL3NyYy9saWJyYXJpZXMvSG9va3Muc29sIjsKaW1wb3J0IHtJUG9vbE1hbmFnZXJ9IGZyb20gInY0LWNvcmUvc3JjL2ludGVyZmFjZXMvSVBvb2xNYW5hZ2VyLnNvbCI7CmltcG9ydCB7UG9vbEtleX0gZnJvbSAidjQtY29yZS9zcmMvdHlwZXMvUG9vbEtleS5zb2wiOwppbXBvcnQge0N1cnJlbmN5LCBDdXJyZW5jeUxpYnJhcnl9IGZyb20gInY0LWNvcmUvc3JjL3R5cGVzL0N1cnJlbmN5LnNvbCI7CmltcG9ydCB7dG9CZWZvcmVTd2FwRGVsdGEsIEJlZm9yZVN3YXBEZWx0YX0gZnJvbSAidjQtY29yZS9zcmMvdHlwZXMvQmVmb3JlU3dhcERlbHRhLnNvbCI7CmltcG9ydCB7Q3VycmVuY3lTZXR0bGVUYWtlfSBmcm9tICJ2NC1jb3JlL3NyYy9saWJyYXJpZXMvQ3VycmVuY3lTZXR0bGVUYWtlLnNvbCI7CmltcG9ydCB7U2FmZUNhc3R9IGZyb20gInY0LWNvcmUvc3JjL2xpYnJhcmllcy9TYWZlQ2FzdC5zb2wiOwoKYWJzdHJhY3QgY29udHJhY3QgQ3VzdG9tQ3VydmVCYXNlIGlzIEJhc2VIb29rIHsKICAgIHVzaW5nIEN1cnJlbmN5TGlicmFyeSBmb3IgQ3VycmVuY3k7CiAgICB1c2luZyBDdXJyZW5jeVNldHRsZVRha2UgZm9yIEN1cnJlbmN5OwogICAgdXNpbmcgU2FmZUNhc3QgZm9yIHVpbnQyNTY7CgogICAgY29uc3RydWN0b3IoSVBvb2xNYW5hZ2VyIF9wb29sTWFuYWdlcikgQmFzZUhvb2soX3Bvb2xNYW5hZ2VyKSB7fQoKICAgIC8vLyBOT1RFOiBJbiB0aGUgaW5oZXJpdGluZyBjb250cmFjdCwgZGVmaW5lIGEgZnVuY3Rpb24gdG8gYWRkIGxpcXVpZGl0eS4uLgoKICAgIC8vLyBAbm90aWNlIFJldHVybnMgdGhlIGFtb3VudCBvZiBvdXRwdXQgdG9rZW5zIGZvciBhbiBleGFjdC1pbnB1dCBzd2FwCiAgICAvLy8gQHBhcmFtIGFtb3VudEluIHRoZSBhbW91bnQgb2YgaW5wdXQgdG9rZW5zCiAgICAvLy8gQHBhcmFtIGlucHV0IHRoZSBpbnB1dCB0b2tlbgogICAgLy8vIEBwYXJhbSBvdXRwdXQgdGhlIG91dHB1dCB0b2tlbgogICAgLy8vIEBwYXJhbSB6ZXJvRm9yT25lIHRydWUgaWYgdGhlIGlucHV0IHRva2VuIGlzIHRva2VuMAogICAgLy8vIEByZXR1cm4gYW1vdW50T3V0IHRoZSBhbW91bnQgb2Ygb3V0cHV0IHRva2VucwogICAgZnVuY3Rpb24gZ2V0QW1vdW50T3V0RnJvbUV4YWN0SW5wdXQodWludDI1NiBhbW91bnRJbiwgQ3VycmVuY3kgaW5wdXQsIEN1cnJlbmN5IG91dHB1dCwgYm9vbCB6ZXJvRm9yT25lKQogICAgICAgIGludGVybmFsCiAgICAgICAgdmlydHVhbAogICAgICAgIHJldHVybnMgKHVpbnQyNTYgYW1vdW50T3V0KTsKCiAgICAvLy8gQG5vdGljZSBSZXR1cm5zIHRoZSBhbW91bnQgb2YgaW5wdXQgdG9rZW5zIGZvciBhbiBleGFjdC1vdXRwdXQgc3dhcAogICAgLy8vIEBwYXJhbSBhbW91bnRPdXQgdGhlIGFtb3VudCBvZiBvdXRwdXQgdG9rZW5zIHRoZSB1c2VyIGV4cGVjdHMgdG8gcmVjZWl2ZQogICAgLy8vIEBwYXJhbSBpbnB1dCB0aGUgaW5wdXQgdG9rZW4KICAgIC8vLyBAcGFyYW0gb3V0cHV0IHRoZSBvdXRwdXQgdG9rZW4KICAgIC8vLyBAcGFyYW0gemVyb0Zvck9uZSB0cnVlIGlmIHRoZSBpbnB1dCB0b2tlbiBpcyB0b2tlbjAKICAgIC8vLyBAcmV0dXJuIGFtb3VudEluIHRoZSBhbW91bnQgb2YgaW5wdXQgdG9rZW5zIHJlcXVpcmVkIHRvIHByb2R1Y2UgYW1vdW50T3V0CiAgICBmdW5jdGlvbiBnZXRBbW91bnRJbkZvckV4YWN0T3V0cHV0KHVpbnQyNTYgYW1vdW50T3V0LCBDdXJyZW5jeSBpbnB1dCwgQ3VycmVuY3kgb3V0cHV0LCBib29sIHplcm9Gb3JPbmUpCiAgICAgICAgaW50ZXJuYWwKICAgICAgICB2aXJ0dWFsCiAgICAgICAgcmV0dXJucyAodWludDI1NiBhbW91bnRJbik7CgogICAgLy8vIEBkZXYgRmFjaWxpdGF0ZSBhIGN1c3RvbSBjdXJ2ZSB2aWEgYmVmb3JlU3dhcCArIHJldHVybiBkZWx0YQogICAgLy8vIEBkZXYgaW5wdXQgdG9rZW5zIGFyZSB0YWtlbiBmcm9tIHRoZSBQb29sTWFuYWdlciwgY3JlYXRpbmcgYSBkZWJ0IHBhaWQgYnkgdGhlIHN3YXBwZXIKICAgIC8vLyBAZGV2IG91dHB1dCB0YWtlbnMgYXJlIHRyYW5zZmVycmVkIGZyb20gdGhlIGhvb2sgdG8gdGhlIFBvb2xNYW5hZ2VyLCBjcmVhdGluZyBhIGNyZWRpdCBjbGFpbWVkIGJ5IHRoZSBzd2FwcGVyCiAgICBmdW5jdGlvbiBiZWZvcmVTd2FwKGFkZHJlc3MsIFBvb2xLZXkgY2FsbGRhdGEga2V5LCBJUG9vbE1hbmFnZXIuU3dhcFBhcmFtcyBjYWxsZGF0YSBwYXJhbXMsIGJ5dGVzIGNhbGxkYXRhKQogICAgICAgIGV4dGVybmFsCiAgICAgICAgb3ZlcnJpZGUKICAgICAgICByZXR1cm5zIChieXRlczQsIEJlZm9yZVN3YXBEZWx0YSwgdWludDI0KQogICAgewogICAgICAgIGJvb2wgZXhhY3RJbnB1dCA9IHBhcmFtcy5hbW91bnRTcGVjaWZpZWQgPCAwOwogICAgICAgIChDdXJyZW5jeSBzcGVjaWZpZWQsIEN1cnJlbmN5IHVuc3BlY2lmaWVkKSA9CiAgICAgICAgICAgIChwYXJhbXMuemVyb0Zvck9uZSA9PSBleGFjdElucHV0KSA/IChrZXkuY3VycmVuY3kwLCBrZXkuY3VycmVuY3kxKSA6IChrZXkuY3VycmVuY3kxLCBrZXkuY3VycmVuY3kwKTsKCiAgICAgICAgdWludDI1NiBzcGVjaWZpZWRBbW91bnQgPSBleGFjdElucHV0ID8gdWludDI1NigtcGFyYW1zLmFtb3VudFNwZWNpZmllZCkgOiB1aW50MjU2KHBhcmFtcy5hbW91bnRTcGVjaWZpZWQpOwogICAgICAgIHVpbnQyNTYgdW5zcGVjaWZpZWRBbW91bnQ7CiAgICAgICAgQmVmb3JlU3dhcERlbHRhIHJldHVybkRlbHRhOwogICAgICAgIGlmIChleGFjdElucHV0KSB7CiAgICAgICAgICAgIC8vIGluIGV4YWN0LWlucHV0IHN3YXBzLCB0aGUgc3BlY2lmaWVkIHRva2VuIGlzIGEgZGVidCB0aGF0IGdldHMgcGFpZCBkb3duIGJ5IHRoZSBzd2FwcGVyCiAgICAgICAgICAgIC8vIHRoZSB1bnNwZWNpZmllZCB0b2tlbiBpcyBjcmVkaXRlZCB0byB0aGUgUG9vbE1hbmFnZXIsIHRoYXQgaXMgY2xhaW1lZCBieSB0aGUgc3dhcHBlcgogICAgICAgICAgICB1bnNwZWNpZmllZEFtb3VudCA9IGdldEFtb3VudE91dEZyb21FeGFjdElucHV0KHNwZWNpZmllZEFtb3VudCwgc3BlY2lmaWVkLCB1bnNwZWNpZmllZCwgcGFyYW1zLnplcm9Gb3JPbmUpOwogICAgICAgICAgICBzcGVjaWZpZWQudGFrZShwb29sTWFuYWdlciwgYWRkcmVzcyh0aGlzKSwgc3BlY2lmaWVkQW1vdW50LCB0cnVlKTsKICAgICAgICAgICAgdW5zcGVjaWZpZWQuc2V0dGxlKHBvb2xNYW5hZ2VyLCBhZGRyZXNzKHRoaXMpLCB1bnNwZWNpZmllZEFtb3VudCwgdHJ1ZSk7CgogICAgICAgICAgICByZXR1cm5EZWx0YSA9IHRvQmVmb3JlU3dhcERlbHRhKHNwZWNpZmllZEFtb3VudC50b0ludDEyOCgpLCAtdW5zcGVjaWZpZWRBbW91bnQudG9JbnQxMjgoKSk7CiAgICAgICAgfSBlbHNlIHsKICAgICAgICAgICAgLy8gZXhhY3RPdXRwdXQKICAgICAgICAgICAgLy8gaW4gZXhhY3Qtb3V0cHV0IHN3YXBzLCB0aGUgdW5zcGVjaWZpZWQgdG9rZW4gaXMgYSBkZWJ0IHRoYXQgZ2V0cyBwYWlkIGRvd24gYnkgdGhlIHN3YXBwZXIKICAgICAgICAgICAgLy8gdGhlIHNwZWNpZmllZCB0b2tlbiBpcyBjcmVkaXRlZCB0byB0aGUgUG9vbE1hbmFnZXIsIHRoYXQgaXMgY2xhaW1lZCBieSB0aGUgc3dhcHBlcgogICAgICAgICAgICB1bnNwZWNpZmllZEFtb3VudCA9IGdldEFtb3VudEluRm9yRXhhY3RPdXRwdXQoc3BlY2lmaWVkQW1vdW50LCB1bnNwZWNpZmllZCwgc3BlY2lmaWVkLCBwYXJhbXMuemVyb0Zvck9uZSk7CiAgICAgICAgICAgIHVuc3BlY2lmaWVkLnRha2UocG9vbE1hbmFnZXIsIGFkZHJlc3ModGhpcyksIHVuc3BlY2lmaWVkQW1vdW50LCB0cnVlKTsKICAgICAgICAgICAgc3BlY2lmaWVkLnNldHRsZShwb29sTWFuYWdlciwgYWRkcmVzcyh0aGlzKSwgc3BlY2lmaWVkQW1vdW50LCB0cnVlKTsKCiAgICAgICAgICAgIHJldHVybkRlbHRhID0gdG9CZWZvcmVTd2FwRGVsdGEoLXNwZWNpZmllZEFtb3VudC50b0ludDEyOCgpLCB1bnNwZWNpZmllZEFtb3VudC50b0ludDEyOCgpKTsKICAgICAgICB9CgogICAgICAgIHJldHVybiAoQmFzZUhvb2suYmVmb3JlU3dhcC5zZWxlY3RvciwgcmV0dXJuRGVsdGEsIDApOwogICAgfQoKICAgIC8vLyBAbm90aWNlIE5vIGxpcXVpZGl0eSB3aWxsIGJlIG1hbmFnZWQgYnkgdjQgUG9vbE1hbmFnZXIKICAgIGZ1bmN0aW9uIGJlZm9yZUFkZExpcXVpZGl0eShhZGRyZXNzLCBQb29sS2V5IGNhbGxkYXRhLCBJUG9vbE1hbmFnZXIuTW9kaWZ5TGlxdWlkaXR5UGFyYW1zIGNhbGxkYXRhLCBieXRlcyBjYWxsZGF0YSkKICAgICAgICBleHRlcm5hbAogICAgICAgIHB1cmUKICAgICAgICBvdmVycmlkZQogICAgICAgIHJldHVybnMgKGJ5dGVzNCkKICAgIHsKICAgICAgICByZXZlcnQoIk5vIHY0IExpcXVpZGl0eSBhbGxvd2VkIik7CiAgICB9CgogICAgZnVuY3Rpb24gZ2V0SG9va1Blcm1pc3Npb25zKCkgcHVibGljIHB1cmUgb3ZlcnJpZGUgcmV0dXJucyAoSG9va3MuUGVybWlzc2lvbnMgbWVtb3J5KSB7CiAgICAgICAgcmV0dXJuIEhvb2tzLlBlcm1pc3Npb25zKHsKICAgICAgICAgICAgYmVmb3JlSW5pdGlhbGl6ZTogZmFsc2UsCiAgICAgICAgICAgIGFmdGVySW5pdGlhbGl6ZTogZmFsc2UsCiAgICAgICAgICAgIGJlZm9yZUFkZExpcXVpZGl0eTogdHJ1ZSwgLy8gLS0gZGlzYWJsZSB2NCBsaXF1aWRpdHkgd2l0aCBhIHJldmVydCAtLSAvLwogICAgICAgICAgICBiZWZvcmVSZW1vdmVMaXF1aWRpdHk6IGZhbHNlLAogICAgICAgICAgICBhZnRlckFkZExpcXVpZGl0eTogZmFsc2UsCiAgICAgICAgICAgIGFmdGVyUmVtb3ZlTGlxdWlkaXR5OiBmYWxzZSwKICAgICAgICAgICAgYmVmb3JlU3dhcDogdHJ1ZSwgLy8gLS0gQ3VzdG9tIEN1cnZlIEhhbmRsZXIgLS0gIC8vCiAgICAgICAgICAgIGFmdGVyU3dhcDogZmFsc2UsCiAgICAgICAgICAgIGJlZm9yZURvbmF0ZTogZmFsc2UsCiAgICAgICAgICAgIGFmdGVyRG9uYXRlOiBmYWxzZSwKICAgICAgICAgICAgYmVmb3JlU3dhcFJldHVybkRlbHRhOiB0cnVlLCAvLyAtLSBFbmFibGVzIEN1c3RvbSBDdXJ2ZXMgLS0gIC8vCiAgICAgICAgICAgIGFmdGVyU3dhcFJldHVybkRlbHRhOiBmYWxzZSwKICAgICAgICAgICAgYWZ0ZXJBZGRMaXF1aWRpdHlSZXR1cm5EZWx0YTogZmFsc2UsCiAgICAgICAgICAgIGFmdGVyUmVtb3ZlTGlxdWlkaXR5UmV0dXJuRGVsdGE6IGZhbHNlCiAgICAgICAgfSk7CiAgICB9Cn0KCmNvbnRyYWN0IENvbnN0YW50U3VtQ3VydmUgaXMgQ3VzdG9tQ3VydmVCYXNlIHsKICAgIHVzaW5nIEN1cnJlbmN5U2V0dGxlVGFrZSBmb3IgQ3VycmVuY3k7CgogICAgY29uc3RydWN0b3IoSVBvb2xNYW5hZ2VyIF9tYW5hZ2VyKSBDdXN0b21DdXJ2ZUJhc2UoX21hbmFnZXIpIHt9CgogICAgZnVuY3Rpb24gZ2V0QW1vdW50T3V0RnJvbUV4YWN0SW5wdXQodWludDI1NiBhbW91bnRJbiwgQ3VycmVuY3ksIEN1cnJlbmN5LCBib29sKQogICAgICAgIGludGVybmFsCiAgICAgICAgcHVyZQogICAgICAgIG92ZXJyaWRlCiAgICAgICAgcmV0dXJucyAodWludDI1NiBhbW91bnRPdXQpCiAgICB7CiAgICAgICAgLy8gaW4gY29uc3RhbnQtc3VtIGN1cnZlLCB0b2tlbnMgdHJhZGUgZXhhY3RseSAxOjEKICAgICAgICBhbW91bnRPdXQgPSBhbW91bnRJbjsKICAgIH0KCiAgICBmdW5jdGlvbiBnZXRBbW91bnRJbkZvckV4YWN0T3V0cHV0KHVpbnQyNTYgYW1vdW50T3V0LCBDdXJyZW5jeSwgQ3VycmVuY3ksIGJvb2wpCiAgICAgICAgaW50ZXJuYWwKICAgICAgICBwdXJlCiAgICAgICAgb3ZlcnJpZGUKICAgICAgICByZXR1cm5zICh1aW50MjU2IGFtb3VudEluKQogICAgewogICAgICAgIC8vIGluIGNvbnN0YW50LXN1bSBjdXJ2ZSwgdG9rZW5zIHRyYWRlIGV4YWN0bHkgMToxCiAgICAgICAgYW1vdW50SW4gPSBhbW91bnRPdXQ7CiAgICB9CgogICAgLy8vIEBub3RpY2UgQWRkIGxpcXVpZGl0eSB0aHJvdWdoIHRoZSBob29rCiAgICAvLy8gQGRldiBOb3QgcHJvZHVjdGlvbi1yZWFkeSwgb25seSBzZXJ2ZXMgYW4gZXhhbXBsZSBvZiBob29rLW93bmVkIGxpcXVpZGl0eQogICAgZnVuY3Rpb24gYWRkTGlxdWlkaXR5KFBvb2xLZXkgY2FsbGRhdGEga2V5LCB1aW50MjU2IGFtb3VudDAsIHVpbnQyNTYgYW1vdW50MSkgZXh0ZXJuYWwgewogICAgICAgIHBvb2xNYW5hZ2VyLnVubG9jaygKICAgICAgICAgICAgYWJpLmVuY29kZUNhbGwodGhpcy5oYW5kbGVBZGRMaXF1aWRpdHksIChrZXkuY3VycmVuY3kwLCBrZXkuY3VycmVuY3kxLCBhbW91bnQwLCBhbW91bnQxLCBtc2cuc2VuZGVyKSkKICAgICAgICApOwogICAgfQoKICAgIC8vLyBAZGV2IEhhbmRsZSBsaXF1aWRpdHkgYWRkaXRpb24gYnkgdGFraW5nIHRva2VucyBmcm9tIHRoZSBzZW5kZXIgYW5kIGNsYWltaW5nIEVSQzY5MDkgdG8gdGhlIGhvb2sgYWRkcmVzcwogICAgZnVuY3Rpb24gaGFuZGxlQWRkTGlxdWlkaXR5KAogICAgICAgIEN1cnJlbmN5IGN1cnJlbmN5MCwKICAgICAgICBDdXJyZW5jeSBjdXJyZW5jeTEsCiAgICAgICAgdWludDI1NiBhbW91bnQwLAogICAgICAgIHVpbnQyNTYgYW1vdW50MSwKICAgICAgICBhZGRyZXNzIHNlbmRlcgogICAgKSBleHRlcm5hbCBzZWxmT25seSByZXR1cm5zIChieXRlcyBtZW1vcnkpIHsKICAgICAgICBjdXJyZW5jeTAuc2V0dGxlKHBvb2xNYW5hZ2VyLCBzZW5kZXIsIGFtb3VudDAsIGZhbHNlKTsKICAgICAgICBjdXJyZW5jeTAudGFrZShwb29sTWFuYWdlciwgYWRkcmVzcyh0aGlzKSwgYW1vdW50MCwgdHJ1ZSk7CgogICAgICAgIGN1cnJlbmN5MS5zZXR0bGUocG9vbE1hbmFnZXIsIHNlbmRlciwgYW1vdW50MSwgZmFsc2UpOwogICAgICAgIGN1cnJlbmN5MS50YWtlKHBvb2xNYW5hZ2VyLCBhZGRyZXNzKHRoaXMpLCBhbW91bnQxLCB0cnVlKTsKCiAgICAgICAgcmV0dXJuIGFiaS5lbmNvZGUoYW1vdW50MCwgYW1vdW50MSk7CiAgICB9Cn0K", }, ] -const html = `

    UNDER CONSTRUCTION

    -

    PROCEED IF YOU ARE BRAVE

    -

    requires using a bleeding edge PR

    -

    ...

    -

    Custom Curves:

    -