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

feat: check withdraw fee in Swap, callMulti example, universal NFT/FT #229

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions examples/swap/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ cache_forge
access_token

localnet.json

.openzeppelin
56 changes: 56 additions & 0 deletions examples/swap/contracts/Swap.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import {SwapHelperLib} from "@zetachain/toolkit/contracts/SwapHelperLib.sol";
import {BytesHelperLib} from "@zetachain/toolkit/contracts/BytesHelperLib.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";

import {RevertContext, RevertOptions} from "@zetachain/protocol-contracts/contracts/Revert.sol";
import "@zetachain/protocol-contracts/contracts/zevm/interfaces/UniversalContract.sol";
Expand Down Expand Up @@ -32,6 +33,7 @@
error Unauthorized();
error ApprovalFailed();
error TransferFailed();
error InsufficientAmount(string);

event TokenSwap(
address sender,
Expand Down Expand Up @@ -73,6 +75,9 @@
bool withdraw;
}

/**
* @notice Swap tokens from a connected chain to another connected chain or ZetaChain
*/
function onCall(
MessageContext calldata context,
address zrc20,
Expand Down Expand Up @@ -120,6 +125,9 @@
withdraw(params, context.sender, gasFee, gasZRC20, out, zrc20);
}

/**
* @notice Swap tokens from ZetaChain optionally withdrawing to a connected chain
*/
function swap(
address inputToken,
uint256 amount,
Expand Down Expand Up @@ -163,6 +171,9 @@
);
}

/**
* @notice Swaps enough tokens to pay gas fees, then swaps the remainder to the target token
*/
function handleGasAndSwap(
address inputToken,
uint256 amount,
Expand All @@ -175,6 +186,11 @@

(gasZRC20, gasFee) = IZRC20(targetToken).withdrawGasFee();

uint256 minInput = quoteMinInput(inputToken, targetToken);
if (amount < minInput) {
revert InsufficientAmount("not enough tokens");
}

if (gasZRC20 == inputToken) {
swapAmount = amount - gasFee;
} else {
Expand All @@ -198,6 +214,9 @@
return (out, gasZRC20, gasFee);
}

/**
* @notice Transfer tokens to the recipient on ZetaChain or withdraw to a connected chain
*/
function withdraw(
Params memory params,
address sender,
Expand Down Expand Up @@ -242,6 +261,10 @@
}
}

/**
* @notice onRevert handles an edge-case when a swap fails when the recipient
* on the destination chain is a contract that cannot accept tokens.
*/
function onRevert(RevertContext calldata context) external onlyGateway {
(address sender, address zrc20) = abi.decode(
context.revertMessage,
Expand All @@ -267,6 +290,39 @@
);
}

/**
* @notice Returns the minimum amount of input tokens required to cover the gas fee for withdrawal
*/
function quoteMinInput(
address inputToken,
address targetToken
) public view returns (uint256) {
(, uint256 gasFee) = IZRC20(targetToken).withdrawGasFee();

if (inputToken == targetToken) {
return gasFee;
}

address zeta = IUniswapV2Router01(uniswapRouter).WETH();

address[] memory path;
if (inputToken == zeta || targetToken == zeta) {
path = new address[](2);
path[0] = inputToken;
path[1] = targetToken;
} else {
path = new address[](3);
path[0] = inputToken;
path[1] = zeta;
path[2] = targetToken;
}

uint256[] memory amountsIn = IUniswapV2Router02(uniswapRouter)
.getAmountsIn(gasFee, path);

return amountsIn[0];
}
Fixed Show fixed Hide fixed

function _authorizeUpgrade(
address newImplementation
) internal override onlyOwner {}
Expand Down
4 changes: 3 additions & 1 deletion examples/swap/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@
"@solana-developers/helpers": "^2.4.0",
"@solana/spl-memo": "^0.2.5",
"@solana/web3.js": "^1.95.8",
"@zetachain/networks": "10.0.0-rc3",
"@zetachain/protocol-contracts": "11.0.0-rc3",
"@zetachain/toolkit": "13.0.0-rc8"
"@zetachain/protocol-contracts-solana": "2.0.0-rc1",
"@zetachain/toolkit": "13.0.0-rc11"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Update @zetachain/protocol-contracts to latest RC version

