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..814d1e3e 100644 --- a/src/pages/developers/tutorials/swap.mdx +++ b/src/pages/developers/tutorials/swap.mdx @@ -40,8 +40,6 @@ The swap contract will: - 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. - ## Setting Up Your Environment To set up your environment, clone the example contracts repository and install @@ -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, @@ -241,18 +334,24 @@ 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 defines 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. +- **`bool withdraw`**: Determines whether to withdraw the swapped token to the + destination chain or to transfer the token 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 @@ -267,21 +366,19 @@ requirements. `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`. + limits, so the contract uses `abi.decode` to extract the params 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. +After decoding the message, the contract proceeds to handle the token swap by +calling `handleGasAndSwap` and `withdraw`. -### Swapping and Withdrawing Tokens +### Handling Gas Swapping 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. +The `handleGasAndSwap` function encapsulates the logic for swapping the required +amount of tokens for gas and swapping the rest for the destination token. #### Swapping for Gas Token @@ -302,28 +399,62 @@ chain. 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`). +method returns the amount of the target token received (`out`). -#### Withdrawing Target Token to Connected Chain +### Withdrawing Target Token to Connected Chain 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`). - -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. - - - {" "} - 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. - +the swapped target tokens. + +`withdraw` dispatches different logic based on whether the token must with +withdrawn and on the type of tokens. + +If the target token is the same as the gas token, the contract approves the +total amount (both target amount and gas). Otherwise, it approves these amount +separately. + +Then it calls `gateway.withdraw` to withdraw tokens to the destination chain. +Use `abi.encodePacked` to convert the `address` into `bytes`. Pass Swap contract +as the revert address and supply the sender address as well as the input token +as a revert message. + +The `withdraw` call results in a simple token transfer on the destination chain, +so it's unlikely that it could fail. However, if the destination happens to be a +contract that does not accept tokens, it might revert, so you need to account +for that possibility and implement revert logic. + +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. + +If `withdraw` is `false`, simply transfer the target token to the recipient on +ZetaChain. + +### Revert Logic + +If the withdraw call on the destination chain reverts, the `onRevert` function +is called. + +First decode the sender and token address from the message (which was supplied +as revert options). + +Next, call `handleGasAndSwap` to swap back to the original token sent from the +source chain. + +Finally, call `gateway.withdraw` to send the tokens back to the source chain. If +the withdrawal also fails, transfer the tokens to the sender on ZetaChain. + +## Companion Contract + +There are two ways to call the universal Swap contract: + +- calling the `depositAndCall` on the EVM gateway on a connected chain directly. + This is convenient, because you don't have to to have a contract on a + connected chain. +- calling a contract on a connected chain, which calls the gateway. This is + useful, when you want to execute additional logic on a connected chain before + executing a swap. An example of such a contract is presented as + `SwapCompanion.sol`. ## Option 1: Deploy on Testnet @@ -388,7 +519,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 +554,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 \