diff --git a/.forge-snapshots/BaseActionsRouter_mock10commands.snap b/.forge-snapshots/BaseActionsRouter_mock10commands.snap index d99d3dfd7..9bb199962 100644 --- a/.forge-snapshots/BaseActionsRouter_mock10commands.snap +++ b/.forge-snapshots/BaseActionsRouter_mock10commands.snap @@ -1 +1 @@ -62824 \ No newline at end of file +62960 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_Bytecode.snap b/.forge-snapshots/V4Router_Bytecode.snap new file mode 100644 index 000000000..a99d2d984 --- /dev/null +++ b/.forge-snapshots/V4Router_Bytecode.snap @@ -0,0 +1 @@ +6214 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn1Hop_oneForZero.snap b/.forge-snapshots/V4Router_ExactIn1Hop_oneForZero.snap new file mode 100644 index 000000000..791b2fa38 --- /dev/null +++ b/.forge-snapshots/V4Router_ExactIn1Hop_oneForZero.snap @@ -0,0 +1 @@ +129205 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn1Hop_zeroForOne.snap b/.forge-snapshots/V4Router_ExactIn1Hop_zeroForOne.snap new file mode 100644 index 000000000..225336b01 --- /dev/null +++ b/.forge-snapshots/V4Router_ExactIn1Hop_zeroForOne.snap @@ -0,0 +1 @@ +136035 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn2Hops.snap b/.forge-snapshots/V4Router_ExactIn2Hops.snap new file mode 100644 index 000000000..d2f4fba94 --- /dev/null +++ b/.forge-snapshots/V4Router_ExactIn2Hops.snap @@ -0,0 +1 @@ +187387 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn3Hops.snap b/.forge-snapshots/V4Router_ExactIn3Hops.snap new file mode 100644 index 000000000..b253014fd --- /dev/null +++ b/.forge-snapshots/V4Router_ExactIn3Hops.snap @@ -0,0 +1 @@ +238768 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactInputSingle.snap b/.forge-snapshots/V4Router_ExactInputSingle.snap new file mode 100644 index 000000000..258b5b38a --- /dev/null +++ b/.forge-snapshots/V4Router_ExactInputSingle.snap @@ -0,0 +1 @@ +134822 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut1Hop_oneForZero.snap b/.forge-snapshots/V4Router_ExactOut1Hop_oneForZero.snap new file mode 100644 index 000000000..6b466d46b --- /dev/null +++ b/.forge-snapshots/V4Router_ExactOut1Hop_oneForZero.snap @@ -0,0 +1 @@ +130046 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut1Hop_zeroForOne.snap b/.forge-snapshots/V4Router_ExactOut1Hop_zeroForOne.snap new file mode 100644 index 000000000..ae545a404 --- /dev/null +++ b/.forge-snapshots/V4Router_ExactOut1Hop_zeroForOne.snap @@ -0,0 +1 @@ +134847 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut2Hops.snap b/.forge-snapshots/V4Router_ExactOut2Hops.snap new file mode 100644 index 000000000..c9d747edd --- /dev/null +++ b/.forge-snapshots/V4Router_ExactOut2Hops.snap @@ -0,0 +1 @@ +186808 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut3Hops.snap b/.forge-snapshots/V4Router_ExactOut3Hops.snap new file mode 100644 index 000000000..0a2546768 --- /dev/null +++ b/.forge-snapshots/V4Router_ExactOut3Hops.snap @@ -0,0 +1 @@ +238813 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOutputSingle.snap b/.forge-snapshots/V4Router_ExactOutputSingle.snap new file mode 100644 index 000000000..a9f7fc114 --- /dev/null +++ b/.forge-snapshots/V4Router_ExactOutputSingle.snap @@ -0,0 +1 @@ +133356 \ No newline at end of file diff --git a/src/V4Router.sol b/src/V4Router.sol new file mode 100644 index 000000000..5e7e81ab3 --- /dev/null +++ b/src/V4Router.sol @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; + +import {PathKey, PathKeyLib} from "./libraries/PathKey.sol"; +import {CalldataDecoder} from "./libraries/CalldataDecoder.sol"; +import {IV4Router} from "./interfaces/IV4Router.sol"; +import {BaseActionsRouter} from "./base/BaseActionsRouter.sol"; +import {DeltaResolver} from "./base/DeltaResolver.sol"; +import {Actions} from "./libraries/Actions.sol"; +import {SafeCast} from "./libraries/SafeCast.sol"; + +/// @title UniswapV4Router +/// @notice Abstract contract that contains all internal logic needed for routing through Uniswap V4 pools +/// @dev the entry point to executing actions in this contract is calling `BaseActionsRouter._executeActions` +/// An inheriting contract should call _executeActions at the point that they wish actions to be executed +abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver { + using SafeCast for *; + using PathKeyLib for PathKey; + using CalldataDecoder for bytes; + using TransientStateLibrary for IPoolManager; + + constructor(IPoolManager _poolManager) BaseActionsRouter(_poolManager) {} + + // TODO native support !! + function _handleAction(uint256 action, bytes calldata params) internal override { + // swap actions and payment actions in different blocks for gas efficiency + if (action < Actions.SETTLE) { + if (action == Actions.SWAP_EXACT_IN) { + _swapExactInput(abi.decode(params, (IV4Router.ExactInputParams))); + } else if (action == Actions.SWAP_EXACT_IN_SINGLE) { + _swapExactInputSingle(abi.decode(params, (IV4Router.ExactInputSingleParams))); + } else if (action == Actions.SWAP_EXACT_OUT) { + _swapExactOutput(abi.decode(params, (IV4Router.ExactOutputParams))); + } else if (action == Actions.SWAP_EXACT_OUT_SINGLE) { + _swapExactOutputSingle(abi.decode(params, (IV4Router.ExactOutputSingleParams))); + } else { + revert UnsupportedAction(action); + } + } else { + if (action == Actions.SETTLE_ALL) { + // equivalent: abi.decode(params, (Currency)) + Currency currency; + assembly ("memory-safe") { + currency := calldataload(params.offset) + } + + int256 delta = poolManager.currencyDelta(address(this), currency); + if (delta > 0) revert InvalidDeltaForAction(); + + // TODO support address(this) paying too + // TODO should it have a maxAmountOut added slippage protection? + _settle(currency, _msgSender(), uint256(-delta)); + } else if (action == Actions.TAKE_ALL) { + // equivalent: abi.decode(params, (Currency, address)) + Currency currency; + address recipient; + assembly ("memory-safe") { + currency := calldataload(params.offset) + recipient := calldataload(add(params.offset, 0x20)) + } + + int256 delta = poolManager.currencyDelta(address(this), currency); + if (delta < 0) revert InvalidDeltaForAction(); + + // TODO should _take have a minAmountOut added slippage check? + // TODO recipient mapping + _take(currency, recipient, uint256(delta)); + } else { + revert UnsupportedAction(action); + } + } + } + + function _swapExactInputSingle(IV4Router.ExactInputSingleParams memory params) private { + _swap( + params.poolKey, + params.zeroForOne, + int256(-int128(params.amountIn)), + params.sqrtPriceLimitX96, + params.hookData + ); + } + + function _swapExactInput(IV4Router.ExactInputParams memory params) private { + unchecked { + // Caching for gas savings + uint256 pathLength = params.path.length; + uint128 amountOut; + uint128 amountIn = params.amountIn; + Currency currencyIn = params.currencyIn; + PathKey memory pathKey; + + for (uint256 i = 0; i < pathLength; i++) { + pathKey = params.path[i]; + (PoolKey memory poolKey, bool zeroForOne) = pathKey.getPoolAndSwapDirection(currencyIn); + // The output delta will always be positive, except for when interacting with certain hook pools + amountOut = _swap(poolKey, zeroForOne, -int256(uint256(amountIn)), 0, pathKey.hookData).toUint128(); + + amountIn = amountOut; + currencyIn = pathKey.intermediateCurrency; + } + + if (amountOut < params.amountOutMinimum) revert TooLittleReceived(); + } + } + + function _swapExactOutputSingle(IV4Router.ExactOutputSingleParams memory params) private { + _swap( + params.poolKey, + params.zeroForOne, + int256(int128(params.amountOut)), + params.sqrtPriceLimitX96, + params.hookData + ); + } + + function _swapExactOutput(IV4Router.ExactOutputParams memory params) private { + unchecked { + // Caching for gas savings + uint256 pathLength = params.path.length; + uint128 amountIn; + uint128 amountOut = params.amountOut; + Currency currencyOut = params.currencyOut; + PathKey memory pathKey; + + for (uint256 i = pathLength; i > 0; i--) { + pathKey = params.path[i - 1]; + (PoolKey memory poolKey, bool oneForZero) = pathKey.getPoolAndSwapDirection(currencyOut); + // The output delta will always be negative, except for when interacting with certain hook pools + amountIn = (-_swap(poolKey, !oneForZero, int256(uint256(amountOut)), 0, pathKey.hookData)).toUint128(); + + amountOut = amountIn; + currencyOut = pathKey.intermediateCurrency; + } + if (amountIn > params.amountInMaximum) revert TooMuchRequested(); + } + } + + function _swap( + PoolKey memory poolKey, + bool zeroForOne, + int256 amountSpecified, + uint160 sqrtPriceLimitX96, + bytes memory hookData + ) private returns (int128 reciprocalAmount) { + unchecked { + BalanceDelta delta = poolManager.swap( + poolKey, + IPoolManager.SwapParams( + zeroForOne, + amountSpecified, + sqrtPriceLimitX96 == 0 + ? (zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1) + : sqrtPriceLimitX96 + ), + hookData + ); + + reciprocalAmount = (zeroForOne == amountSpecified < 0) ? delta.amount1() : delta.amount0(); + } + } +} diff --git a/src/base/BaseActionsRouter.sol b/src/base/BaseActionsRouter.sol index e88b1fe87..cd0b7019c 100644 --- a/src/base/BaseActionsRouter.sol +++ b/src/base/BaseActionsRouter.sol @@ -11,7 +11,7 @@ abstract contract BaseActionsRouter is SafeCallback { using CalldataDecoder for bytes; /// @notice emitted when different numbers of parameters and actions are provided - error LengthMismatch(); + error InputLengthMismatch(); /// @notice emitted when an inheriting contract does not support an action error UnsupportedAction(uint256 action); @@ -30,7 +30,7 @@ abstract contract BaseActionsRouter is SafeCallback { (uint256[] calldata actions, bytes[] calldata params) = data.decodeActionsRouterParams(); uint256 numActions = actions.length; - if (numActions != params.length) revert LengthMismatch(); + if (numActions != params.length) revert InputLengthMismatch(); for (uint256 actionIndex = 0; actionIndex < numActions; actionIndex++) { uint256 action = actions[actionIndex]; @@ -49,6 +49,6 @@ abstract contract BaseActionsRouter is SafeCallback { /// @dev The other context functions, _msgData and _msgValue, are not supported by this contract /// In many contracts this will be the address that calls the initial entry point that calls `_executeActions` /// `msg.sender` shouldnt be used, as this will be the v4 pool manager contract that calls `unlockCallback` - /// If using ReentrancyLock.sol, this function can return Locker.get() - locker of the contract + /// If using ReentrancyLock.sol, this function can return _getLocker() function _msgSender() internal view virtual returns (address); } diff --git a/src/base/DeltaResolver.sol b/src/base/DeltaResolver.sol index 289acc101..e67721cce 100644 --- a/src/base/DeltaResolver.sol +++ b/src/base/DeltaResolver.sol @@ -7,6 +7,9 @@ import {ImmutableState} from "./ImmutableState.sol"; /// @notice Abstract contract used to sync, send, and settle funds to the pool manager /// @dev Note that sync() is called before any erc-20 transfer in `settle`. abstract contract DeltaResolver is ImmutableState { + /// @notice Emitted trying to settle a positive delta, or take a negative delta + error InvalidDeltaForAction(); + /// @notice Take an amount of currency out of the PoolManager /// @param currency Currency to take /// @param recipient Address to receive the currency diff --git a/src/interfaces/IV4Router.sol b/src/interfaces/IV4Router.sol new file mode 100644 index 000000000..3dfc4d88c --- /dev/null +++ b/src/interfaces/IV4Router.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PathKey} from "../libraries/PathKey.sol"; + +/// @title IV4Router +/// @notice Interface containing all the structs and errors for different v4 swap types +interface IV4Router { + /// @notice Emitted when an exactInput swap does not receive its minAmountOut + error TooLittleReceived(); + /// @notice Emitted when an exactOutput is asked for more than its maxAmountIn + error TooMuchRequested(); + + /// @notice Parameters for a single-hop exact-input swap + struct ExactInputSingleParams { + PoolKey poolKey; + bool zeroForOne; + uint128 amountIn; + uint128 amountOutMinimum; + uint160 sqrtPriceLimitX96; + bytes hookData; + } + + /// @notice Parameters for a multi-hop exact-input swap + struct ExactInputParams { + Currency currencyIn; + PathKey[] path; + uint128 amountIn; + uint128 amountOutMinimum; + } + + /// @notice Parameters for a single-hop exact-output swap + struct ExactOutputSingleParams { + PoolKey poolKey; + bool zeroForOne; + uint128 amountOut; + uint128 amountInMaximum; + uint160 sqrtPriceLimitX96; + bytes hookData; + } + + /// @notice Parameters for a multi-hop exact-output swap + struct ExactOutputParams { + Currency currencyOut; + PathKey[] path; + uint128 amountOut; + uint128 amountInMaximum; + } +} diff --git a/src/libraries/Actions.sol b/src/libraries/Actions.sol index ca3e29fc3..53b4f6803 100644 --- a/src/libraries/Actions.sol +++ b/src/libraries/Actions.sol @@ -5,17 +5,25 @@ pragma solidity ^0.8.24; /// @dev These are suggested common commands, however additional commands should be defined as required library Actions { // pool actions - uint256 constant SWAP = 0x00; - uint256 constant INCREASE_LIQUIDITY = 0x01; - uint256 constant DECREASE_LIQUIDITY = 0x02; - uint256 constant DONATE = 0x03; + uint256 constant INCREASE_LIQUIDITY = 0x00; + uint256 constant DECREASE_LIQUIDITY = 0x01; + uint256 constant SWAP_EXACT_IN_SINGLE = 0x02; + uint256 constant SWAP_EXACT_IN = 0x03; + uint256 constant SWAP_EXACT_OUT_SINGLE = 0x04; + uint256 constant SWAP_EXACT_OUT = 0x05; + uint256 constant DONATE = 0x06; // closing deltas on the pool manager uint256 constant SETTLE = 0x10; - uint256 constant TAKE = 0x11; - uint256 constant CLOSE_CURRENCY = 0x12; - uint256 constant CLOSE_PAIR = 0x13; - uint256 constant CLEAR = 0x14; + uint256 constant SETTLE_ALL = 0x11; + + uint256 constant TAKE = 0x12; + uint256 constant TAKE_ALL = 0x13; + uint256 constant TAKE_PORTION = 0x14; + + uint256 constant CLOSE_CURRENCY = 0x15; + uint256 constant CLOSE_PAIR = 0x16; + uint256 constant CLEAR = 0x17; // minting/burning 6909s to close deltas uint256 constant MINT_6909 = 0x20; diff --git a/src/libraries/PathKey.sol b/src/libraries/PathKey.sol index 352d0a30e..38884eb41 100644 --- a/src/libraries/PathKey.sol +++ b/src/libraries/PathKey.sol @@ -19,9 +19,9 @@ library PathKeyLib { pure returns (PoolKey memory poolKey, bool zeroForOne) { - (Currency currency0, Currency currency1) = currencyIn < params.intermediateCurrency - ? (currencyIn, params.intermediateCurrency) - : (params.intermediateCurrency, currencyIn); + Currency currencyOut = params.intermediateCurrency; + (Currency currency0, Currency currency1) = + currencyIn < currencyOut ? (currencyIn, currencyOut) : (currencyOut, currencyIn); zeroForOne = currencyIn == currency0; poolKey = PoolKey(currency0, currency1, params.fee, params.tickSpacing, params.hooks); diff --git a/src/libraries/SafeCast.sol b/src/libraries/SafeCast.sol new file mode 100644 index 000000000..75fddf530 --- /dev/null +++ b/src/libraries/SafeCast.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.sol"; + +/// @title Safe casting methods +/// @notice Contains methods for safely casting between types +library SafeCast { + using CustomRevert for bytes4; + + error SafeCastOverflow(); + + /// @notice Cast a int128 to a uint128, revert on overflow or underflow + /// @param x The int128 to be casted + /// @return y The casted integer, now type uint128 + function toUint128(int128 x) internal pure returns (uint128 y) { + if (x < 0) SafeCastOverflow.selector.revertWith(); + y = uint128(x); + } +} diff --git a/test/BaseActionsRouter.t.sol b/test/BaseActionsRouter.t.sol index 8dafcb9d9..08793ed21 100644 --- a/test/BaseActionsRouter.t.sol +++ b/test/BaseActionsRouter.t.sol @@ -22,14 +22,14 @@ contract BaseActionsRouterTest is Test, Deployers, GasSnapshot { function test_swap_suceeds() public { Plan memory plan = ActionsRouterPlanner.init(); for (uint256 i = 0; i < 10; i++) { - plan.add(Actions.SWAP, ""); + plan.add(Actions.SWAP_EXACT_IN, ""); } bytes memory data = plan.encode(); assertEq(router.swapCount(), 0); - router.executeAction(data); + router.executeActions(data); snapLastCall("BaseActionsRouter_mock10commands"); assertEq(router.swapCount(), 10); } @@ -43,7 +43,7 @@ contract BaseActionsRouterTest is Test, Deployers, GasSnapshot { assertEq(router.increaseLiqCount(), 0); bytes memory data = plan.encode(); - router.executeAction(data); + router.executeActions(data); assertEq(router.increaseLiqCount(), 10); } @@ -56,7 +56,7 @@ contract BaseActionsRouterTest is Test, Deployers, GasSnapshot { assertEq(router.decreaseLiqCount(), 0); bytes memory data = plan.encode(); - router.executeAction(data); + router.executeActions(data); assertEq(router.decreaseLiqCount(), 10); } @@ -69,7 +69,7 @@ contract BaseActionsRouterTest is Test, Deployers, GasSnapshot { assertEq(router.donateCount(), 0); bytes memory data = plan.encode(); - router.executeAction(data); + router.executeActions(data); assertEq(router.donateCount(), 10); } @@ -82,7 +82,7 @@ contract BaseActionsRouterTest is Test, Deployers, GasSnapshot { assertEq(router.clearCount(), 0); bytes memory data = plan.encode(); - router.executeAction(data); + router.executeActions(data); assertEq(router.clearCount(), 10); } @@ -95,7 +95,7 @@ contract BaseActionsRouterTest is Test, Deployers, GasSnapshot { assertEq(router.settleCount(), 0); bytes memory data = plan.encode(); - router.executeAction(data); + router.executeActions(data); assertEq(router.settleCount(), 10); } @@ -108,7 +108,7 @@ contract BaseActionsRouterTest is Test, Deployers, GasSnapshot { assertEq(router.takeCount(), 0); bytes memory data = plan.encode(); - router.executeAction(data); + router.executeActions(data); assertEq(router.takeCount(), 10); } @@ -121,7 +121,7 @@ contract BaseActionsRouterTest is Test, Deployers, GasSnapshot { assertEq(router.mintCount(), 0); bytes memory data = plan.encode(); - router.executeAction(data); + router.executeActions(data); assertEq(router.mintCount(), 10); } @@ -134,7 +134,7 @@ contract BaseActionsRouterTest is Test, Deployers, GasSnapshot { assertEq(router.burnCount(), 0); bytes memory data = plan.encode(); - router.executeAction(data); + router.executeActions(data); assertEq(router.burnCount(), 10); } } diff --git a/test/Quoter.t.sol b/test/Quoter.t.sol index 84ac914e0..3a6e01860 100644 --- a/test/Quoter.t.sol +++ b/test/Quoter.t.sol @@ -641,7 +641,7 @@ contract QuoterTest is Test, Deployers { function getExactInputParams(MockERC20[] memory _tokenPath, uint256 amountIn) internal - view + pure returns (IQuoter.QuoteExactParams memory params) { PathKey[] memory path = new PathKey[](_tokenPath.length - 1); @@ -651,13 +651,12 @@ contract QuoterTest is Test, Deployers { params.exactCurrency = Currency.wrap(address(_tokenPath[0])); params.path = path; - params.recipient = address(this); params.exactAmount = uint128(amountIn); } function getExactOutputParams(MockERC20[] memory _tokenPath, uint256 amountOut) internal - view + pure returns (IQuoter.QuoteExactParams memory params) { PathKey[] memory path = new PathKey[](_tokenPath.length - 1); @@ -667,7 +666,6 @@ contract QuoterTest is Test, Deployers { params.exactCurrency = Currency.wrap(address(_tokenPath[_tokenPath.length - 1])); params.path = path; - params.recipient = address(this); params.exactAmount = uint128(amountOut); } } diff --git a/test/mocks/MockBaseActionsRouter.sol b/test/mocks/MockBaseActionsRouter.sol index 071b23ea7..7e8e186d4 100644 --- a/test/mocks/MockBaseActionsRouter.sol +++ b/test/mocks/MockBaseActionsRouter.sol @@ -6,7 +6,6 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {BaseActionsRouter} from "../../src/base/BaseActionsRouter.sol"; import {Actions} from "../../src/libraries/Actions.sol"; import {ReentrancyLock} from "../../src/base/ReentrancyLock.sol"; -import {Locker} from "../../src/libraries/Locker.sol"; contract MockBaseActionsRouter is BaseActionsRouter, ReentrancyLock { uint256 public swapCount; @@ -21,13 +20,13 @@ contract MockBaseActionsRouter is BaseActionsRouter, ReentrancyLock { constructor(IPoolManager _poolManager) BaseActionsRouter(_poolManager) {} - function executeAction(bytes calldata params) external isNotLocked { + function executeActions(bytes calldata params) external isNotLocked { _executeActions(params); } function _handleAction(uint256 action, bytes calldata params) internal override { if (action < Actions.SETTLE) { - if (action == Actions.SWAP) _swap(params); + if (action == Actions.SWAP_EXACT_IN) _swap(params); else if (action == Actions.INCREASE_LIQUIDITY) _increaseLiquidity(params); else if (action == Actions.DECREASE_LIQUIDITY) _decreaseLiquidity(params); else if (action == Actions.DONATE) _donate(params); @@ -43,7 +42,7 @@ contract MockBaseActionsRouter is BaseActionsRouter, ReentrancyLock { } function _msgSender() internal view override returns (address) { - return Locker.get(); + return _getLocker(); } function _settle(bytes calldata /* params **/ ) internal { diff --git a/test/position-managers/Gas.t.sol b/test/position-managers/PositionManager.gas.t.sol similarity index 99% rename from test/position-managers/Gas.t.sol rename to test/position-managers/PositionManager.gas.t.sol index 3e23d93fc..b3320bed3 100644 --- a/test/position-managers/Gas.t.sol +++ b/test/position-managers/PositionManager.gas.t.sol @@ -23,7 +23,7 @@ import {IMulticall} from "../../src/interfaces/IMulticall.sol"; import {Planner} from "../shared/Planner.sol"; import {PosmTestSetup} from "../shared/PosmTestSetup.sol"; -contract GasTest is Test, PosmTestSetup, GasSnapshot { +contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { using FixedPointMathLib for uint256; using CurrencyLibrary for Currency; using PoolIdLibrary for PoolKey; diff --git a/test/router/V4Router.gas.t.sol b/test/router/V4Router.gas.t.sol new file mode 100644 index 000000000..bfbd76fd7 --- /dev/null +++ b/test/router/V4Router.gas.t.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; + +import {IV4Router} from "../../src/interfaces/IV4Router.sol"; +import {RoutingTestHelpers} from "../shared/RoutingTestHelpers.sol"; +import {Plan, ActionsRouterPlanner} from "../shared/ActionsRouterPlanner.sol"; +import {Actions} from "../../src/libraries/Actions.sol"; + +contract V4RouterTest is RoutingTestHelpers, GasSnapshot { + using CurrencyLibrary for Currency; + using ActionsRouterPlanner for Plan; + + function setUp() public { + setupRouterCurrenciesAndPoolsWithLiquidity(); + plan = ActionsRouterPlanner.init(); + } + + function test_gas_bytecodeSize() public { + snapSize("V4Router_Bytecode", address(router)); + } + + function test_gas_swapExactInputSingle_zeroForOne() public { + uint256 amountIn = 1 ether; + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(key0.currency0, key0.currency1, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactInputSingle"); + } + + function test_gas_swapExactIn_1Hop_zeroForOne() public { + uint256 amountIn = 1 ether; + + tokenPath.push(currency0); + tokenPath.push(currency1); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency0, currency1, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactIn1Hop_zeroForOne"); + } + + function test_swapExactIn_1Hop_oneForZero() public { + uint256 amountIn = 1 ether; + + tokenPath.push(currency1); + tokenPath.push(currency0); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency1, currency0, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactIn1Hop_oneForZero"); + } + + function test_gas_swapExactIn_2Hops() public { + uint256 amountIn = 1 ether; + + tokenPath.push(currency0); + tokenPath.push(currency1); + tokenPath.push(currency2); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency0, currency2, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactIn2Hops"); + } + + function test_gas_swapExactIn_3Hops() public { + uint256 amountIn = 1 ether; + + tokenPath.push(currency0); + tokenPath.push(currency1); + tokenPath.push(currency2); + tokenPath.push(currency3); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency0, currency3, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactIn3Hops"); + } + + function test_gas_swapExactOutputSingle_zeroForOne() public { + uint256 amountOut = 1 ether; + + IV4Router.ExactOutputSingleParams memory params = + IV4Router.ExactOutputSingleParams(key0, true, uint128(amountOut), 0, 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(key0.currency0, key0.currency1, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactOutputSingle"); + } + + function test_gas_swapExactOut_1Hop_zeroForOne() public { + uint256 amountOut = 1 ether; + + tokenPath.push(currency0); + tokenPath.push(currency1); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency0, currency1, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactOut1Hop_zeroForOne"); + } + + function test_gas_swapExactOut_1Hop_oneForZero() public { + uint256 amountOut = 1 ether; + + tokenPath.push(currency1); + tokenPath.push(currency0); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency1, currency0, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactOut1Hop_oneForZero"); + } + + function test_gas_swapExactOut_2Hops() public { + uint256 amountOut = 1 ether; + + tokenPath.push(currency0); + tokenPath.push(currency1); + tokenPath.push(currency2); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency0, currency2, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactOut2Hops"); + } + + function test_gas_swapExactOut_3Hops() public { + uint256 amountOut = 1 ether; + + tokenPath.push(currency0); + tokenPath.push(currency1); + tokenPath.push(currency2); + tokenPath.push(currency3); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency0, currency3, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactOut3Hops"); + } +} diff --git a/test/router/V4Router.t.sol b/test/router/V4Router.t.sol new file mode 100644 index 000000000..194da80be --- /dev/null +++ b/test/router/V4Router.t.sol @@ -0,0 +1,315 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {IV4Router} from "../../src/interfaces/IV4Router.sol"; +import {RoutingTestHelpers} from "../shared/RoutingTestHelpers.sol"; +import {Plan, ActionsRouterPlanner} from "../shared/ActionsRouterPlanner.sol"; +import {Actions} from "../../src/libraries/Actions.sol"; + +contract V4RouterTest is RoutingTestHelpers { + using CurrencyLibrary for Currency; + using ActionsRouterPlanner for Plan; + + function setUp() public { + setupRouterCurrenciesAndPoolsWithLiquidity(); + plan = ActionsRouterPlanner.init(); + } + + function test_swapExactInputSingle_zeroForOne() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 992054607780215625; + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); + + uint256 prevBalance0 = key0.currency0.balanceOf(address(this)); + uint256 prevBalance1 = key0.currency1.balanceOf(address(this)); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(key0.currency0, key0.currency1, address(this)); + + router.executeActions(data); + + uint256 newBalance0 = key0.currency0.balanceOf(address(this)); + uint256 newBalance1 = key0.currency1.balanceOf(address(this)); + + assertEq(prevBalance0 - newBalance0, amountIn); + assertEq(newBalance1 - prevBalance1, expectedAmountOut); + } + + function test_swapExactInputSingle_oneForZero() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 992054607780215625; + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(key0, false, uint128(amountIn), 0, 0, bytes("")); + + uint256 prevBalance0 = key0.currency0.balanceOf(address(this)); + uint256 prevBalance1 = key0.currency1.balanceOf(address(this)); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(key0.currency1, key0.currency0, address(this)); + + router.executeActions(data); + + uint256 newBalance0 = key0.currency0.balanceOf(address(this)); + uint256 newBalance1 = key0.currency1.balanceOf(address(this)); + + assertEq(prevBalance1 - newBalance1, amountIn); + assertEq(newBalance0 - prevBalance0, expectedAmountOut); + } + + function test_swapExactIn_1Hop_zeroForOne() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 992054607780215625; + + tokenPath.push(currency0); + tokenPath.push(currency1); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + + uint256 prevBalance0 = currency0.balanceOfSelf(); + uint256 prevBalance1 = currency1.balanceOfSelf(); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency0, currency1, address(this)); + + router.executeActions(data); + + uint256 newBalance0 = currency0.balanceOfSelf(); + uint256 newBalance1 = currency1.balanceOfSelf(); + + assertEq(prevBalance0 - newBalance0, amountIn); + assertEq(newBalance1 - prevBalance1, expectedAmountOut); + } + + function test_swapExactIn_1Hop_oneForZero() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 992054607780215625; + + tokenPath.push(currency1); + tokenPath.push(currency0); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + uint256 prevBalance0 = currency0.balanceOfSelf(); + uint256 prevBalance1 = currency1.balanceOfSelf(); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency1, currency0, address(this)); + + router.executeActions(data); + + uint256 newBalance0 = currency0.balanceOfSelf(); + uint256 newBalance1 = currency1.balanceOfSelf(); + + assertEq(prevBalance1 - newBalance1, amountIn); + assertEq(newBalance0 - prevBalance0, expectedAmountOut); + } + + function test_swapExactIn_2Hops() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 984211133872795298; + + tokenPath.push(currency0); + tokenPath.push(currency1); + tokenPath.push(currency2); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + + uint256 prevBalance0 = currency0.balanceOfSelf(); + uint256 prevBalance1 = currency1.balanceOfSelf(); + uint256 prevBalance2 = currency2.balanceOfSelf(); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency0, currency2, address(this)); + + router.executeActions(data); + + uint256 newBalance0 = currency0.balanceOfSelf(); + uint256 newBalance1 = currency1.balanceOfSelf(); + uint256 newBalance2 = currency2.balanceOfSelf(); + + assertEq(prevBalance0 - newBalance0, amountIn); + assertEq(prevBalance1 - newBalance1, 0); + assertEq(newBalance2 - prevBalance2, expectedAmountOut); + assertEq(currency0.balanceOf(address(router)), 0); + assertEq(currency1.balanceOf(address(router)), 0); + assertEq(currency2.balanceOf(address(router)), 0); + } + + function test_swapExactIn_3Hops() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 976467664490096191; + + tokenPath.push(currency0); + tokenPath.push(currency1); + tokenPath.push(currency2); + tokenPath.push(currency3); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + + uint256 prevBalance0 = currency0.balanceOfSelf(); + uint256 prevBalance3 = currency3.balanceOfSelf(); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency0, currency3, address(this)); + + router.executeActions(data); + + uint256 newBalance0 = currency0.balanceOfSelf(); + uint256 newBalance3 = currency3.balanceOfSelf(); + + assertEq(prevBalance0 - newBalance0, amountIn); + assertEq(newBalance3 - prevBalance3, expectedAmountOut); + assertEq(currency0.balanceOf(address(router)), 0); + assertEq(currency1.balanceOf(address(router)), 0); + assertEq(currency2.balanceOf(address(router)), 0); + assertEq(currency3.balanceOf(address(router)), 0); + } + + function test_swapExactOutputSingle_zeroForOne() public { + uint256 amountOut = 1 ether; + uint256 expectedAmountIn = 1008049273448486163; + + IV4Router.ExactOutputSingleParams memory params = + IV4Router.ExactOutputSingleParams(key0, true, uint128(amountOut), 0, 0, bytes("")); + + uint256 prevBalance0 = key0.currency0.balanceOf(address(this)); + uint256 prevBalance1 = key0.currency1.balanceOf(address(this)); + + plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(key0.currency0, key0.currency1, address(this)); + + router.executeActions(data); + + uint256 newBalance0 = key0.currency0.balanceOf(address(this)); + uint256 newBalance1 = key0.currency1.balanceOf(address(this)); + + assertEq(prevBalance0 - newBalance0, expectedAmountIn); + assertEq(newBalance1 - prevBalance1, amountOut); + } + + function test_swapExactOutputSingle_oneForZero() public { + uint256 amountOut = 1 ether; + uint256 expectedAmountIn = 1008049273448486163; + + IV4Router.ExactOutputSingleParams memory params = + IV4Router.ExactOutputSingleParams(key0, false, uint128(amountOut), 0, 0, bytes("")); + + uint256 prevBalance0 = key0.currency0.balanceOf(address(this)); + uint256 prevBalance1 = key0.currency1.balanceOf(address(this)); + + plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(key0.currency1, key0.currency0, address(this)); + + router.executeActions(data); + + uint256 newBalance0 = key0.currency0.balanceOf(address(this)); + uint256 newBalance1 = key0.currency1.balanceOf(address(this)); + + assertEq(prevBalance1 - newBalance1, expectedAmountIn); + assertEq(newBalance0 - prevBalance0, amountOut); + } + + function test_swapExactOut_1Hop_zeroForOne() public { + uint256 amountOut = 1 ether; + uint256 expectedAmountIn = 1008049273448486163; + + tokenPath.push(currency0); + tokenPath.push(currency1); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + uint256 prevBalance0 = currency0.balanceOfSelf(); + uint256 prevBalance1 = currency1.balanceOfSelf(); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency0, currency1, address(this)); + + router.executeActions(data); + + uint256 newBalance0 = currency0.balanceOfSelf(); + uint256 newBalance1 = currency1.balanceOfSelf(); + + assertEq(prevBalance0 - newBalance0, expectedAmountIn); + assertEq(newBalance1 - prevBalance1, amountOut); + } + + function test_swapExactOut_1Hop_oneForZero() public { + uint256 amountOut = 1 ether; + uint256 expectedAmountIn = 1008049273448486163; + + tokenPath.push(currency1); + tokenPath.push(currency0); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + uint256 prevBalance0 = currency0.balanceOfSelf(); + uint256 prevBalance1 = currency1.balanceOfSelf(); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency1, currency0, address(this)); + + router.executeActions(data); + + uint256 newBalance0 = currency0.balanceOfSelf(); + uint256 newBalance1 = currency1.balanceOfSelf(); + + assertEq(prevBalance1 - newBalance1, expectedAmountIn); + assertEq(newBalance0 - prevBalance0, amountOut); + } + + function test_swapExactOut_2Hops() public { + uint256 amountOut = 1 ether; + uint256 expectedAmountIn = 1016204441757464409; + + tokenPath.push(currency0); + tokenPath.push(currency1); + tokenPath.push(currency2); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + uint256 prevBalance0 = currency0.balanceOfSelf(); + uint256 prevBalance1 = currency1.balanceOfSelf(); + uint256 prevBalance2 = currency2.balanceOfSelf(); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency0, currency2, address(this)); + + router.executeActions(data); + + uint256 newBalance0 = currency0.balanceOfSelf(); + uint256 newBalance1 = currency1.balanceOfSelf(); + uint256 newBalance2 = currency2.balanceOfSelf(); + + assertEq(prevBalance0 - newBalance0, expectedAmountIn); + assertEq(prevBalance1 - newBalance1, 0); + assertEq(newBalance2 - prevBalance2, amountOut); + assertEq(currency0.balanceOf(address(router)), 0); + assertEq(currency1.balanceOf(address(router)), 0); + assertEq(currency2.balanceOf(address(router)), 0); + } + + function test_swapExactOut_3Hops() public { + uint256 amountOut = 1 ether; + uint256 expectedAmountIn = 1024467570922834110; + + tokenPath.push(currency0); + tokenPath.push(currency1); + tokenPath.push(currency2); + tokenPath.push(currency3); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + uint256 prevBalance0 = currency0.balanceOfSelf(); + uint256 prevBalance3 = currency3.balanceOfSelf(); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency0, currency3, address(this)); + + router.executeActions(data); + + uint256 newBalance0 = currency0.balanceOfSelf(); + uint256 newBalance3 = currency3.balanceOfSelf(); + + assertEq(prevBalance0 - newBalance0, expectedAmountIn); + assertEq(newBalance3 - prevBalance3, amountOut); + assertEq(currency0.balanceOf(address(router)), 0); + assertEq(currency1.balanceOf(address(router)), 0); + assertEq(currency2.balanceOf(address(router)), 0); + assertEq(currency3.balanceOf(address(router)), 0); + } +} diff --git a/test/shared/ActionsRouterPlanner.sol b/test/shared/ActionsRouterPlanner.sol index 8ea06662e..09803eeb0 100644 --- a/test/shared/ActionsRouterPlanner.sol +++ b/test/shared/ActionsRouterPlanner.sol @@ -1,12 +1,17 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {Actions} from "../../src/libraries/Actions.sol"; + struct Plan { uint256[] actions; bytes[] params; } library ActionsRouterPlanner { + using ActionsRouterPlanner for Plan; + function init() internal pure returns (Plan memory plan) { return Plan({actions: new uint256[](0), params: new bytes[](0)}); } @@ -33,4 +38,14 @@ library ActionsRouterPlanner { function encode(Plan memory plan) internal pure returns (bytes memory) { return abi.encode(plan.actions, plan.params); } + + function finalizeSwap(Plan memory plan, Currency inputCurrency, Currency outputCurrency, address recipient) + internal + pure + returns (bytes memory) + { + plan = plan.add(Actions.SETTLE_ALL, abi.encode(inputCurrency)); + plan = plan.add(Actions.TAKE_ALL, abi.encode(outputCurrency, recipient)); + return plan.encode(); + } } diff --git a/test/shared/RoutingTestHelpers.sol b/test/shared/RoutingTestHelpers.sol new file mode 100644 index 000000000..b1af43cf1 --- /dev/null +++ b/test/shared/RoutingTestHelpers.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {PoolModifyLiquidityTest} from "@uniswap/v4-core/src/test/PoolModifyLiquidityTest.sol"; +import {V4RouterImplementation} from "../shared/implementation/V4RouterImplementation.sol"; +import {Plan, ActionsRouterPlanner} from "../shared/ActionsRouterPlanner.sol"; +import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {PathKey} from "../../src/libraries/PathKey.sol"; +import {Actions} from "../../src/libraries/Actions.sol"; + +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {PositionManager} from "../../src/PositionManager.sol"; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {LiquidityOperations} from "./LiquidityOperations.sol"; +import {IV4Router} from "../../src/interfaces/IV4Router.sol"; + +/// @notice A shared test contract that wraps the v4-core deployers contract and exposes basic helpers for swapping with the router. +contract RoutingTestHelpers is Test, Deployers { + using ActionsRouterPlanner for Plan; + + PoolModifyLiquidityTest positionManager; + V4RouterImplementation router; + + PoolKey key0; + PoolKey key1; + PoolKey key2; + + // currency0 and currency1 are defined in Deployers.sol + Currency currency2; + Currency currency3; + + Currency[] tokenPath; + Plan plan; + + function setupRouterCurrenciesAndPoolsWithLiquidity() public { + deployFreshManager(); + + router = new V4RouterImplementation(manager); + positionManager = new PoolModifyLiquidityTest(manager); + + MockERC20[] memory tokens = deployTokensMintAndApprove(4); + + currency0 = Currency.wrap(address(tokens[0])); + currency1 = Currency.wrap(address(tokens[1])); + currency2 = Currency.wrap(address(tokens[2])); + currency3 = Currency.wrap(address(tokens[3])); + + key0 = createPoolWithLiquidity(currency0, currency1, address(0)); + key1 = createPoolWithLiquidity(currency1, currency2, address(0)); + key2 = createPoolWithLiquidity(currency2, currency3, address(0)); + } + + function deployTokensMintAndApprove(uint8 count) internal returns (MockERC20[] memory) { + MockERC20[] memory tokens = deployTokens(count, 2 ** 128); + for (uint256 i = 0; i < count; i++) { + tokens[i].approve(address(router), type(uint256).max); + } + return tokens; + } + + function createPoolWithLiquidity(Currency currencyA, Currency currencyB, address hookAddr) + internal + returns (PoolKey memory _key) + { + if (Currency.unwrap(currencyA) > Currency.unwrap(currencyB)) (currencyA, currencyB) = (currencyB, currencyA); + _key = PoolKey(currencyA, currencyB, 3000, 60, IHooks(hookAddr)); + + manager.initialize(_key, SQRT_PRICE_1_1, ZERO_BYTES); + MockERC20(Currency.unwrap(currencyA)).approve(address(positionManager), type(uint256).max); + MockERC20(Currency.unwrap(currencyB)).approve(address(positionManager), type(uint256).max); + positionManager.modifyLiquidity(_key, IPoolManager.ModifyLiquidityParams(-887220, 887220, 200 ether, 0), "0x"); + } + + function _getExactInputParams(Currency[] memory _tokenPath, uint256 amountIn) + internal + pure + returns (IV4Router.ExactInputParams memory params) + { + PathKey[] memory path = new PathKey[](_tokenPath.length - 1); + for (uint256 i = 0; i < _tokenPath.length - 1; i++) { + path[i] = PathKey(_tokenPath[i + 1], 3000, 60, IHooks(address(0)), bytes("")); + } + + params.currencyIn = _tokenPath[0]; + params.path = path; + params.amountIn = uint128(amountIn); + params.amountOutMinimum = 0; + } + + function _getExactOutputParams(Currency[] memory _tokenPath, uint256 amountOut) + internal + pure + returns (IV4Router.ExactOutputParams memory params) + { + PathKey[] memory path = new PathKey[](_tokenPath.length - 1); + for (uint256 i = _tokenPath.length - 1; i > 0; i--) { + path[i - 1] = PathKey(_tokenPath[i - 1], 3000, 60, IHooks(address(0)), bytes("")); + } + + params.currencyOut = _tokenPath[_tokenPath.length - 1]; + params.path = path; + params.amountOut = uint128(amountOut); + params.amountInMaximum = type(uint128).max; + } +} diff --git a/test/shared/implementation/V4RouterImplementation.sol b/test/shared/implementation/V4RouterImplementation.sol new file mode 100644 index 000000000..eaaaf9efc --- /dev/null +++ b/test/shared/implementation/V4RouterImplementation.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; +import {V4Router} from "../../../src/V4Router.sol"; +import {ReentrancyLock} from "../../../src/base/ReentrancyLock.sol"; + +contract V4RouterImplementation is V4Router, ReentrancyLock { + constructor(IPoolManager _poolManager) V4Router(_poolManager) {} + + function executeActions(bytes calldata params) external isNotLocked { + _executeActions(params); + } + + function _pay(Currency token, address payer, uint256 amount) internal override { + IERC20Minimal(Currency.unwrap(token)).transferFrom(payer, address(poolManager), amount); + } + + function _msgSender() internal view override returns (address) { + return _getLocker(); + } +}