feat: Single swap methods.

- Remove duplicate test case for single swp with permit2. This test came from the days when we could still have a 0 minimum amount.
This commit is contained in:
TAMARA LIPOWSKI
2025-03-16 00:15:55 -04:00
committed by Diana Carvalho
parent 65178681e0
commit 64345663d0
2 changed files with 187 additions and 10 deletions

View File

@@ -222,6 +222,117 @@ 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.
* - If `unwrapEth` is true, the contract converts the resulting WETH back into native ETH before sending it to the receiver.
* - 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 greater 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
* @param tokenOut The address of the output token. Use `address(0)` for native ETH
* @param minAmountOut The minimum acceptable amount of the output token. Reverts if this condition is not met. This should always be set to avoid losing funds due to slippage.
* @param wrapEth If true, wraps the input token (native ETH) 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 swapData Encoded swap details.
*
* @return amountOut The total amount of the output token received by the receiver, after deducting fees if applicable.
*/
function singleSwap(
uint256 amountIn,
address tokenIn,
address tokenOut,
uint256 minAmountOut,
bool wrapEth,
bool unwrapEth,
uint256 nTokens,
address receiver,
bytes calldata swapData
) public payable whenNotPaused nonReentrant returns (uint256 amountOut) {
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
return _singleSwap(
amountIn,
tokenIn,
tokenOut,
minAmountOut,
wrapEth,
unwrapEth,
nTokens,
receiver,
swapData
);
}
/**
* @notice Executes a single swap operation.
* This function enables 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.
* - 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 greater 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
* @param tokenOut The address of the output token. Use `address(0)` for native ETH
* @param minAmountOut The minimum acceptable amount of the output token. Reverts if this condition is not met. This should always be set to avoid losing funds due to slippage.
* @param wrapEth If true, wraps the input token (native ETH) 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 swapData Encoded swap details.
*
* @return amountOut The total amount of the output token received by the receiver, after deducting fees if applicable.
*/
function singleSwapPermit2(
uint256 amountIn,
address tokenIn,
address tokenOut,
uint256 minAmountOut,
bool wrapEth,
bool unwrapEth,
uint256 nTokens,
address receiver,
IAllowanceTransfer.PermitSingle calldata permitSingle,
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.
if (tokenIn != address(0)) {
permit2.permit(msg.sender, permitSingle, signature);
permit2.transferFrom(
msg.sender,
address(this),
uint160(amountIn),
permitSingle.details.token
);
}
return _singleSwap(
amountIn,
tokenIn,
tokenOut,
minAmountOut,
wrapEth,
unwrapEth,
nTokens,
receiver,
swapData
);
}
/**
* @notice Internal implementation of the core swap logic shared between splitSwap() and splitSwapPermit2().
*
@@ -291,6 +402,76 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
}
}
/**
* @notice Internal implementation of the core swap logic shared between singleSwap() and singleSwapPermit2().
*
* @notice This function centralizes the swap execution logic.
* @notice For detailed documentation on parameters and behavior, see the documentation for
* singleSwap() and singleSwapPermit2() functions.
*
*/
function _singleSwap(
uint256 amountIn,
address tokenIn,
address tokenOut,
uint256 minAmountOut,
bool wrapEth,
bool unwrapEth,
uint256 nTokens,
address receiver,
bytes calldata swap_
) internal returns (uint256 amountOut) {
if (receiver == address(0)) {
revert TychoRouter__AddressZero();
}
if (minAmountOut == 0) {
revert TychoRouter__UndefinedMinAmountOut();
}
// Assume funds are already in the router.
if (wrapEth) {
_wrapETH(amountIn);
tokenIn = address(_weth);
}
uint256 initialBalance = tokenIn == address(0)
? address(this).balance
: IERC20(tokenIn).balanceOf(address(this));
amountOut =
_callExecutor(swap_.executor(), amountIn, swap_.protocolData());
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 (fee > 0) {
uint256 feeAmount = (amountOut * fee) / 10000;
amountOut -= feeAmount;
IERC20(tokenOut).safeTransfer(feeReceiver, feeAmount);
}
if (amountOut < minAmountOut) {
revert TychoRouter__NegativeSlippage(amountOut, minAmountOut);
}
if (unwrapEth) {
_unwrapETH(amountOut);
}
if (tokenOut == address(0)) {
Address.sendValue(payable(receiver), amountOut);
} else {
IERC20(tokenOut).safeTransfer(receiver, amountOut);
}
}
/**
* @dev Executes sequential swaps as defined by the provided swap graph.
*

View File

@@ -366,7 +366,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
function testSplitSwapChecked() public {
function testSingleSwapChecked() public {
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
// Does permit2 token approval and transfer
// Checks amount out at the end
@@ -387,11 +387,9 @@ contract TychoRouterTest is TychoRouterTestSetup {
bytes memory swap = encodeSplitSwap(
uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
uint256 minAmountOut = 2600 * 1e18;
uint256 amountOut = tychoRouter.splitSwapPermit2(
uint256 amountOut = tychoRouter.singleSwapPermit2(
amountIn,
WETH_ADDR,
DAI_ADDR,
@@ -402,7 +400,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
ALICE,
permitSingle,
signature,
pleEncode(swaps)
swap
);
uint256 expectedAmount = 2659881924818443699787;
@@ -454,7 +452,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
vm.stopPrank();
}
function testSplitSwapCheckedNoPermit2() public {
function testSingleSwapCheckedNoPermit2() public {
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
// Checks amount out at the end
uint256 amountIn = 1 ether;
@@ -471,11 +469,9 @@ contract TychoRouterTest is TychoRouterTestSetup {
bytes memory swap = encodeSplitSwap(
uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
uint256 minAmountOut = 2600 * 1e18;
uint256 amountOut = tychoRouter.splitSwap(
uint256 amountOut = tychoRouter.singleSwap(
amountIn,
WETH_ADDR,
DAI_ADDR,
@@ -484,7 +480,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
false,
2,
ALICE,
pleEncode(swaps)
swap
);
uint256 expectedAmount = 2659881924818443699787;