From 805cff7068f39b4253a34f7ac095841be0053aff Mon Sep 17 00:00:00 2001 From: Denis Fadeev Date: Mon, 24 Mar 2025 11:30:37 +0300 Subject: [PATCH 1/3] wip --- examples/swap/contracts/Swap.sol | 205 ++++++++++++++++++++++-------- examples/swap/package.json | 4 +- examples/swap/scripts/localnet.sh | 4 +- examples/swap/tasks/deploy.ts | 13 +- 4 files changed, 168 insertions(+), 58 deletions(-) diff --git a/examples/swap/contracts/Swap.sol b/examples/swap/contracts/Swap.sol index 3ab18e11..ed0a143d 100644 --- a/examples/swap/contracts/Swap.sol +++ b/examples/swap/contracts/Swap.sol @@ -5,7 +5,9 @@ import {SystemContract, IZRC20} from "@zetachain/toolkit/contracts/SystemContrac import {SwapHelperLib} from "@zetachain/toolkit/contracts/SwapHelperLib.sol"; import {BytesHelperLib} from "@zetachain/toolkit/contracts/BytesHelperLib.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; import {RevertContext, RevertOptions} from "@zetachain/protocol-contracts/contracts/Revert.sol"; import "@zetachain/protocol-contracts/contracts/zevm/interfaces/UniversalContract.sol"; @@ -24,10 +26,12 @@ contract Swap is OwnableUpgradeable { address public uniswapRouter; + address public wzeta; GatewayZEVM public gateway; uint256 constant BITCOIN = 8332; uint256 constant BITCOIN_TESTNET = 18334; uint256 public gasLimit; + uint24 public constant POOL_FEE = 3000; // 0.3% fee tier error InvalidAddress(); error Unauthorized(); @@ -58,13 +62,18 @@ contract Swap is address payable gatewayAddress, address uniswapRouterAddress, uint256 gasLimitAmount, - address owner + address owner, + address wzetaAddress ) public initializer { - if (gatewayAddress == address(0) || uniswapRouterAddress == address(0)) - revert InvalidAddress(); + if ( + gatewayAddress == address(0) || + uniswapRouterAddress == address(0) || + wzetaAddress == address(0) + ) revert InvalidAddress(); __UUPSUpgradeable_init(); __Ownable_init(owner); uniswapRouter = uniswapRouterAddress; + wzeta = wzetaAddress; gateway = GatewayZEVM(gatewayAddress); gasLimit = gasLimitAmount; } @@ -191,36 +200,162 @@ contract Swap is if (withdraw) { (gasZRC20, gasFee) = IZRC20(targetToken).withdrawGasFee(); - uint256 minInput = quoteMinInput(inputToken, targetToken); - if (amount < minInput) { - revert InsufficientAmount( - "The input amount is less than the min amount required to cover the withdraw gas fee" - ); - } if (gasZRC20 == inputToken) { + if (amount < gasFee) { + revert InsufficientAmount( + "The input amount is less than the gas fee required for withdrawal" + ); + } swapAmount = amount - gasFee; } else { - inputForGas = SwapHelperLib.swapTokensForExactTokens( - uniswapRouter, + inputForGas = swapTokensForExactTokens( inputToken, gasFee, - gasZRC20, - amount + gasZRC20 ); + if (amount < inputForGas) { + revert InsufficientAmount( + "The input amount is less than the amount required to cover the gas fee" + ); + } swapAmount = amount - inputForGas; } } - uint256 out = SwapHelperLib.swapExactTokensForTokens( - uniswapRouter, + uint256 out = swapExactTokensForTokens( inputToken, swapAmount, - targetToken, - 0 + targetToken ); return (out, gasZRC20, gasFee); } + /** + * @notice Swap exact tokens for tokens using Uniswap V3 + */ + function swapExactTokensForTokens( + address inputToken, + uint256 amountIn, + address outputToken + ) internal returns (uint256) { + // Approve router to spend input tokens + IERC20(inputToken).approve(uniswapRouter, amountIn); + + // Try direct swap first + try + ISwapRouter(uniswapRouter).exactInputSingle( + ISwapRouter.ExactInputSingleParams({ + tokenIn: inputToken, + tokenOut: outputToken, + fee: POOL_FEE, + recipient: address(this), + deadline: block.timestamp + 15 minutes, + amountIn: amountIn, + amountOutMinimum: 0, // Let Uniswap handle slippage + sqrtPriceLimitX96: 0 + }) + ) + returns (uint256 amountOut) { + return amountOut; + } catch { + // If direct swap fails, try through WZETA + // First swap: inputToken -> WZETA + uint256 wzetaAmount = ISwapRouter(uniswapRouter).exactInputSingle( + ISwapRouter.ExactInputSingleParams({ + tokenIn: inputToken, + tokenOut: wzeta, + fee: POOL_FEE, + recipient: address(this), + deadline: block.timestamp + 15 minutes, + amountIn: amountIn, + amountOutMinimum: 0, // Let Uniswap handle slippage + sqrtPriceLimitX96: 0 + }) + ); + + // Approve router to spend WZETA + IERC20(wzeta).approve(uniswapRouter, wzetaAmount); + + // Second swap: WZETA -> outputToken + return + ISwapRouter(uniswapRouter).exactInputSingle( + ISwapRouter.ExactInputSingleParams({ + tokenIn: wzeta, + tokenOut: outputToken, + fee: POOL_FEE, + recipient: address(this), + deadline: block.timestamp + 15 minutes, + amountIn: wzetaAmount, + amountOutMinimum: 0, // Let Uniswap handle slippage + sqrtPriceLimitX96: 0 + }) + ); + } + } + + /** + * @notice Swap tokens for exact tokens using Uniswap V3 + */ + function swapTokensForExactTokens( + address inputToken, + uint256 amountOut, + address outputToken + ) internal returns (uint256) { + // Approve router to spend input tokens + IERC20(inputToken).approve(uniswapRouter, type(uint256).max); + + // Try direct swap first + try + ISwapRouter(uniswapRouter).exactOutputSingle( + ISwapRouter.ExactOutputSingleParams({ + tokenIn: inputToken, + tokenOut: outputToken, + fee: POOL_FEE, + recipient: address(this), + deadline: block.timestamp + 15 minutes, + amountOut: amountOut, + amountInMaximum: type(uint256).max, // Let Uniswap handle slippage + sqrtPriceLimitX96: 0 + }) + ) + returns (uint256 amountIn) { + return amountIn; + } catch { + // If direct swap fails, try through WZETA + // First swap: inputToken -> WZETA + uint256 wzetaAmount = ISwapRouter(uniswapRouter).exactOutputSingle( + ISwapRouter.ExactOutputSingleParams({ + tokenIn: inputToken, + tokenOut: wzeta, + fee: POOL_FEE, + recipient: address(this), + deadline: block.timestamp + 15 minutes, + amountOut: amountOut, + amountInMaximum: type(uint256).max, // Let Uniswap handle slippage + sqrtPriceLimitX96: 0 + }) + ); + + // Approve router to spend WZETA + IERC20(wzeta).approve(uniswapRouter, wzetaAmount); + + // Second swap: WZETA -> outputToken + return + ISwapRouter(uniswapRouter).exactOutputSingle( + ISwapRouter.ExactOutputSingleParams({ + tokenIn: wzeta, + tokenOut: outputToken, + fee: POOL_FEE, + recipient: address(this), + deadline: block.timestamp + 15 minutes, + amountOut: amountOut, + amountInMaximum: type(uint256).max, // Let Uniswap handle slippage + sqrtPriceLimitX96: 0 + }) + ); + } + } + /** * @notice Transfer tokens to the recipient on ZetaChain or withdraw to a connected chain */ @@ -300,40 +435,6 @@ contract Swap is ); } - /** - * @notice Returns the minimum amount of input tokens required to cover the gas fee for withdrawal - */ - function quoteMinInput( - address inputToken, - address targetToken - ) public view returns (uint256) { - (address gasZRC20, uint256 gasFee) = IZRC20(targetToken) - .withdrawGasFee(); - - if (inputToken == gasZRC20) { - return gasFee; - } - - address zeta = IUniswapV2Router01(uniswapRouter).WETH(); - - address[] memory path; - if (inputToken == zeta || gasZRC20 == zeta) { - path = new address[](2); - path[0] = inputToken; - path[1] = gasZRC20; - } else { - path = new address[](3); - path[0] = inputToken; - path[1] = zeta; - path[2] = gasZRC20; - } - - uint256[] memory amountsIn = IUniswapV2Router02(uniswapRouter) - .getAmountsIn(gasFee, path); - - return amountsIn[0]; - } - function _authorizeUpgrade( address newImplementation ) internal override onlyOwner {} diff --git a/examples/swap/package.json b/examples/swap/package.json index b88c083e..fe4aff7d 100644 --- a/examples/swap/package.json +++ b/examples/swap/package.json @@ -57,7 +57,9 @@ "@solana-developers/helpers": "^2.4.0", "@solana/spl-memo": "^0.2.5", "@solana/web3.js": "^1.95.8", + "@uniswap/v3-core": "^1.0.1", + "@uniswap/v3-periphery": "^1.4.4", "@zetachain/protocol-contracts": "12.0.0-rc1", "@zetachain/toolkit": "13.0.0-rc17" } -} +} \ No newline at end of file diff --git a/examples/swap/scripts/localnet.sh b/examples/swap/scripts/localnet.sh index 37af70af..eec0e057 100755 --- a/examples/swap/scripts/localnet.sh +++ b/examples/swap/scripts/localnet.sh @@ -17,11 +17,11 @@ ZRC20_SOL=$(jq -r '.addresses[] | select(.type=="ZRC-20 SOL on 901") | .address' WZETA=$(jq -r '.addresses[] | select(.type=="wzeta" and .chain=="zetachain") | .address' localnet.json) GATEWAY_ETHEREUM=$(jq -r '.addresses[] | select(.type=="gatewayEVM" and .chain=="ethereum") | .address' localnet.json) GATEWAY_ZETACHAIN=$(jq -r '.addresses[] | select(.type=="gatewayZEVM" and .chain=="zetachain") | .address' localnet.json) -UNISWAP_ROUTER=$(jq -r '.addresses[] | select(.type=="uniswapRouterInstance" and .chain=="zetachain") | .address' localnet.json) +UNISWAP_ROUTER=$(jq -r '.addresses[] | select(.type=="uniswapV3Router" and .chain=="zetachain") | .address' localnet.json) SENDER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 DEFAULT_MNEMONIC="grape subway rack mean march bubble carry avoid muffin consider thing street" -CONTRACT_SWAP=$(npx hardhat deploy --name Swap --network localhost --gateway "$GATEWAY_ZETACHAIN" --uniswap-router "$UNISWAP_ROUTER" | jq -r '.contractAddress') +CONTRACT_SWAP=$(npx hardhat deploy --name Swap --network localhost --gateway "$GATEWAY_ZETACHAIN" --uniswap-router "$UNISWAP_ROUTER" --wzeta "$WZETA" | jq -r '.contractAddress') COMPANION=$(npx hardhat deploy-companion --gateway "$GATEWAY_ETHEREUM" --network localhost --json | jq -r '.contractAddress') npx hardhat evm-swap \ diff --git a/examples/swap/tasks/deploy.ts b/examples/swap/tasks/deploy.ts index 1a4c680c..0e184a19 100644 --- a/examples/swap/tasks/deploy.ts +++ b/examples/swap/tasks/deploy.ts @@ -15,7 +15,13 @@ const main = async (args: any, hre: HardhatRuntimeEnvironment) => { const contract = await hre.upgrades.deployProxy( factory as any, - [args.gateway, args.uniswapRouter, args.gasLimit, signer.address], + [ + args.gateway, + args.uniswapRouter, + args.gasLimit, + signer.address, + args.wzeta, + ], { kind: "uups" } ); @@ -30,7 +36,7 @@ const main = async (args: any, hre: HardhatRuntimeEnvironment) => { task("deploy", "Deploy the contract", main) .addOptionalParam("name", "Contract to deploy", "Swap") - .addOptionalParam("uniswapRouter", "Uniswap v2 Router address") + .addOptionalParam("uniswapRouter", "Uniswap v3 Router address") .addOptionalParam( "gateway", "Gateway address (default: ZetaChain Gateway)", @@ -41,4 +47,5 @@ task("deploy", "Deploy the contract", main) "Gas limit for the transaction", 1000000, types.int - ); + ) + .addOptionalParam("wzeta", "WZETA token address"); From 98f8810bf29aa1cdd04fc0a4026491ebaa17fe35 Mon Sep 17 00:00:00 2001 From: Denis Fadeev Date: Tue, 25 Mar 2025 10:48:46 +0300 Subject: [PATCH 2/3] fix --- examples/swap/contracts/Swap.sol | 88 ++++++++++++-------------------- 1 file changed, 32 insertions(+), 56 deletions(-) diff --git a/examples/swap/contracts/Swap.sol b/examples/swap/contracts/Swap.sol index ed0a143d..a76da196 100644 --- a/examples/swap/contracts/Swap.sol +++ b/examples/swap/contracts/Swap.sol @@ -258,38 +258,26 @@ contract Swap is returns (uint256 amountOut) { return amountOut; } catch { - // If direct swap fails, try through WZETA - // First swap: inputToken -> WZETA - uint256 wzetaAmount = ISwapRouter(uniswapRouter).exactInputSingle( - ISwapRouter.ExactInputSingleParams({ - tokenIn: inputToken, - tokenOut: wzeta, - fee: POOL_FEE, + // If direct swap fails, try through WZETA using exactInput for multi-hop + // The path is encoded as (tokenIn, fee, WZETA, fee, tokenOut) + bytes memory path = abi.encodePacked( + inputToken, + POOL_FEE, + wzeta, + POOL_FEE, + outputToken + ); + + ISwapRouter.ExactInputParams memory params = ISwapRouter + .ExactInputParams({ + path: path, recipient: address(this), deadline: block.timestamp + 15 minutes, amountIn: amountIn, - amountOutMinimum: 0, // Let Uniswap handle slippage - sqrtPriceLimitX96: 0 - }) - ); + amountOutMinimum: 0 // Let Uniswap handle slippage + }); - // Approve router to spend WZETA - IERC20(wzeta).approve(uniswapRouter, wzetaAmount); - - // Second swap: WZETA -> outputToken - return - ISwapRouter(uniswapRouter).exactInputSingle( - ISwapRouter.ExactInputSingleParams({ - tokenIn: wzeta, - tokenOut: outputToken, - fee: POOL_FEE, - recipient: address(this), - deadline: block.timestamp + 15 minutes, - amountIn: wzetaAmount, - amountOutMinimum: 0, // Let Uniswap handle slippage - sqrtPriceLimitX96: 0 - }) - ); + return ISwapRouter(uniswapRouter).exactInput(params); } } @@ -321,38 +309,26 @@ contract Swap is returns (uint256 amountIn) { return amountIn; } catch { - // If direct swap fails, try through WZETA - // First swap: inputToken -> WZETA - uint256 wzetaAmount = ISwapRouter(uniswapRouter).exactOutputSingle( - ISwapRouter.ExactOutputSingleParams({ - tokenIn: inputToken, - tokenOut: wzeta, - fee: POOL_FEE, + // If direct swap fails, try through WZETA using exactOutput for multi-hop + // The path is encoded as (tokenOut, fee, WZETA, fee, tokenIn) in reverse order + bytes memory path = abi.encodePacked( + outputToken, + POOL_FEE, + wzeta, + POOL_FEE, + inputToken + ); + + ISwapRouter.ExactOutputParams memory params = ISwapRouter + .ExactOutputParams({ + path: path, recipient: address(this), deadline: block.timestamp + 15 minutes, amountOut: amountOut, - amountInMaximum: type(uint256).max, // Let Uniswap handle slippage - sqrtPriceLimitX96: 0 - }) - ); + amountInMaximum: type(uint256).max // Let Uniswap handle slippage + }); - // Approve router to spend WZETA - IERC20(wzeta).approve(uniswapRouter, wzetaAmount); - - // Second swap: WZETA -> outputToken - return - ISwapRouter(uniswapRouter).exactOutputSingle( - ISwapRouter.ExactOutputSingleParams({ - tokenIn: wzeta, - tokenOut: outputToken, - fee: POOL_FEE, - recipient: address(this), - deadline: block.timestamp + 15 minutes, - amountOut: amountOut, - amountInMaximum: type(uint256).max, // Let Uniswap handle slippage - sqrtPriceLimitX96: 0 - }) - ); + return ISwapRouter(uniswapRouter).exactOutput(params); } } From 1d76710b1d270c86f3a6e2ed78f5f22a97ed4dc4 Mon Sep 17 00:00:00 2001 From: Denis Fadeev Date: Tue, 25 Mar 2025 11:22:10 +0300 Subject: [PATCH 3/3] read wzeta address from gateway --- examples/swap/contracts/Swap.sol | 127 +++--------------------- examples/swap/contracts/SwapLibrary.sol | 115 +++++++++++++++++++++ examples/swap/scripts/localnet.sh | 2 +- examples/swap/tasks/deploy.ts | 11 +- 4 files changed, 131 insertions(+), 124 deletions(-) create mode 100644 examples/swap/contracts/SwapLibrary.sol diff --git a/examples/swap/contracts/Swap.sol b/examples/swap/contracts/Swap.sol index a76da196..76dad538 100644 --- a/examples/swap/contracts/Swap.sol +++ b/examples/swap/contracts/Swap.sol @@ -18,6 +18,7 @@ import {GatewayZEVM} from "@zetachain/protocol-contracts/contracts/zevm/GatewayZ import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {SwapLibrary} from "./SwapLibrary.sol"; contract Swap is UniversalContract, @@ -62,19 +63,15 @@ contract Swap is address payable gatewayAddress, address uniswapRouterAddress, uint256 gasLimitAmount, - address owner, - address wzetaAddress + address owner ) public initializer { - if ( - gatewayAddress == address(0) || - uniswapRouterAddress == address(0) || - wzetaAddress == address(0) - ) revert InvalidAddress(); + if (gatewayAddress == address(0) || uniswapRouterAddress == address(0)) + revert InvalidAddress(); __UUPSUpgradeable_init(); __Ownable_init(owner); uniswapRouter = uniswapRouterAddress; - wzeta = wzetaAddress; gateway = GatewayZEVM(gatewayAddress); + wzeta = gateway.zetaToken(); gasLimit = gasLimitAmount; } @@ -208,10 +205,12 @@ contract Swap is } swapAmount = amount - gasFee; } else { - inputForGas = swapTokensForExactTokens( + inputForGas = SwapLibrary.swapTokensForExactTokens( inputToken, gasFee, - gasZRC20 + gasZRC20, + uniswapRouter, + wzeta ); if (amount < inputForGas) { revert InsufficientAmount( @@ -222,116 +221,16 @@ contract Swap is } } - uint256 out = swapExactTokensForTokens( + uint256 out = SwapLibrary.swapExactTokensForTokens( inputToken, swapAmount, - targetToken + targetToken, + uniswapRouter, + wzeta ); return (out, gasZRC20, gasFee); } - /** - * @notice Swap exact tokens for tokens using Uniswap V3 - */ - function swapExactTokensForTokens( - address inputToken, - uint256 amountIn, - address outputToken - ) internal returns (uint256) { - // Approve router to spend input tokens - IERC20(inputToken).approve(uniswapRouter, amountIn); - - // Try direct swap first - try - ISwapRouter(uniswapRouter).exactInputSingle( - ISwapRouter.ExactInputSingleParams({ - tokenIn: inputToken, - tokenOut: outputToken, - fee: POOL_FEE, - recipient: address(this), - deadline: block.timestamp + 15 minutes, - amountIn: amountIn, - amountOutMinimum: 0, // Let Uniswap handle slippage - sqrtPriceLimitX96: 0 - }) - ) - returns (uint256 amountOut) { - return amountOut; - } catch { - // If direct swap fails, try through WZETA using exactInput for multi-hop - // The path is encoded as (tokenIn, fee, WZETA, fee, tokenOut) - bytes memory path = abi.encodePacked( - inputToken, - POOL_FEE, - wzeta, - POOL_FEE, - outputToken - ); - - ISwapRouter.ExactInputParams memory params = ISwapRouter - .ExactInputParams({ - path: path, - recipient: address(this), - deadline: block.timestamp + 15 minutes, - amountIn: amountIn, - amountOutMinimum: 0 // Let Uniswap handle slippage - }); - - return ISwapRouter(uniswapRouter).exactInput(params); - } - } - - /** - * @notice Swap tokens for exact tokens using Uniswap V3 - */ - function swapTokensForExactTokens( - address inputToken, - uint256 amountOut, - address outputToken - ) internal returns (uint256) { - // Approve router to spend input tokens - IERC20(inputToken).approve(uniswapRouter, type(uint256).max); - - // Try direct swap first - try - ISwapRouter(uniswapRouter).exactOutputSingle( - ISwapRouter.ExactOutputSingleParams({ - tokenIn: inputToken, - tokenOut: outputToken, - fee: POOL_FEE, - recipient: address(this), - deadline: block.timestamp + 15 minutes, - amountOut: amountOut, - amountInMaximum: type(uint256).max, // Let Uniswap handle slippage - sqrtPriceLimitX96: 0 - }) - ) - returns (uint256 amountIn) { - return amountIn; - } catch { - // If direct swap fails, try through WZETA using exactOutput for multi-hop - // The path is encoded as (tokenOut, fee, WZETA, fee, tokenIn) in reverse order - bytes memory path = abi.encodePacked( - outputToken, - POOL_FEE, - wzeta, - POOL_FEE, - inputToken - ); - - ISwapRouter.ExactOutputParams memory params = ISwapRouter - .ExactOutputParams({ - path: path, - recipient: address(this), - deadline: block.timestamp + 15 minutes, - amountOut: amountOut, - amountInMaximum: type(uint256).max // Let Uniswap handle slippage - }); - - return ISwapRouter(uniswapRouter).exactOutput(params); - } - } - /** * @notice Transfer tokens to the recipient on ZetaChain or withdraw to a connected chain */ diff --git a/examples/swap/contracts/SwapLibrary.sol b/examples/swap/contracts/SwapLibrary.sol new file mode 100644 index 00000000..7f5d97e3 --- /dev/null +++ b/examples/swap/contracts/SwapLibrary.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; + +library SwapLibrary { + uint24 constant POOL_FEE = 3000; // 0.3% fee tier + + /** + * @notice Swap exact tokens for tokens using Uniswap V3 + */ + function swapExactTokensForTokens( + address inputToken, + uint256 amountIn, + address outputToken, + address uniswapRouter, + address wzeta + ) internal returns (uint256) { + // Approve router to spend input tokens + IERC20(inputToken).approve(uniswapRouter, amountIn); + + // Try direct swap first + try + ISwapRouter(uniswapRouter).exactInputSingle( + ISwapRouter.ExactInputSingleParams({ + tokenIn: inputToken, + tokenOut: outputToken, + fee: POOL_FEE, + recipient: address(this), + deadline: block.timestamp + 15 minutes, + amountIn: amountIn, + amountOutMinimum: 0, // Let Uniswap handle slippage + sqrtPriceLimitX96: 0 + }) + ) + returns (uint256 amountOut) { + return amountOut; + } catch { + // If direct swap fails, try through WZETA using exactInput for multi-hop + // The path is encoded as (tokenIn, fee, WZETA, fee, tokenOut) + bytes memory path = abi.encodePacked( + inputToken, + POOL_FEE, + wzeta, + POOL_FEE, + outputToken + ); + + ISwapRouter.ExactInputParams memory params = ISwapRouter + .ExactInputParams({ + path: path, + recipient: address(this), + deadline: block.timestamp + 15 minutes, + amountIn: amountIn, + amountOutMinimum: 0 // Let Uniswap handle slippage + }); + + return ISwapRouter(uniswapRouter).exactInput(params); + } + } + + /** + * @notice Swap tokens for exact tokens using Uniswap V3 + */ + function swapTokensForExactTokens( + address inputToken, + uint256 amountOut, + address outputToken, + address uniswapRouter, + address wzeta + ) internal returns (uint256) { + // Approve router to spend input tokens + IERC20(inputToken).approve(uniswapRouter, type(uint256).max); + + // Try direct swap first + try + ISwapRouter(uniswapRouter).exactOutputSingle( + ISwapRouter.ExactOutputSingleParams({ + tokenIn: inputToken, + tokenOut: outputToken, + fee: POOL_FEE, + recipient: address(this), + deadline: block.timestamp + 15 minutes, + amountOut: amountOut, + amountInMaximum: type(uint256).max, // Let Uniswap handle slippage + sqrtPriceLimitX96: 0 + }) + ) + returns (uint256 amountIn) { + return amountIn; + } catch { + // If direct swap fails, try through WZETA using exactOutput for multi-hop + // The path is encoded as (tokenOut, fee, WZETA, fee, tokenIn) in reverse order + bytes memory path = abi.encodePacked( + outputToken, + POOL_FEE, + wzeta, + POOL_FEE, + inputToken + ); + + ISwapRouter.ExactOutputParams memory params = ISwapRouter + .ExactOutputParams({ + path: path, + recipient: address(this), + deadline: block.timestamp + 15 minutes, + amountOut: amountOut, + amountInMaximum: type(uint256).max // Let Uniswap handle slippage + }); + + return ISwapRouter(uniswapRouter).exactOutput(params); + } + } +} diff --git a/examples/swap/scripts/localnet.sh b/examples/swap/scripts/localnet.sh index eec0e057..2e30c45d 100755 --- a/examples/swap/scripts/localnet.sh +++ b/examples/swap/scripts/localnet.sh @@ -21,7 +21,7 @@ UNISWAP_ROUTER=$(jq -r '.addresses[] | select(.type=="uniswapV3Router" and .chai SENDER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 DEFAULT_MNEMONIC="grape subway rack mean march bubble carry avoid muffin consider thing street" -CONTRACT_SWAP=$(npx hardhat deploy --name Swap --network localhost --gateway "$GATEWAY_ZETACHAIN" --uniswap-router "$UNISWAP_ROUTER" --wzeta "$WZETA" | jq -r '.contractAddress') +CONTRACT_SWAP=$(npx hardhat deploy --name Swap --network localhost --gateway "$GATEWAY_ZETACHAIN" --uniswap-router "$UNISWAP_ROUTER" | jq -r '.contractAddress') COMPANION=$(npx hardhat deploy-companion --gateway "$GATEWAY_ETHEREUM" --network localhost --json | jq -r '.contractAddress') npx hardhat evm-swap \ diff --git a/examples/swap/tasks/deploy.ts b/examples/swap/tasks/deploy.ts index 0e184a19..9a700a58 100644 --- a/examples/swap/tasks/deploy.ts +++ b/examples/swap/tasks/deploy.ts @@ -15,13 +15,7 @@ const main = async (args: any, hre: HardhatRuntimeEnvironment) => { const contract = await hre.upgrades.deployProxy( factory as any, - [ - args.gateway, - args.uniswapRouter, - args.gasLimit, - signer.address, - args.wzeta, - ], + [args.gateway, args.uniswapRouter, args.gasLimit, signer.address], { kind: "uups" } ); @@ -47,5 +41,4 @@ task("deploy", "Deploy the contract", main) "Gas limit for the transaction", 1000000, types.int - ) - .addOptionalParam("wzeta", "WZETA token address"); + );