Merge branch 'main' into encoding/tnl/ENG-4071-usv4
This commit is contained in:
8
.gitmodules
vendored
8
.gitmodules
vendored
@@ -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
|
||||
|
||||
63
CHANGELOG.md
63
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)
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -4340,7 +4340,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tycho-execution"
|
||||
version = "0.36.1"
|
||||
version = "0.39.0"
|
||||
dependencies = [
|
||||
"alloy",
|
||||
"alloy-primitives",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tycho-execution"
|
||||
version = "0.36.1"
|
||||
version = "0.39.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
29
foundry/lib/Constants.sol
Normal file
29
foundry/lib/Constants.sol
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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:];
|
||||
}
|
||||
}
|
||||
|
||||
110
foundry/lib/Payments.sol
Normal file
110
foundry/lib/Payments.sol
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
foundry/lib/PaymentsImmutables.sol
Normal file
23
foundry/lib/PaymentsImmutables.sol
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
57
foundry/lib/Permit2Payments.sol
Normal file
57
foundry/lib/Permit2Payments.sol
Normal file
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
1
foundry/lib/v4-core
Submodule
1
foundry/lib/v4-core
Submodule
Submodule foundry/lib/v4-core added at e50237c438
1
foundry/lib/v4-periphery
Submodule
1
foundry/lib/v4-periphery
Submodule
Submodule foundry/lib/v4-periphery added at cf451c4f55
@@ -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/
|
||||
@uniswap/v3-core/=lib/v3-core/
|
||||
@uniswap/v4-core/=lib/v4-core/
|
||||
@uniswap/v4-periphery/=lib/v4-periphery/
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.28;
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import "@interfaces/ICallbackVerifier.sol";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.28;
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import "@interfaces/IExecutor.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 "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.28;
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
import "@interfaces/IExecutor.sol";
|
||||
import {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
191
foundry/src/executors/UniswapV4Executor.sol
Normal file
191
foundry/src/executors/UniswapV4Executor.sol
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
177
foundry/test/executors/UniswapV4Executor.t.sol
Normal file
177
foundry/test/executors/UniswapV4Executor.t.sol
Normal file
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
37
foundry/test/executors/UniswapV4Utils.sol
Normal file
37
foundry/test/executors/UniswapV4Utils.sol
Normal file
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user