Skip to content

Swap: use Uniswap v3 pools #251

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 23 additions & 47 deletions examples/swap/contracts/Swap.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand All @@ -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();
Expand Down Expand Up @@ -66,6 +71,7 @@ contract Swap is
__Ownable_init(owner);
uniswapRouter = uniswapRouterAddress;
gateway = GatewayZEVM(gatewayAddress);
wzeta = gateway.zetaToken();
gasLimit = gasLimitAmount;
}

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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 {}
Expand Down
115 changes: 115 additions & 0 deletions examples/swap/contracts/SwapLibrary.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
Comment on lines +13 to +61

Check warning

Code scanning / Slither

Unused return Medium


/**
* @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);
}
}
Comment on lines +66 to +114
}
4 changes: 3 additions & 1 deletion examples/swap/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
2 changes: 1 addition & 1 deletion examples/swap/scripts/localnet.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
2 changes: 1 addition & 1 deletion examples/swap/tasks/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down
Loading