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/CHANGELOG.md b/CHANGELOG.md index dcd440d..011c20b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,66 @@ +## [0.39.0](https://github.com/propeller-heads/tycho-execution/compare/0.38.0...0.39.0) (2025-02-14) + + +### Features + +* **univ4:** Implement swapping with multiple hops ([21a8c1a](https://github.com/propeller-heads/tycho-execution/commit/21a8c1a27a8370bf7471b206e78b6a2fcf38ce00)) +* **univ4:** Refactor input and handle single swap case ([be7883a](https://github.com/propeller-heads/tycho-execution/commit/be7883affc2e481fce76dcd762215efb83905478)) + + +### Bug Fixes + +* Fix PLE tests that break after foundry update ([69d03f0](https://github.com/propeller-heads/tycho-execution/commit/69d03f060872bf9cce313f2420dc95c6d7554dec)) +* **univ4:** Append callback data instead of prepending ([4d0f5ce](https://github.com/propeller-heads/tycho-execution/commit/4d0f5cec64af9c65f5a03685d4c89bb0dd0a897c)) +* **univ4:** Make slither happy ([8a8bc69](https://github.com/propeller-heads/tycho-execution/commit/8a8bc697eb68308aedf74bd605d2f555328df99c)) + +## [0.38.0](https://github.com/propeller-heads/tycho-execution/compare/0.37.0...0.38.0) (2025-02-13) + + +### Features + +* Add a production foundry profile ([dae38ce](https://github.com/propeller-heads/tycho-execution/commit/dae38ceaf9b407d3ee93535ea0032804cbca9d59)) +* Support uniswap v4 callback in TychoRouter ([591d73b](https://github.com/propeller-heads/tycho-execution/commit/591d73ba717deb1773f5c10f9085cc1175df2536)) + + +### Bug Fixes + +* Verify that the executor exists in the uni v4 callback ([4c5e3bf](https://github.com/propeller-heads/tycho-execution/commit/4c5e3bf6a9070878c684ae8d029451178201d428)) + +## [0.37.0](https://github.com/propeller-heads/tycho-execution/compare/0.36.2...0.37.0) (2025-02-12) + + +### Features + +* add callback ([ed90cb4](https://github.com/propeller-heads/tycho-execution/commit/ed90cb4ef1d43e09a7cd6f824ef4214598851b9f)) +* add new pair test ([7ca647f](https://github.com/propeller-heads/tycho-execution/commit/7ca647f009ad8cb71c6e8a08e64ee02285c9ae08)) +* add router params ([e62c332](https://github.com/propeller-heads/tycho-execution/commit/e62c332451d7cf0d2fc471faa7af7b26fd1a000d)) +* add test for UniswapV4Executor ([4599f07](https://github.com/propeller-heads/tycho-execution/commit/4599f07df0d4c4131f87cd41ec7bcb8b1dd47bde)) +* add univ4 executor ([cb4c8f4](https://github.com/propeller-heads/tycho-execution/commit/cb4c8f4e51d4f1900149288339c6a6fc75a515b1)) +* handle amounts in unlockCallback ([b2097ca](https://github.com/propeller-heads/tycho-execution/commit/b2097ca4a5600161166636c7f2b58f845540ed9a)) +* move encoding to test ([c264084](https://github.com/propeller-heads/tycho-execution/commit/c264084783561b3de4eeac413ed6155076ff11d5)) +* support multi swap decoding ([d998c88](https://github.com/propeller-heads/tycho-execution/commit/d998c88cfef300e41714c6c3c6164e761d14e2de)) +* update solc and add V4Router into UniswapV4Executor ([bdd3daf](https://github.com/propeller-heads/tycho-execution/commit/bdd3daffba3853ad084f7d3454e3c72fd6a1679c)) + + +### Bug Fixes + +* _pay and msgSender ([d790682](https://github.com/propeller-heads/tycho-execution/commit/d79068282aebd1e65ae32e79ec3127da25f091af)) +* add equality check, amountInOrOut check, update _decodeData ([b47cff3](https://github.com/propeller-heads/tycho-execution/commit/b47cff3fc915b8146d62b085a7a5239d85d9d993)) +* git submodules and strict equality check in v4 executor ([a8cc84d](https://github.com/propeller-heads/tycho-execution/commit/a8cc84ddce7c90aa40d69090577ef15cc95d8edf)) +* handle native token balance changes ([0c40e9e](https://github.com/propeller-heads/tycho-execution/commit/0c40e9e97923d5bad61aa812ba739c2fe4260cf8)) +* reciever issue ([ae0b07b](https://github.com/propeller-heads/tycho-execution/commit/ae0b07b2a47b93430841ce8bf437215d2f94e3bb)) +* remove executeActions wrapper, strict equality checks and rename swap return ([2371ab2](https://github.com/propeller-heads/tycho-execution/commit/2371ab2a1fb96164a54c796cb0557d64e50c2350)) +* remove extra _receiver and redundant asserts ([ff3209b](https://github.com/propeller-heads/tycho-execution/commit/ff3209b1c861c015568c3daa691f74d95ef0c978)) +* rm callback fn ([1a36c33](https://github.com/propeller-heads/tycho-execution/commit/1a36c33bc614d744cfa161dd85d6cccc671e592e)) +* rm redundant transfer ([24d4e76](https://github.com/propeller-heads/tycho-execution/commit/24d4e762a2841909245d7a4434c13f37398ae482)) + +## [0.36.2](https://github.com/propeller-heads/tycho-execution/compare/0.36.1...0.36.2) (2025-02-12) + + +### Bug Fixes + +* Miscellaneous audit remarks ([582533f](https://github.com/propeller-heads/tycho-execution/commit/582533fa31b1c2096566df00b7e07350f677a647)) + ## [0.36.1](https://github.com/propeller-heads/tycho-execution/compare/0.36.0...0.36.1) (2025-02-11) ## [0.36.0](https://github.com/propeller-heads/tycho-execution/compare/0.35.1...0.36.0) (2025-02-11) diff --git a/Cargo.lock b/Cargo.lock index 0774d3a..56600d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4340,7 +4340,7 @@ dependencies = [ [[package]] name = "tycho-execution" -version = "0.36.1" +version = "0.39.0" dependencies = [ "alloy", "alloy-primitives", diff --git a/Cargo.toml b/Cargo.toml index 77d4746..c07ebf0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tycho-execution" -version = "0.36.1" +version = "0.39.0" edition = "2021" [[bin]] diff --git a/foundry/foundry.toml b/foundry/foundry.toml index a3c8d7b..7cc18b3 100644 --- a/foundry/foundry.toml +++ b/foundry/foundry.toml @@ -2,10 +2,20 @@ 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 +optimizer_runs = 200 +via_ir = true + +[profile.production] +src = 'src' +out = 'out' +libs = ['lib'] +auto_detect_sol = true +evm_version = 'cancun' +optimizer = true +optimizer_runs = 44444444 via_ir = true [rpc_endpoints] 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 4b8aa1f..844507e 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"; @@ -9,11 +9,14 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; import "@permit2/src/interfaces/IAllowanceTransfer.sol"; import "@uniswap/v3-updated/CallbackValidationV2.sol"; import "./ExecutionDispatcher.sol"; import "./CallbackVerificationDispatcher.sol"; import {LibSwap} from "../lib/LibSwap.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {SafeCallback} from "@uniswap/v4-periphery/src/base/SafeCallback.sol"; error TychoRouter__WithdrawalFailed(); error TychoRouter__AddressZero(); @@ -27,7 +30,8 @@ contract TychoRouter is ExecutionDispatcher, CallbackVerificationDispatcher, Pausable, - ReentrancyGuard + ReentrancyGuard, + SafeCallback { IAllowanceTransfer public immutable permit2; IWETH private immutable _weth; @@ -64,24 +68,24 @@ contract TychoRouter is address private immutable _usv3Factory; - constructor(address _permit2, address weth, address usv3Factory) { + constructor( + IPoolManager _poolManager, + address _permit2, + address weth, + address usv3Factory + ) SafeCallback(_poolManager) { + if ( + _permit2 == address(0) || weth == address(0) + || usv3Factory == address(0) + ) { + revert TychoRouter__AddressZero(); + } permit2 = IAllowanceTransfer(_permit2); _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _weth = IWETH(weth); - - if (usv3Factory == address(0)) { - revert TychoRouter__AddressZero(); - } _usv3Factory = usv3Factory; } - /** - * @dev Unpauses the contract - */ - function unpause() external onlyRole(UNPAUSER_ROLE) { - _unpause(); - } - /** * @notice Executes a swap operation based on a predefined swap graph, supporting internal token amount splits. * This function enables multi-step swaps, optional ETH wrapping/unwrapping, and validates the output amount @@ -161,8 +165,7 @@ contract TychoRouter is _unwrapETH(amountOut); } if (tokenOut == address(0)) { - // slither-disable-next-line arbitrary-send-eth - payable(receiver).transfer(amountOut); + Address.sendValue(payable(receiver), amountOut); } else { IERC20(tokenOut).safeTransfer(receiver, amountOut); } @@ -257,6 +260,13 @@ contract TychoRouter is _pause(); } + /** + * @dev Unpauses the contract + */ + function unpause() external onlyRole(UNPAUSER_ROLE) { + _unpause(); + } + /** * @dev Allows granting roles to multiple accounts in a single call. */ @@ -432,4 +442,27 @@ contract TychoRouter is return (amountIn, tokenIn); } + + function _unlockCallback(bytes calldata data) + internal + override + returns (bytes memory) + { + require(data.length >= 20, "Invalid data length"); + bytes4 selector = bytes4(data[data.length - 4:]); + address executor = + address(uint160(bytes20(data[data.length - 24:data.length - 4]))); + bytes memory protocolData = data[:data.length - 24]; + + if (!executors[executor]) { + revert ExecutionDispatcher__UnapprovedExecutor(); + } + + // slither-disable-next-line controlled-delegatecall,low-level-calls + (bool success,) = executor.delegatecall( + abi.encodeWithSelector(selector, protocolData) + ); + require(success, "delegatecall to uniswap v4 callback failed"); + return ""; + } } 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..57fe796 --- /dev/null +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -0,0 +1,191 @@ +// 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(); + +contract UniswapV4Executor is IExecutor, V4Router { + using SafeERC20 for IERC20; + using CurrencyLibrary for Currency; + + struct UniswapV4Pool { + address intermediaryToken; + uint24 fee; + int24 tickSpacing; + } + + constructor(IPoolManager _poolManager) V4Router(_poolManager) {} + + function swap(uint256 amountIn, bytes calldata data) + external + payable + returns (uint256 calculatedAmount) + { + ( + address tokenIn, + address tokenOut, + uint256 amountOutMin, + bool zeroForOne, + address callbackExecutor, + bytes4 callbackSelector, + UniswapV4Executor.UniswapV4Pool[] memory pools + ) = _decodeData(data); + + bytes memory swapData; + if (pools.length == 1) { + PoolKey memory key = PoolKey({ + currency0: Currency.wrap(zeroForOne ? tokenIn : tokenOut), + currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn), + fee: pools[0].fee, + tickSpacing: pools[0].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: uint128(amountIn), + amountOutMinimum: uint128(amountOutMin), + hookData: bytes("") + }) + ); + params[1] = abi.encode(key.currency0, amountIn); + params[2] = abi.encode(key.currency1, amountOutMin); + swapData = abi.encode(actions, params); + } else { + PathKey[] memory path = new PathKey[](pools.length); + for (uint256 i = 0; i < pools.length; i++) { + path[i] = PathKey({ + intermediateCurrency: Currency.wrap(pools[i].intermediaryToken), + fee: pools[i].fee, + tickSpacing: pools[i].tickSpacing, + hooks: IHooks(address(0)), + hookData: bytes("") + }); + } + + bytes memory actions = abi.encodePacked( + uint8(Actions.SWAP_EXACT_IN), + uint8(Actions.SETTLE_ALL), + uint8(Actions.TAKE_ALL) + ); + + bytes[] memory params = new bytes[](3); + + Currency currencyIn = Currency.wrap(tokenIn); + params[0] = abi.encode( + IV4Router.ExactInputParams({ + currencyIn: currencyIn, + path: path, + amountIn: uint128(amountIn), + amountOutMinimum: uint128(amountOutMin) + }) + ); + params[1] = abi.encode(currencyIn, amountIn); + params[2] = abi.encode(Currency.wrap(tokenOut), amountOutMin); + swapData = abi.encode(actions, params); + } + bytes memory fullData = + abi.encodePacked(swapData, callbackExecutor, callbackSelector); + uint256 tokenOutBalanceBefore; + + tokenOutBalanceBefore = tokenOut == address(0) + ? address(this).balance + : IERC20(tokenOut).balanceOf(address(this)); + + executeActions(fullData); + + uint256 tokenOutBalanceAfter; + + tokenOutBalanceAfter = tokenOut == address(0) + ? address(this).balance + : IERC20(tokenOut).balanceOf(address(this)); + + calculatedAmount = tokenOutBalanceAfter - tokenOutBalanceBefore; + + return calculatedAmount; + } + + // necessary to convert bytes memory to bytes calldata + function executeActions(bytes memory unlockData) public { + // slither-disable-next-line unused-return + poolManager.unlock(unlockData); + } + + function _decodeData(bytes calldata data) + internal + pure + returns ( + address tokenIn, + address tokenOut, + uint256 amountOutMin, + bool zeroForOne, + address callbackExecutor, + bytes4 callbackSelector, + UniswapV4Pool[] memory pools + ) + { + if (data.length < 123) { + revert UniswapV4Executor__InvalidDataLength(); + } + + tokenIn = address(bytes20(data[0:20])); + tokenOut = address(bytes20(data[20:40])); + amountOutMin = uint256(bytes32(data[40:72])); + zeroForOne = (data[72] != 0); + callbackExecutor = address(bytes20(data[73:93])); + callbackSelector = bytes4(data[93:97]); + + uint256 poolsLength = (data.length - 97) / 26; // 26 bytes per pool object + pools = new UniswapV4Pool[](poolsLength); + bytes memory poolsData = data[97:]; + uint256 offset = 0; + for (uint256 i = 0; i < poolsLength; i++) { + address intermediaryToken; + uint24 fee; + int24 tickSpacing; + + // slither-disable-next-line assembly + assembly { + intermediaryToken := mload(add(poolsData, add(offset, 20))) + fee := shr(232, mload(add(poolsData, add(offset, 52)))) + tickSpacing := shr(232, mload(add(poolsData, add(offset, 55)))) + } + pools[i] = UniswapV4Pool(intermediaryToken, fee, tickSpacing); + offset += 26; + } + } + + function _pay(Currency token, address, 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..811dcc1 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 @@ -51,14 +51,16 @@ contract LibPrefixLengthEncodedByteArrayTest is Test { assertEq(this.size(multiple), 3); } - function testFailInvalidLength() public view { + function test_RevertIf_InvalidLength() public { // Length prefix larger than remaining data + vm.expectRevert(); bytes memory invalid = hex"0004414243"; this.next(invalid); } - function testFailIncompletePrefix() public view { + function test_RevertIf_IncompletePrefix() public { // Only 1 byte instead of 2 bytes prefix + vm.expectRevert(); bytes memory invalid = hex"01"; this.next(invalid); } 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..28eed5c 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -1,8 +1,10 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.28; +pragma solidity ^0.8.26; +import "@src/executors/UniswapV4Executor.sol"; import {TychoRouter} from "@src/TychoRouter.sol"; import "./TychoRouterTestSetup.sol"; +import "./executors/UniswapV4Utils.sol"; contract TychoRouterTest is TychoRouterTestSetup { bytes32 public constant EXECUTOR_SETTER_ROLE = @@ -254,7 +256,7 @@ contract TychoRouterTest is TychoRouterTestSetup { tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(tychoRouterAddr); - assertEq(daiBalance, 2630432278145144658455); + assertEq(daiBalance, 2659881924818443699787); assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } @@ -291,7 +293,7 @@ contract TychoRouterTest is TychoRouterTestSetup { tychoRouter.exposedSwap(amountIn, 3, pleEncode(swaps)); uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr); - assertEq(usdcBalance, 2610580090); + assertEq(usdcBalance, 2644659787); assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } @@ -352,7 +354,7 @@ contract TychoRouterTest is TychoRouterTestSetup { tychoRouter.exposedSwap(amountIn, 4, pleEncode(swaps)); uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr); - assertEq(usdcBalance, 2581503157); + assertEq(usdcBalance, 2615491639); assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); } @@ -400,7 +402,7 @@ contract TychoRouterTest is TychoRouterTestSetup { pleEncode(swaps) ); - uint256 expectedAmount = 2630432278145144658455; + uint256 expectedAmount = 2659881924818443699787; assertEq(amountOut, expectedAmount); uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); assertEq(daiBalance, expectedAmount); @@ -442,7 +444,7 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.expectRevert( abi.encodeWithSelector( TychoRouter__NegativeSlippage.selector, - 2630432278145144658455, // actual amountOut + 2659881924818443699787, // actual amountOut minAmountOut ) ); @@ -511,11 +513,11 @@ contract TychoRouterTest is TychoRouterTestSetup { pleEncode(swaps) ); - uint256 expectedAmount = 2604127955363693211871; + uint256 expectedAmount = 2633283105570259262790; assertEq(amountOut, expectedAmount); uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); assertEq(daiBalance, expectedAmount); - assertEq(IERC20(DAI_ADDR).balanceOf(FEE_RECEIVER), 26304322781451446584); + assertEq(IERC20(DAI_ADDR).balanceOf(FEE_RECEIVER), 26598819248184436997); vm.stopPrank(); } @@ -567,7 +569,7 @@ contract TychoRouterTest is TychoRouterTestSetup { "", pleEncode(swaps) ); - uint256 expectedAmount = 2630432278145144658455; + uint256 expectedAmount = 2659881924818443699787; assertEq(amountOut, expectedAmount); uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(ALICE); assertEq(daiBalance, expectedAmount); @@ -617,7 +619,7 @@ contract TychoRouterTest is TychoRouterTestSetup { pleEncode(swaps) ); - uint256 expectedAmount = 1132829934891544187; // 1.13 ETH + uint256 expectedAmount = 1120007305574805922; // 1.12 ETH assertEq(amountOut, expectedAmount); assertEq(ALICE.balance, expectedAmount); @@ -695,7 +697,7 @@ contract TychoRouterTest is TychoRouterTestSetup { // `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` (bool success,) = tychoRouterAddr.call( - hex"4860f9ed0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067c43ba900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000000000000000000000000000000000000679cb5b10000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000415bfd02ffd61c11192d1b54d76e0af125afbb32568aad37ec35f918bd5fb304cd314954213ed77c0d071301ddc45243ad57e86fe18f2905b682acc4f1a43ad8dc1c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005c005a00010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625abc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950000000000" + hex"4860f9ed0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067d481bb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067acfbc3000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041f2740fde9662d8bc1f8fe8e8fc29447c1832d625f06f4a56ee5103ad555c12323af5d50eb840f73d17873383ae3b7573956d5df7b2bf76bddba768c2837894a51b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005c005a00010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625abc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950000000000" ); vm.stopPrank(); @@ -717,14 +719,14 @@ contract TychoRouterTest is TychoRouterTestSetup { // Approve permit2 vm.startPrank(ALICE); - IERC20(WETH_ADDR).approve(address(permit2Address), type(uint256).max); + // IERC20(WETH_ADDR).approve(address(permit2Address), type(uint256).max); // Encoded solution generated using // `test_split_swap_strategy_encoder_simple_route_wrap` // but manually replacing the executor address // `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` (bool success,) = tychoRouterAddr.call{value: 1 ether}( - hex"4860f9ed0000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067c9179300000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067a1919b000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041cea77a63613f6a02aaee522c91f9569b8377a7f0200d141fafa3e1c42011e1c668555b49a1e7dd960091d0e33764ad24db6550bc761e228864495b478f1a23721b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005c005a00020000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625abc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950000000000" + hex"4860f9ed0000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067d4806b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067acfa73000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041c36406a750c499ac7f79f7666650f0d4f20fc27bb49ab68121c0be6554cb5cab6caf90dc3aab2e21083a8fa46976521a1e9df41ce74be59abf03e0d3691541e91c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005c005a00020000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625abc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950000000000" ); vm.stopPrank(); @@ -753,7 +755,7 @@ contract TychoRouterTest is TychoRouterTestSetup { // `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` (bool success,) = tychoRouterAddr.call( - hex"4860f9ed0000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000000000000000000000000000000000000067c9185300000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067a1925b000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041fd1c3dfce5afcb47988cc68165d5de64186cedbeb7eee6fc9cd087bceeaacdfe1ab799d60e0c628f24edfd9819b94ed60846dd23240c481f1d6e5470a7815a891c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005c005a00010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625ab6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950100000000" + hex"4860f9ed0000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000000000000000000000000000000000000067d4809800000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067acfaa000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004146411c70ec7fee0d5d260803cb220f5365792426c5d94f7a0a4d37abb05205752c5418b1fadd059570a71f0911814e546728e1f21876f2a1c6d38d34bd235fd61c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005c005a00010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625ab6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950100000000" ); vm.stopPrank(); @@ -784,7 +786,7 @@ contract TychoRouterTest is TychoRouterTestSetup { // `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` (bool success,) = tychoRouterAddr.call( - hex"4860f9ed0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067c48ea700000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000000000000000000000000000000000000679d08af00000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004197c2ff7801fa573e4e8e4af1df41499045485c2b48d090833dc85be38e002c1a1e7ef354285d79c2dcb40c4837e5156069de9aaf42365aef54fdc4cca2c76ccb1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000170005a00028000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625abc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139500005a00010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625abc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d0139500005a02030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625ab6b175474e89094c44da98b954eedeac495271d0fae461ca67b15dc8dc81ce7615e0320da1a9ab8d53ede3eca2a72b3aecc820e955b36f38437d0139501005a01030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625ab2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a4163ede3eca2a72b3aecc820e955b36f38437d013950100000000000000000000000000000000" + hex"4860f9ed0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067d4810d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067acfb15000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041ecaab75f0791c9683b001ea2f0e01a0a6aaf03e6e49c83e9c8a8e588a38e3be9230d962926628ffbf6a5370cda559ff0e7876a63ed38eebe33dbef5b5e2e46ef1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000170005a00028000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625abc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d0139500005a00010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625abc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2bb2b8038a1640196fbe3e38816f3e67cba72d9403ede3eca2a72b3aecc820e955b36f38437d0139500005a02030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625ab6b175474e89094c44da98b954eedeac495271d0fae461ca67b15dc8dc81ce7615e0320da1a9ab8d53ede3eca2a72b3aecc820e955b36f38437d0139501005a01030000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625ab2260fac5e5542a773aa44fbcfedf7c193bc2c599004375dff511095cc5a197a54140a24efef3a4163ede3eca2a72b3aecc820e955b36f38437d013950100000000000000000000000000000000" ); vm.stopPrank(); @@ -850,4 +852,89 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); } + + function testSwapSingleUSV4Callback() public { + uint256 amountIn = 100 ether; + deal(USDE_ADDR, tychoRouterAddr, amountIn); + + UniswapV4Executor.UniswapV4Pool[] memory pools = + new UniswapV4Executor.UniswapV4Pool[](1); + pools[0] = UniswapV4Executor.UniswapV4Pool({ + intermediaryToken: USDT_ADDR, + fee: uint24(100), + tickSpacing: int24(1) + }); + + bytes memory protocolData = UniswapV4Utils.encodeExactInput( + USDE_ADDR, + USDT_ADDR, + uint256(1), + true, + address(usv4Executor), + SafeCallback.unlockCallback.selector, + pools + ); + + bytes memory swap = encodeSwap( + uint8(0), + uint8(1), + uint24(0), + address(usv4Executor), + bytes4(0), + protocolData + ); + + bytes[] memory swaps = new bytes[](1); + swaps[0] = swap; + + tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); + + assertEq(IERC20(USDT_ADDR).balanceOf(tychoRouterAddr), 99943852); + } + + function testSwapMultipleUSV4Callback() public { + // This test has two uniswap v4 hops that will be executed inside of the V4 pool manager + // USDE -> USDT -> WBTC + uint256 amountIn = 100 ether; + deal(USDE_ADDR, tychoRouterAddr, amountIn); + + UniswapV4Executor.UniswapV4Pool[] memory pools = + new UniswapV4Executor.UniswapV4Pool[](2); + pools[0] = UniswapV4Executor.UniswapV4Pool({ + intermediaryToken: USDT_ADDR, + fee: uint24(100), + tickSpacing: int24(1) + }); + pools[1] = UniswapV4Executor.UniswapV4Pool({ + intermediaryToken: WBTC_ADDR, + fee: uint24(3000), + tickSpacing: int24(60) + }); + + bytes memory protocolData = UniswapV4Utils.encodeExactInput( + USDE_ADDR, + WBTC_ADDR, + uint256(1), + true, + address(usv4Executor), + SafeCallback.unlockCallback.selector, + pools + ); + + bytes memory swap = encodeSwap( + uint8(0), + uint8(1), + uint24(0), + address(usv4Executor), + bytes4(0), + protocolData + ); + + bytes[] memory swaps = new bytes[](1); + swaps[0] = swap; + + tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); + + assertEq(IERC20(WBTC_ADDR).balanceOf(tychoRouterAddr), 102718); + } } diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 839e3ee..b0bbf2e 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -1,17 +1,23 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; +pragma solidity ^0.8.26; import "../src/executors/UniswapV2Executor.sol"; +import "../src/executors/UniswapV3Executor.sol"; +import "../src/executors/UniswapV4Executor.sol"; import "./Constants.sol"; import "./mock/MockERC20.sol"; import "@src/TychoRouter.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {WETH} from "../lib/permit2/lib/solmate/src/tokens/WETH.sol"; -import "../src/executors/UniswapV3Executor.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; contract TychoRouterExposed is TychoRouter { - constructor(address _permit2, address weth, address usv3Factory) - TychoRouter(_permit2, weth, usv3Factory) - {} + constructor( + IPoolManager _poolManager, + address _permit2, + address weth, + address usv3Factory + ) TychoRouter(_poolManager, _permit2, weth, usv3Factory) {} function wrapETH(uint256 amount) external payable { return _wrapETH(amount); @@ -36,16 +42,20 @@ contract TychoRouterTestSetup is Test, Constants { address permit2Address = address(0x000000000022D473030F116dDEE9F6B43aC78BA3); UniswapV2Executor public usv2Executor; UniswapV3Executor public usv3Executor; + UniswapV4Executor public usv4Executor; MockERC20[] tokens; function setUp() public { - uint256 forkBlock = 21000000; + uint256 forkBlock = 21817316; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); vm.startPrank(ADMIN); address factoryV3 = address(0x1F98431c8aD98523631AE4a59f267346ea31F984); - tychoRouter = - new TychoRouterExposed(permit2Address, WETH_ADDR, factoryV3); + address poolManagerAddress = 0x000000000004444c5dc75cB358380D2e3dE08A90; + IPoolManager poolManager = IPoolManager(poolManagerAddress); + tychoRouter = new TychoRouterExposed( + poolManager, permit2Address, WETH_ADDR, factoryV3 + ); tychoRouterAddr = address(tychoRouter); tychoRouter.grantRole(keccak256("FUND_RESCUER_ROLE"), FUND_RESCUER); tychoRouter.grantRole(keccak256("FEE_SETTER_ROLE"), FEE_SETTER); @@ -59,10 +69,12 @@ contract TychoRouterTestSetup is Test, Constants { usv2Executor = new UniswapV2Executor(); usv3Executor = new UniswapV3Executor(); + usv4Executor = new UniswapV4Executor(poolManager); vm.startPrank(EXECUTOR_SETTER); - address[] memory executors = new address[](2); + address[] memory executors = new address[](3); executors[0] = address(usv2Executor); executors[1] = address(usv3Executor); + executors[2] = address(usv4Executor); tychoRouter.setExecutors(executors); vm.stopPrank(); diff --git a/foundry/test/executors/BalancerV2Executor.t.sol b/foundry/test/executors/BalancerV2Executor.t.sol index f537cbf..7581146 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"; @@ -83,7 +83,7 @@ contract BalancerV2ExecutorTest is assertEq(balanceAfter - balanceBefore, amountOut); } - function testDecodeIntegration() public { + function testDecodeIntegration() public view { // Generated by the SwapEncoder - test_encode_balancer_v2 bytes memory protocolData = hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2ba100000625a3754423978a60c9317c58a424e3d5c6ee304399dbdb9c8ef030ab642b10820db8f560002000000000000000000141d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e01"; diff --git a/foundry/test/executors/UniswapV2Executor.t.sol b/foundry/test/executors/UniswapV2Executor.t.sol index 765d79c..f377f5a 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"; @@ -94,7 +94,7 @@ contract UniswapV2ExecutorTest is UniswapV2ExecutorExposed, Test, Constants { assertGe(finalBalance, amountOut); } - function testDecodeIntegration() public { + function testDecodeIntegration() public view { // Generated by the ExecutorStrategyEncoder - test_executor_strategy_encode bytes memory protocolData = hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc288e6a0c2ddd26feeb64f039a2c41296fcb3f5640000000000000000000000000000000000000000100"; 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..e35b676 --- /dev/null +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import "../../src/executors/UniswapV4Executor.sol"; +import "./UniswapV4Utils.sol"; +import "@src/executors/UniswapV4Executor.sol"; +import {Constants} from "../Constants.sol"; +import {Test} from "../../lib/forge-std/src/Test.sol"; +import {SafeCallback} from "@uniswap/v4-periphery/src/base/SafeCallback.sol"; + +contract UniswapV4ExecutorExposed is UniswapV4Executor { + constructor(IPoolManager _poolManager) UniswapV4Executor(_poolManager) {} + + function decodeData(bytes calldata data) + external + pure + returns ( + address tokenIn, + address tokenOut, + uint256 amountOutMin, + bool zeroForOne, + address callbackExecutor, + bytes4 callbackSelector, + UniswapV4Pool[] memory pools + ) + { + 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 { + uint256 minAmountOut = 100; + bool zeroForOne = true; + uint24 pool1Fee = 500; + int24 tickSpacing1 = 60; + uint24 pool2Fee = 1000; + int24 tickSpacing2 = -10; + + UniswapV4Executor.UniswapV4Pool[] memory pools = + new UniswapV4Executor.UniswapV4Pool[](2); + pools[0] = UniswapV4Executor.UniswapV4Pool({ + intermediaryToken: USDT_ADDR, + fee: pool1Fee, + tickSpacing: tickSpacing1 + }); + pools[1] = UniswapV4Executor.UniswapV4Pool({ + intermediaryToken: USDE_ADDR, + fee: pool2Fee, + tickSpacing: tickSpacing2 + }); + + bytes memory data = UniswapV4Utils.encodeExactInput( + USDE_ADDR, + USDT_ADDR, + minAmountOut, + zeroForOne, + address(uniswapV4Exposed), + SafeCallback.unlockCallback.selector, + pools + ); + + ( + address tokenIn, + address tokenOut, + uint256 amountOutMin, + bool zeroForOneDecoded, + address callbackExecutor, + bytes4 callbackSelector, + UniswapV4Executor.UniswapV4Pool[] memory decodedPools + ) = uniswapV4Exposed.decodeData(data); + + assertEq(tokenIn, USDE_ADDR); + assertEq(tokenOut, USDT_ADDR); + assertEq(amountOutMin, minAmountOut); + assertEq(zeroForOneDecoded, zeroForOne); + assertEq(callbackExecutor, address(uniswapV4Exposed)); + assertEq(callbackSelector, SafeCallback.unlockCallback.selector); + assertEq(decodedPools.length, 2); + assertEq(decodedPools[0].intermediaryToken, USDT_ADDR); + assertEq(decodedPools[0].fee, pool1Fee); + assertEq(decodedPools[0].tickSpacing, tickSpacing1); + assertEq(decodedPools[1].intermediaryToken, USDE_ADDR); + assertEq(decodedPools[1].fee, pool2Fee); + assertEq(decodedPools[1].tickSpacing, tickSpacing2); + } + + function testSingleSwap() public { + uint256 amountIn = 100 ether; + deal(USDE_ADDR, address(uniswapV4Exposed), amountIn); + uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager); + uint256 usdeBalanceBeforeSwapExecutor = + USDE.balanceOf(address(uniswapV4Exposed)); + + UniswapV4Executor.UniswapV4Pool[] memory pools = + new UniswapV4Executor.UniswapV4Pool[](1); + pools[0] = UniswapV4Executor.UniswapV4Pool({ + intermediaryToken: USDT_ADDR, + fee: uint24(100), + tickSpacing: int24(1) + }); + + bytes memory data = UniswapV4Utils.encodeExactInput( + USDE_ADDR, + USDT_ADDR, + uint256(1), + true, + address(uniswapV4Exposed), + SafeCallback.unlockCallback.selector, + pools + ); + + 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 testMultipleSwap() public { + // USDE -> USDT -> WBTC + uint256 amountIn = 100 ether; + deal(USDE_ADDR, address(uniswapV4Exposed), amountIn); + uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager); + uint256 usdeBalanceBeforeSwapExecutor = + USDE.balanceOf(address(uniswapV4Exposed)); + + UniswapV4Executor.UniswapV4Pool[] memory pools = + new UniswapV4Executor.UniswapV4Pool[](2); + pools[0] = UniswapV4Executor.UniswapV4Pool({ + intermediaryToken: USDT_ADDR, + fee: uint24(100), + tickSpacing: int24(1) + }); + pools[1] = UniswapV4Executor.UniswapV4Pool({ + intermediaryToken: WBTC_ADDR, + fee: uint24(3000), + tickSpacing: int24(60) + }); + + bytes memory data = UniswapV4Utils.encodeExactInput( + USDE_ADDR, + WBTC_ADDR, + uint256(1), + true, + address(uniswapV4Exposed), + SafeCallback.unlockCallback.selector, + pools + ); + + uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); + assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn); + assertEq( + USDE.balanceOf(address(uniswapV4Exposed)), + usdeBalanceBeforeSwapExecutor - amountIn + ); + assertTrue( + IERC20(WBTC_ADDR).balanceOf(address(uniswapV4Exposed)) == amountOut + ); + } +} diff --git a/foundry/test/executors/UniswapV4Utils.sol b/foundry/test/executors/UniswapV4Utils.sol new file mode 100644 index 0000000..ffa0b21 --- /dev/null +++ b/foundry/test/executors/UniswapV4Utils.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import "@src/executors/UniswapV4Executor.sol"; + +library UniswapV4Utils { + function encodeExactInput( + address tokenIn, + address tokenOut, + uint256 amountOutMin, + bool zeroForOne, + address callbackExecutor, + bytes4 callbackSelector, + UniswapV4Executor.UniswapV4Pool[] memory pools + ) public pure returns (bytes memory) { + bytes memory encodedPools; + + for (uint256 i = 0; i < pools.length; i++) { + encodedPools = abi.encodePacked( + encodedPools, + pools[i].intermediaryToken, + bytes3(pools[i].fee), + pools[i].tickSpacing + ); + } + + return abi.encodePacked( + tokenIn, + tokenOut, + amountOutMin, + zeroForOne, + callbackExecutor, + bytes4(callbackSelector), + encodedPools + ); + } +} 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";