Merge pull request #100 from propeller-heads/router/hr/ENG-4298-min-amount-out-transfer-from

feat: add safeTransferFrom in swap and move core swap logic inside _swapChecked
This commit is contained in:
dianacarvalho1
2025-03-05 17:06:46 +00:00
committed by GitHub
2 changed files with 141 additions and 70 deletions

View File

@@ -56,7 +56,6 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
// ✷✷✷✷✷✷ ✷✷✷✷✷ ✷✷✷✷✷✷✷✷ ✷✷✷✷✷✷ ✷✷✷✷✷✷ ✷✷✷✷✷✷✷✷
error TychoRouter__AddressZero();
error TychoRouter__AmountZero();
error TychoRouter__EmptySwaps();
error TychoRouter__NegativeSlippage(uint256 amount, uint256 minAmount);
error TychoRouter__AmountInDiffersFromConsumed(
@@ -144,6 +143,103 @@ 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 _swapChecked(
amountIn,
tokenIn,
tokenOut,
minAmountOut,
wrapEth,
unwrapEth,
nTokens,
receiver,
swaps
);
}
/**
* @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 `_swap` 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 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. If it's 0, no check is performed.
* @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 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 swapPermit2(
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 swaps
) 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 _swapChecked(
amountIn,
tokenIn,
tokenOut,
minAmountOut,
wrapEth,
unwrapEth,
nTokens,
receiver,
swaps
);
}
/**
* @notice Internal implementation of the core swap logic shared between swap() and swapPermit2().
*
* @notice This function centralizes the swap execution logic.
* @notice For detailed documentation on parameters and behavior, see the documentation for
* swap() and swapPermit2() functions.
*
*/
function _swapChecked(
uint256 amountIn,
address tokenIn,
address tokenOut,
uint256 minAmountOut,
bool wrapEth,
bool unwrapEth,
uint256 nTokens,
address receiver,
bytes calldata swaps
) internal returns (uint256 amountOut) {
if (receiver == address(0)) {
revert TychoRouter__AddressZero();
}
@@ -191,70 +287,6 @@ 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.
*
* @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 `_swap` 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 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. If it's 0, no check is performed.
* @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 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 swapPermit2(
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 swaps
) external payable whenNotPaused 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 swap(
amountIn,
tokenIn,
tokenOut,
minAmountOut,
wrapEth,
unwrapEth,
nTokens,
receiver,
swaps
);
}
/**
* @dev Executes sequential swaps as defined by the provided swap graph.
*

View File

@@ -420,10 +420,10 @@ contract TychoRouterTest is TychoRouterTestSetup {
// Checks amount out at the end
uint256 amountIn = 1 ether;
// Assume Alice has already transferred tokens to the TychoRouter
deal(WETH_ADDR, tychoRouterAddr, amountIn);
deal(WETH_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
// Approve the tokenIn to be transferred to the router
IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn);
bytes memory protocolData = encodeUniswapV2Swap(
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
@@ -457,7 +457,44 @@ contract TychoRouterTest is TychoRouterTestSetup {
vm.stopPrank();
}
function testSwapCheckedFailure() public {
function testSwapCheckedLessApprovalFailure() public {
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
// Fails while transferring the tokenIn to the router due to insufficient approval
uint256 amountIn = 1 ether;
deal(WETH_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
// Approve less than the amountIn
IERC20(WETH_ADDR).approve(address(tychoRouterAddr), amountIn - 1);
bytes memory protocolData = encodeUniswapV2Swap(
WETH_ADDR, WETH_DAI_POOL, tychoRouterAddr, false
);
bytes memory swap = encodeSwap(
uint8(0), uint8(1), uint24(0), address(usv2Executor), protocolData
);
bytes[] memory swaps = new bytes[](1);
swaps[0] = swap;
uint256 minAmountOut = 2600 * 1e18;
vm.expectRevert();
uint256 amountOut = tychoRouter.swap(
amountIn,
WETH_ADDR,
DAI_ADDR,
minAmountOut,
false,
false,
2,
ALICE,
pleEncode(swaps)
);
vm.stopPrank();
}
function testSwapCheckedNegativeSlippageFailure() public {
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
// Does permit2 token approval and transfer
// Checks amount out at the end and fails
@@ -760,7 +797,9 @@ contract TychoRouterTest is TychoRouterTestSetup {
// address with the USV2 executor address.
// Tests swapping WETH -> DAI on a USV2 pool without permit2
deal(WETH_ADDR, tychoRouterAddr, 1 ether);
deal(WETH_ADDR, ALICE, 1 ether);
vm.startPrank(ALICE);
IERC20(WETH_ADDR).approve(address(tychoRouterAddr), 1 ether);
uint256 balancerBefore = IERC20(DAI_ADDR).balanceOf(ALICE);
// Encoded solution generated using `test_split_swap_strategy_encoder_simple_route_no_permit2`
// but manually replacing the executor address