The current RC versions appear to be intentionally aligned for compatibility, but there's a newer release candidate available for protocol-contracts:

  • Current: 11.0.0-rc3
  • Latest: 11.0.0-rc4
🔗 Analysis chain

Verify compatibility between RC versions and consider using stable releases.

The dependencies use different release candidate versions which may lead to compatibility issues:

  • @zetachain/networks: rc3
  • @zetachain/protocol-contracts: rc3
  • @zetachain/protocol-contracts-solana: rc1
  • @zetachain/toolkit: rc11

Let's verify the latest available versions:

Consider:

  1. Using stable versions if available for production use
  2. Ensuring all RC versions are compatible with each other
  3. Adding a comment in package.json explaining why RC versions are needed
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check latest versions of ZetaChain packages

echo "Checking latest versions from npm registry..."
for pkg in "@zetachain/networks" "@zetachain/protocol-contracts" "@zetachain/protocol-contracts-solana" "@zetachain/toolkit"; do
  echo "Package: $pkg"
  curl -s "https://registry.npmjs.org/$pkg" | jq -r '.["dist-tags"]'
done

Length of output: 2119

}
}
6 changes: 5 additions & 1 deletion examples/swap/scripts/localnet.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,18 @@ CONTRACT_SWAP=$(npx hardhat deploy --name Swap --network localhost --gateway "$G
COMPANION=$(npx hardhat deploy-companion --gateway "$GATEWAY_ETHEREUM" --network localhost --json | jq -r '.contractAddress')

npx hardhat companion-swap \
--skip-checks \
--network localhost \
--contract "$COMPANION" \
--universal-contract "$CONTRACT_SWAP" \
--amount 1 \
--target "$ZRC20_BNB" \
--recipient "$SENDER"
--recipient "$SENDER"

npx hardhat localnet-check

npx hardhat companion-swap \
--skip-checks \
--network localhost \
--contract "$COMPANION" \
--universal-contract "$CONTRACT_SWAP" \
Expand All @@ -43,6 +45,7 @@ npx hardhat companion-swap \
npx hardhat localnet-check

npx hardhat evm-swap \
--skip-checks \
--network localhost \
--receiver "$CONTRACT_SWAP" \
--amount 1 \
Expand All @@ -52,6 +55,7 @@ npx hardhat evm-swap \
npx hardhat localnet-check

npx hardhat evm-swap \
--skip-checks \
--network localhost \
--receiver "$CONTRACT_SWAP" \
--amount 1 \
Expand Down
15 changes: 15 additions & 0 deletions examples/swap/tasks/companionSwap.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import ERC20_ABI from "@openzeppelin/contracts/build/contracts/ERC20.json";
import { task, types } from "hardhat/config";
import type { HardhatRuntimeEnvironment } from "hardhat/types";
import { isInputAmountSufficient } from "./evmSwap";
import { ZetaChainClient } from "@zetachain/toolkit/client";

const main = async (args: any, hre: HardhatRuntimeEnvironment) => {
const { ethers } = hre;
Expand All @@ -9,6 +11,18 @@ const main = async (args: any, hre: HardhatRuntimeEnvironment) => {
const factory = (await hre.ethers.getContractFactory("SwapCompanion")) as any;
const contract = factory.attach(args.contract).connect(signer);

const client = new ZetaChainClient({ network: "testnet", signer });

if (!args.skipChecks) {
await isInputAmountSufficient({
hre,
client,
amount: args.amount,
erc20: args.erc20,
target: args.target,
});
}

let tx;
if (args.erc20) {
const erc20Contract = new ethers.Contract(
Expand Down Expand Up @@ -58,4 +72,5 @@ task("companion-swap", "Swap native gas tokens", main)
.addOptionalParam("erc20", "ERC-20 token address")
.addParam("target", "ZRC-20 address of the token to swap for")
.addParam("amount", "Amount of tokens to swap")
.addFlag("skipChecks", "Skip checks for minimum amount")
.addParam("recipient", "Recipient address");
39 changes: 39 additions & 0 deletions examples/swap/tasks/evmSwap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,51 @@ import type { HardhatRuntimeEnvironment } from "hardhat/types";

import { ZetaChainClient } from "@zetachain/toolkit/client";

export const isInputAmountSufficient = async ({
hre,
client,
erc20,
amount,
target,
}: any) => {
Comment on lines +6 to +12
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add proper TypeScript types instead of using 'any'.

Replace the 'any' type with a proper interface definition for better type safety and code maintainability.

+interface InputAmountCheckParams {
+  hre: HardhatRuntimeEnvironment;
+  client: ZetaChainClient;
+  erc20?: string;
+  amount: string;
+  target: string;
+}

-export const isInputAmountSufficient = async ({
-  hre,
-  client,
-  erc20,
-  amount,
-  target,
-}: any) => {
+export const isInputAmountSufficient = async ({
+  hre,
+  client,
+  erc20,
+  amount,
+  target,
+}: InputAmountCheckParams) => {

Committable suggestion skipped: line range outside the PR's diff.

const inputZRC20 = await (erc20
? client.getZRC20FromERC20(erc20)
: client.getZRC20GasToken(hre.network.name));

const minAmount = await client.getWithdrawFeeInInputToken(inputZRC20, target);

const minAmountFormatted = hre.ethers.utils.formatUnits(
minAmount.amount,
minAmount.decimals
);

const value = hre.ethers.utils.parseUnits(amount, minAmount.decimals);
Comment on lines +19 to +24
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure consistent decimal units when parsing amounts

When parsing the amount, you use minAmount.decimals as the decimal parameter. This assumes that the input token and the minAmount share the same decimals, which might not be the case if their decimals differ. To prevent incorrect amount comparisons due to mismatched decimal units, retrieve the decimals of the input token (inputZRC20) and use it consistently.

Apply this diff:

   const minAmountFormatted = hre.ethers.utils.formatUnits(
     minAmount.amount,
-    minAmount.decimals
+    inputTokenDecimals
   );

-  const value = hre.ethers.utils.parseUnits(amount, minAmount.decimals);
+  const value = hre.ethers.utils.parseUnits(amount, inputTokenDecimals);

Ensure that inputTokenDecimals corresponds to the decimals of inputZRC20, which can be retrieved as follows:

const inputTokenDecimals = minAmount.decimals; // Or fetch from inputZRC20 if necessary


if (value.lt(minAmount.amount)) {
throw new Error(
`Input amount ${amount} is less than minimum amount ${minAmountFormatted} required for a withdrawal to the destination chain`
);
}
};

export const evmDepositAndCall = async (
args: any,
hre: HardhatRuntimeEnvironment
) => {
try {
const [signer] = await hre.ethers.getSigners();
const client = new ZetaChainClient({ network: "testnet", signer });

if (!args.skipChecks) {
await isInputAmountSufficient({
hre,
client,
amount: args.amount,
erc20: args.erc20,
target: args.target,
});
}

const tx = await client.evmDepositAndCall({
amount: args.amount,
erc20: args.erc20,
Expand Down Expand Up @@ -72,6 +110,7 @@ task("evm-swap", "Swap tokens from EVM", evmDepositAndCall)
.addOptionalParam("revertMessage", "Revert message", "0x")
.addParam("amount", "amount of ETH to send with the transaction")
.addOptionalParam("erc20", "ERC-20 token address")
.addFlag("skipChecks", "Skip checks for minimum amount")
.addParam("target", "ZRC-20 address of the token to swap for")
.addParam("recipient", "Recipient address")
.addOptionalParam(
Expand Down
Loading
Loading