Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Swap from ZetaChain tutorial #393

Merged
merged 1 commit into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/pages/developers/tutorials/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
},
"swap-any": {
"title": "Swap Any Token",
"readTime": "20 min",
"readTime": "60 min",
"description": "Enhance the omnichain swap app with the ability to swap to any token"
},
"staking": {
Expand Down
162 changes: 143 additions & 19 deletions src/pages/developers/tutorials/swap-any.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ to keep on ZetaChain.
Copy the existing swap example into a new file `SwapToAnyToken.sol` and make the
necessary changes:

```solidity filename="contracts/SwapToAnyToken.sol" {8,11,23,35,41-43,45-51,58,73,78-83}
```solidity filename="contracts/SwapToAnyToken.sol" {8,11,23,37,45-47,49-56,64,73,79,94,99-114,117-127}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;

Expand All @@ -47,6 +47,8 @@ contract SwapToAnyToken is zContract, OnlySystem {
bool withdraw;
}

receive() external payable {}

function onCrossChainCall(
zContext calldata context,
address zrc20,
Expand All @@ -61,30 +63,49 @@ contract SwapToAnyToken is zContract, OnlySystem {

if (context.chainID == BITCOIN) {
params.target = BytesHelperLib.bytesToAddress(message, 0);
params.to = abi.encodePacked(BytesHelperLib.bytesToAddress(message, 20));
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)
);
(
address targetToken,
bytes memory recipient,
bool withdrawFlag
) = abi.decode(message, (address, bytes, bool));
params.target = targetToken;
params.to = recipient;
params.withdraw = withdrawFlag;
}

swapAndWithdraw(
zrc20,
amount,
params.target,
params.to,
params.withdraw
);
}

