diff --git a/examples/swap/contracts/Swap.sol b/examples/swap/contracts/Swap.sol index 3ab18e11..76dad538 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"; @@ -16,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, @@ -24,10 +27,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(); @@ -66,6 +71,7 @@ contract Swap is __Ownable_init(owner); uniswapRouter = uniswapRouterAddress; gateway = GatewayZEVM(gatewayAddress); + wzeta = gateway.zetaToken(); gasLimit = gasLimitAmount; } @@ -191,32 +197,36 @@ 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 = SwapLibrary.swapTokensForExactTokens( inputToken, gasFee, gasZRC20, - amount + uniswapRouter, + wzeta ); + 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 = SwapLibrary.swapExactTokensForTokens( inputToken, swapAmount, targetToken, - 0 + uniswapRouter, + wzeta ); return (out, gasZRC20, gasFee); } @@ -300,40 +310,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/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/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..2e30c45d 100755 --- a/examples/swap/scripts/localnet.sh +++ b/examples/swap/scripts/localnet.sh @@ -17,7 +17,7 @@ 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" diff --git a/examples/swap/tasks/deploy.ts b/examples/swap/tasks/deploy.ts index 1a4c680c..9a700a58 100644 --- a/examples/swap/tasks/deploy.ts +++ b/examples/swap/tasks/deploy.ts @@ -30,7 +30,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)",