From 7aa56dbe1be2ded6f4299ef3aeecd2aca98a1c5a Mon Sep 17 00:00:00 2001 From: Denis Fadeev Date: Mon, 16 Dec 2024 11:15:13 +0300 Subject: [PATCH] Merge swap tutorials in one (#517) Co-authored-by: Hernan Clich <62221379+hernan-clich@users.noreply.github.com> --- .../developers/frontend/universalkit.mdx | 2 +- src/pages/developers/tutorials/_meta.json | 5 - src/pages/developers/tutorials/swap-any.mdx | 446 ------------------ src/pages/developers/tutorials/swap.mdx | 355 +++++++++----- 4 files changed, 230 insertions(+), 578 deletions(-) delete mode 100644 src/pages/developers/tutorials/swap-any.mdx diff --git a/src/pages/developers/frontend/universalkit.mdx b/src/pages/developers/frontend/universalkit.mdx index 2cc14bf0..e150bd31 100644 --- a/src/pages/developers/frontend/universalkit.mdx +++ b/src/pages/developers/frontend/universalkit.mdx @@ -140,7 +140,7 @@ The Swap component provides the following functionality: - Transfer native gas and ERC-20 tokens on connected chains For cross-chain swaps the component depends on a universal swap contract. -Complete [the swap tutorial](/developers/tutorials/swap-any) and deploy your own +Complete [the swap tutorial](/developers/tutorials/swap) and deploy your own contract, or use the contract address from the example below. contract or use the contract address from the example below. diff --git a/src/pages/developers/tutorials/_meta.json b/src/pages/developers/tutorials/_meta.json index c5788ba5..6d64d482 100644 --- a/src/pages/developers/tutorials/_meta.json +++ b/src/pages/developers/tutorials/_meta.json @@ -19,11 +19,6 @@ "readTime": "30 min", "description": "Implement a universal swap app compatible with chains like Ethereum, Solana and Bitcoin" }, - "swap-any": { - "title": "Swap Any Token", - "readTime": "60 min", - "description": "Enhance the universal swap app with the ability to swap to any token" - }, "nft": { "title": "NFT", "readTime": "60 min", diff --git a/src/pages/developers/tutorials/swap-any.mdx b/src/pages/developers/tutorials/swap-any.mdx deleted file mode 100644 index e4c1869e..00000000 --- a/src/pages/developers/tutorials/swap-any.mdx +++ /dev/null @@ -1,446 +0,0 @@ ---- -title: Swap to Any Token ---- - -import { Alert } from "~/components/shared"; - -In the previous [Swap](/developers/tutorials/swap) tutorial, you created a -universal swap contract that allows users to exchange tokens from one connected -blockchain for a token on another blockchain. In that implementation, the -swapped token was always withdrawn to the destination chain. This tutorial -expands on that by enhancing the contract to support swapping tokens to any -token (such as ZRC-20, ERC-20, or ZETA) and offering users the flexibility to -either withdraw the token to the destination chain or retain it on ZetaChain. - -The ability to keep swapped tokens on ZetaChain can be particularly useful if -you intend to utilize ZRC-20 tokens in non-universal contracts that aren't yet -equipped to accept tokens from connected chains. It is also useful if the -destination token is ZETA, which you may want to keep on ZetaChain for further -use. - -In this enhanced version, you will modify the original swap contract to support -this additional functionality. You will also deploy the modified contract to -localnet and interact with it by swapping tokens from a connected EVM chain. - -This tutorial relies on the Gateway, which is currently available only on localnet and testnet. - -## Setting Up Your Environment - -To get started, clone the example contracts repository and install the -dependencies by running the following commands: - -``` -git clone https://github.com/zeta-chain/example-contracts - -cd example-contracts/examples/swap - -yarn -``` - -## Understanding the SwapToAnyToken Contract - -The `SwapToAnyToken` contract builds on the previous swap contract by allowing -users to swap tokens to any target token and giving them the option to either -withdraw the swapped tokens to the destination chain or keep them on ZetaChain. -This added flexibility makes the contract more versatile for a variety of use -cases. - -```solidity -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {SystemContract, IZRC20} from "@zetachain/toolkit/contracts/SystemContract.sol"; -import {SwapHelperLib} from "@zetachain/toolkit/contracts/SwapHelperLib.sol"; -import {BytesHelperLib} from "@zetachain/toolkit/contracts/BytesHelperLib.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {RevertContext, RevertOptions} from "@zetachain/protocol-contracts/contracts/Revert.sol"; -import "@zetachain/protocol-contracts/contracts/zevm/interfaces/UniversalContract.sol"; -import "@zetachain/protocol-contracts/contracts/zevm/interfaces/IGatewayZEVM.sol"; -import "@zetachain/protocol-contracts/contracts/zevm/interfaces/IWZETA.sol"; -import {GatewayZEVM} from "@zetachain/protocol-contracts/contracts/zevm/GatewayZEVM.sol"; - -contract SwapToAnyToken is UniversalContract { - address public immutable uniswapRouter; - GatewayZEVM public gateway; - uint256 constant BITCOIN = 18332; - uint256 public immutable gasLimit; - - error InvalidAddress(); - error Unauthorized(); - error ApprovalFailed(); - error TransferFailed(); - - modifier onlyGateway() { - if (msg.sender != address(gateway)) revert Unauthorized(); - _; - } - - constructor( - address payable gatewayAddress, - address uniswapRouterAddress, - uint256 gasLimitAmount - ) { - if (gatewayAddress == address(0) || uniswapRouterAddress == address(0)) - revert InvalidAddress(); - uniswapRouter = uniswapRouterAddress; - gateway = GatewayZEVM(gatewayAddress); - gasLimit = gasLimitAmount; - } - - struct Params { - address target; - bytes to; - bool withdraw; - } - - function onCall( - MessageContext calldata context, - address zrc20, - uint256 amount, - bytes calldata message - ) external onlyGateway { - Params memory params = Params({ - target: address(0), - to: bytes(""), - withdraw: true - }); - - if (context.chainID == BITCOIN) { - params.target = BytesHelperLib.bytesToAddress(message, 0); - params.to = abi.encodePacked( - BytesHelperLib.bytesToAddress(message, 20) - ); - if (message.length >= 41) { - params.withdraw = BytesHelperLib.bytesToBool(message, 40); - } - } else { - ( - address targetToken, - bytes memory recipient, - bool withdrawFlag - ) = abi.decode(message, (address, bytes, bool)); - params.target = targetToken; - params.to = recipient; - params.withdraw = withdrawFlag; - } - - (uint256 out, address gasZRC20, uint256 gasFee) = handleGasAndSwap( - zrc20, - amount, - params.target - ); - withdraw(params, context.sender, gasFee, gasZRC20, out, zrc20); - } - - function swap( - address inputToken, - uint256 amount, - address targetToken, - bytes memory recipient, - bool withdrawFlag - ) public { - bool success = IZRC20(inputToken).transferFrom( - msg.sender, - address(this), - amount - ); - if (!success) { - revert TransferFailed(); - } - - (uint256 out, address gasZRC20, uint256 gasFee) = handleGasAndSwap( - inputToken, - amount, - targetToken - ); - - withdraw( - Params({ - target: targetToken, - to: recipient, - withdraw: withdrawFlag - }), - msg.sender, - gasFee, - gasZRC20, - out, - inputToken - ); - } - - function handleGasAndSwap( - address inputToken, - uint256 amount, - address targetToken - ) internal returns (uint256, address, uint256) { - uint256 inputForGas; - address gasZRC20; - uint256 gasFee; - uint256 swapAmount; - - (gasZRC20, gasFee) = IZRC20(targetToken).withdrawGasFee(); - - if (gasZRC20 == inputToken) { - swapAmount = amount - gasFee; - } else { - inputForGas = SwapHelperLib.swapTokensForExactTokens( - uniswapRouter, - inputToken, - gasFee, - gasZRC20, - amount - ); - swapAmount = amount - inputForGas; - } - - uint256 out = SwapHelperLib.swapExactTokensForTokens( - uniswapRouter, - inputToken, - swapAmount, - targetToken, - 0 - ); - return (out, gasZRC20, gasFee); - } - - function withdraw( - Params memory params, - address sender, - uint256 gasFee, - address gasZRC20, - uint256 out, - address inputToken - ) public { - if (params.withdraw) { - if (gasZRC20 == params.target) { - if (!IZRC20(gasZRC20).approve(address(gateway), out + gasFee)) { - revert ApprovalFailed(); - } - } else { - if (!IZRC20(gasZRC20).approve(address(gateway), gasFee)) { - revert ApprovalFailed(); - } - if (!IZRC20(params.target).approve(address(gateway), out)) { - revert ApprovalFailed(); - } - } - gateway.withdraw( - abi.encodePacked(params.to), - out, - params.target, - RevertOptions({ - revertAddress: address(this), - callOnRevert: true, - abortAddress: address(0), - revertMessage: abi.encode(sender, inputToken), - onRevertGasLimit: gasLimit - }) - ); - } else { - bool success = IWETH9(params.target).transfer( - address(uint160(bytes20(params.to))), - out - ); - if (!success) { - revert TransferFailed(); - } - } - } - - function onRevert(RevertContext calldata context) external onlyGateway { - (address sender, address zrc20) = abi.decode( - context.revertMessage, - (address, address) - ); - (uint256 out, , ) = handleGasAndSwap( - context.asset, - context.amount, - zrc20 - ); - - gateway.withdraw( - abi.encodePacked(sender), - out, - zrc20, - RevertOptions({ - revertAddress: sender, - callOnRevert: false, - abortAddress: address(0), - revertMessage: "", - onRevertGasLimit: gasLimit - }) - ); - } - - fallback() external payable {} - - receive() external payable {} -} -``` - -The contract introduces a key enhancement: a `withdraw` flag. This flag -determines whether the swapped tokens should be withdrawn to a connected chain -or remain on ZetaChain. Additionally, the contract supports both cross-chain -calls and direct interactions on ZetaChain, making it useful for scenarios where -tokens are already on ZetaChain and you don’t need to involve a connected chain. - -### Differences Between Swap and SwapToAnyToken Contracts - -In this new version, the core structure remains similar, but several key changes -have been made to extend its functionality. - -First, the `Params` struct has been updated to include a `withdraw` flag. This -allows users to specify whether they want the swapped tokens withdrawn to a -connected chain or kept on ZetaChain. The `onCall` function now decodes this -additional flag from the incoming message. For EVM chains and Solana, the -contract decodes the `withdraw` flag alongside other parameters. For Bitcoin, -due to the smaller message size allowed by its OP_RETURN, the contract checks if -the message length is sufficient before extracting the `withdraw` flag. - -The `swapAndWithdraw` function has also been modified to conditionally handle -gas fees based on whether the tokens will be withdrawn. If the `withdraw` flag -is set to `true`, the contract proceeds with the usual gas fee calculation and -deduction. If the flag is `false`, it skips the gas fee handling and simply -swaps the full amount of tokens. - -Once the tokens are swapped, the contract either withdraws them to the -destination chain or transfers them directly on ZetaChain. When `withdraw` is -`true`, it follows the same withdrawal process as the original contract, using -the gateway to send tokens to the connected chain. However, if `withdraw` is -`false`, it transfers the tokens directly to the recipient on ZetaChain without -involving the gateway. - -Additionally, a new public `swap` function has been introduced, which allows -users to interact with the contract directly on ZetaChain. This function is -particularly useful if you already have tokens on ZetaChain and want to swap -them without making a cross-chain call. It takes in parameters similar to those -in `onCall`, transfers the input tokens from the sender to the contract, and -then calls `swapAndWithdraw` to perform the swap and handle withdrawal or direct -transfer based on the `withdraw` flag. - -Finally, the contract now imports the `IWETH9` interface to handle direct token -transfers when `withdraw` is `false`. This interface facilitates the transfer of -wrapped tokens on ZetaChain. - -## Starting Localnet - -To simulate ZetaChain’s behavior locally, start the local development -environment by running: - -``` -npx hardhat localnet -``` - -## Deploying the Contract - -Once your environment is set up, compile the contract and deploy it to localnet -using the following command: - -``` -npx hardhat deploy \ - --name SwapToAnyToken \ - --network localhost \ - --gateway 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707 \ - --uniswap-router 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 -``` - -After deployment, you should see an output similar to this: - -``` -🔑 Using account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 - -🚀 Successfully deployed contract on localhost. -📜 Contract address: 0xc351628EB244ec633d5f21fBD6621e1a683B1181 -``` - -## Swap and Withdraw Tokens to Connected Chain - -To swap tokens from a connected EVM chain and withdraw them to the destination -chain, use the following command: - -``` -npx hardhat swap-from-evm \ - --network localhost \ - --receiver 0xc351628EB244ec633d5f21fBD6621e1a683B1181 \ - --amount 1 \ - --target 0x05BA149A7bd6dC1F937fA9046A9e05C05f3b18b0 \ - --recipient 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 \ - --withdraw true -``` - -- EVM gateway is called with 1 native gas token (ETH) and a message that - contains target ZRC-20 token address, receiver address and a boolean that - indicates to withdraw a token to the destination chain or not. -- `onCall` is called -- If `withdraw` is: - - true, withdraw the ZRC-20 to the destination chain as a native token - - false, send target ZRC-20 to the recipient - -In the command above the `withdraw` is `true`, so the target ZRC-20 token will -be transferred to the destination chain. - -## Swap Tokens Without Withdrawing - -If you want to swap tokens and keep them on ZetaChain rather than withdrawing -them, set the `withdraw` flag. - -``` -npx hardhat swap-from-evm \ - --network localhost \ - --receiver 0xc351628EB244ec633d5f21fBD6621e1a683B1181 \ - --amount 1 \ - --target 0x05BA149A7bd6dC1F937fA9046A9e05C05f3b18b0 \ - --recipient 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 \ - --withdraw false -``` - -In the command above the `withdraw` is `false`, so the target ZRC-20 token will -be transferred to the recipient on ZetaChain. - -## Swap on ZetaChain and Withdraw Tokens to Connected Chain - -To swap a ZRC-20 token for another ZRC-20 on ZetaChain and withdraw to a -connected chain, run: - -``` -npx hardhat swap-from-zetachain \ - --network localhost \ - --contract 0xc351628EB244ec633d5f21fBD6621e1a683B1181 \ - --amount 1 \ - --target 0x65a45c57636f9BcCeD4fe193A602008578BcA90b \ - --recipient 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 \ - --zrc20 0x05BA149A7bd6dC1F937fA9046A9e05C05f3b18b0 \ - --withdraw true -``` - -## Swap on ZetaChain Without Withdrawing - -To swap a ZRC-20 token for another ZRC-20 on ZetaChain and transfer it to a -recipient on ZetaChain, run: - -``` -npx hardhat swap-from-zetachain \ - --network localhost \ - --contract 0xc351628EB244ec633d5f21fBD6621e1a683B1181 \ - --amount 1 \ - --target 0x65a45c57636f9BcCeD4fe193A602008578BcA90b \ - --recipient 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 \ - --zrc20 0x05BA149A7bd6dC1F937fA9046A9e05C05f3b18b0 \ - --withdraw false -``` - -## Conclusion - -In this tutorial, you extended the functionality of the original swap contract -by adding the ability to swap tokens to any token and decide whether to withdraw -them to a connected chain or keep them on ZetaChain. You also learned how to -deploy the contract and interact with it both via cross-chain calls and directly -on ZetaChain, providing greater flexibility for a variety of use cases. - -## Source Code - -You can find the source code for this tutorial in the example contracts -repository: - -https://github.com/zeta-chain/example-contracts/tree/main/examples/swap diff --git a/src/pages/developers/tutorials/swap.mdx b/src/pages/developers/tutorials/swap.mdx index 9004c158..c68337f4 100644 --- a/src/pages/developers/tutorials/swap.mdx +++ b/src/pages/developers/tutorials/swap.mdx @@ -29,18 +29,16 @@ assets. The swap contract will: -- Accept a contract call from a connected chain containing native gas or - supported ERC-20 tokens and a message. -- Decode the message, which should include: - - Target token address (represented as ZRC-20) - - Recipient address on the destination chain -- Query withdraw gas fee of the target token. -- Swap a fraction of the input token for a ZRC-20 gas token to cover the - withdrawal fee using the Uniswap v2 liquidity pools. -- Swap the remaining input token amount for the target token ZRC-20. -- Withdraw ZRC-20 tokens to the destination chain. - -This tutorial relies on the Gateway, which is currently available only on localnet and testnet. +1. Accept a contract call from a connected chain containing native gas or + supported ERC-20 tokens and a message. +2. Decode the message to extract: + - The target token's address (represented as ZRC-20). + - The recipient's address on the destination chain. +3. Query the withdrawal gas fee for the target token. +4. Swap part of the input token for ZRC-20 gas tokens to cover the withdrawal + fee using Uniswap v2 liquidity pools. +5. Swap the remaining input token amount for the target ZRC-20 token. +6. Withdraw the ZRC-20 tokens to the destination chain. ## Setting Up Your Environment @@ -75,30 +73,59 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {RevertContext, RevertOptions} from "@zetachain/protocol-contracts/contracts/Revert.sol"; import "@zetachain/protocol-contracts/contracts/zevm/interfaces/UniversalContract.sol"; import "@zetachain/protocol-contracts/contracts/zevm/interfaces/IGatewayZEVM.sol"; +import "@zetachain/protocol-contracts/contracts/zevm/interfaces/IWZETA.sol"; import {GatewayZEVM} from "@zetachain/protocol-contracts/contracts/zevm/GatewayZEVM.sol"; -contract Swap is UniversalContract { - address public immutable uniswapRouter; +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"; + +contract Swap is + UniversalContract, + Initializable, + UUPSUpgradeable, + OwnableUpgradeable +{ + address public uniswapRouter; GatewayZEVM public gateway; - uint256 constant BITCOIN = 18332; - uint256 public immutable gasLimit; + uint256 constant BITCOIN = 8332; + uint256 constant BITCOIN_TESTNET = 18332; + uint256 public gasLimit; error InvalidAddress(); error Unauthorized(); error ApprovalFailed(); + error TransferFailed(); + + event TokenSwap( + address sender, + bytes indexed recipient, + address indexed inputToken, + address indexed targetToken, + uint256 inputAmount, + uint256 outputAmount + ); modifier onlyGateway() { if (msg.sender != address(gateway)) revert Unauthorized(); _; } - constructor( + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( address payable gatewayAddress, address uniswapRouterAddress, - uint256 gasLimitAmount - ) { + uint256 gasLimitAmount, + address owner + ) public initializer { if (gatewayAddress == address(0) || uniswapRouterAddress == address(0)) revert InvalidAddress(); + __UUPSUpgradeable_init(); + __Ownable_init(owner); uniswapRouter = uniswapRouterAddress; gateway = GatewayZEVM(gatewayAddress); gasLimit = gasLimitAmount; @@ -107,6 +134,7 @@ contract Swap is UniversalContract { struct Params { address target; bytes to; + bool withdraw; } function onCall( @@ -115,19 +143,29 @@ contract Swap is UniversalContract { uint256 amount, bytes calldata message ) external onlyGateway { - Params memory params = Params({target: address(0), to: bytes("")}); - if (context.chainID == BITCOIN) { + Params memory params = Params({ + target: address(0), + to: bytes(""), + withdraw: true + }); + + if (context.chainID == BITCOIN_TESTNET || context.chainID == BITCOIN) { params.target = BytesHelperLib.bytesToAddress(message, 0); params.to = abi.encodePacked( BytesHelperLib.bytesToAddress(message, 20) ); + if (message.length >= 41) { + params.withdraw = BytesHelperLib.bytesToBool(message, 40); + } } else { - (address targetToken, bytes memory recipient) = abi.decode( - message, - (address, bytes) - ); + ( + address targetToken, + bytes memory recipient, + bool withdrawFlag + ) = abi.decode(message, (address, bytes, bool)); params.target = targetToken; params.to = recipient; + params.withdraw = withdrawFlag; } (uint256 out, address gasZRC20, uint256 gasFee) = handleGasAndSwap( @@ -135,9 +173,60 @@ contract Swap is UniversalContract { amount, params.target ); + emit TokenSwap( + context.sender, + params.to, + zrc20, + params.target, + amount, + out + ); withdraw(params, context.sender, gasFee, gasZRC20, out, zrc20); } + function swap( + address inputToken, + uint256 amount, + address targetToken, + bytes memory recipient, + bool withdrawFlag + ) public { + bool success = IZRC20(inputToken).transferFrom( + msg.sender, + address(this), + amount + ); + if (!success) { + revert TransferFailed(); + } + + (uint256 out, address gasZRC20, uint256 gasFee) = handleGasAndSwap( + inputToken, + amount, + targetToken + ); + emit TokenSwap( + msg.sender, + recipient, + inputToken, + targetToken, + amount, + out + ); + withdraw( + Params({ + target: targetToken, + to: recipient, + withdraw: withdrawFlag + }), + msg.sender, + gasFee, + gasZRC20, + out, + inputToken + ); + } + function handleGasAndSwap( address inputToken, uint256 amount, @@ -163,14 +252,14 @@ contract Swap is UniversalContract { swapAmount = amount - inputForGas; } - uint256 outputAmount = SwapHelperLib.swapExactTokensForTokens( + uint256 out = SwapHelperLib.swapExactTokensForTokens( uniswapRouter, inputToken, swapAmount, targetToken, 0 ); - return (outputAmount, gasZRC20, gasFee); + return (out, gasZRC20, gasFee); } function withdraw( @@ -178,40 +267,43 @@ contract Swap is UniversalContract { address sender, uint256 gasFee, address gasZRC20, - uint256 outputAmount, + uint256 out, address inputToken ) public { - if (gasZRC20 == params.target) { - if ( - !IZRC20(gasZRC20).approve( - address(gateway), - outputAmount + gasFee - ) - ) { - revert ApprovalFailed(); + if (params.withdraw) { + if (gasZRC20 == params.target) { + if (!IZRC20(gasZRC20).approve(address(gateway), out + gasFee)) { + revert ApprovalFailed(); + } + } else { + if (!IZRC20(gasZRC20).approve(address(gateway), gasFee)) { + revert ApprovalFailed(); + } + if (!IZRC20(params.target).approve(address(gateway), out)) { + revert ApprovalFailed(); + } } + gateway.withdraw( + abi.encodePacked(params.to), + out, + params.target, + RevertOptions({ + revertAddress: address(this), + callOnRevert: true, + abortAddress: address(0), + revertMessage: abi.encode(sender, inputToken), + onRevertGasLimit: gasLimit + }) + ); } else { - if (!IZRC20(gasZRC20).approve(address(gateway), gasFee)) { - revert ApprovalFailed(); - } - if ( - !IZRC20(params.target).approve(address(gateway), outputAmount) - ) { - revert ApprovalFailed(); + bool success = IWETH9(params.target).transfer( + address(uint160(bytes20(params.to))), + out + ); + if (!success) { + revert TransferFailed(); } } - gateway.withdraw( - params.to, - outputAmount, - params.target, - RevertOptions({ - revertAddress: address(this), - callOnRevert: true, - abortAddress: address(0), - revertMessage: abi.encode(sender, inputToken), - onRevertGasLimit: gasLimit - }) - ); } function onRevert(RevertContext calldata context) external onlyGateway { @@ -224,6 +316,7 @@ contract Swap is UniversalContract { context.amount, zrc20 ); + gateway.withdraw( abi.encodePacked(sender), out, @@ -238,92 +331,102 @@ contract Swap is UniversalContract { ); } - fallback() external payable {} - - receive() external payable {} + function _authorizeUpgrade( + address newImplementation + ) internal override onlyOwner {} } ``` ### Decoding the Message -The contract defines a `Params` struct to store two crucial pieces of +The contract uses a `Params` struct to store the following pieces of information: - **`address target`**: The ZRC-20 address of the target token on ZetaChain. - **`bytes to`**: The recipient's address on the destination chain, stored as - `bytes` because the recipient could be on an EVM chain (like Ethereum or BNB) - or on a non-EVM chain like Bitcoin. + `bytes` to support both EVM chains (e.g., Ethereum, BNB) and non-EVM chains + like Bitcoin. +- **`bool withdraw`**: Indicates whether to withdraw the swapped token to the + destination chain or transfer it to the recipient on ZetaChain. When the `onCall` function is invoked, it receives a `message` parameter that -needs to be decoded to extract the swap details. The encoding of this message -varies depending on the source chain due to different limitations and -requirements. - -- **For Bitcoin**: Since Bitcoin has an upper limit of 80 bytes for OP_RETURN - messages, the contract uses a more efficient encoding. It extracts the - `params.target` by reading the first 20 bytes of the `message` and converting - it to an `address` using the `bytesToAddress` helper method. The recipient's - address is then obtained by reading the next 20 bytes and packing it into - `bytes` using `abi.encodePacked`. - -- **For EVM Chains And Solana**: EVM chains don't have strict message size - limits, so the contract uses `abi.decode` to extract the `params.target` and - `params.to` directly from the `message`. - -The `context.chainID` is utilized to determine the source chain and apply the -appropriate decoding logic. - -After decoding the message, the contract proceeds to handle the token swap and -withdrawal process by calling the `swapAndWithdraw` function with the -appropriate parameters. - -### Swapping and Withdrawing Tokens - -The `swapAndWithdraw` function encapsulates the logic for swapping tokens and -withdrawing them to the connected chain. By separating this logic into its own -function, the code becomes cleaner and easier to maintain. - -#### Swapping for Gas Token - -The contract first addresses the gas fee required for the withdrawal on the -destination chain. It uses the `withdrawGasFee` method of the target token's -ZRC-20 contract to obtain the gas fee amount (`gasFee`) and the gas fee token -address (`gasZRC20`). - -If the incoming token (`inputToken`) is the same as the gas fee token -(`gasZRC20`), it deducts the gas fee directly from the incoming amount. -Otherwise, it swaps a portion of the incoming tokens for the required gas fee -using the `swapTokensForExactTokens` helper method. This ensures that the -contract has enough gas tokens to cover the withdrawal fee on the destination -chain. +must be decoded to extract the swap details. The decoding logic adapts to the +source chain's specific requirements and limitations. + +- **For Bitcoin**: Due to Bitcoin's 80-byte OP_RETURN limit, the contract + employs an efficient encoding method. The target token address + (`params.target`) is extracted from the first 20 bytes of the `message`, + converted into an `address` using a helper function. The recipient’s address + is extracted from the next 20 bytes and encoded as `bytes` format. +- **For EVM Chains and Solana**: Without strict size limitations on messages, + the contract uses `abi.decode` to extract all parameters directly. + +The source chain is identified using `context.chainID`, which determines the +appropriate decoding logic. After decoding, the contract proceeds to handle the +token swap by invoking `handleGasAndSwap` and, if required, initiating a +withdrawal. + +--- + +### Handling Gas and Swapping Tokens + +The `handleGasAndSwap` function handles both obtaining gas tokens for withdrawal +fees and swapping the remaining tokens for the target token. + +The contract ensures sufficient gas tokens to cover the withdrawal fee on the +destination chain by calculating the required amount through the ZRC-20 +contract's `withdrawGasFee` method. This method provides the fee amount +(`gasFee`) and the gas token address (`gasZRC20`). + +If the incoming token is already the gas token, the required gas fee is deducted +directly. Otherwise, the contract swaps a portion of the incoming tokens for the +gas fee using a helper function. This ensures the contract is always prepared +for cross-chain withdrawal operations. + +After addressing the gas fee, the remaining tokens are swapped for the target +token using ZetaChain's internal liquidity pools. This step ensures that the +recipient receives the correct token as specified in the `Params`. + +### Withdrawing Target Token to Connected Chain + +Once the gas and target tokens are prepared, the contract determines the +appropriate action based on the `withdraw` parameter: + +- **If `withdraw` is `true`**: The target token and gas tokens are approved, + either combined or separately depending on whether they are the same. The + contract calls `gateway.withdraw` to transfer the tokens to the destination + chain. The recipient's address is encoded using `abi.encodePacked`. The Swap + contract is supplied as the revert address, while the sender's address and + input token are included as a revert message for potential recovery. The + ZRC-20 contract inherently ensures that tokens are withdrawn to the correct + connected chain. +- **If `withdraw` is `false`**: The target token is transferred directly to the + recipient on ZetaChain, bypassing the withdrawal process. -#### Swapping for Target Token +### Revert Logic -Next, the contract swaps the remaining tokens (`swapAmount`) for the target -token specified in `targetToken`. It uses the `swapExactTokensForTokens` helper -method to perform this swap through ZetaChain's internal liquidity pools. This -method returns the amount of the target token received (`outputAmount`). +If a withdrawal fails on the destination chain, the `onRevert` function is +invoked to recover the funds. The sender's address and the original token are +decoded from the revert message, ensuring the correct data for recovery. -#### Withdrawing Target Token to Connected Chain +The contract swaps the reverted tokens back to the original token sent from the +source chain. Finally, it attempts to withdraw the tokens back to the source +chain. If this withdrawal also fails, the tokens are transferred directly to the +sender on ZetaChain. This approach minimizes the risk of lost funds and ensures +a robust fallback mechanism. -At this stage, the contract holds the required gas fee in `gasZRC20` tokens and -the swapped target tokens in `targetToken`. It needs to approve the -`GatewayZEVM` contract to spend these tokens before initiating the withdrawal. -If the gas fee token is the same as the target token, it approves the total -amount (gas fee plus output amount) for the gateway to spend. If they are -different, it approves each token separately—the gas fee token (`gasZRC20`) and -the target token (`targetToken`). +### Companion Contract -Finally, the contract calls the `gateway.withdraw` method to send the tokens to -the recipient on the connected chain. The `withdraw` method handles the -cross-chain transfer, ensuring that the recipient receives the swapped tokens on -their native chain, whether it's an EVM chain or Bitcoin. +The Swap contract can be called in two ways: - - {" "} - Note that you don't have to specify which chain to withdraw to because each ZRC-20 contract knows which connected chain - it is associated with. For example, ZRC-20 Ethereum USDC can only be withdrawn to Ethereum. - +1. **Directly via `depositAndCall`**: This method uses the EVM gateway on a + connected chain, eliminating the need for an intermediary contract. It is + suitable for straightforward swaps without additional logic on the connected + chain. +2. **Through a companion contract**: This approach is useful when additional + logic must be executed on the connected chain before initiating the swap. The + tutorial provides an example of such a companion contract in + `SwapCompanion.sol`. ## Option 1: Deploy on Testnet @@ -388,7 +491,7 @@ You should see output similar to: To swap gas tokens for ERC-20 tokens, run the following command: ``` -npx hardhat swap-from-evm \ +npx hardhat evm-swap \ --network localhost \ --receiver 0xc351628EB244ec633d5f21fBD6621e1a683B1181 \ --amount 1 \ @@ -423,7 +526,7 @@ To swap ERC-20 tokens for gas tokens, adjust the command by specifying the ERC-20 token you're swapping from using the `--erc20` parameter: ``` -npx hardhat swap-from-evm \ +npx hardhat evm-swap \ --network localhost \ --receiver 0xc351628EB244ec633d5f21fBD6621e1a683B1181 \ --amount 1 \