function swapAndWithdraw(
address inputToken,
uint256 amount,
address targetToken,
bytes memory recipient,
bool withdraw
) internal {
uint256 inputForGas;
address gasZRC20;
uint256 gasFee;

if (params.withdraw) {
(gasZRC20, gasFee) = IZRC20(params.target).withdrawGasFee();
if (withdraw) {
(gasZRC20, gasFee) = IZRC20(targetToken).withdrawGasFee();

inputForGas = SwapHelperLib.swapTokensForExactTokens(
systemContract,
zrc20,
inputToken,
gasFee,
gasZRC20,
amount
Expand All @@ -93,19 +114,41 @@ contract SwapToAnyToken is zContract, OnlySystem {

uint256 outputAmount = SwapHelperLib.swapExactTokensForTokens(
systemContract,
zrc20,
params.withdraw ? amount - inputForGas : amount,
params.target,
inputToken,
withdraw ? amount - inputForGas : amount,
targetToken,
0
);

if (params.withdraw) {
IZRC20(gasZRC20).approve(params.target, gasFee);
IZRC20(params.target).withdraw(params.to, outputAmount);
if (withdraw) {
IZRC20(gasZRC20).approve(targetToken, gasFee);
IZRC20(targetToken).withdraw(recipient, outputAmount);
} else {
IWETH9(params.target).transfer(address(uint160(bytes20(params.to))), outputAmount);
address wzeta = systemContract.wZetaContractAddress();
if (targetToken == wzeta) {
IWETH9(wzeta).withdraw(outputAmount);
address payable recipientAddress = payable(
address(uint160(bytes20(recipient)))
);
recipientAddress.transfer(outputAmount);
} else {
address recipientAddress = address(uint160(bytes20(recipient)));
IWETH9(targetToken).transfer(recipientAddress, outputAmount);
}
}
}

function swap(
address inputToken,
uint256 amount,
address targetToken,
bytes memory recipient,
bool withdraw
) public {
IZRC20(inputToken).transferFrom(msg.sender, address(this), amount);

swapAndWithdraw(inputToken, amount, targetToken, recipient, withdraw);
}
}
```

Expand All @@ -122,16 +165,40 @@ If the contract is being called from Bitcoin, use `bytesToBool` to decode the
last value in the `message`, and set it as the value of `params.withdraw`.

If the contract is being called from an EVM chain, use `abi.decode` to decode
all values: target token, recipient and withdraw.
all values: target token, recipient and the withdraw flag.

Next, add `params.withdraw` as the last argument to the `swapAndWithdraw`
function call.

### Swap and Withdraw Function

Add `bool withdraw` as the last parameter to the function definition.

If a user wants to withdraw the tokens, query the gas token and the gas fee.
Since a user now has an option to not withdraw, this step has become optional.

Modify the amount passed to `swapExactTokensForTokens`. If a user withdraws
token, subtract the withdraw fee in input token amount.

Finally, add a conditional to either withdraw ZRC-20 tokens to a connnected
chain or transfer the target token to the recipient on ZetaChain.
Finally, add a conditional to either withdraw ZRC-20 tokens to a connected chain
or transfer the target token to the recipient on ZetaChain. If a user doesn't
want to withdraw a token you need to consider two scenarios:

- If the target token is WZETA, unwrap it and transfer native ZETA to the
recipient.
- If the target token is not WZETA, transfer it to the recipient as any other
ERC-20-compatible token.

### Swap Function

Create a new public `swap` function to make it possible for users to call the
"swap and withdraw" function. Compared to "swap and withdraw", which is internal
and is not meant to be called directly, the "swap" function is public and is
meant to be called from ZetaChain. The purpose of "swap" is to allow users to
swap tokens they have on ZetaChain for other tokens and optionally also withdraw
them. For example, when a user has a ZRC-20 ETH and they want to swap it for
ZRC-20 BTC (without withdrawing), or swap it for ZRC-20 BNB and withdraw it to
the BNB chain as a native BNB token.

## Update the Interact Task

Expand All @@ -151,6 +218,63 @@ Add an optional parameter `withdraw`, which determines if a user wants to
withdraw the target token to the destination chain. By default set the value to
`true`, and pass withdraw as the third value in the message.

## Add a Swap Task

While the interact task is meant to be called on a connected chain to trigger a
universal contract, the swap task is meant to be called on ZetaChain directly to
swap an asset already on ZetaChain for a different asset optionally withdrawing
it.

```
import { task } from "hardhat/config";
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { parseEther } from "@ethersproject/units";
import { ethers } from "ethers";

const main = async (args: any, hre: HardhatRuntimeEnvironment) => {
const [signer] = await hre.ethers.getSigners();

if (!/zeta_(testnet|mainnet)/.test(hre.network.name)) {
throw new Error('🚨 Please use either "zeta_testnet" or "zeta_mainnet".');
}

const factory = await hre.ethers.getContractFactory("SwapToAnyToken");
const contract = factory.attach(args.contract);

const amount = parseEther(args.amount);
const inputToken = args.inputToken;
const targetToken = args.targetToken;
const recipient = ethers.utils.arrayify(args.recipient);
const withdraw = JSON.parse(args.withdraw);

const erc20Factory = await hre.ethers.getContractFactory("ERC20");
const inputTokenContract = erc20Factory.attach(args.inputToken);

const approval = await inputTokenContract.approve(args.contract, amount);
await approval.wait();

const tx = await contract.swap(
inputToken,
amount,
targetToken,
recipient,
withdraw
);

await tx.wait();
console.log(`Transaction hash: ${tx.hash}`);
};

task("swap", "Interact with the Swap contract from ZetaChain", main)
.addFlag("json", "Output JSON")
.addParam("contract", "Contract address")
.addParam("amount", "Token amount to send")
.addParam("inputToken", "Input token address")
.addParam("targetToken", "Target token address")
.addParam("recipient", "Recipient address")
.addParam("withdraw", "Withdraw flag (true/false)");
```

## Compile and Deploy the Contract

```
Expand Down
49 changes: 32 additions & 17 deletions src/pages/developers/tutorials/swap.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ npx hardhat omnichain Swap targetToken:address recipient

## Universal App Contract

```solidity filename="contracts/Swap.sol" {6-7,12,18-21,29-65}
```solidity filename="contracts/Swap.sol" {6-7,12,18-21,29-45,48-78}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;

Expand Down Expand Up @@ -121,27 +121,39 @@ contract Swap is zContract, OnlySystem {
params.to = recipient;
}

(address gasZRC20, uint256 gasFee) = IZRC20(params.target)
.withdrawGasFee();
swapAndWithdraw(zrc20, amount, params.target, params.to);
}

function swapAndWithdraw(
address inputToken,
uint256 amount,
address targetToken,
bytes memory recipient
) internal {
uint256 inputForGas;
address gasZRC20;
uint256 gasFee;

uint256 inputForGas = SwapHelperLib.swapTokensForExactTokens(
(gasZRC20, gasFee) = IZRC20(targetToken).withdrawGasFee();

inputForGas = SwapHelperLib.swapTokensForExactTokens(
systemContract,
zrc20,
inputToken,
gasFee,
gasZRC20,
amount
);

uint256 outputAmount = SwapHelperLib.swapExactTokensForTokens(
systemContract,
zrc20,
inputToken,
amount - inputForGas,
params.target,
targetToken,
0
);

IZRC20(gasZRC20).approve(params.target, gasFee);
IZRC20(params.target).withdraw(params.to, outputAmount);
IZRC20(gasZRC20).approve(targetToken, gasFee);
IZRC20(targetToken).withdraw(recipient, outputAmount);
}
}
```
Expand Down Expand Up @@ -173,25 +185,28 @@ offset of 20 bytes and then use `abi.encodePacked` to convert the address to
If it's an EVM chain, use `abi.decode` to decode the `message` into the
`params.target` and `params.to`.

### Swapping for Gas Token
### Swap and Withdraw Function

#### Swapping for Gas Token

Next, use the `withdrawGasFee` method of the target token ZRC-20 to get the gas
fee token address and the gas fee amount. If the target token is the gas token
of the destination chain (for example, BNB), `gasZRC20` will be the same
`params.target`. However, if the target token is an ERC-20, like USDC on BNB,
`gasZRC20` will tell you the address of the ZRC-20 of the destination chain.
Create a new function called `swapAndWithdraw`. Use the `withdrawGasFee` method
of the target token ZRC-20 to get the gas fee token address and the gas fee
amount. If the target token is the gas token of the destination chain (for
example, BNB), `gasZRC20` will be the same `params.target`. However, if the
target token is an ERC-20, like USDC on BNB, `gasZRC20` will tell you the
address of the ZRC-20 of the destination chain.

Use the `swapTokensForExactTokens` helper method to swap the incoming token for
the gas coin using the internal liquidity pools. The method returns the amount
of the incoming token that was used to pay for the gas.

### Swapping for Target Token
#### Swapping for Target Token

Next, swap the incoming amount minus the amount spent swapping for a gas fee for
the target token on the destination chain using the `swapExactTokensForTokens`
helper method.

### Withdraw Target Token to Connected Chain
#### Withdraw Target Token to Connected Chain

At this point the contract has the required `gasFee` amount of `gasZRC20` token
of the connected chain and an `outputAmount` amount of `params.target` token.
Expand Down
Loading