Merge pull request #58 from propeller-heads/router/hr/ENG-4105-uniswap-v4-executor

This commit is contained in:
Harsh Vardhan Roy
2025-02-13 01:29:21 +05:30
committed by GitHub
33 changed files with 554 additions and 77 deletions

8
.gitmodules vendored
View File

@@ -3,7 +3,7 @@
url = https://github.com/OpenZeppelin/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts
[submodule "foundry/lib/permit2"] [submodule "foundry/lib/permit2"]
path = foundry/lib/permit2 path = foundry/lib/permit2
url = https://github.com/Uniswap/permit2 url = https://github.com/uniswap/permit2
[submodule "foundry/lib/v2-core"] [submodule "foundry/lib/v2-core"]
path = foundry/lib/v2-core path = foundry/lib/v2-core
url = https://github.com/uniswap/v2-core url = https://github.com/uniswap/v2-core
@@ -13,3 +13,9 @@
[submodule "foundry/lib/v3-core"] [submodule "foundry/lib/v3-core"]
path = foundry/lib/v3-core path = foundry/lib/v3-core
url = https://github.com/Uniswap/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

View File

@@ -2,8 +2,8 @@
src = 'src' src = 'src'
out = 'out' out = 'out'
libs = ['lib'] libs = ['lib']
solc = "0.8.28" auto_detect_sol = true
evm_version = 'shanghai' evm_version = 'cancun'
optimizer = true optimizer = true
optimizer_runs = 1000 optimizer_runs = 1000
via_ir = true via_ir = true

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28; pragma solidity ^0.8.26;
interface ICallbackVerifier { interface ICallbackVerifier {
error UnauthorizedCaller(string exchange, address sender); 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. * @dev This method should revert if the sender is not a verified sender of the exchange.
*/ */
function verifyCallback(address sender, bytes calldata data) function verifyCallback(
external address sender,
returns ( bytes calldata data
uint256 amountOwed, ) external returns (uint256 amountOwed, address tokenOwed);
address tokenOwed
);
} }

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28; pragma solidity ^0.8.26;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
@@ -20,10 +20,10 @@ interface IExecutor {
* @return calculatedAmount The amount of the output token swapped, depending on * @return calculatedAmount The amount of the output token swapped, depending on
* the givenAmount inputted. * the givenAmount inputted.
*/ */
function swap(uint256 givenAmount, bytes calldata data) function swap(
external uint256 givenAmount,
payable bytes calldata data
returns (uint256 calculatedAmount); ) external payable returns (uint256 calculatedAmount);
} }
interface IExecutorErrors { interface IExecutorErrors {

29
foundry/lib/Constants.sol Normal file
View 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;
}

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28; pragma solidity ^0.8.26;
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

View File

@@ -1,58 +1,44 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28; pragma solidity ^0.8.26;
library LibSwap { library LibSwap {
/// Returns the InToken index into an array of tokens /// Returns the InToken index into an array of tokens
function tokenInIndex(bytes calldata swap) function tokenInIndex(
internal bytes calldata swap
pure ) internal pure returns (uint8 res) {
returns (uint8 res)
{
res = uint8(swap[0]); res = uint8(swap[0]);
} }
/// The OutToken index into an array of tokens /// The OutToken index into an array of tokens
function tokenOutIndex(bytes calldata swap) function tokenOutIndex(
internal bytes calldata swap
pure ) internal pure returns (uint8 res) {
returns (uint8 res)
{
res = uint8(swap[1]); res = uint8(swap[1]);
} }
/// The relative amount of token quantity routed into this swap /// The relative amount of token quantity routed into this swap
function splitPercentage(bytes calldata swap) function splitPercentage(
internal bytes calldata swap
pure ) internal pure returns (uint24 res) {
returns (uint24 res)
{
res = uint24(bytes3(swap[2:5])); res = uint24(bytes3(swap[2:5]));
} }
/// The address of the executor contract /// The address of the executor contract
function executor(bytes calldata swap) function executor(bytes calldata swap) internal pure returns (address res) {
internal
pure
returns (address res)
{
res = address(uint160(bytes20(swap[5:25]))); res = address(uint160(bytes20(swap[5:25])));
} }
/// The selector to be used of the executor contract /// The selector to be used of the executor contract
function executorSelector(bytes calldata swap) function executorSelector(
internal bytes calldata swap
pure ) internal pure returns (bytes4 res) {
returns (bytes4 res)
{
res = bytes4(swap[25:29]); res = bytes4(swap[25:29]);
} }
/// Remaining bytes are interpreted as protocol data /// Remaining bytes are interpreted as protocol data
function protocolData(bytes calldata swap) function protocolData(
internal bytes calldata swap
pure ) internal pure returns (bytes calldata res) {
returns (bytes calldata res)
{
res = swap[29:]; res = swap[29:];
} }
} }

110
foundry/lib/Payments.sol Normal file
View 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);
}
}
}
}

