feat: (WIP) Support selection of transfer into router

- For protocols like Balancer and Curve, which expect funds to be in the router at the time of swap, we must support also transferring funds from the user into the router. Doing this in the router would mean we are dealing with transfers in two different places: in the router main methods and in the executors. To avoid this, we are now performing transfers just in the executors, and two transfer types have been added to support transfers into the router.

TODO:
- Add this for Balancer and Curve (only added for USV4 atm).
- TODO consider renaming TRANSFER_FROM and TRANSFER_PERMIT2 to include "pool" in the name
This commit is contained in:
TAMARA LIPOWSKI
2025-04-11 23:25:45 -04:00
committed by Diana Carvalho
parent 59a80dc392
commit a301a1cef3
19 changed files with 426 additions and 240 deletions

View File

@@ -99,7 +99,7 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
/**
* @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. This function performs a transferFrom to retrieve tokens from the caller.
* 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.
@@ -130,11 +130,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
address receiver,
bytes calldata swaps
) public payable whenNotPaused nonReentrant returns (uint256 amountOut) {
if (address(tokenIn) != address(0)) {
IERC20(tokenIn).safeTransferFrom(
msg.sender, address(this), amountIn
);
}
return _splitSwapChecked(
amountIn,
tokenIn,
@@ -187,15 +182,9 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
bytes calldata signature,
bytes calldata swaps
) external payable whenNotPaused nonReentrant returns (uint256 amountOut) {
// For native ETH, assume funds already in our router. Else, transfer and handle approval.
// For native ETH, assume funds already in our router. Else, handle approval.
if (tokenIn != address(0)) {
permit2.permit(msg.sender, permitSingle, signature);
permit2.transferFrom(
msg.sender,
address(this),
uint160(amountIn),
permitSingle.details.token
);
}
return _splitSwapChecked(
@@ -214,7 +203,7 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
/**
* @notice Executes a swap operation based on a predefined swap graph with no split routes.
* This function enables multi-step swaps, optional ETH wrapping/unwrapping, and validates the output amount
* against a user-specified minimum. This function performs a transferFrom to retrieve tokens from the caller.
* 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.
@@ -243,7 +232,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
address receiver,
bytes calldata swaps
) public payable whenNotPaused nonReentrant returns (uint256 amountOut) {
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
return _sequentialSwapChecked(
amountIn,
tokenIn,
@@ -292,15 +280,9 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
bytes calldata signature,
bytes calldata swaps
) external payable whenNotPaused nonReentrant returns (uint256 amountOut) {
// For native ETH, assume funds already in our router. Else, transfer and handle approval.
// For native ETH, assume funds already in our router. Else, handle approval.
if (tokenIn != address(0)) {
permit2.permit(msg.sender, permitSingle, signature);
permit2.transferFrom(
msg.sender,
address(this),
uint160(amountIn),
permitSingle.details.token
);
}
return _sequentialSwapChecked(
@@ -318,7 +300,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
/**
* @notice Executes a single swap operation.
* This function enables optional ETH wrapping/unwrapping, and validates the output amount against a user-specified minimum.
* This function performs a transferFrom to retrieve tokens from the caller.
*
* @dev
* - If `wrapEth` is true, the contract wraps the provided native ETH into WETH and uses it as the sell token.
@@ -346,7 +327,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
address receiver,
bytes calldata swapData
) public payable whenNotPaused nonReentrant returns (uint256 amountOut) {
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
return _singleSwap(
amountIn,
tokenIn,
@@ -395,15 +375,9 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
bytes calldata signature,
bytes calldata swapData
) external payable whenNotPaused nonReentrant returns (uint256 amountOut) {
// For native ETH, assume funds already in our router. Else, transfer and handle approval.
// For native ETH, assume funds already in our router. Else, handle approval.
if (tokenIn != address(0)) {
permit2.permit(msg.sender, permitSingle, signature);
permit2.transferFrom(
msg.sender,
address(this),
uint160(amountIn),
permitSingle.details.token
);
}
return _singleSwap(
@@ -449,23 +423,7 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
_wrapETH(amountIn);
tokenIn = address(_weth);
}
uint256 initialBalance = tokenIn == address(0)
? address(this).balance
: IERC20(tokenIn).balanceOf(address(this));
amountOut = _splitSwap(amountIn, nTokens, swaps);
uint256 currentBalance = tokenIn == address(0)
? address(this).balance
: IERC20(tokenIn).balanceOf(address(this));
uint256 amountConsumed = initialBalance - currentBalance;
if (tokenIn != tokenOut && amountConsumed != amountIn) {
revert TychoRouter__AmountInDiffersFromConsumed(
amountIn, amountConsumed
);
}
if (amountOut < minAmountOut) {
revert TychoRouter__NegativeSlippage(amountOut, minAmountOut);
@@ -512,25 +470,10 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
tokenIn = address(_weth);
}
uint256 initialBalance = tokenIn == address(0)
? address(this).balance
: IERC20(tokenIn).balanceOf(address(this));
(address executor, bytes calldata protocolData) =
swap_.decodeSingleSwap();
amountOut = _callExecutor(executor, amountIn, protocolData);
uint256 currentBalance = tokenIn == address(0)
? address(this).balance
: IERC20(tokenIn).balanceOf(address(this));
uint256 amountConsumed = initialBalance - currentBalance;
if (amountConsumed != amountIn) {
revert TychoRouter__AmountInDiffersFromConsumed(
amountIn, amountConsumed
);
}
if (amountOut < minAmountOut) {
revert TychoRouter__NegativeSlippage(amountOut, minAmountOut);
@@ -577,10 +520,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
tokenIn = address(_weth);
}
uint256 initialBalance = tokenIn == address(0)
? address(this).balance
: IERC20(tokenIn).balanceOf(address(this));
amountOut = _sequentialSwap(amountIn, swaps);
uint256 currentBalance = tokenIn == address(0)