Merge branch 'main' into router/hr/ENG-4035-Balancer-V2-Executor

This commit is contained in:
Harsh Vardhan Roy
2025-01-28 22:43:02 +05:30
committed by GitHub
17 changed files with 898 additions and 107 deletions

View File

@@ -49,6 +49,7 @@ contract CallbackVerificationDispatcher {
// slither-disable-next-line dead-code
function _callVerifyCallback(bytes calldata data)
internal
view
returns (
uint256 amountOwed,
uint256 amountReceived,

View File

@@ -50,29 +50,21 @@ contract ExecutionDispatcher {
* @dev Calls an executor, assumes swap.protocolData contains
* protocol-specific data required by the executor.
*/
// slither-disable-next-line dead-code
function _callExecutor(uint256 amount, bytes calldata data)
internal
returns (uint256 calculatedAmount)
{
address executor;
bytes4 decodedSelector;
bytes memory protocolData;
(executor, decodedSelector, protocolData) =
_decodeExecutorAndSelector(data);
// slither-disable-next-line delegatecall-loop
function _callExecutor(
address executor,
bytes4 selector,
uint256 amount,
bytes calldata data
) internal returns (uint256 calculatedAmount) {
if (!executors[executor]) {
revert ExecutionDispatcher__UnapprovedExecutor();
}
bytes4 selector = decodedSelector == bytes4(0)
? IExecutor.swap.selector
: decodedSelector;
// slither-disable-next-line low-level-calls
selector = selector == bytes4(0) ? IExecutor.swap.selector : selector;
// slither-disable-next-line controlled-delegatecall,low-level-calls
(bool success, bytes memory result) = executor.delegatecall(
abi.encodeWithSelector(selector, amount, protocolData)
abi.encodeWithSelector(selector, amount, data)
);
if (!success) {
@@ -87,16 +79,4 @@ contract ExecutionDispatcher {
calculatedAmount = abi.decode(result, (uint256));
}
// slither-disable-next-line dead-code
function _decodeExecutorAndSelector(bytes calldata data)
internal
pure
returns (address executor, bytes4 selector, bytes memory protocolData)
{
require(data.length >= 24, "Invalid data length");
executor = address(uint160(bytes20(data[:20])));
selector = bytes4(data[20:24]);
protocolData = data[24:];
}
}

View File

@@ -1,26 +1,37 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
import "../lib/IWETH.sol";
import "../lib/bytes/LibPrefixLengthEncodedByteArray.sol";
import "./CallbackVerificationDispatcher.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
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 "@permit2/src/interfaces/IAllowanceTransfer.sol";
import "./ExecutionDispatcher.sol";
import "./CallbackVerificationDispatcher.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
import {LibSwap} from "../lib/LibSwap.sol";
error TychoRouter__WithdrawalFailed();
error TychoRouter__AddressZero();
error TychoRouter__NegativeSlippage(uint256 amount, uint256 minAmount);
error TychoRouter__MessageValueMismatch(uint256 value, uint256 amount);
contract TychoRouter is
AccessControl,
ExecutionDispatcher,
CallbackVerificationDispatcher,
Pausable
Pausable,
ReentrancyGuard
{
IAllowanceTransfer public immutable permit2;
IWETH private immutable _weth;
using SafeERC20 for IERC20;
using LibPrefixLengthEncodedByteArray for bytes;
using LibSwap for bytes;
//keccak256("NAME_OF_ROLE") : save gas on deployment
bytes32 public constant EXECUTOR_SETTER_ROLE =
@@ -48,9 +59,10 @@ contract TychoRouter is
);
event FeeSet(uint256 indexed oldFee, uint256 indexed newFee);
constructor(address _permit2) {
constructor(address _permit2, address weth) {
permit2 = IAllowanceTransfer(_permit2);
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_weth = IWETH(weth);
}
/**
@@ -77,22 +89,116 @@ contract TychoRouter is
}
/**
* @dev Executes a swap graph supporting internal splits token amount
* splits, checking that the user gets more than minUserAmount of buyToken.
* @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
* against a user-specified minimum.
*
* @dev
* - If `wrapEth` is true, the contract wraps the provided native ETH into WETH and uses it as the sell token.
* - If `unwrapEth` is true, the contract converts the resulting WETH back into native ETH before sending it to the receiver.
* - For ERC20 tokens, Permit2 is used to approve and transfer tokens from the caller to the router.
* - Swaps are executed sequentially using the `_splitSwap` function.
* - A fee is deducted from the output token if `fee > 0`, and the remaining amount is sent to the receiver.
* - Reverts with `TychoRouter__NegativeSlippage` if the output amount is less than `minAmountOut` and `minAmountOut` is bigger than 0.
*
* @param amountIn The input token amount to be swapped.
* @param tokenIn The address of the input token. Use `address(0)` for native ETH when `wrapEth` is true.
* @param tokenOut The address of the output token. Use `address(0)` for native ETH when `unwrapEth` is true.
* @param minAmountOut The minimum acceptable amount of the output token. Reverts if this condition is not met. If it's 0, no check is performed.
* @param wrapEth If true, treats the input token as native ETH and wraps it into WETH.
* @param unwrapEth If true, unwraps the resulting WETH into native ETH and sends it to the receiver.
* @param nTokens The total number of tokens involved in the swap graph (used to initialize arrays for internal calculations).
* @param receiver The address to receive the output tokens.
* @param permitSingle A Permit2 structure containing token approval details for the input token. Ignored if `wrapEth` is true.
* @param signature A valid signature authorizing the Permit2 approval. Ignored if `wrapEth` is true.
* @param swaps Encoded swap graph data containing details of each swap.
*
* @return amountOut The total amount of the output token received by the receiver, after deducting fees if applicable.
*/
function swap(
uint256 amountIn,
address tokenIn,
uint256 minUserAmount,
address tokenOut,
uint256 minAmountOut,
bool wrapEth,
bool unwrapEth,
uint256 nTokens,
bytes calldata swaps,
address receiver,
IAllowanceTransfer.PermitSingle calldata permitSingle,
bytes calldata signature
) external whenNotPaused returns (uint256 amountOut) {
amountOut = 0;
// TODO
bytes calldata signature,
bytes calldata swaps
) external payable whenNotPaused nonReentrant returns (uint256 amountOut) {
require(receiver != address(0), "Invalid receiver address");
// For native ETH, assume funds already in our router. Else, transfer and handle approval.
if (wrapEth) {
_wrapETH(amountIn);
} else if (tokenIn != address(0)) {
permit2.permit(msg.sender, permitSingle, signature);
permit2.transferFrom(
msg.sender,
address(this),
uint160(amountIn),
permitSingle.details.token
);
}
amountOut = _swap(amountIn, nTokens, swaps);
if (fee > 0) {
uint256 feeAmount = (amountOut * fee) / 10000;
amountOut -= feeAmount;
IERC20(tokenOut).safeTransfer(feeReceiver, feeAmount);
}
if (minAmountOut > 0 && amountOut < minAmountOut) {
revert TychoRouter__NegativeSlippage(amountOut, minAmountOut);
}
if (unwrapEth) {
_unwrapETH(amountOut);
// slither-disable-next-line arbitrary-send-eth
payable(receiver).transfer(amountOut);
} else {
IERC20(tokenOut).safeTransfer(receiver, amountOut);
}
}
function _swap(uint256 amountIn, uint256 nTokens, bytes calldata swaps_)
internal
returns (uint256)
{
uint256 currentAmountIn;
uint256 currentAmountOut;
uint8 tokenInIndex;
uint8 tokenOutIndex;
uint24 split;
bytes calldata swapData;
uint256[] memory remainingAmounts = new uint256[](nTokens);
uint256[] memory amounts = new uint256[](nTokens);
amounts[0] = amountIn;
remainingAmounts[0] = amountIn;
while (swaps_.length > 0) {
(swapData, swaps_) = swaps_.next();
tokenInIndex = swapData.tokenInIndex();
tokenOutIndex = swapData.tokenOutIndex();
split = swapData.splitPercentage();
currentAmountIn = split > 0
? (amounts[tokenInIndex] * split) / 0xffffff
: remainingAmounts[tokenInIndex];
currentAmountOut = _callExecutor(
swapData.executor(),
swapData.executorSelector(),
currentAmountIn,
swapData.protocolData()
);
amounts[tokenOutIndex] += currentAmountOut;
remainingAmounts[tokenOutIndex] += currentAmountOut;
remainingAmounts[tokenInIndex] -= currentAmountIn;
}
return amounts[tokenOutIndex];
}
/**
@@ -209,6 +315,27 @@ contract TychoRouter is
}
}
/**
* @dev Wraps a defined amount of ETH.
* @param amount of native ETH to wrap.
*/
function _wrapETH(uint256 amount) internal {
if (msg.value > 0 && msg.value != amount) {
revert TychoRouter__MessageValueMismatch(msg.value, amount);
}
_weth.deposit{value: amount}();
}
/**
* @dev Unwraps a defined amount of WETH.
* @param amount of WETH to unwrap.
*/
function _unwrapETH(uint256 amount) internal {
uint256 unwrapAmount =
amount == 0 ? _weth.balanceOf(address(this)) : amount;
_weth.withdraw(unwrapAmount);
}
/**
* @dev Allows this contract to receive native token
*/

View File

@@ -10,8 +10,10 @@ error UniswapV2Executor__InvalidDataLength();
contract UniswapV2Executor is IExecutor {
using SafeERC20 for IERC20;
// slither-disable-next-line locked-ether
function swap(uint256 givenAmount, bytes calldata data)
external
payable
returns (uint256 calculatedAmount)
{
address target;