View 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);
}
}

View 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());
}
}

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28; pragma solidity ^0.8.26;
/** /**
* @title Propellerheads PrefixLengthEncoded Byte Array Library * @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. * @dev Pop the first element of an array and returns it with the remaining data.
*/ */
function next(bytes calldata encoded) function next(
internal bytes calldata encoded
pure ) internal pure returns (bytes calldata elem, bytes calldata res) {
returns (bytes calldata elem, bytes calldata res)
{
assembly { assembly {
switch iszero(encoded.length) switch iszero(encoded.length)
case 1 { case 1 {
@@ -46,7 +44,11 @@ library LibPrefixLengthEncodedByteArray {
assembly { assembly {
let offset := encoded.offset let offset := encoded.offset
let end := add(encoded.offset, encoded.length) let end := add(encoded.offset, encoded.length)
for {} lt(offset, end) {} { for {
} lt(offset, end) {
} {
offset := add(offset, add(shr(240, calldataload(offset)), 2)) offset := add(offset, add(shr(240, calldataload(offset)), 2))
s := add(s, 1) s := add(s, 1)
} }
@@ -56,11 +58,9 @@ library LibPrefixLengthEncodedByteArray {
/** /**
* @dev Cast an encoded array into a Solidity array. * @dev Cast an encoded array into a Solidity array.
*/ */
function toArray(bytes calldata encoded) function toArray(
internal bytes calldata encoded
pure ) internal pure returns (bytes[] memory arr) {
returns (bytes[] memory arr)
{
bytes calldata elem; bytes calldata elem;
uint256 idx = 0; uint256 idx = 0;
arr = new bytes[](LibPrefixLengthEncodedByteArray.size(encoded)); arr = new bytes[](LibPrefixLengthEncodedByteArray.size(encoded));

1
foundry/lib/v4-core Submodule

Submodule foundry/lib/v4-core added at e50237c438

View File

@@ -6,3 +6,5 @@
@balancer-labs/v2-interfaces=lib/balancer-v2-monorepo/pkg/interfaces @balancer-labs/v2-interfaces=lib/balancer-v2-monorepo/pkg/interfaces
@uniswap/v3-updated/=lib/v3-updated/ @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/

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28; pragma solidity ^0.8.26;
import "@interfaces/ICallbackVerifier.sol"; import "@interfaces/ICallbackVerifier.sol";

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28; pragma solidity ^0.8.26;
import "@interfaces/IExecutor.sol"; import "@interfaces/IExecutor.sol";

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28; pragma solidity ^0.8.26;
import "../lib/IWETH.sol"; import "../lib/IWETH.sol";
import "../lib/bytes/LibPrefixLengthEncodedByteArray.sol"; import "../lib/bytes/LibPrefixLengthEncodedByteArray.sol";

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28; pragma solidity ^0.8.26;
import "@interfaces/IExecutor.sol"; import "@interfaces/IExecutor.sol";
import { import {

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28; pragma solidity ^0.8.26;
import "@interfaces/IExecutor.sol"; import "@interfaces/IExecutor.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28; pragma solidity ^0.8.26;
import "@interfaces/IExecutor.sol"; import "@interfaces/IExecutor.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

View File

@@ -0,0 +1,144 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
import "@interfaces/IExecutor.sol";
import {
IERC20,
SafeERC20
} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {
Currency, CurrencyLibrary
} from "@uniswap/v4-core/src/types/Currency.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {V4Router} from "@uniswap/v4-periphery/src/V4Router.sol";
import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
import {IV4Router} from "@uniswap/v4-periphery/src/interfaces/IV4Router.sol";
import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol";
error UniswapV4Executor__InvalidDataLength();
error UniswapV4Executor__SwapFailed();
contract UniswapV4Executor is IExecutor, V4Router {
using SafeERC20 for IERC20;
using CurrencyLibrary for Currency;
constructor(IPoolManager _poolManager) V4Router(_poolManager) {}
function swap(uint256, bytes calldata data)
external
payable
returns (uint256 calculatedAmount)
{
(address tokenIn, address tokenOut, bool isExactInput, uint256 amount) =
_decodeData(data);
uint256 tokenOutBalanceBefore;
uint256 tokenInBalanceBefore;
tokenOutBalanceBefore = tokenOut == address(0)
? address(this).balance
: IERC20(tokenOut).balanceOf(address(this));
tokenInBalanceBefore = tokenIn == address(0)
? address(this).balance
: IERC20(tokenIn).balanceOf(address(this));
_executeActions(data);
uint256 tokenOutBalanceAfter;
uint256 tokenInBalanceAfter;
tokenOutBalanceAfter = tokenOut == address(0)
? address(this).balance
: IERC20(tokenOut).balanceOf(address(this));
tokenInBalanceAfter = tokenIn == address(0)
? address(this).balance
: IERC20(tokenIn).balanceOf(address(this));
if (isExactInput) {
calculatedAmount = tokenOutBalanceAfter - tokenOutBalanceBefore;
} else {
calculatedAmount = tokenInBalanceBefore - tokenInBalanceAfter;
}
return calculatedAmount;
}
function _decodeData(bytes calldata data)
internal
pure
returns (
address tokenIn,
address tokenOut,
bool isExactInput,
uint256 amount
)
{
(bytes memory actions, bytes[] memory params) =
abi.decode(data, (bytes, bytes[]));
// First byte of actions determines the swap type
uint8 action = uint8(bytes1(actions[0]));
if (action == uint8(Actions.SWAP_EXACT_IN_SINGLE)) {
IV4Router.ExactInputSingleParams memory swapParams =
abi.decode(params[0], (IV4Router.ExactInputSingleParams));
tokenIn = swapParams.zeroForOne
? address(uint160(swapParams.poolKey.currency0.toId()))
: address(uint160(swapParams.poolKey.currency1.toId()));
tokenOut = swapParams.zeroForOne
? address(uint160(swapParams.poolKey.currency1.toId()))
: address(uint160(swapParams.poolKey.currency0.toId()));
isExactInput = true;
amount = swapParams.amountIn;
} else if (action == uint8(Actions.SWAP_EXACT_OUT_SINGLE)) {
IV4Router.ExactOutputSingleParams memory swapParams =
abi.decode(params[0], (IV4Router.ExactOutputSingleParams));
tokenIn = swapParams.zeroForOne
? address(uint160(swapParams.poolKey.currency0.toId()))
: address(uint160(swapParams.poolKey.currency1.toId()));
tokenOut = swapParams.zeroForOne
? address(uint160(swapParams.poolKey.currency1.toId()))
: address(uint160(swapParams.poolKey.currency0.toId()));
isExactInput = false;
amount = swapParams.amountOut;
} else if (action == uint8(Actions.SWAP_EXACT_IN)) {
IV4Router.ExactInputParams memory swapParams =
abi.decode(params[0], (IV4Router.ExactInputParams));
tokenIn = address(uint160(swapParams.currencyIn.toId()));
PathKey memory lastPath =
swapParams.path[swapParams.path.length - 1];
tokenOut = address(uint160(lastPath.intermediateCurrency.toId()));
isExactInput = true;
amount = swapParams.amountIn;
} else if (action == uint8(Actions.SWAP_EXACT_OUT)) {
IV4Router.ExactOutputParams memory swapParams =
abi.decode(params[0], (IV4Router.ExactOutputParams));
PathKey memory firstPath = swapParams.path[0];
tokenIn = address(uint160(firstPath.intermediateCurrency.toId()));
tokenOut = address(uint160(swapParams.currencyOut.toId()));
isExactInput = false;
amount = swapParams.amountOut;
}
}
function _pay(Currency token, address payer, uint256 amount)
internal
override
{
IERC20(Currency.unwrap(token)).safeTransfer(
address(poolManager), amount
);
}
function msgSender() public view override returns (address) {
return address(this);
}
}

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28; pragma solidity ^0.8.26;
import "@src/CallbackVerificationDispatcher.sol"; import "@src/CallbackVerificationDispatcher.sol";
import "./TychoRouterTestSetup.sol"; import "./TychoRouterTestSetup.sol";

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28; pragma solidity ^0.8.26;
import "forge-std/Test.sol"; import "forge-std/Test.sol";
@@ -27,6 +27,9 @@ contract Constants is Test {
address BAL_ADDR = address(0xba100000625a3754423978a60c9317c58a424e3D); address BAL_ADDR = address(0xba100000625a3754423978a60c9317c58a424e3D);
address USDC_ADDR = address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); address USDC_ADDR = address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
address WBTC_ADDR = address(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); address WBTC_ADDR = address(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599);
address INCH_ADDR = address(0x111111111117dC0aa78b770fA6A738034120C302);
address USDE_ADDR = address(0x4c9EDD5852cd905f086C759E8383e09bff1E68B3);
address USDT_ADDR = address(0xdAC17F958D2ee523a2206206994597C13D831ec7);
// uniswap v2 // uniswap v2
address WETH_DAI_POOL = 0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11; address WETH_DAI_POOL = 0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11;

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28; pragma solidity ^0.8.26;
import "@src/ExecutionDispatcher.sol"; import "@src/ExecutionDispatcher.sol";
import "./TychoRouterTestSetup.sol"; import "./TychoRouterTestSetup.sol";

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity ^0.8.0; pragma solidity ^0.8.26;
import {Test} from "forge-std/Test.sol"; import {Test} from "forge-std/Test.sol";
import {LibPrefixLengthEncodedByteArray} from import {LibPrefixLengthEncodedByteArray} from

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28; pragma solidity ^0.8.26;
import "forge-std/Test.sol"; import "forge-std/Test.sol";
import "../lib/LibSwap.sol"; import "../lib/LibSwap.sol";

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28; pragma solidity ^0.8.26;
import {TychoRouter} from "@src/TychoRouter.sol"; import {TychoRouter} from "@src/TychoRouter.sol";
import "./TychoRouterTestSetup.sol"; import "./TychoRouterTestSetup.sol";

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13; pragma solidity ^0.8.26;
import "../src/executors/UniswapV2Executor.sol"; import "../src/executors/UniswapV2Executor.sol";
import "./Constants.sol"; import "./Constants.sol";

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28; pragma solidity ^0.8.26;
import "@src/executors/BalancerV2Executor.sol"; import "@src/executors/BalancerV2Executor.sol";
import {Test} from "../../lib/forge-std/src/Test.sol"; import {Test} from "../../lib/forge-std/src/Test.sol";

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28; pragma solidity ^0.8.26;
import "@src/executors/UniswapV2Executor.sol"; import "@src/executors/UniswapV2Executor.sol";
import {Test} from "../../lib/forge-std/src/Test.sol"; import {Test} from "../../lib/forge-std/src/Test.sol";

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28; pragma solidity ^0.8.26;
import "@src/executors/UniswapV3Executor.sol"; import "@src/executors/UniswapV3Executor.sol";
import {Test} from "../../lib/forge-std/src/Test.sol"; import {Test} from "../../lib/forge-std/src/Test.sol";

View File

@@ -0,0 +1,117 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
import "@src/executors/UniswapV4Executor.sol";
import {Test} from "../../lib/forge-std/src/Test.sol";
import {Constants} from "../Constants.sol";
import {console} from "forge-std/console.sol";
contract UniswapV4ExecutorExposed is UniswapV4Executor {
constructor(IPoolManager _poolManager) UniswapV4Executor(_poolManager) {}
function decodeData(bytes calldata data)
external
pure
returns (
address tokenIn,
address tokenOut,
bool isExactInput,
uint256 amount
)
{
return _decodeData(data);
}
}
contract UniswapV4ExecutorTest is Test, Constants {
using SafeERC20 for IERC20;
UniswapV4ExecutorExposed uniswapV4Exposed;
IERC20 USDE = IERC20(USDE_ADDR);
IERC20 USDT = IERC20(USDT_ADDR);
address poolManager = 0x000000000004444c5dc75cB358380D2e3dE08A90;
function setUp() public {
uint256 forkBlock = 21817316;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
uniswapV4Exposed =
new UniswapV4ExecutorExposed(IPoolManager(poolManager));
}
function testDecodeParams() public view {
uint24 expectedPoolFee = 500;
uint128 expectedAmount = 100;
bytes memory data = _encodeExactInputSingle(
USDE_ADDR, USDT_ADDR, expectedPoolFee, false, 1, expectedAmount
);
(address tokenIn, address tokenOut, bool isExactInput, uint256 amount) =
uniswapV4Exposed.decodeData(data);
assertEq(tokenIn, USDE_ADDR);
assertEq(tokenOut, USDT_ADDR);
assertTrue(isExactInput);
assertEq(amount, expectedAmount);
}
function testSwap() public {
uint256 amountIn = 100 ether;
deal(USDE_ADDR, address(uniswapV4Exposed), amountIn);
uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager);
uint256 usdeBalanceBeforeSwapExecutor =
USDE.balanceOf(address(uniswapV4Exposed));
bytes memory data = _encodeExactInputSingle(
USDE_ADDR, USDT_ADDR, 100, true, 1, uint128(amountIn)
);
uint256 amountOut = uniswapV4Exposed.swap(amountIn, data);
assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn);
assertEq(
USDE.balanceOf(address(uniswapV4Exposed)),
usdeBalanceBeforeSwapExecutor - amountIn
);
assertTrue(USDT.balanceOf(address(uniswapV4Exposed)) == amountOut);
}
function _encodeExactInputSingle(
address tokenIn,
address tokenOut,
uint24 fee,
bool zeroForOne,
uint24 tickSpacing,
uint128 amountIn
) internal pure returns (bytes memory) {
PoolKey memory key = PoolKey({
currency0: Currency.wrap(zeroForOne ? tokenIn : tokenOut),
currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn),
fee: fee,
tickSpacing: int24(tickSpacing),
hooks: IHooks(address(0))
});
bytes memory actions = abi.encodePacked(
uint8(Actions.SWAP_EXACT_IN_SINGLE),
uint8(Actions.SETTLE_ALL),
uint8(Actions.TAKE_ALL)
);
bytes[] memory params = new bytes[](3);
params[0] = abi.encode(
IV4Router.ExactInputSingleParams({
poolKey: key,
zeroForOne: zeroForOne,
amountIn: amountIn,
amountOutMinimum: 0,
hookData: bytes("")
})
);
params[1] = abi.encode(key.currency0, amountIn);
params[2] = abi.encode(key.currency1, 0);
return abi.encode(actions, params);
}
}

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: Unlicense // SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.28; pragma solidity ^0.8.26;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";