Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
181 changes: 129 additions & 52 deletions examples/swap/contracts/Swap.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
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 @@ -24,10 +26,12 @@
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 @@ -58,13 +62,18 @@
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;
}
Expand Down Expand Up @@ -191,36 +200,138 @@

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 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
*/
Expand Down Expand Up @@ -300,40 +411,6 @@
);
}

/**
* @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
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"
}
}
}
4 changes: 2 additions & 2 deletions examples/swap/scripts/localnet.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
13 changes: 10 additions & 3 deletions examples/swap/tasks/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
);

Expand All @@ -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)",
Expand All @@ -41,4 +47,5 @@ task("deploy", "Deploy the contract", main)
"Gas limit for the transaction",
1000000,
types.int
);
)
.addOptionalParam("wzeta", "WZETA token address");
Loading