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 \