diff --git a/.gitmodules b/.gitmodules index 0c139fa..eacb977 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,7 +3,7 @@ url = https://github.com/OpenZeppelin/openzeppelin-contracts [submodule "foundry/lib/permit2"] path = foundry/lib/permit2 - url = https://github.com/Uniswap/permit2 + url = https://github.com/uniswap/permit2 [submodule "foundry/lib/v2-core"] path = foundry/lib/v2-core url = https://github.com/uniswap/v2-core @@ -13,3 +13,9 @@ [submodule "foundry/lib/v3-core"] path = foundry/lib/v3-core url = https://github.com/Uniswap/v3-core +[submodule "foundry/lib/v4-core"] + path = foundry/lib/v4-core + url = https://github.com/Uniswap/v4-core +[submodule "foundry/lib/v4-periphery"] + path = foundry/lib/v4-periphery + url = https://github.com/Uniswap/v4-periphery diff --git a/foundry/foundry.toml b/foundry/foundry.toml index a3c8d7b..e573137 100644 --- a/foundry/foundry.toml +++ b/foundry/foundry.toml @@ -2,8 +2,8 @@ src = 'src' out = 'out' libs = ['lib'] -solc = "0.8.28" -evm_version = 'shanghai' +auto_detect_sol = true +evm_version = 'cancun' optimizer = true optimizer_runs = 1000 via_ir = true diff --git a/foundry/interfaces/ICallbackVerifier.sol b/foundry/interfaces/ICallbackVerifier.sol index 6161e55..612279c 100644 --- a/foundry/interfaces/ICallbackVerifier.sol +++ b/foundry/interfaces/ICallbackVerifier.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; interface ICallbackVerifier { error UnauthorizedCaller(string exchange, address sender); @@ -7,10 +7,8 @@ interface ICallbackVerifier { /** * @dev This method should revert if the sender is not a verified sender of the exchange. */ - function verifyCallback(address sender, bytes calldata data) - external - returns ( - uint256 amountOwed, - address tokenOwed - ); + function verifyCallback( + address sender, + bytes calldata data + ) external returns (uint256 amountOwed, address tokenOwed); } diff --git a/foundry/interfaces/IExecutor.sol b/foundry/interfaces/IExecutor.sol index 9c5a55c..0a60022 100644 --- a/foundry/interfaces/IExecutor.sol +++ b/foundry/interfaces/IExecutor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -20,10 +20,10 @@ interface IExecutor { * @return calculatedAmount The amount of the output token swapped, depending on * the givenAmount inputted. */ - function swap(uint256 givenAmount, bytes calldata data) - external - payable - returns (uint256 calculatedAmount); + function swap( + uint256 givenAmount, + bytes calldata data + ) external payable returns (uint256 calculatedAmount); } interface IExecutorErrors { diff --git a/foundry/lib/Constants.sol b/foundry/lib/Constants.sol new file mode 100644 index 0000000..c801b58 --- /dev/null +++ b/foundry/lib/Constants.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +/// @title Constant state +/// @notice Constant state used by the Universal Router +library Constants { + /// @dev Used for identifying cases when a v2 pair has already received input tokens + uint256 internal constant ALREADY_PAID = 0; + + /// @dev Used as a flag for identifying the transfer of ETH instead of a token + address internal constant ETH = address(0); + + /// @dev The length of the bytes encoded address + uint256 internal constant ADDR_SIZE = 20; + + /// @dev The length of the bytes encoded fee + uint256 internal constant V3_FEE_SIZE = 3; + + /// @dev The offset of a single token address (20) and pool fee (3) + uint256 internal constant NEXT_V3_POOL_OFFSET = ADDR_SIZE + V3_FEE_SIZE; + + /// @dev The offset of an encoded pool key + /// Token (20) + Fee (3) + Token (20) = 43 + uint256 internal constant V3_POP_OFFSET = NEXT_V3_POOL_OFFSET + ADDR_SIZE; + + /// @dev The minimum length of an encoding that contains 2 or more pools + uint256 internal constant MULTIPLE_V3_POOLS_MIN_LENGTH = + V3_POP_OFFSET + NEXT_V3_POOL_OFFSET; +} diff --git a/foundry/lib/IWETH.sol b/foundry/lib/IWETH.sol index 3b4a359..8ab98fa 100644 --- a/foundry/lib/IWETH.sol +++ b/foundry/lib/IWETH.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; diff --git a/foundry/lib/LibSwap.sol b/foundry/lib/LibSwap.sol index f581d8c..1770005 100644 --- a/foundry/lib/LibSwap.sol +++ b/foundry/lib/LibSwap.sol @@ -1,58 +1,44 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; library LibSwap { /// Returns the InToken index into an array of tokens - function tokenInIndex(bytes calldata swap) - internal - pure - returns (uint8 res) - { + function tokenInIndex( + bytes calldata swap + ) internal pure returns (uint8 res) { res = uint8(swap[0]); } /// The OutToken index into an array of tokens - function tokenOutIndex(bytes calldata swap) - internal - pure - returns (uint8 res) - { + function tokenOutIndex( + bytes calldata swap + ) internal pure returns (uint8 res) { res = uint8(swap[1]); } /// The relative amount of token quantity routed into this swap - function splitPercentage(bytes calldata swap) - internal - pure - returns (uint24 res) - { + function splitPercentage( + bytes calldata swap + ) internal pure returns (uint24 res) { res = uint24(bytes3(swap[2:5])); } /// The address of the executor contract - function executor(bytes calldata swap) - internal - pure - returns (address res) - { + function executor(bytes calldata swap) internal pure returns (address res) { res = address(uint160(bytes20(swap[5:25]))); } /// The selector to be used of the executor contract - function executorSelector(bytes calldata swap) - internal - pure - returns (bytes4 res) - { + function executorSelector( + bytes calldata swap + ) internal pure returns (bytes4 res) { res = bytes4(swap[25:29]); } /// Remaining bytes are interpreted as protocol data - function protocolData(bytes calldata swap) - internal - pure - returns (bytes calldata res) - { + function protocolData( + bytes calldata swap + ) internal pure returns (bytes calldata res) { res = swap[29:]; } } diff --git a/foundry/lib/Payments.sol b/foundry/lib/Payments.sol new file mode 100644 index 0000000..8027dbf --- /dev/null +++ b/foundry/lib/Payments.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.26; + +import {Constants} from "./Constants.sol"; +import {ActionConstants} from "@uniswap/v4-periphery/src/libraries/ActionConstants.sol"; +import {BipsLibrary} from "@uniswap/v4-periphery/src/libraries/BipsLibrary.sol"; +import {PaymentsImmutables} from "./PaymentsImmutables.sol"; +import {SafeTransferLib} from "solmate/src/utils/SafeTransferLib.sol"; +import {ERC20} from "solmate/src/tokens/ERC20.sol"; + +/// @title Payments contract +/// @notice Performs various operations around the payment of ETH and tokens +abstract contract Payments is PaymentsImmutables { + using SafeTransferLib for ERC20; + using SafeTransferLib for address; + using BipsLibrary for uint256; + + error InsufficientToken(); + error InsufficientETH(); + + /// @notice Pays an amount of ETH or ERC20 to a recipient + /// @param token The token to pay (can be ETH using Constants.ETH) + /// @param recipient The address that will receive the payment + /// @param value The amount to pay + function pay(address token, address recipient, uint256 value) internal { + if (token == Constants.ETH) { + recipient.safeTransferETH(value); + } else { + if (value == ActionConstants.CONTRACT_BALANCE) { + value = ERC20(token).balanceOf(address(this)); + } + + ERC20(token).safeTransfer(recipient, value); + } + } + + /// @notice Pays a proportion of the contract's ETH or ERC20 to a recipient + /// @param token The token to pay (can be ETH using Constants.ETH) + /// @param recipient The address that will receive payment + /// @param bips Portion in bips of whole balance of the contract + function payPortion( + address token, + address recipient, + uint256 bips + ) internal { + if (token == Constants.ETH) { + uint256 balance = address(this).balance; + uint256 amount = balance.calculatePortion(bips); + recipient.safeTransferETH(amount); + } else { + uint256 balance = ERC20(token).balanceOf(address(this)); + uint256 amount = balance.calculatePortion(bips); + ERC20(token).safeTransfer(recipient, amount); + } + } + + /// @notice Sweeps all of the contract's ERC20 or ETH to an address + /// @param token The token to sweep (can be ETH using Constants.ETH) + /// @param recipient The address that will receive payment + /// @param amountMinimum The minimum desired amount + function sweep( + address token, + address recipient, + uint256 amountMinimum + ) internal { + uint256 balance; + if (token == Constants.ETH) { + balance = address(this).balance; + if (balance < amountMinimum) revert InsufficientETH(); + if (balance > 0) recipient.safeTransferETH(balance); + } else { + balance = ERC20(token).balanceOf(address(this)); + if (balance < amountMinimum) revert InsufficientToken(); + if (balance > 0) ERC20(token).safeTransfer(recipient, balance); + } + } + + /// @notice Wraps an amount of ETH into WETH + /// @param recipient The recipient of the WETH + /// @param amount The amount to wrap (can be CONTRACT_BALANCE) + function wrapETH(address recipient, uint256 amount) internal { + if (amount == ActionConstants.CONTRACT_BALANCE) { + amount = address(this).balance; + } else if (amount > address(this).balance) { + revert InsufficientETH(); + } + if (amount > 0) { + WETH9.deposit{value: amount}(); + if (recipient != address(this)) { + WETH9.transfer(recipient, amount); + } + } + } + + /// @notice Unwraps all of the contract's WETH into ETH + /// @param recipient The recipient of the ETH + /// @param amountMinimum The minimum amount of ETH desired + function unwrapWETH9(address recipient, uint256 amountMinimum) internal { + uint256 value = WETH9.balanceOf(address(this)); + if (value < amountMinimum) { + revert InsufficientETH(); + } + if (value > 0) { + WETH9.withdraw(value); + if (recipient != address(this)) { + recipient.safeTransferETH(value); + } + } + } +} diff --git a/foundry/lib/PaymentsImmutables.sol b/foundry/lib/PaymentsImmutables.sol new file mode 100644 index 0000000..1894169 --- /dev/null +++ b/foundry/lib/PaymentsImmutables.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.26; + +import {IWETH9} from "@uniswap/v4-periphery/src/interfaces/external/IWETH9.sol"; +import {IPermit2} from "permit2/src/interfaces/IPermit2.sol"; + +struct PaymentsParameters { + address permit2; + address weth9; +} + +contract PaymentsImmutables { + /// @notice WETH9 address + IWETH9 internal immutable WETH9; + + /// @notice Permit2 address + IPermit2 internal immutable PERMIT2; + + constructor(PaymentsParameters memory params) { + WETH9 = IWETH9(params.weth9); + PERMIT2 = IPermit2(params.permit2); + } +} diff --git a/foundry/lib/Permit2Payments.sol b/foundry/lib/Permit2Payments.sol new file mode 100644 index 0000000..edb09e2 --- /dev/null +++ b/foundry/lib/Permit2Payments.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.26; + +import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; +import {SafeCast160} from "permit2/src/libraries/SafeCast160.sol"; +import {Payments} from "./Payments.sol"; + +/// @title Payments through Permit2 +/// @notice Performs interactions with Permit2 to transfer tokens +abstract contract Permit2Payments is Payments { + using SafeCast160 for uint256; + + error FromAddressIsNotOwner(); + + /// @notice Performs a transferFrom on Permit2 + /// @param token The token to transfer + /// @param from The address to transfer from + /// @param to The recipient of the transfer + /// @param amount The amount to transfer + function permit2TransferFrom( + address token, + address from, + address to, + uint160 amount + ) internal { + PERMIT2.transferFrom(from, to, amount, token); + } + + /// @notice Performs a batch transferFrom on Permit2 + /// @param batchDetails An array detailing each of the transfers that should occur + /// @param owner The address that should be the owner of all transfers + function permit2TransferFrom( + IAllowanceTransfer.AllowanceTransferDetails[] calldata batchDetails, + address owner + ) internal { + uint256 batchLength = batchDetails.length; + for (uint256 i = 0; i < batchLength; ++i) { + if (batchDetails[i].from != owner) revert FromAddressIsNotOwner(); + } + PERMIT2.transferFrom(batchDetails); + } + + /// @notice Either performs a regular payment or transferFrom on Permit2, depending on the payer address + /// @param token The token to transfer + /// @param payer The address to pay for the transfer + /// @param recipient The recipient of the transfer + /// @param amount The amount to transfer + function payOrPermit2Transfer( + address token, + address payer, + address recipient, + uint256 amount + ) internal { + if (payer == address(this)) pay(token, recipient, amount); + else permit2TransferFrom(token, payer, recipient, amount.toUint160()); + } +} diff --git a/foundry/lib/bytes/LibPrefixLengthEncodedByteArray.sol b/foundry/lib/bytes/LibPrefixLengthEncodedByteArray.sol index b5773fd..7368b58 100644 --- a/foundry/lib/bytes/LibPrefixLengthEncodedByteArray.sol +++ b/foundry/lib/bytes/LibPrefixLengthEncodedByteArray.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; /** * @title Propellerheads PrefixLengthEncoded Byte Array Library @@ -16,11 +16,9 @@ library LibPrefixLengthEncodedByteArray { /** * @dev Pop the first element of an array and returns it with the remaining data. */ - function next(bytes calldata encoded) - internal - pure - returns (bytes calldata elem, bytes calldata res) - { + function next( + bytes calldata encoded + ) internal pure returns (bytes calldata elem, bytes calldata res) { assembly { switch iszero(encoded.length) case 1 { @@ -46,7 +44,11 @@ library LibPrefixLengthEncodedByteArray { assembly { let offset := encoded.offset let end := add(encoded.offset, encoded.length) - for {} lt(offset, end) {} { + for { + + } lt(offset, end) { + + } { offset := add(offset, add(shr(240, calldataload(offset)), 2)) s := add(s, 1) } @@ -56,11 +58,9 @@ library LibPrefixLengthEncodedByteArray { /** * @dev Cast an encoded array into a Solidity array. */ - function toArray(bytes calldata encoded) - internal - pure - returns (bytes[] memory arr) - { + function toArray( + bytes calldata encoded + ) internal pure returns (bytes[] memory arr) { bytes calldata elem; uint256 idx = 0; arr = new bytes[](LibPrefixLengthEncodedByteArray.size(encoded)); diff --git a/foundry/lib/v4-core b/foundry/lib/v4-core new file mode 160000 index 0000000..e50237c --- /dev/null +++ b/foundry/lib/v4-core @@ -0,0 +1 @@ +Subproject commit e50237c43811bd9b526eff40f26772152a42daba diff --git a/foundry/lib/v4-periphery b/foundry/lib/v4-periphery new file mode 160000 index 0000000..cf451c4 --- /dev/null +++ b/foundry/lib/v4-periphery @@ -0,0 +1 @@ +Subproject commit cf451c4f55f36ea64c2007d331e3a3574225fc8b diff --git a/foundry/remappings.txt b/foundry/remappings.txt index 2d2e7e6..04240ad 100644 --- a/foundry/remappings.txt +++ b/foundry/remappings.txt @@ -5,4 +5,6 @@ @uniswap-v2/=lib/v2-core/ @balancer-labs/v2-interfaces=lib/balancer-v2-monorepo/pkg/interfaces @uniswap/v3-updated/=lib/v3-updated/ -@uniswap/v3-core/=lib/v3-core/ \ No newline at end of file +@uniswap/v3-core/=lib/v3-core/ +@uniswap/v4-core/=lib/v4-core/ +@uniswap/v4-periphery/=lib/v4-periphery/ \ No newline at end of file diff --git a/foundry/src/CallbackVerificationDispatcher.sol b/foundry/src/CallbackVerificationDispatcher.sol index 87ccc8e..8df5848 100644 --- a/foundry/src/CallbackVerificationDispatcher.sol +++ b/foundry/src/CallbackVerificationDispatcher.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@interfaces/ICallbackVerifier.sol"; diff --git a/foundry/src/ExecutionDispatcher.sol b/foundry/src/ExecutionDispatcher.sol index 9e084c0..73bd554 100644 --- a/foundry/src/ExecutionDispatcher.sol +++ b/foundry/src/ExecutionDispatcher.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@interfaces/IExecutor.sol"; diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index c2e0b52..311c916 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "../lib/IWETH.sol"; import "../lib/bytes/LibPrefixLengthEncodedByteArray.sol"; diff --git a/foundry/src/executors/BalancerV2Executor.sol b/foundry/src/executors/BalancerV2Executor.sol index 79777cb..14340cf 100644 --- a/foundry/src/executors/BalancerV2Executor.sol +++ b/foundry/src/executors/BalancerV2Executor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@interfaces/IExecutor.sol"; import { diff --git a/foundry/src/executors/UniswapV2Executor.sol b/foundry/src/executors/UniswapV2Executor.sol index 9f447f1..7239a8a 100644 --- a/foundry/src/executors/UniswapV2Executor.sol +++ b/foundry/src/executors/UniswapV2Executor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@interfaces/IExecutor.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; diff --git a/foundry/src/executors/UniswapV3Executor.sol b/foundry/src/executors/UniswapV3Executor.sol index 64ae64d..ac986c5 100644 --- a/foundry/src/executors/UniswapV3Executor.sol +++ b/foundry/src/executors/UniswapV3Executor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@interfaces/IExecutor.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol new file mode 100644 index 0000000..4a67727 --- /dev/null +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import "@interfaces/IExecutor.sol"; +import { + IERC20, + SafeERC20 +} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import { + Currency, CurrencyLibrary +} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {V4Router} from "@uniswap/v4-periphery/src/V4Router.sol"; +import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol"; +import {IV4Router} from "@uniswap/v4-periphery/src/interfaces/IV4Router.sol"; +import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol"; + +error UniswapV4Executor__InvalidDataLength(); +error UniswapV4Executor__SwapFailed(); + +contract UniswapV4Executor is IExecutor, V4Router { + using SafeERC20 for IERC20; + using CurrencyLibrary for Currency; + + constructor(IPoolManager _poolManager) V4Router(_poolManager) {} + + function swap(uint256, bytes calldata data) + external + payable + returns (uint256 calculatedAmount) + { + (address tokenIn, address tokenOut, bool isExactInput, uint256 amount) = + _decodeData(data); + + uint256 tokenOutBalanceBefore; + uint256 tokenInBalanceBefore; + + tokenOutBalanceBefore = tokenOut == address(0) + ? address(this).balance + : IERC20(tokenOut).balanceOf(address(this)); + + tokenInBalanceBefore = tokenIn == address(0) + ? address(this).balance + : IERC20(tokenIn).balanceOf(address(this)); + + _executeActions(data); + + uint256 tokenOutBalanceAfter; + uint256 tokenInBalanceAfter; + + tokenOutBalanceAfter = tokenOut == address(0) + ? address(this).balance + : IERC20(tokenOut).balanceOf(address(this)); + + tokenInBalanceAfter = tokenIn == address(0) + ? address(this).balance + : IERC20(tokenIn).balanceOf(address(this)); + + if (isExactInput) { + calculatedAmount = tokenOutBalanceAfter - tokenOutBalanceBefore; + } else { + calculatedAmount = tokenInBalanceBefore - tokenInBalanceAfter; + } + + return calculatedAmount; + } + + function _decodeData(bytes calldata data) + internal + pure + returns ( + address tokenIn, + address tokenOut, + bool isExactInput, + uint256 amount + ) + { + (bytes memory actions, bytes[] memory params) = + abi.decode(data, (bytes, bytes[])); + + // First byte of actions determines the swap type + uint8 action = uint8(bytes1(actions[0])); + + if (action == uint8(Actions.SWAP_EXACT_IN_SINGLE)) { + IV4Router.ExactInputSingleParams memory swapParams = + abi.decode(params[0], (IV4Router.ExactInputSingleParams)); + + tokenIn = swapParams.zeroForOne + ? address(uint160(swapParams.poolKey.currency0.toId())) + : address(uint160(swapParams.poolKey.currency1.toId())); + tokenOut = swapParams.zeroForOne + ? address(uint160(swapParams.poolKey.currency1.toId())) + : address(uint160(swapParams.poolKey.currency0.toId())); + isExactInput = true; + amount = swapParams.amountIn; + } else if (action == uint8(Actions.SWAP_EXACT_OUT_SINGLE)) { + IV4Router.ExactOutputSingleParams memory swapParams = + abi.decode(params[0], (IV4Router.ExactOutputSingleParams)); + + tokenIn = swapParams.zeroForOne + ? address(uint160(swapParams.poolKey.currency0.toId())) + : address(uint160(swapParams.poolKey.currency1.toId())); + tokenOut = swapParams.zeroForOne + ? address(uint160(swapParams.poolKey.currency1.toId())) + : address(uint160(swapParams.poolKey.currency0.toId())); + isExactInput = false; + amount = swapParams.amountOut; + } else if (action == uint8(Actions.SWAP_EXACT_IN)) { + IV4Router.ExactInputParams memory swapParams = + abi.decode(params[0], (IV4Router.ExactInputParams)); + + tokenIn = address(uint160(swapParams.currencyIn.toId())); + PathKey memory lastPath = + swapParams.path[swapParams.path.length - 1]; + tokenOut = address(uint160(lastPath.intermediateCurrency.toId())); + isExactInput = true; + amount = swapParams.amountIn; + } else if (action == uint8(Actions.SWAP_EXACT_OUT)) { + IV4Router.ExactOutputParams memory swapParams = + abi.decode(params[0], (IV4Router.ExactOutputParams)); + + PathKey memory firstPath = swapParams.path[0]; + tokenIn = address(uint160(firstPath.intermediateCurrency.toId())); + tokenOut = address(uint160(swapParams.currencyOut.toId())); + isExactInput = false; + amount = swapParams.amountOut; + } + } + + function _pay(Currency token, address payer, uint256 amount) + internal + override + { + IERC20(Currency.unwrap(token)).safeTransfer( + address(poolManager), amount + ); + } + + function msgSender() public view override returns (address) { + return address(this); + } +} diff --git a/foundry/test/CallbackVerificationDispatcher.t.sol b/foundry/test/CallbackVerificationDispatcher.t.sol index 7a99f13..fd2577e 100644 --- a/foundry/test/CallbackVerificationDispatcher.t.sol +++ b/foundry/test/CallbackVerificationDispatcher.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@src/CallbackVerificationDispatcher.sol"; import "./TychoRouterTestSetup.sol"; diff --git a/foundry/test/Constants.sol b/foundry/test/Constants.sol index 835f4d6..ba8fbef 100644 --- a/foundry/test/Constants.sol +++ b/foundry/test/Constants.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "forge-std/Test.sol"; @@ -27,6 +27,9 @@ contract Constants is Test { address BAL_ADDR = address(0xba100000625a3754423978a60c9317c58a424e3D); address USDC_ADDR = address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); address WBTC_ADDR = address(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); + address INCH_ADDR = address(0x111111111117dC0aa78b770fA6A738034120C302); + address USDE_ADDR = address(0x4c9EDD5852cd905f086C759E8383e09bff1E68B3); + address USDT_ADDR = address(0xdAC17F958D2ee523a2206206994597C13D831ec7); // uniswap v2 address WETH_DAI_POOL = 0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11; diff --git a/foundry/test/ExecutionDispatcher.t.sol b/foundry/test/ExecutionDispatcher.t.sol index a88dc44..7f2fb81 100644 --- a/foundry/test/ExecutionDispatcher.t.sol +++ b/foundry/test/ExecutionDispatcher.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@src/ExecutionDispatcher.sol"; import "./TychoRouterTestSetup.sol"; diff --git a/foundry/test/LibPrefixLengthEncodedByteArray.t.sol b/foundry/test/LibPrefixLengthEncodedByteArray.t.sol index d56b298..123f565 100644 --- a/foundry/test/LibPrefixLengthEncodedByteArray.t.sol +++ b/foundry/test/LibPrefixLengthEncodedByteArray.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.26; import {Test} from "forge-std/Test.sol"; import {LibPrefixLengthEncodedByteArray} from diff --git a/foundry/test/LibSwap.t.sol b/foundry/test/LibSwap.t.sol index eedc931..0627740 100644 --- a/foundry/test/LibSwap.t.sol +++ b/foundry/test/LibSwap.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "forge-std/Test.sol"; import "../lib/LibSwap.sol"; diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 489d366..f7df52a 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import {TychoRouter} from "@src/TychoRouter.sol"; import "./TychoRouterTestSetup.sol"; diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 839e3ee..2d63fe2 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; +pragma solidity ^0.8.26; import "../src/executors/UniswapV2Executor.sol"; import "./Constants.sol"; diff --git a/foundry/test/executors/BalancerV2Executor.t.sol b/foundry/test/executors/BalancerV2Executor.t.sol index f537cbf..117b153 100644 --- a/foundry/test/executors/BalancerV2Executor.t.sol +++ b/foundry/test/executors/BalancerV2Executor.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@src/executors/BalancerV2Executor.sol"; import {Test} from "../../lib/forge-std/src/Test.sol"; diff --git a/foundry/test/executors/UniswapV2Executor.t.sol b/foundry/test/executors/UniswapV2Executor.t.sol index 765d79c..dbcffbc 100644 --- a/foundry/test/executors/UniswapV2Executor.t.sol +++ b/foundry/test/executors/UniswapV2Executor.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@src/executors/UniswapV2Executor.sol"; import {Test} from "../../lib/forge-std/src/Test.sol"; diff --git a/foundry/test/executors/UniswapV3Executor.t.sol b/foundry/test/executors/UniswapV3Executor.t.sol index b73d580..4c9c1e9 100644 --- a/foundry/test/executors/UniswapV3Executor.t.sol +++ b/foundry/test/executors/UniswapV3Executor.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import "@src/executors/UniswapV3Executor.sol"; import {Test} from "../../lib/forge-std/src/Test.sol"; diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol new file mode 100644 index 0000000..124c15f --- /dev/null +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import "@src/executors/UniswapV4Executor.sol"; +import {Test} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "../Constants.sol"; +import {console} from "forge-std/console.sol"; + +contract UniswapV4ExecutorExposed is UniswapV4Executor { + constructor(IPoolManager _poolManager) UniswapV4Executor(_poolManager) {} + + function decodeData(bytes calldata data) + external + pure + returns ( + address tokenIn, + address tokenOut, + bool isExactInput, + uint256 amount + ) + { + return _decodeData(data); + } +} + +contract UniswapV4ExecutorTest is Test, Constants { + using SafeERC20 for IERC20; + + UniswapV4ExecutorExposed uniswapV4Exposed; + IERC20 USDE = IERC20(USDE_ADDR); + IERC20 USDT = IERC20(USDT_ADDR); + address poolManager = 0x000000000004444c5dc75cB358380D2e3dE08A90; + + function setUp() public { + uint256 forkBlock = 21817316; + vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); + uniswapV4Exposed = + new UniswapV4ExecutorExposed(IPoolManager(poolManager)); + } + + function testDecodeParams() public view { + uint24 expectedPoolFee = 500; + uint128 expectedAmount = 100; + + bytes memory data = _encodeExactInputSingle( + USDE_ADDR, USDT_ADDR, expectedPoolFee, false, 1, expectedAmount + ); + + (address tokenIn, address tokenOut, bool isExactInput, uint256 amount) = + uniswapV4Exposed.decodeData(data); + + assertEq(tokenIn, USDE_ADDR); + assertEq(tokenOut, USDT_ADDR); + assertTrue(isExactInput); + assertEq(amount, expectedAmount); + } + + function testSwap() public { + uint256 amountIn = 100 ether; + deal(USDE_ADDR, address(uniswapV4Exposed), amountIn); + uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager); + uint256 usdeBalanceBeforeSwapExecutor = + USDE.balanceOf(address(uniswapV4Exposed)); + + bytes memory data = _encodeExactInputSingle( + USDE_ADDR, USDT_ADDR, 100, true, 1, uint128(amountIn) + ); + + uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); + assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn); + assertEq( + USDE.balanceOf(address(uniswapV4Exposed)), + usdeBalanceBeforeSwapExecutor - amountIn + ); + assertTrue(USDT.balanceOf(address(uniswapV4Exposed)) == amountOut); + } + + function _encodeExactInputSingle( + address tokenIn, + address tokenOut, + uint24 fee, + bool zeroForOne, + uint24 tickSpacing, + uint128 amountIn + ) internal pure returns (bytes memory) { + PoolKey memory key = PoolKey({ + currency0: Currency.wrap(zeroForOne ? tokenIn : tokenOut), + currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn), + fee: fee, + tickSpacing: int24(tickSpacing), + hooks: IHooks(address(0)) + }); + + bytes memory actions = abi.encodePacked( + uint8(Actions.SWAP_EXACT_IN_SINGLE), + uint8(Actions.SETTLE_ALL), + uint8(Actions.TAKE_ALL) + ); + + bytes[] memory params = new bytes[](3); + + params[0] = abi.encode( + IV4Router.ExactInputSingleParams({ + poolKey: key, + zeroForOne: zeroForOne, + amountIn: amountIn, + amountOutMinimum: 0, + hookData: bytes("") + }) + ); + + params[1] = abi.encode(key.currency0, amountIn); + params[2] = abi.encode(key.currency1, 0); + + return abi.encode(actions, params); + } +} diff --git a/foundry/test/mock/MockERC20.sol b/foundry/test/mock/MockERC20.sol index 013c439..1ba6525 100644 --- a/foundry/test/mock/MockERC20.sol +++ b/foundry/test/mock/MockERC20.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Unlicense -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";